SuperAPI AI Directory

re-pwn

General & other

View repository

Updated 10-16 05:06

About this skill

CTF pwn getting started (stack overflow / format string / ret2libc)

When to use / when not to use

  • Use: CTF pwn challenge—stack overflow (overwrite return address), format string (leak / arbitrary write), ret2libc (leak libc and get shell)
  • Use: you have the challenge binary + matching libc, and need to write an exploit script that can get shell / print the flag
  • Use: entry-level ROP (ret2win / ret2shellcode / ret2libc / the first few levels of ret2csu)
  • Do not use: engineered complex ROP chains / heap exploitation (fastbin / tcache) → [[re-exploit]]
  • Do not use: pure reversing challenges (recover the algorithm to find the flag) → the reversing branch of [[re-ctf]] ([[re-binary-core]])
  • Do not use: Windows targets ([[re-x64dbg]] / [[re-windbg]]); macOS targets ([[re-lldb]])
  • Note: exploit verification is dynamic execution, run inside a sandbox by default (the highest principle of [[re-analyze/platform-tips]], see [[re-sandbox]]); after local verification passes in the sandbox, then consider connecting to the remote (pitfall 4)

Tool preparation

Refer to [[re-analyze/platform-tips]]—exploit verification is sandboxed by default; do static analysis first (disassembly / checksec) then dynamic debugging, in line with the "static first" approach.

pwntools (installed via pip) — the main workhorse for exploit scripts

  • pip install pwntools(official PyPI, current stable version 4.15.0; best 64-bit system support)
  • Python compatibility matrix (this library maintains only this one copy in re-pwn, other skills reference this location): 4.15.0 supports Python 3.8+, but the official release has already declaredthis is the last version supporting Python 2 and Python <3.10—subsequent versions are prepared according to Python >=3.10; to run on 3.8/3.9 you need to pin pwntools==4.15.0
  • Distro packages (versions may lag behind, functionality is the same): Debian/Ubuntu sudo apt install python3-pwntools、Fedora sudo dnf install python3-pwntools、Arch sudo pacman -S python-pwntools(official package from the Extra repository)
  • Python 3.12+ (e.g., Ubuntu 24.04 ships with 3.12) installing directly with pip will report PEP 668 externally-managed-environment—countermeasure: create a venv (python3 -m venv ~/venvs/pwn && source ~/venvs/pwn/bin/activate then pip install), or use the distro apt package, or pip install --break-system-packages(system-level, be aware of the risk)
  • Verify: pwn --version / python3 -c "import pwn; print(pwn.version)"

checksec — protection check (built into pwntools)

  • Available as soon as pwntools is installed: pwn checksec --file ./target
  • built into pwndbg checksec command (available once pwndbg is installed, see [[re-gdb]])
  • Verify: pwn checksec --file ./target output contains the Arch/RELRO/Stack Canary/NX/PIE lines

gdb + pwndbg/gef (see [[re-gdb]]) — dynamic verification

  • gdb: Debian/Ubuntu sudo apt install gdb、Fedora sudo dnf install gdb、Arch sudo pacman -S gdb、macOS brew install gdb(lldb recommended)
  • 32-bit target: Debian/Ubuntu need sudo apt install gdb-multiarch(needed for 64-bit gdb to debug a 32-bit target); running a 32-bit dynamically linked program also needs 32-bit runtime libraries sudo apt install libc6-i386
  • choose one of pwndbg / gef (installation methods are as specified in [[re-gdb]]: pwndbg uses the official portable install script, gef uses curl to ~/.gdbinit); verify: entering gdb shows the pwndbg/gef banner

one_gadget (optional) — one-click libc RCE

  • gem install one_gadget(Ruby gem, one_gadget 1.10.0 requires Ruby ≥ 3.1 (declared in gemspec); Debian/Ubuntu first sudo apt install ruby; macOS has Ruby built in,sudo gem install one_gadget)
  • Arch: sudo pacman -S one_gadget(official package from the Extra repository); Fedora has no package → install via gem
  • Verify: one_gadget /lib/x86_64-linux-gnu/libc.so.6(List candidate one_gadgets and their constraints)

Procedure

