下面介绍下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
是我们可以任意指定的。
参考资料: