MIPS栈溢出原理
MIPS的函数调用
小知识:
Mips 调用函数时不会将返回地址放入栈中,而在非叶子函数中,为了调用下一个函数,会将上一个函数的返回地址压栈
叶子函数,即该函数中不会调用任何其他函数
非叶子函数,即该函数需要调用其他函数
mips的函数调用过程
当函数A执行到调用函数B的指令时,函数调用指令复制当前pc寄存器的值到ra寄存器中,即ra中存放返回地址
他这个没有专门的控制ra的指令,在ja指令执行的时候,给ra赋值了
程序跳转到函数B的时候,如果是非叶子函数,函数B会先把函数A的返回地址压栈(即ra寄存器的值压栈),叶子函数没有这个操作,返回地址就只存在于ra寄存器中
main函数一般是一个非叶子函数,我们几乎可以在任何mian开头看到
sw $ra,0x20+var_s4($sp)
这条指令
函数B执行完之后,叶子函数直接使用jr $ra
指令返回函数A,而非叶子函数则需要,从堆栈中取出返回地址,然后将返回地址放入ra寄存器,再使用jr $ra
指令
下面是两个程序(叶子和非叶子)的分析
叶子函数
这个程序中,add函数为叶子函数,我们去分析add函数调用之前做了什么。
这里使用
mipsel-linux-gnu-gcc -o tree tree.c -static
进行编译然后使用
qemu-mipsel -g 1234 tree
与ida连用的动态调试IDA remote另一篇文章会详细讲,这里就不赘述
下断点到main函数,能看到在400578
处有看到将ra压栈的操作,这个就是操作系统的某个地址。
重点在0040059c
这个行,调用add函数,先不关心参数调用,只看ra寄存器的相关操作
执行完00400578后,寄存器以及栈中的内容
继续执行至0040059c,此时ra值没有改变
执行jal add
跳转过来之后,ra的值变了,值正好是jr add的下一条可用指令(nop的目的只是为了对齐)
再看add的所有指令,会发现,只有跳回main函数指令出现了ra寄存器
非叶子函数
add中调用了printf()函数。
执行进入add函数,与叶子函数相同
进入之后,抬高堆栈后,执行了ra压栈操作,即00400544
这一行的操作
上图是函数执行完之后,准备返回main,在0040059c
这行把main函数的返回地址放回ra。
然后通过jr $ra
返回main。
参数
mips函数调用传递参数规则,前四个参数通过$a1- $a3寄存器传递,其他参数通过栈传递。
从main函数中来看,先将参数数字放到临时栈中(蓝色框中),然后将第五个参数去取出,放入add的栈中,然后将前四个栈放到a0-a3寄存器中(红框)
我们来尝试画出其栈图,下图是main函数的栈图
红框为上个函数的返回地址,蓝框为局部变量,绿的是第五个参数。
栈溢出
我们使用下面代码来做实验
代码大意就是从passwd这个文件读取文件。
尝试使用大量字符串
运行发现报错
栈溢出的目的是覆盖返回地址,上面说过,main一般是一个典型的非叶子函数,而且passwd文件的读取是在main中执行的,
所以我们目标应该在main刚进来的ra位置,使用ida+qemuGdb调试。
运行至main
并且关注ra的值存放的位置,右键 -> jump a new window
执行完ra压栈之后,栈中的数据
下面让程序读完passwd文件,这个地方有个小方法,读取一般是在循环中一个字符一个字符读的,所以向下单步执行,如果遇到多次循环,就可以尝试吧断点下载循环执行完的下一行。
上图发现循环,尝试在循环外下断,不要断在nop上,情况允许就尽量断在nop下一行
ida下使用f9可以继续运行,直到下一个断点,运行到00400500
后可以看到,地址存放的地方已经被覆盖
接下来可以使用cyclic等工具来计算长度,编写poc或exp利用。