下面介绍下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"
它的汇编代码如下:

从上面的代码中可以发现,在write函数被调用(call <write@plt>)之前时,有3个MOV指令,先将write函数的各个参数存入栈中(C语言中参数以从右向左的顺序入栈)。
参数入栈后,执行call指令,call相当于push ip, jmp 
参数入栈后栈的情况如下:

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

其中0x08048457为write函数执行完后的返回地址
现在重新梳理一下,调用write函数时依次压入栈中的分别是长度len、数组buf的首地址、文件描述符fd、write函数的返回地址,如下图所示:
那么如果程序中存在缓存区溢出漏洞(比如read函数读取的数据长度比实际缓存区长时),我们可以通过覆盖函数的返回地址,来控制程序的执行流程。
漏洞函数:
1  | char buffer[128];  | 
我们可以将函数的返回地址覆写为write函数的地址,然后在栈中构造write函数的返回地址和参数,这样我们便可以使用write函数来泄露内存中的信息,比如某函数在libc中的地址等等。

被修改后的栈是这样的:
当正常的函数调用ret指令返回时,ret指令相当于pop eip,也就是说write的地址会赋给eip,相当于执行了一条jmp <write>
现在栈变成了这样:
是不是和上面讲的write函数的栈一样啦?不过也不一样,现在这个栈中的fd,buf,len是我们可以任意指定的。
参考资料: