栈迁移原理与应用

栈溢出
(stackoverflow)
当外界输入过长时,将超过局部变量(数组之类)的范围,造成数据溢出,覆盖了返回地址
栈溢出能使我们覆盖栈上的某些区域的值,甚至是当前函数的返回地址ret,覆盖了ret为一个合法的地址,就有可能实现攻击者想要执行的命令。
栈迁移是什么
要想完成栈溢出攻击,需要输入足够长,足以完成恶意指令的写入。有些情况下,输入长度受限制,填满缓冲区之后仅能容纳一个ret和一个ebp。

pop eip不合法(正常应该是:pop eax ,jmp eax,不用一定是eax),后面不再说,画流程图的时候没注意到,翻回去修改图上丢了很多东西,不重写了。
如图所示,当上层函数调用foo函数,即eip执行到call foo指令时,call指令以及foo函数开始的指令依次做了如下事情:
- 牢记foo结束后应该从哪里继续执行(保存当前eip下面的位置到栈中,即ret)
- 牢记上层函数的栈底位置(保存当前ebp的内容到栈中,即old ebp);
- 牢记foo函数栈开始的位置(保存当前栈顶内容到ebp,便于foo函数栈内寻址)
以上三件事对应了上图右侧上两个方框中的汇编指令,当call foo指令执行完后,栈中的内容如下图左,之后程序就由foo函数接管了。

当foo函数执行结束时,eip即将执行leave和ret两条指令恢复现场,此时栈中内容如上图右所示。leave与ret指令所作的事情:
- 清空当前函数栈以还原栈空间(直接移动栈顶指针esp到当前函数的栈底而ebp);
- 还原栈底(将此时esp所指上层函数栈底old ebp弹入ebp寄存器中);
- 还原执行流(将此时esp所指的上层函数调用foo时的地址弹入eip寄存器内);
这三步恰好是之前的逆过程。
在做回到调用者位置这件事时,栈顶指针的位置将完全由ebp寄存器的内容所控制(mov esp,ebp),而ebp寄存器的内容则可以由栈中的数据控制(pop ebp),由此,攻击者可以通过修改原old ebp内容,则能篡改ebp寄存器中的内容,从而(有可能)篡改esp进而控制eip。这一过程为栈迁移的思想。如下图

上文说(有可能),是因为leave所代表的子指令是有先后执行顺序的,即无法先执行pop ebp,再执行mov esp,ebp,无法先影响到ebp再影响esp。
但是,将栈上的ret部分覆盖为另一组leave ret指令(gadget)的地址,即程序会执行两次leave指令,一次ret指令。就可以先修改ebp再影响esp
1 | 第一次leave: |
栈迁移过程
一
确定缓冲区变量在溢出时,至少能覆盖栈上的的ebp和ret。之后选取一段能执行提权的地址。
就像之前做过的题目设置是复制输入内容到bss段,然后payload就可以写shellcode再将返回地址覆盖为这个地址。
还是图解说的明白

二
寻找gadget(leava ret )地址,记该地址为LeaveRetAddr
三
设置缓冲区变量,使其将栈上的ebp覆盖为HijackAddr-4,将ret覆盖为LeaveRetAddr
四
执行程序至结束,会发生以下事情
- 执行指令:mov esp,ebp还原栈顶指针到当前栈底;此时esp指向栈上被篡改的ebp数据,即HijackAddr-4;
- 执行指令:pop ebp,将篡改的HijackAddr-4放入ebp寄存器内,此时esp上移,指向栈上被篡改的ret数据

- 执行指令: pop eip,将LeaveRetAddr放入eip寄存器内,篡改执行流,来执行第二次leave指令;
- 执行指令:mov esp,ebp,将HijackAddr-4移入esp寄存器内,即栈顶指针被骗到指向HijackAddr-4

- 执行指令:pop ebp ,ebp寄存器仍为HijackAddr-4,但esp上移四字节。指向HijackAddr

- 执行指令pop eip,将HijackAddr移入eip内,成功篡改执行流至shellcode区域
五
执行shellcode,完成攻击
例题
buuctf_ciscn_2019_es_2
查看保护和反汇编
checksec

32位堆栈不可执行
ida
main函数
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
vul函数
1 | int vul() |
两次read可以输入
虽然有溢出但是余出的长度只有八字节。
在hack中有system函数,但是只是简单的输出flag字符串
1 | int hack() |
如果能将/bin/sh作为参数调用system函数就能getshell了
这道题用栈迁移来解
解题过程
寻找gadget(leave_ret)

leave_ret_addr=0x080484b8
第一次read(我的意思是输入)用来泄露ebp的地址
printf当遇到\x00时才会停止输出,将下一处的终止符覆盖就可以打印出ebp的地址,就像覆盖泄露canary一样来泄露ebp的地址
还要说的是,栈也是有地址的,后面用gdb的时候会说哪里是栈的地址
1 | paylaod1=b'a'*0x27+b'b' |
现在我们要确定s在栈上的位置,好在第二次read时对栈进行布局。
选择的是用ebp+偏移量的方法来表示s的地址
gdb调试
从其他师傅处得来的经验,选择在nop处下断点

上面的是ida的,当然也没有忘记gdb怎么用,也挺好用的

总之断点地址:0x080485fc
下断点后运行(r),第一次read时输入aaaa,终止(ctrl+c),查看栈(stack)

所说数据存储在栈上,栈也是有地址的,这个地址指向的才是数据,也有栈指向一个地址,这个地址指向数据的。
上图左侧黄颜色的就是栈的地址
计算s到ebp的偏移量

0x38字节,所以s地址:ebp-0x38
第二次paylaod的布局图(0x10=16)

payload2
1 | leave_ret_addr=0x080484b8 |
exp
1 | from pwn import* |
总结
看文章要慢