write函数


下面介绍下write这个普遍用来泄漏信息的函数:

write函数原型是write(fd, addr, len),即将addr作为起始地址,读取len字节的数据到文件流fd(0表示标准输入流stdin、1表示标准输出流stdout)。如果顺利,write()会返回实际写入的字节数,当有错误发生时则返回-1,错误代码存入errno中。

write函数的优点是可以读取任意长度的内存信息,即它的打印长度只受len参数控制,缺点是需要传递3个参数,特别是在x64环境下,可能会带来一些困扰。在x64环境下,函数的参数是通过寄存器传递的,rdi对应第一个参数,rsi对应第二个参数,rdx对应第三个参数,往往凑不出类似“pop rdi; ret”“pop rsi; ret”“pop rdx; ret”等3个传参的gadget。此时,可以考虑使用libc_csu_init函数的通用gadget。简单的说,就是通过libc_csu_init函数的两段代码来实现3个参数的传递,这两段代码普遍存在于x64二进制程序中,只不过是间接地传递参数,而不像原来,是通过pop指令直接传递参数。

举个例子:

1
write(1,"Hello World\n",13);

这条语句作用是向标准输出中打印"Hello World"
它的汇编代码如下:

asm

从上面的代码中可以发现,在write函数被调用(call <write@plt>)之前时,有3个MOV指令,先将write函数的各个参数存入栈中(C语言中参数以从右向左的顺序入栈)。

参数入栈后,执行call指令,call相当于push ip, jmp ,也就是先将IP(函数的返回地址,即下一条指令的地址)压入栈中,然后JMP至被调用函数的地址。

参数入栈后栈的情况如下:

参数入栈后栈的情况

call <write>后栈的情况如下:

call write

其中0x08048457write函数执行完后的返回地址

现在重新梳理一下,调用write函数时依次压入栈中的分别是长度len、数组buf的首地址、文件描述符fdwrite函数的返回地址,如下图所示:

write

那么如果程序中存在缓存区溢出漏洞(比如read函数读取的数据长度比实际缓存区长时),我们可以通过覆盖函数的返回地址,来控制程序的执行流程。
漏洞函数:

1
2
char buffer[128];
read(0,buffer,256);

我们可以将函数的返回地址覆写为write函数的地址,然后在栈中构造write函数的返回地址和参数,这样我们便可以使用write函数来泄露内存中的信息,比如某函数在libc中的地址等等。

leak

被修改后的栈是这样的:

8被修改后的栈

当正常的函数调用ret指令返回时,ret指令相当于pop eip,也就是说write的地址会赋给eip,相当于执行了一条jmp <write>

现在栈变成了这样:

9现在的栈

是不是和上面讲的write函数的栈一样啦?不过也不一样,现在这个栈中的fd,buf,len是我们可以任意指定的。

参考资料:

https://blog.csdn.net/SmalOSnail/article/details/53234562