agentleFS
Wang200935Wang200935/security-agent-skills/skills/exploit-dev/binary-exploitation/SKILL.md

binary-exploitation

Skill4 starsChanged 3 months agoMITView on GitHub

Solve CTF binary exploitation / pwn challenges involving stack, heap,

  • Installs packages

The file

Show its rules and text

We split this skill into the rules it gives an agent

34 rules. About 0 are common, 15 are distinctive, and 19 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈15

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 19

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill for practical CTF exploitation, not just static reversing.

    line 28
  2. Very specific:

    Only exploit CTF/lab/authorized binaries and endpoints.

    line 32
  3. Distinctive:

    Verify reliability: run multiple times locally, then remote with conservative retries.

    line 54
  4. Distinctive:

    No canary + NX off → shellcode / ret2shellcode.

    line 60
  5. Distinctive:

    No canary + NX on → ret2win, ROP, ret2libc.

    line 61
  6. Very specific:

    Write with %n/%hn/%hhn to GOT, return address, hooks, .fini_array, or app variable.

    line 68
  7. Distinctive:

    Look for UAF, double free, overflow into chunk metadata, off-by-null, largebin/unsorted leaks.

    line 73
  8. Distinctive:

    Check NX, badchars, seccomp, architecture, syscall ABI.

    line 90
  9. Distinctive:

    Use staged read if payload length is limited.

    line 91
  10. Very specific:

    Run seccomp-tools dump ./chall when available.

    line 95
  11. Very specific:

    If execve blocked but open/read/write allowed, leak flag via ORW ROP/shellcode.

    line 96
  12. Distinctive:

    If open blocked, check openat, sendfile, writev, mmap/mprotect availability.

    line 97
  13. Distinctive:

    When a vulnerability's control-flow guard appears unreachable through static analysis alone, binary patching provides runtime confirmation …

    line 110
  14. Very specific:

    Pitfall: After bypassing a guard, the unchecked code path may have hidden assumptions (e.g., a truncated 32-bit pointer from mov r12d, r14d …

    line 139
  15. Distinctive:

    When the challenge provides a Dockerfile but no explicit libc, extract the exact libc from the Docker base image's packages:

    line 143
  16. Very specific:

    When combined with Full RELRO on the binary, GOT overwrite is blocked.

    line 180
  17. Very specific:

    When you need the exact offset between leaked stack addresses (like lol) and function frames (like main_rbp):

    line 187
  18. Very specific:

    Place win/system address at a known stack location

    line 236
  19. Distinctive:

    If you can solve the split-value problem (perhaps through creative use of existing stack data), the attack is:

    line 241
  20. Very specific:

    Wait for choice> ; if a menu appears, send 1 to start an instance.

    line 255
  21. Very specific:

    Use menu 3 for status and 2 to stop/restart only when needed.

    line 257
  22. Distinctive:

    If the primitive only appears after replacing a conditional jump with NOPs, record it as a dead/guarded path and pivot to finding a real …

    line 269
  23. Very specific:

    Keep local exploit payloads and remote dependencies separate in notes: record the exact payload/offset/address, then explicitly mark …

    line 309
  24. Very specific:

    stdout_ptr probe: If test_addr == stdout_ptr (PIE+0x4010), stdout is now pointing to 0x41, which is unmapped.

    line 328
  25. Very specific:

    After *target = value, only printf runs before _exit(0).

    line 348
  26. Very specific:

    When overwriting stdout to point to a fake FILE at address F, the vtable at F + 0xd8 must point to a location V such that *(V + 0x38) (for …

    line 355
  27. Very specific:

    Arbitrary write: Use far call (via mmap'd shellcode at fixed address 0x1337000).

    line 418
  28. Distinctive:

    Use zig as a zero-setup cross-compiler:

    line 436
  29. Distinctive:

    Use COOP to build arbitrary read/write from existing virtual functions

    line 470
  30. Very specific:

    When GLIBC_TUNABLES=glibc.malloc.tcache_count=0 disables tcache, all freed chunks go to fastbins.

    line 476
  31. Very specific:

    Use stack overlap with menu choice byte 'e' (0x65) as fake chunk size 0x60 (validated by argc=1 as PREV_INUSE)

    line 484
  32. Very specific:

    When GLIBC_TUNABLES=glibc.malloc.tcache_count=0 disables tcache, ALL freed chunks go to fastbins.

    line 523
  33. Very specific:

    Use MSB of stack return address as chunk's allocated_size (IS_MMAPED bit = 50% probability)

    line 551
  34. Distinctive:

    Create calc → Delete calc → Create int_arr (overlap)

    line 584

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

CTF Pwn / Binary Exploitation

Purpose

Use this skill for practical CTF exploitation, not just static reversing. The objective is to turn a vulnerability into a reliable local and remote exploit while documenting offsets, protections, leaks, and environment assumptions.

Safety Boundary

Only exploit CTF/lab/authorized binaries and endpoints. Rate-limit remote attempts. Do not aim these techniques at real services without explicit permission.

First Workflow

  1. Preserve artifacts: binary, libc, ld, Dockerfile, source, remote host/port, challenge description.
  2. Identify target: file, arch, stripped/static, libc version, interpreter.
  3. Check protections: NX, PIE, RELRO, stack canary, FORTIFY, seccomp, ASLR assumptions.
  4. Run safely locally: reproduce prompt, input length, crash, intended interaction.
  5. Find bug family: stack overflow, format string, heap UAF/double-free/overflow, integer bug, OOB, command injection inside binary, race, sandbox/seccomp.
  6. Determine exploit primitive:
    • control RIP/EIP?
    • arbitrary read/write?
    • info leak?
    • stack pivot?
    • heap overlap / tcache poisoning?
  7. Build exploit incrementally with pwntools:
    • local process first
    • cyclic offset
    • leak phase
    • calculate base addresses
    • final payload
    • remote mode switch
  8. Verify reliability: run multiple times locally, then remote with conservative retries.
  9. Write up: protections → bug → primitive → leak → control-flow/data-only exploit → flag.

Bug Family Decision Tree

Stack overflow

  • No canary + NX off → shellcode / ret2shellcode.
  • No canary + NX on → ret2win, ROP, ret2libc.
  • Canary present → leak canary, off-by-one, format string, partial overwrite, brute force only when intended.
  • PIE on → leak code pointer or use partial overwrite if low bytes stable.

Format string

  • Leak stack/libc/canary with %p, %s, positional args.
  • CRITICAL — heap-based buffer pitfall (x86-64): When the format string buffer is allocated on the HEAP (via malloc/scanf("%s")) rather than on the stack, you CANNOT embed target addresses in the format string and access them via %N$ positions. The format string DATA is on the heap, while printf's varargs read from the CALLER'S stack frame. Embedded addresses in a heap-based format string are invisible to %N$p. Detection: register with AAAA%N$p for increasing N and check if 0x4141414141414141 ever appears in the output. If the marker never appears at any N, the buffer is on the heap. Workarounds: (a) Use existing stack pointers as targets (%37$n writes to the address already stored at position 37), (b) Partial overwrite of return address low bytes since the return address IS on the stack at a known position, (c) If a local variable on the stack already points to controlled heap data, use it as a springboard.
  • Write with %n/%hn/%hhn to GOT, return address, hooks, .fini_array, or app variable.
  • Full RELRO blocks GOT overwrite; prefer return address, hooks if libc permits, or data-only target.

Heap exploitation

  • Identify allocator and version: glibc tcache/fastbin/unsorted/bin hardening changes matter.
  • Look for UAF, double free, overflow into chunk metadata, off-by-null, largebin/unsorted leaks.
  • Parser off-by-N: Custom protocol parsers that grow heap buffers via realloc-doubling are prone to off-by-N when boundary/terminator detection copies prefix bytes without re-checking remaining capacity after the bounds test. Common in network daemons with multipart/boundary-based framing. Critical operand-order pitfall: never infer a guard from the mnemonic alone. In Intel syntax, cmp r13, r15; jbe skip skips when r13 <= r15 (capacity <= index), so the copy fires when capacity > index — e.g. a \r delimiter arrives before the buffer is full. This is the opposite of cmp r15, r13; jbe skip. Verify with runtime probes on the original binary before treating a bug as patch-only. Another pitfall: the copy loop may use sub rcx, r15 pre-adjustment so that [rcx+r15] resolves back to the original buffer pointer regardless of data_index. This can mask whether the copy source is the heap buffer or a stack scratch buffer — trace both the destination register (often rbp restored from [rsp+0x30]) and the copy loop's addressing mode to determine the actual target. Copy loop first-byte pitfall: optimized loops may initialize edx to a constant (e.g. mov edx, 0xd for CR, or xor edx, edx) then jmp over the first movzx; the first overflow byte is that initialized value, not src[0]. Always check the copy loop's entry jump target and recalculate byte-to-offset mapping. See references/network-service-binary-analysis.md and references/ooonvifd-ais3-2024.md.
  • Critical: preserve chunk SIZE on overflow. When overflowing into adjacent tcache chunks, you MUST preserve the chunk's SIZE field at the chunk header (offset +0x00 from the chunk start, or +0x08 offset in some views depending on whether you count from the data or the metadata). A corrupted size crashes the binary during subsequent free() with "realloc(): invalid next size" or "free(): invalid size". The SIZE field includes the PREV_INUSE bit in the low bit — ensure this is set (OR with 0x1) for in-use adjacent chunks. In tcache, common sizes are 0x21 (0x20 data), 0x31 (0x30 data), 0x41 (0x40 data), 0x51 (0x50 data), etc. All include 0x1 for PREV_INUSE.
  • Critical: null-byte truncation in payload addresses. When a write primitive routes through strlen() before malloc()/memcpy() (common in XML/SOAP attribute-value allocators), libc/heap addresses containing null bytes get truncated, causing the allocation to land in the wrong tcache bin. The system() address on x86-64 almost always contains null bytes. Workarounds: use one-gadgets without nulls in key bytes, do partial overwrites in multiple stages, or check if the binary uses position-based length (distance between XML quotes) instead of strlen. See references/null-bytes-in-heap-payloads.md.
  • Convert to leak + arbitrary write, then target __free_hook/system on older glibc, FILE structures, exit handlers, stack, or app function pointers depending on version.

ROP and ret2libc

  • Need leak for ASLR: puts/GOT, libc address, stack pointer, PIE base.
  • Find gadgets with ROPgadget, ropper, pwntools ROP.
  • x86_64 SysV requires stack alignment; add ret before system if needed.
  • For static binaries, use syscall chains or ret2csu.
  • 6-byte partial overwrite: When addresses contain null bytes in upper 2 positions (bytes 6-7 = 0x00 for all PIE/libc/stack addresses on x86-64), use 6-byte overwrites instead of 8-byte. The upper bytes are already 0x00 in both original and target values. This also avoids the premature-null-byte problem when payload bytes themselves are 0x00. Always check the low byte too (junk_rbp = (pie + 0x4000) | 0xff to avoid byte-0 null).
  • cdq; ret for rdx=0: At libc+0xb5173 (glibc 2.41), 99 c3 = cdq; ret. When eax=0 (set by return 0 in exit path), cdq sign-extends to edx=0. Use this with execve() when you need rdx=0 but can't find pop rdx.
  • execve over system: system("/bin/sh") may fail silently on some glibc builds. Prefer execve("/bin/sh", NULL, NULL) with cdq for rdx and pop rsi; pop r15; ret for rsi.
  • strncat inter-block null pitfall: When filling a buffer via multiple strncat calls, each call adds a null terminator BETWEEN blocks. Subsequent writes find the FIRST null (at the earliest inter-block gap), not the end of the buffer. This redirects overwrites to the wrong position. To reach the target offset (e.g., saved rbp), you must either: (a) advance byte-by-byte through all inter-block nulls, or (b) use a single contiguous fill. Do NOT add extra full-hash transactions thinking they "advance" the null — they OVERFLOW past the buffer, corrupting the target area with random hash data. See references/strncat-hash-mining-overflow.md.

Shellcode

  • Check NX, badchars, seccomp, architecture, syscall ABI.
  • Use staged read if payload length is limited.
  • For seccomp, inspect allowed syscalls and use ORW: open/read/write.

Seccomp / sandbox

  • Run seccomp-tools dump ./chall when available.
  • If execve blocked but open/read/write allowed, leak flag via ORW ROP/shellcode.
  • If open blocked, check openat, sendfile, writev, mmap/mprotect availability.

Default Tools

python3 -m pip install --user pwntools ropper capstone unicorn z3-solver
~/homebrew/bin/brew install gdb radare2 rizin patchelf one_gadget

On macOS, Linux pwn challenges usually need Docker or a Linux VM/container for faithful glibc behavior. Prefer the provided Dockerfile if present.

Binary Patching for Verification and Guard Bypass

When a vulnerability's control-flow guard appears unreachable through static analysis alone, binary patching provides runtime confirmation without GDB complexity:

int3 Insertion — "Is this code path ever reached?"

Insert a single int3 (0xCC) byte at the target code path entry. If the process dies with SIGTRAP after sending the trigger input, the path IS reachable.

# Insert int3 at overflow copy entry (runtime offset 0x2990)
data = bytearray(open('binary', 'rb').read())
data[0x2990] = 0xCC  # replaces first byte of target instruction
open('binary', 'wb').write(data)

NOP Guard Bypass — "What happens if this check never fires?"

Replace a conditional jump with NOPs to force the fall-through path:

# jbe → 6-byte NOP sled (66 90 × 3)
data[0x2996:0x2996+6] = b'\x66\x90\x66\x90\x66\x90'

Common guard bypass targets:

  • jbe (0F 86) — jump if below or equal (unsigned). NOP to always take overflow path.
  • je (74 / 0F 84) — jump if equal. NOP to skip bounds checks.
  • jne (75 / 0F 85) — jump if not equal. NOP to bypass validation checks.

PIE-aware file offset: Runtime offsets from objdump map to file offsets via: file_offset = runtime_offset_in_section - section_vaddr + section_file_offset. For typical PIE binaries where .text vaddr and file offset both start at 0x1000, they cancel: file_offset = runtime_offset. Always verify raw bytes match the expected instruction before patching.

Pitfall: After bypassing a guard, the unchecked code path may have hidden assumptions (e.g., a truncated 32-bit pointer from mov r12d, r14d that was never meant to run). Trace the full code path beyond the guard. See references/ooonvifd-ais3-2024.md for a complete worked example.

Extracting and Verifying Remote libc

When the challenge provides a Dockerfile but no explicit libc, extract the exact libc from the Docker base image's packages:

# Find the package version from the Dockerfile's Ubuntu version
# Download the correct deb (check for security updates: .1, .2 suffixes)
curl -sL -o libc6.deb "http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6_<version>_amd64.deb"
mkdir libc_extract && cd libc_extract
ar x ../libc6.deb && tar xf data.tar.zst
nm -D usr/lib/x86_64-linux-gnu/libc.so.6 | grep -w puts
# Verify the puts offset ends in the same 3 hex digits as the remote leak
# For glibc 2.41-6ubuntu1.2: puts = 0x8db60

Common puts offsets by version:

  • glibc 2.41-6ubuntu1.2: 0x8db60
  • glibc 2.41-6ubuntu1: 0x8e320 (check remote: leak last 3 digits must match)

The libc_base must be page-aligned (end in 000). Verify: (puts_leak - puts_offset) % 0x1000 == 0.

Full RELRO + Possible Sealed libc Pattern

Modern glibc (2.38+) on Ubuntu MAY apply "sealing" — mprotect on ALL libc pages (including .data and .bss) after initialization finishes but before main runs. This means:

  • setvbuf() in constructors works (runs before sealing)
  • Any write to libc during main fails with SIGSEGV
  • The .data section LOAD segment may show RW flags but is read-only at runtime

However, sealing is NOT universal. On glibc 2.41-6ubuntu1.2 (Ubuntu 25.04) in a Docker + xinetd environment, .data and .bss were confirmed WRITABLE via live probing. Always test before assuming — use the probe pattern:

io.sendline(f'0x{libc_base + 0x210000:x} 0x41'.encode())
# If "write" appears in response, .data is writable

When combined with Full RELRO on the binary, GOT overwrite is blocked. But if libc is NOT sealed, FILE structure fields and legacy hooks in .bss become viable targets — constrained only by execution flow (what the program calls between the write and termination).

Stack Layout Discovery via /proc/pid/mem (Docker Local Debug)

When you need the exact offset between leaked stack addresses (like lol) and function frames (like main_rbp):

# Inside Docker with the binary running
pid = p.pid
mem = open(f'/proc/{pid}/mem', 'rb')
mem.seek(init_rbp - 0x300)
data = mem.read(0x300)
mem.close()

# Scan for main's frame: look for return address to __libc_start_call_main
# __libc_start_call_main is typically around libc+0x2a500
# main's return address = libc+0x2a578 (the `call exit` after main returns)
for i in range(0, len(data), 8):
    val = u64(data[i:i+8])
    if 0x2a500 <= (val - libc_base) <= 0x2a600:
        # Found main's return address at addr
        main_rbp = addr - 8
        break

This is essential for challenges where you need to construct fake FILE structures on the stack and need precise frame offsets.

_exit(0) Implementation (glibc 2.34+)

On Linux, _exit is a THIN syscall wrapper. It does NOT call __run_exit_handlers, __call_tls_dtors, or any atexit/fini handlers:

_exit:
    endbr64
    mov <tls_errno_ptr>, %rsi
    mov $0xe7, %edx          # SYS_exit_group = 231
    jmp _exit+0x19
_exit+0x19:
    mov %edx, %eax
    syscall                  # exit_group(0) — process terminates
    ...                      # error path (hlt) — never reached on success

Key implications for exploitation:

  • Stack return address overwrite is USELESS if followed by _exit(0) — the return address is never consumed
  • .fini_array overwrite is USELESS — _exit skips fini functions
  • __exit_funcs corruption is USELESS — _exit doesn't call __run_exit_handlers

The One-Write-Exit Challenge Pattern

A recurring CTF pattern: one arbitrary write + _exit(0) + Full RELRO + sealed libc. The three leaks (PIE, libc, stack) suggest the author intends a stack-based solution, but the single write creates a "chicken-and-egg" problem:

You need to BOTH:

  1. Place win/system address at a known stack location
  2. Redirect stdout (via BSS pointer overwrite) to a fake FILE whose vtable chain reaches that location

But *target = value makes stdout = value AND the stack value variable = the same value. You cannot have value = win_addr (for the vtable chain) AND value = fake_stack_addr (for stdout redirect) simultaneously.

If you can solve the split-value problem (perhaps through creative use of existing stack data), the attack is:

  1. scanf: target = stdout_ptr (PIE+0x4010), value = fake_stack_addr
  2. printf uses fake FILE → vtable at fake+0xd8 → __xsputn (offset 0x38) or __overflow (offset 0x18 for unbuffered) → shell

Docker Desktop on macOS: The binary is at /Applications/Docker.app/Contents/Resources/bin/docker (not on default PATH). Start the daemon with open -a Docker. Build x86-64 images with --platform linux/amd64. Under Rosetta 2 emulation, ASLR is disabled — /proc/1/maps shows stable addresses useful for exploit development. See references/docker-macos-x86-pwn.md for full workflow.

CTFd Auth + Hashcash Instance Wrapper Pattern

Some CTF pwn endpoints are not the vulnerable service directly. They first run a CTFd token gate, then a hashcash proof-of-work, then an instance-management menu. Do not send the exploit payload immediately after PoW until you verify whether the wrapper prints a menu.

Typical flow observed in ONVIF-style service challenges:

  1. Connect to the advertised host/port and wait for ctfd token> .
  2. Send the CTFd token.
  3. Parse quick usage: python3 pow_solver.py <bits> '<resource>' or equivalent, solve hashcash, send stamp.
  4. Wait for choice> ; if a menu appears, send 1 to start an instance.
  5. Parse Connect to: nc <host> <port> and send HTTP/protocol payloads to that instance port, not to the wrapper port.
  6. Use menu 3 for status and 2 to stop/restart only when needed; avoid brute-force/restart loops on shared infrastructure.

Pitfall: after PoW, if the first response is a menu like 1. Start instance, a raw HTTP request will be interpreted as an invalid menu choice and the connection may reset. Build small wrapper scripts that separate start instance from attack instance.

Exploit Verification Discipline: Patched vs Real Binary

Binary patching is excellent for reachability and hypothesis testing, but never promote a patch-only behavior into the exploit primitive unless the same control flow is reachable in the unpatched binary.

Required checks after an int3/NOP experiment:

  1. Restore the original binary bytes and rerun the exact trigger.
  2. Trace or log the relevant registers at the guarded block in the original binary.
  3. Confirm the guard condition permits the dangerous path under attacker-controlled input.
  4. If the primitive only appears after replacing a conditional jump with NOPs, record it as a dead/guarded path and pivot to finding a real path that violates the guard (stale state, integer overflow, allocator failure, alternate entry, etc.).

For multipart/realloc parsers, a common false lead is: NOP a jbe guarding an overflow-copy loop, observe heap metadata corruption, then assume the remote is exploitable. The real task is to prove index > capacity (not merely index == capacity) in the unmodified binary.

Practical Checklist

  • Save checksec output.
  • Save crash proof and cyclic offset.
  • Record exact libc/ld and remote/local differences.
  • Avoid relying on GDB-only addresses.
  • Use context.binary = ELF(...) and args.REMOTE in pwntools.
  • Add assertions for leaks and base calculations.
  • Test local exploit 5+ times before remote.
  • Keep exploit idempotent and readable.

Reference Files

  • references/pwn-playbook.md — practical pwn taxonomy and exploitation decision tree.
  • references/sealed-libc-one-write-exit.md — sealed libc + one write + _exit(0) challenge pattern and solution approaches.
  • references/heap-playbook.md — glibc heap/tcache challenge workflow.
  • references/rop-format-string-cheatsheet.md — ROP, ret2libc, ret2csu, format-string patterns.
  • references/network-service-binary-analysis.md — reversing network daemons, context struct mapping, MIME boundary parsing off-by-N, CTFd auth wrapper pattern.
  • references/full-relro-exit-arbitrary-write.md — Full RELRO + _exit(0) + single arbitrary write: probing writability, self-referencing vtable trick, glibc 2.41 offsets, attack surface analysis.
  • references/docker-macos-x86-pwn.md — Docker Desktop on Apple Silicon for x86-64 pwn: finding the binary, starting the daemon, building/running amd64 containers, memory layout stability under Rosetta 2, installing debug tools, reading /proc/1/mem.
  • references/null-bytes-in-heap-payloads.md — Null-byte truncation pitfall when heap exploit payloads route through strlen() before allocation (common in XML/SOAP attribute-value based allocators). Workarounds: one-gadgets, partial overwrite, position-based allocators.
  • references/strncat-hash-mining-overflow.md — Exploitation of strncat-based buffer overflows where the appended data is a SHA256 hash of attacker-controlled input. Covers: null-stopping behavior of strncat for byte-by-byte controlled overwrite, information leak via counter*32 out-of-bounds read, the critical inter-block null byte pitfall, and full exploit sequence for a 512-byte ledger buffer with no canary.
  • references/ooonvifd-ais3-2024.md — Full binary reverse engineering of the ooonvifd challenge (AIS3 2024, 500pt): SOAP/MIME multipart parser analysis, context struct mapping, boundary mismatch overflow guard analysis, jbe vs jb pitfall, heap layout, exploit attempt log, and unresolved next steps.
  • references/ctfd-hashcash-instance-wrapper.md — CTFd token + hashcash PoW + instance menu wrapper flow, with a reusable Python skeleton and pitfalls.
  • references/qemu-tcg-kernel-exploit.md — QEMU TCG iret/far call privilege bug: arbitrary physical read/write from inside guest VM. Complete C exploit with GDT leak, kernel text leak, modprobe_path overwrite, modprobe trigger. KPTI-on vs off handling.
  • references/kernel-vmlinux-extraction.md — Extracting vmlinux from bzImage: finding gzip header, decompressing, using strings + ELF header parsing to find kernel symbol offsets (modprobe_path, etc.).

Scripts

  • scripts/pwn_triage.py — first-pass binary checks, strings/import hints, protection helper integration.
  • scripts/pwntools_exploit_skeleton.py — reusable local/remote exploit template.
  • scripts/malloc_trace.c — LD_PRELOAD malloc/calloc/realloc/free tracer for validating heap layout, chunk-header corruption, and allocation order when GDB attach is unreliable.

Practical lessons: picoCTF-style ret2win

  • For simple 64-bit non-PIE ret2win binaries with source like char buf[32]; read(0, buf, 0x80);, check mitigations first, then compute the saved-RIP offset as buffer_size + saved_rbp when no canary is present. In the Echo Escape 1 pattern, the offset was 40 bytes (32 + 8) and the target was a fixed win() address (0x401256), so the payload was b'A'*40 + p64(win). Verify locally before remote; the final flag may still require a live nc endpoint even when local flag.txt/dummy output proves control-flow hijack.
  • Keep local exploit payloads and remote dependencies separate in notes: record the exact payload/offset/address, then explicitly mark missing runtime dependency (HOST/PORT, remote flag file, libc/ld) instead of inventing a remote flag.

Full RELRO + _exit(0) + Single Arbitrary Write

A common modern CTF pattern: one arbitrary write (*target = value) with three leaks (PIE, libc, stack) but _exit(0) prevents stack return-addr hijack and Full RELRO blocks GOT overwrite. The _exit on glibc ≥ 2.34 calls the exit_group syscall directly — no exit handlers, no __run_exit_handlers, no TLS destructors. Nothing to hook.

Memory writability probing

Probe which regions are writable by writing 0x41 to candidate addresses and checking for the "write XX -> ADDR" printf output. Empty output = crash = address is read-only.

# Probe pattern
io.sendline(f'0x{test_addr:x} 0x41'.encode())
resp = io.recvall(timeout=2)
writable = b'write' in resp  # True = writable, False = read-only / crash

Two critical pitfalls:

  1. stdout_ptr probe: If test_addr == stdout_ptr (PIE+0x4010), stdout is now pointing to 0x41, which is unmapped. Even a successful write produces NO output because printf can't write to the fake FILE. You get empty response — indistinguishable from a crash. NEVER use stdout_ptr as a probe target.
  2. Internal _IO_2_1_stdout_ field probe: Writing to fields WITHIN the FILE structure (e.g., _flags, _IO_write_base, vtable) CORRUPTS the very FILE printf is writing through. The write succeeds (no SIGSEGV) but printf may fail silently — giving a FALSE NEGATIVE. Probe .bss hooks (__free_hook) or non-stdout .data addresses first to establish the baseline writability of the page, THEN test individual fields knowing the page itself is writable.

Common writable targets (tested on modern Ubuntu):

  • Binary BSS: PIE_base + 0x4010 (stdout ptr), +0x4020 (stdin ptr), +0x4028 (completed.0)
  • Binary .got entries inside RELRO range: CRASH (read-only)
  • libc .data / .bss: CRASH on some modern systems (entire libc may be read-only at runtime)
  • Stack addresses near leaked lol: WRITABLE

Attack surface analysis

After *target = value, only printf runs before _exit(0). Attack options:

  1. Overwrite stdout FILE pointer (BSS, writable) → redirect to fake FILE whose vtable calls win
  2. Overwrite vtable pointer in _IO_2_1_stdout_ (libc .data, may be RO on modern systems)
  3. Construct fake FILE on stack → overwrite stdout to point there

Self-referencing vtable trick

When overwriting stdout to point to a fake FILE at address F, the vtable at F + 0xd8 must point to a location V such that *(V + 0x38) (for __xsputn) or *(V + 0x18) (for __overflow) contains win's address.

The self-reference: set V such that V + 0x38 = F + 0xd8 (the vtable slot itself). Then *(V + 0x38) = *(F + 0xd8) = V — the function called is V itself. Works only if V is a valid code address.

For F on stack: choose V = addr_of_value_variable. Then F = V - 0x38 = (main_rbp - 0x10) - 0x38 = main_rbp - 0x48. The value variable contains what scanf stored for %zx. Need value = win_addr AND F = main_rbp - 0x48 simultaneously — which requires knowing main_rbp (offset from lol).

Glibc 2.41 on Ubuntu 25.04 (Plucky)

Critical offsets for libc6 2.41-6ubuntu1.2:

  • puts: 0x8db60
  • system: 0x5c110
  • _IO_2_1_stdout_: 0x2115c0 (vtable pointer at +0xd8 = 0x211698)
  • _IO_2_1_stdin_: 0x2108e0
  • _IO_file_jumps: 0x20efd0 (__xsputn at +0x38 = 0x20f008)
  • _exit: just exit_group syscall (0xe7) with no handler calls
  • RELRO in libc: 0x20cb58–0x210000; .data (0x210000–0x2116c8) rw-p from ELF but may be r--p at runtime (sealing)
  • .bss: 0x2116e0–0x21ee78

__free_hook and friends — STILL PRESENT in glibc 2.41

Despite glibc 2.34 removing the hooks from the API, Ubuntu's build of glibc 2.41-6ubuntu1.2 still exports these symbols as weak (V) in BSS. They may be writable depending on the kernel's page-mapping behaviour:

SymbolOffsetType
__memalign_hook0x2171d0V (weak)
__realloc_hook0x2171d8V
__malloc_hook0x2171e0V
__free_hook0x2171e8V
__after_morecore_hook0x2171f8V
__malloc_initialize_hook0x217200B

These are in .bss (0x2116e0–0x21ee78). Writability varies: in Docker with ld-linux direct execution, __free_hook was writable; in the challenge's Docker environment, it was also writable (confirmed via successful write + printf output). However, on some systems the BSS pages may be mapped read-only (sealing) — always probe.

Critical constraint: these hooks only fire when their corresponding function is called. In a one-write-_exit(0) challenge, neither malloc() nor free() is called during the normal execution path after the write (printf with simple formats doesn't allocate, _exit directly calls exit_group). The hooks being writable is useful only if you can ALSO trigger the corresponding allocation function.

Identifying the exact remote libc

Compute libc_base = puts_leak - puts_offset. Base must be page-aligned (end in 000). If not, wrong offset. Scan offsets ending in same 3 hex digits as the leak.

Probe writability of critical addresses BEFORE assuming they work:

# Test if an address is writable on the remote
io.sendline(f'0x{test_addr:x} 0x41'.encode())
resp = io.recvall(timeout=2)
writable = b'write' in resp  # True ONLY if write + printf both succeed
# WARNING: if test_addr = stdout_ptr, printf output is lost even if write succeeded!
# Always probe non-stdout addresses first.

Finding stack layout

Without local execution, the offset between init_program's rbp (from lol = init_rbp - 0xc) and main's rbp is unknown. It depends on __libc_start_main and __libc_csu_init stack usage. Typical gap: 0x80–0x200 bytes. Brute-force by trying different gaps and overwriting stdout to point to main_rbp - F where F aligns the vtable with a controlled stack location.

See references/full-relro-exit-arbitrary-write.md for the complete technique reference and probing scripts.

Kernel Exploitation via QEMU TCG Bug

A virtualization-layer vulnerability class: QEMU's TCG (Tiny Code Generator) incorrectly handles privilege levels during iret/iretq and far call instructions. When executed from ring 3, QEMU accesses the stack using ring 0 page tables — but without switching CR3. This allows an unprivileged guest process to read/write physical kernel memory.

Applicability: Any CTF kernel pwn challenge running on QEMU < 9.1 with TCG (no KVM). The challenge hint file pointing to kqx.io/post/qemu-nday/ is a strong indicator.

Primitives:

  • Arbitrary read: Construct an iretq frame pointing RSP at the target physical address. QEMU pops RIP/CS/RFLAGS/RSP/SS from that address into registers. When KPTI is on, this triggers a page fault whose error code leaks data (3 bytes per call). Loop with fork() per byte.
  • Arbitrary write: Use far call (via mmap'd shellcode at fixed address 0x1337000). Set RSP to target physical address before the call. The far call pushes return address onto that location — writing 8 bytes (partially controllable, especially the low byte via code positioning). Write one byte per call.

KPTI implications:

  • KPTI-off: arb_read and arb_write both work directly on kernel physical addresses.
  • KPTI-on: arb_read still works (leaks via page-fault error code). arb_write fails because user CR3 doesn't map kernel pages. Fall back to fw_cfg DMA technique (uses SIGFPE handler to push DMA descriptor onto shared kernel stack, then I/O port writes to BIOS_CFG_DMA_ADDR trigger physical DMA).

Common kernel targets:

  • modprobe_path: overwrite with path to controlled script (e.g., /tmp/x\0). Trigger via execve() of a file with unknown magic bytes. The kernel runs /tmp/x as root.
  • core_pattern: redirect core dumps to pipe through attacker-controlled binary.

modprobe_path offset for Linux 6.1.81: virtual address 0xffffffff83877760, physical offset 0x3877760 from kernel image base. Extract this per-kernel via the vmlinux extraction method (see references/kernel-vmlinux-extraction.md).

Exploit skeleton: see references/qemu-tcg-kernel-exploit.md for a complete C exploit with GDT leak, kernel text leak from shared stack (ist3 at GDT+0x1000), modprobe_path overwrite, and modprobe trigger.

KPTI check: the run.sh -cpu kvm64 flag enables KPTI on modern QEMU (kvm64 includes PCID). Only pti=off or nopti on kernel cmdline disables it. Without those flags, assume KPTI is ON and plan for it.

Cross-Compilation for Linux Exploits from macOS

Use zig as a zero-setup cross-compiler:

~/homebrew/bin/brew install zig
~/homebrew/bin/zig cc -target x86_64-linux-musl -static -O2 -o exploit exploit.c

Produces fully static Linux binaries that run on any kernel. No glibc/libc dependencies.

AT&T syntax required: zig's clang backend rejects Intel-syntax inline assembly (.intel_syntax noprefix). Rewrite all inline asm in AT&T syntax (movq %0, %%rsp instead of mov rsp, %0).

2025-2026 Advanced Exploitation Techniques (HITCON CTF 2025 / DEF CON 33 / snakeCTF 2025)

AArch64 PAC + BTI + Relative Vtables Bypass (HITCON CTF 2025 calc)

PAC (Pointer Authentication Code): ARM64 security feature that signs pointers (return addresses, function pointers) with cryptographic codes. paciasp signs return address on function entry; autiasp verifies on return. Forging signed pointers requires knowing the secret key.

BTI (Branch Target Identification): ARM64's version of CET. Indirect branches (BR/BLR) must land on bti c or paciasp landing pads. IMPORTANT: RET instructions are NOT affected by BTI — return instructions can target any location. BTI doesn't prevent ROP; PAC does.

Relative Vtables (Clang/LLVM): vtable entries are 4-byte signed offsets relative to the vtable base, not raw 8-byte pointers. Virtual call pattern:

LDRSW X9, [X8, #0xC]    ; X8 = vtable, load 4-byte offset
ADD  X8, X8, X9         ; X8 = vtable_base + offset = real function addr
BLR  X8                 ; indirect call to resolved address

This makes forging vtables infeasible — you can't just write a fake function pointer. The offset is 32-bit, so target must be within ±2GB of vtable.

COOP (Counterfeit Object-Oriented Programming): Bypass for BTI/CFI. Instead of ROP, forge C++ objects with fake vtables and chain existing virtual functions to build arbitrary read/write primitives:

  1. Find virtual functions that act as useful primitives (e.g., return this->a->b = arbitrary read)
  2. Forge objects with vtable pointing to existing functions
  3. Chain multiple virtual calls to achieve arbitrary code execution
  4. With ~200 virtual functions in typical C++ binary, scan with IDA scripts for useful gadgets

Exploitation path (HITCON calc):

  1. UAF → overlap int_arr and calc object on heap
  2. Forge calc vtable via int_arr values
  3. Use COOP to build arbitrary read/write from existing virtual functions
  4. Overwrite __sF (FILE structures) in libc .bss with fake structure containing system() address + /bin/sh pointer
  5. Trigger system("sh") during program exit via fflush(NULL)

glibc 2.42 mp_ Overwrite: Re-enabling Tcache (snakeCTF 2025 old-school)

When GLIBC_TUNABLES=glibc.malloc.tcache_count=0 disables tcache, all freed chunks go to fastbins. To re-enable tcache:

  1. Fastbin dup (double-free) → control fastbin freelist
  2. malloc_consolidate to get libc pointers into chunks (libc leak)
  3. IS_MMAPED bit bypass: Set IS_MMAPED (0x2) in chunk header to skip malloc_usable_size's in-use check — musable() returns (size & ~0x7) - 16 directly without validating next chunk
  4. Fake fastbin chain from __fpu_control: Build fake fastbin headers through writable libc symbols starting at __fpu_control (conveniently after a chunk header with size 0x22 + IS_MMAPED)
  5. Overwrite mp_.tcache_bins: Chain fake headers until reaching mp_ (malloc_par global struct) and set tcache_bins/tcache_count to non-zero values
  6. With tcache re-enabled, allocate near __libc_argv to leak stack
  7. Use stack overlap with menu choice byte 'e' (0x65) as fake chunk size 0x60 (validated by argc=1 as PREV_INUSE)
  8. Overwrite allocated chunks array pointer → control future allocations → overwrite return address

Key mp_ offsets (glibc 2.41):

  • mp_ in .data section: libc_base + offset_to_mp_
  • mp_.tcache_bins: controls max bin count (default 64)
  • mp_.tcache_count: controls max chunks per bin (default 7)
  • mp_.tcache_max_bytes: controls max chunk size in tcache

Modern Protection Bypass Summary (2025-2026)

ProtectionArchitectureBypass Technique
PAC (Pointer Auth)AArch64COOP with existing signed virtual functions; or find autiasp-free code paths
BTI (Branch Target ID)AArch64RET not affected; COOP uses valid bti c landing pads via virtual calls
Relative VtablesAArch64/x86_64Can't forge vtable → use COOP with existing vtable entries; offset-based resolution
CET/IBTx86_64Similar to BTI; COOP or data-only attacks
Shadow Stackx86_64Cannot overwrite return address → use data-only attacks or exception handler hijack
MTE (Memory Tagging)AArch64Tag mismatch on free → spray matching tags or find tag-aware corruption
Safe-Linkingx86_64 glibc 2.32+XOR target address with (addr >> 12) — leak heap address first to decode
Sealed libcx86_64 glibc 2.38+Probe writability before assuming — .data/.bss may be mprotect'd RO

DEF CON 33: AI-Assisted Pwn (MCP-Based Reversing)

At DEF CON 33, team used IDA MCP + LLM (GPT-5) to solve a CTF Finals challenge in 12 minutes:

  1. LLM reads IDA decompilation via MCP tool calls
  2. LLM automatically renames functions, identifies protocol, finds flag exfil path
  3. LLM writes exploit script based on decompilation
  4. LLM runs script, analyzes output, updates decompilation with findings
  5. Loop: gather knowledge → formulate hypothesis → create exploit → analyze output → update IDA

For CTF solvers: Install IDA MCP or GhidraMCP, use LLM to accelerate binary analysis. The LLM excels at:

  • Protocol reverse engineering from decompilation
  • Function renaming from context clues
  • Exploit script generation from vulnerability patterns
  • Output analysis and iterative refinement

glibc 2.42 mp_ Overwrite: Re-enabling Tcache (snakeCTF 2025 old-school)

When GLIBC_TUNABLES=glibc.malloc.tcache_count=0 disables tcache, ALL freed chunks go to fastbins. Full exploit chain on glibc 2.42 (latest at time of writing):

Step 1 — Fastbin dup (double-free):

  • alloc(0x68) # A, alloc(0x68) # B
  • resize(A, 0) (frees A but doesn't update allocated fields → dangling pointer)
  • free(B), free(A) (double-free A)
  • Fastbin freelist: A → B → A (circular)

Step 2 — Libc leak via malloc_consolidate:

  • Standard unsorted bin approach blocked (all chunks stay in fastbins, size ≤ 0x68)
  • Control top chunk size → force malloc_consolidate by requesting allocation larger than top chunk
  • Consolidated fastbin chunks pass through unsorted bin → contain libc pointers → leak

Step 3 — IS_MMAPED bit bypass for size validation:

  • Writing to freed chunks requires malloc_usable_size() validation
  • musable() checks the in-use bit in the NEXT chunk's header (needs careful alignment)
  • Bypass: set IS_MMAPED bit (0x2) in chunk header → musable() skips in-use check, returns (size & ~0x7) - 16 directly

Step 4 — Fake fastbin chain from __fpu_control → mp_ overwrite:

  • __fpu_control in libc is conveniently after a chunk header with size 0x22 + IS_MMAPED
  • Build fake fastbin headers through writable libc symbols until reaching mp_ struct
  • Overwrite mp_.tcache_bins and mp_.tcache_count to non-zero → tcache re-enabled

Step 5 — Stack leak + control flow hijack:

  • With tcache enabled, allocate near __libc_argv → leak stack address
  • Allocate chunks overlapping stack: use menu choice byte 'e' (0x65) as fake chunk size 0x60
  • argc=1 on stack acts as PREV_INUSE bit → chunk validates with usable size 0x58
  • Overwrite allocated chunks array pointer → control future allocations
  • Use MSB of stack return address as chunk's allocated_size (IS_MMAPED bit = 50% probability)
  • Overwrite chunk_edit function's return address → code execution

Reference: https://snakectf.org/writeups/2025-quals/pwn/old-school

HITCON CTF 2025 calc — AArch64 PAC + BTI + Relative Vtables Bypass (COOP)

Full COOP exploitation with relative vtables (not just theory — actual CTF solve, 5 solves):

BTI correction: RET instructions are NOT affected by BTI. BTI only affects indirect branches (BR/BLR). ROP still works with BTI — PAC is the real threat to ROP.

Relative vtables (Clang/LLVM): vtable entries are 4-byte signed offsets, not 8-byte pointers. Virtual call pattern:

LDRSW X9, [X8, #0xC]    ; X8 = vtable, load 4-byte offset
ADD  X8, X8, X9         ; X8 = vtable_base + offset = real function
BLR  X8                 ; indirect call

Forging vtables in writable memory (heap) is no longer viable — the 4-byte offset is relative to vtable base, and heap is non-executable.

Constrained COOP approach:

  1. We still control the vtable POINTER in the object
  2. We can only point to legitimate vtables in the binary's read-only sections
  3. Enumerate ALL relative vtables in .rodata and .data.rel.ro via IDA Python script
  4. Scan ~200 virtual functions for useful primitives (arbitrary read, arbitrary write)
  5. Build exploit primitives by chaining existing virtual functions

IDA Python enumeration script:

  • Scan .rodata and .data.rel.ro for vtable patterns: entry[0]=0, entry[1]=type_info offset, entry[2+]=function offsets
  • Validate: vtable + [vtable + index] points to a valid function entry
  • Filter: skip functions calling realloc() (too large, not useful)
  • Output: vtable_addr → [(entry_index, function_addr), ...]

Final exploitation (UAF → overlap int_arr and calc object → forge vtable):

  1. Create calc → Delete calc → Create int_arr (overlap)
  2. Control int_0–int_5 → forge entire calc object including vtable pointer
  3. Point vtable to legitimate vtable in binary's .rodata
  4. Use COOP to build arbitrary read/write from existing virtual functions
  5. Overwrite __sF (FILE structures) in libc .bss with fake structure containing system() + "sh"
  6. Trigger system("sh") during fflush(NULL) on program exit
  7. Constraint: single arbitrary write = 4 bytes only → fake __sF must be close to original (same high 32 bits)

Key lesson: COOP works even with relative vtables — the constraint is you can only use EXISTING vtables from .rodata, not forge new ones. ~200 virtual functions in a typical binary provide enough gadgets.

Reference: https://bruce30262.github.io/hitcon-ctf-2025-calc/

Maintenance Rule

Patch this skill whenever a pwn challenge teaches a new exploitation primitive, libc-version quirk, gadget pattern, heap technique, seccomp bypass, kernel exploitation technique, or remote reliability fix.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.