本文以C语言中的write
函数为例,以汇编代码的形式,探讨该函数运行时内存中的情况。
这是我们常见的一个write()
函数:
write(fd,buf,len);
这个函数有三个参数:文件描述符fd、数组buf的地址、读取数据的长度len。
它的作用是从buf中读取长度len个字节的数据。
举个例子:
write(1,"Hello World\n",13);
这条语句作用是向标准输出中打印"Hello World"
它的汇编代码如下:
从上面的代码中可以发现,在write
函数被调用(call <write@plt>
)之前时,有3
个MOV
指令,先将write
函数的各个参数存入栈中(C语言中参数以从右向左的顺序入栈)。
参数入栈后,执行call
指令,call
相当于push ip
, jmp <addr>
,也就是先将IP
(函数的返回地址,即下一条指令的地址)压入栈中,然后JMP
至被调用函数的地址。
参数入栈后栈的情况如下:
call <write>
后栈的情况如下:
其中0x08048457
为write
函数执行完后的返回地址。
现在重新梳理一下,调用write
函数时依次压入栈中的分别是长度len
、数组buf
的首地址、文件描述符fd
、write
函数的返回地址,如下图所示:
那么如果程序中存在缓存区溢出漏洞(比如read
函数读取的数据长度比实际缓存区长时),我们可以通过覆盖函数的返回地址,来控制程序的执行流程。
漏洞函数:
char buffer[100];
read(0,buffer,128);
我们可以将函数的返回地址覆写为write
函数的地址,然后在栈中构造write
函数的返回地址和参数,这样我们便可以使用write
函数来泄露内存中的信息,比如某函数在libc
中的地址等等。
被修改后的栈是这样的:
当正常的函数调用ret
指令返回时,ret
指令相当于pop ip
,也就是说write
的地址会赋给ip
,相当于执行了一条jmp <write>
现在栈变成了这样:
是不是和上面讲的write
函数的栈一样啦?
不过也不一样,现在这个栈中的fd
,buf
,len
是我们可以任意指定的,即: 指哪儿就可以打哪儿 :)