Execute in order; for each step's artifact (checksec output, offset, libc base, flag) record the evidence path + sha256 (see [[re-triage]] for the evidence preservation method), for citation in the writeup.

  1. Vulnerability identification (checksec protections / disassembly to find dangerous functions):

    file target && sha256sum target > target.sha256
    pwn checksec --file target        # NX / PIE / Canary / RELRO —— 决定利用路线(见坑 1)
    
    • Disassemble/decompile to finddangerous functions and input points(with [[re-binary-core]] as the base, [[re-ghidra]] / [[re-ida]] / [[re-radare2]]):
      • Stack overflow:gets / strcpy / scanf("%s") / manual read(fd, buf, 大长度) / sprintf —— focus on whether the buffer size and input length are limited (read(fd, buf, 0x40) with a 0x20 buf is the overflow point)
      • Format string:printf(user_input) / sprintf(buf, user_input)(the first argument is not a constant string)
      • Backdoor/flag function: strings contains /bin/sh or the function name contains win/backdoor/flag; use xref to confirm reachability
    • Record: vulnerability type, dangerous function address, input channel (stdin / remote socket), protection status (determines which route ② takes)
  2. Stack overflow exploitation (offset calculation, overwriting the return address):

    # 偏移计算:cyclic 生成模式串,崩溃后定位返回地址在输入中的偏移
    python3 -c "from pwn import *; print(cyclic(200, n=8))" > /tmp/patt
    # 本地崩溃(沙箱内)→ gdb/pwndbg 看 $rsp,或直接 pwntools 一体化:
    
    from pwn import *
    p = process('./target'); p.sendline(cyclic(200, n=8)); p.wait()
    core = p.corefile  # 需开启 core dump
    print("offset:", cyclic_find(core.pc, n=8))   # 64 位 rip 在 core.pc
    
    • Overwrite the return address to the backdoor function (ret2win):offset * b'A' + p64(win_addr)
    • Note for 64-bit: before calling a function, arguments go into registers (rdi/rsi/rdx), not the stack——you need pop rdi; ret -type gadgets to pass arguments (pitfall 5)
    • NX off (pwn checksec shows NX disabled) → you can put shellcode on the stack and point the return address to it (ret2shellcode); if NX is on, go to ③
    • Each round, use gdb ([[re-gdb]]) to break at the return point and verify the return address is indeed controlled (x/10gx $rsp)
  3. ret2libc (PLT/GOT, libc version identification):

    • Prerequisite: the overflow is controllable but the stack is non-executable (NX on) and there is no backdoor——the goal is to call libc's system("/bin/sh"):
    objdump -d target | grep -E '<puts@plt>|<printf@plt>|<system@plt>'   # PLT 可调函数
    objdump -R target | grep puts                                          # GOT 存 libc 实际地址
    
    • Leak the libc address: the first ROP stage calls puts@plt(puts@got)(rdi = puts@got, see pitfall 5) → prints out the real address of puts in libc → return to main and run the second stage
    • libc version identification: if you have the libc file provided with the challenge, compute the offset locally; if not, use the leaked low 12 bits (the in-page offset is unchanged) + an online database (libc.rip / libc-database, see pitfall 2):
      one_gadget ./libc.so.6    # 若有 libc 文件:直接列 one_gadget
      # 或用泄露的 puts 地址后 3 位 hex 查 libc-database: ./find puts 7a0
      
    • Compute: libc_base = leaked_puts - puts_offset;system_addr = libc_base + system_offset;binsh_addr = libc_base + binsh_offset(readelf -s libc.so.6 | grep system and strings -tx libc.so.6 | grep /bin/sh)
    • Second-stage ROP:pop rdi; ret → binsh_addr → system → wrap up (alignment, pitfall 5)
  4. Format string (leak / write):

    # 泄露:把输入原样做 format 的参数,%p 逐个试偏移
    python3 -c "print('%p.'*20)" | ./target
    
    from pwn import *
    p = process('./target')
    p.sendline(b'%6$p %7$p')      # %N$ 是逻辑参数列表第 N 个参数: format 占 rdi,%1$–%5$ 来自 rsi/rdx/rcx/r8/r9,%6$/%7$ 即调用者栈上第 1/2 个 8 字节
    print(p.recvline())
    
    • Leak:%N$p reads the Nth argument of printf's logical argument list, not the Nth stack slot; under SysV AMD64 the format occupies RDI, the first 5 integer/pointer variadic arguments come from RSI/RDX/RCX/R8/R9, and only from the 6th onward do they come from the stack; when there is no corresponding actual argument, what is read is leftover from the call site, so the offset must be probed (go back and forth several times to locate the flag / return address / libc address, see pitfall 4 on environment differences)
    • Arbitrary write:%N$n writes the number of bytes already output to the address pointed to by the Nth argument——payload layout = target address (first 8 bytes) + offset to that address + %<len>c%N$n write in segments (small first, then large, or use %hhn to write byte by byte and save on byte count)
    • Classic targets: change a GOT entry (when RELRO is partial) to the system / win function address; or change the return address to a one_gadget
    • pwntools assistance: fmtstr_payload(offset, {got_addr: win_addr}, write_size='byte')(offsets come from actual testing, not ABI constants that can be applied directly)
  5. Local verification (pwntools script):

    from pwn import *
    context.binary = './target'            # 自动取架构/位数/保护
    context.log_level = 'debug'
    p = process('./target')                # 本地(沙箱内);远程: remote('host', port)
    payload = b'A'*offset + p64(win_addr)
    p.sendline(payload)
    p.interactive()                        # 拿到 shell 后 cat flag
    
    • Verification discipline: run locally first → record sha256 of flag and payload → then connect to remote (pitfall 4); on failure, use gdb to locate first (set breakpoint at return point to see whether the return address hits)
    • Deliverables: exploit.py (commented version) + run output (flag) + flag sha256 — for writeup reference

