大纲
在《从汇编层看64位程序运行——ROP攻击以控制程序执行流程》一文中,我们通过“篡改”函数的返回地址来达成修改程序执行流程的目的。
但是这个方案存在一个问题:它会导致栈溢出。这是因为ret指令会导致栈的pop,call指令会导致栈的push。而案例中,foo7调用foo函数时,使用的是ROP攻击方法——没有使用call指令。这样就导致整个流程call了1次(main–>foo7),即压栈1次;而ret了2次,foo7一次,foo一次。
在foo7进入foo之后,我们修改了foo的返回地址。这个地址是0x7fffffffdef8,而它属于main函数的栈帧。这样的栈溢出,并没有导致我们程序出问题。这是因为0x7fffffffdef8保存的是main函数调用foo7时,push到栈中的临时数据(即第7个参数,foo函数的地址)。所以在从foo回到main函数后,这个空间的值就不会被逻辑所使用,进而没有发生问题。
但是由于call一次、ret两次,最终会导致栈会被多pop一次。于是回到main函数后,rsp的值会比正确的时候大0x08(即main的栈帧小了0x08),而减小的空间正好是上一段分析的main函数push到foo7中的foo函数地址。
我们知道,很多时候栈上变量的表达是通过rbp寄存器(rbp寄存器的值一直是正确的)来进行的(见《从汇编层看64位程序运行——栈上变量的rbp表达》),所以这次rsp寄存器的变动,一般不会影响main函数临时变量的取值,也不会影响程序执行。
即使后面调用了其他函数,甚至push栈传递数据的行为,也不会造成程序的崩溃。这是因为此时rsp指向了main函数栈上变量地址空间的末尾。调用其他函数时,只会让栈在此基础上增长,并不会影响到main函数关键的栈帧空间。
从main函数退出时,汇编会执行Leave指令。这步指令会
mv %rbp,%rsp
这样rsp寄存器的值就回到进入main函数时正确的值,进而导致代码进入main函数的调用者时,堆栈由恢复到正确的状态,所以程序也没有出问题。
整体调试过程如下:
main
进入时寄存器状态
rbp:1
rsp:df18
分析过程
刚进入main函数时,栈底寄存器rbp的值是1,栈顶寄存器rsp的值是df18。
+4行将rbp寄存器的值压入栈后,栈顶寄存器rsp的值变成df10。
+5行让rbp和rsp寄存器中的值相同,即rbp和rsp都是df10。
+8行预先给main函数分配了栈空间(0x10)。这样rsp变成df00,rbp还是df10。
+23行会foo函数的地址压入栈,这样rsp会变成def8。(0x7fffffffdef8,即后续在foo函数中被用作返回RIP的空间。)
离开时寄存器状态
rbp:df10
rsp:def8
foo7
进入时寄存器状态
rbp:df10
rsp:def8
分析过程
执行call指令,进入foo函数内部。由于call会将next_rip压入栈,所以rsp的值会继续减少0x08,变成def0。
可以看到栈顶的值就是next_rip的值。
将main函数的栈底寄存器的值保存到栈上,这样rsp变成了dee8。
此时的栈顶rsp就是可以用于给foo7函数预分配栈上空间的值。但是由于要用rbp做变量表达转换,所以会将rbp的值赋值为rsp的值。这样rbp和rsp的值都是dee8了。
由于中间过程没有进入出入栈的操作,所以一直到+75行,rsp和rbp的值都没变。
但是我们修改了栈上空间的值,即foo7的返回地址,
+75会抛出之前保存的rbp的值,这样rbp的值会还原到df10;同时rip会退栈,变成def0。
这样,foo7的返回地址(foo函数的入口地址)就处于栈的顶端。
离开时寄存器状态
rbp:df10
rsp:def0
foo
进入时寄存器状态
rbp:df10
rsp:def8
分析过程
foo7最后的ret会从栈中pop出next_rip,然后跳转到foo。这样退栈行为,让rsp变成def8。
+4行,会将rbp压入栈,这样rsp变成了def0。
+5行会让rbp等于rsp。这样foo函数就会可以在自己的栈底上分配空间和修改变量。
+8行会给foo函数分配0x10大小的栈上空间。这样rsp的值就会变成dee0。
+12和+16行分别会赋值一个栈上变量以及修改foo函数的返回地址
打印输出函数执行完,栈的状态没有变
+36行的Leave做了几件事:
- mov %rbp, %rsp。即还原栈顶寄存器(对应于+5行),这样rsp变成def0。
- pop %rbp。这样保存在栈顶def0的df10会被赋值给rbp。而rsp也因为退栈,从而变成了def8。
离开时寄存器状态
rbp:df10
rsp:def8
main
进入时寄存器状态
rbp:df10
rsp:df00
分析过程
foo最后的ret会将foo的next_rip(即51fb)从栈中pop出来,这样rsp进一步变成了df00。
再次回到main函数后,rsp的值不再是离开main是的def8。
+63行又会让rsp进一步改变,变成df08。
+72的Leave,会干两件事:
- mov %rbp, %rsp。还原栈顶寄存器(对应于+5行)。这样rsp变成df10。
- pop出rbp。rsp变成df18。rbp变成1。