Cross-domain combination

  • [[re-ctf]]: this skill is the pwn branch sub-skill of the CTF practice gateway — after the gateway's step 1 challenge-type identification recognizes a pwn challenge, it always dispatches this skill
  • [[re-binary-core]]: disassembly/decompilation to locate dangerous functions, backdoor functions, PLT/GOT ([[re-ghidra]] / [[re-ida]] / [[re-radare2]]); the import table approach of [[re-imports]] to locate puts/system
  • [[re-gdb]]: dynamic verification (break at return point / format string argument verification / get rip from core file), pwndbg's checksec commands
  • [[re-angr]]: symbolic execution assistance when overflow offset is uncertain / validation logic is complex (symbolize input to find constraints that overwrite the return address); [[re-z3]] used for computation verification of format string write addresses (minor use)
  • [[re-exploit]]: advanced continuation — complex ROP (SROP / ret2dlresolve), heap exploitation, seccomp bypass
  • [[re-vuln]]: pwn challenges are simplified vulnerabilities — challenge exploitation ideas map to real vulnerability discovery (crash analysis [[re-crash-triage]] / bug finding [[re-fuzzing]])
  • [[re-sandbox]] / [[re-analyze/platform-tips]]: all dynamic execution and exploit verification runs in a sandbox by default (highest principle); remote targets are for verification only, do not commit real systems

Common pitfalls and traps

  • All protections enabled (NX/PIE/Canary combination) requires combined exploitation: Symptom — a single ret2shellcode or ret2libc does not work: NX on makes the stack non-executable, PIE on changes the binary address every time, Canary has a random canary before the return address and aborts as soon as it is overwritten; Cause — combined protections are the norm in modern challenges, and one route cannot solve all protections; Countermeasure — combine according to checksec results: Canary → first leak the canary via format string then overflow (payload = pad + canary + pad + ret); PIE → first leak a binary address to calculate base (format string or puts leak), gadget/backdoor address = base + offset; NX+PIE+Canary all enabled = first two leaks (canary + libc/binary base) then ROP; the last 12 bits of the leaked value are the unchanged in-page offset, directly combine to calculate base (same as pitfall 2)
  • libc version mismatch (all offsets wrong): Symptom — local exploit works, but system//bin/sh offset calculated from the leaked address crashes immediately on remote, or the base calculated from the leaked address using local libc does not match at all; Cause — different libc versions have different symbol offsets (glibc 2.27 and 2.31 differ greatly in puts offset), and local /lib/x86_64-linux-gnu/libc.so.6 is inconsistent with the remote environment; Countermeasure — if the challenge provides a libc file, use it (strings -tx / readelf -s to get offsets); if not provided, use the leaked low 12 bits + libc-database (./find puts 7a0) or online libc.rip to match the version, then use the matched libc to calculate all offsets; before attacking remotely, first confirm the remote libc matches local (use format string to leak the __libc_start_main address for comparison)
  • Works locally but fails remotely (environment difference): Symptom — the exploit runs locally in a sandbox, but after connecting remotely the same payload fails (segfault/no output/timeout); Cause — differences between local and remote environments: libc version (pitfall 2), binary bit width/architecture, pwntools context configuration (wrong bit width/endianness makes p64 all wrong), stdin interaction timing (the program has printing delays/prompts), remote flag path is read-only; Countermeasures — first match context.binary , locally use the same libc version and system provided by the challenge (patchelf --set-interpreter + --replace-needed switch to the same local libc to get it working); remote scripts uniformly remote() + carry sleep/recvuntil timing handling; local validation add setvbuf/prompt difference simulation; ultimately use the remote libc file provided by the challenge as the standard
  • 64-bit argument passing (rdi): Symptom — in 64-bit program ROP, directly "placing arguments on the stack" (32-bit habit),system reports a segfault or calls a nonexistent address; Cause — in the x86-64 SysV ABI the first 6 arguments go in registers (rdi/rsi/rdx/rcx/r8/r9), not on the stack; ROP needs pop rdi; ret to pop the value on the stack into rdi and then ret to the target function; second cause —call after $rsp is not 16-byte aligned, movaps -type instructions crash directly (common in newer glibc versions); Countermeasures —ROPgadget --binary target | grep 'pop rdi' or ropper --search "pop rdi" to find gadgets (see [[re-exploit]]); gadget chain before each call segment = pop rdi; ret → arguments → target address;system before the stack address first +8(align ret) or use ret gadget to adjust; only 32-bit targets use pure stack argument passing
  • Format string offset determined incorrectly / write crashes: Symptom —%6$p the value read does not match the expected stack layout, or %n after writing the program crashes directly; Cause — wrong offset number (argument positions differ across compilations/platforms, and the input itself also occupies a position on the stack); write target address not writable (GOT is read-only under full RELRO, writing to an illegal address); wrong segmented write order (writing a small value from a large value causes an infinite loop); Countermeasures — first use %p to fully dump and align the stack layout (python3 -c "print('%p.'*30)"), record which argument position the input itself falls into (it is often the offset baseline); before writing pwn checksec confirm whether GOT is writable (only partial RELRO works);%hhn write byte by byte and sort from smallest to largest; at each step use gdb to verify that the target address content has indeed been changed