binary-exploitation
Skill4 starsChanged 3 months agoMITView on GitHub
Solve CTF binary exploitation / pwn challenges involving stack, heap,
- Installs packages
In this repository80 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .cursor/rules/security-agent-skills.mdcCursor rule
- ai-mcp-securitySkill
- modern-attack-surfacesSkill
- offensive-toolkitSkill
- security-and-hardeningSkill
- security-auditSkill
- security-code-reviewSkill
- ctf-jail-escapeSkill
- ctf-kernel-exploitationSkill
- ctf-misc-toolkitSkill
- ctf-orchestratorSkill
- ctf-playbookSkill
- ctf-technique-atlasSkill
- ctf-training-loopSkill
- ctf-web-exploitationSkill
- ctf-writeup-disciplineSkill
- ctf-writeup-screenshotsSkill
- crypto-attack-patternsSkill
- crypto-ctf-attacksSkill
- cryptography-fundamentalsSkill
- crypto-toolkitSkill
- encoding-realignmentSkill
- exploit-developmentSkill
- exploit-poc-builderSkill
- kernel-exploitationSkill
- zero-day-huntingSkill
- bluetooth-jammer-sweepSkill
- esp32-serial-diagSkill
- flipper-zero-backupSkill
- flipper-zero-firmwareSkill
- hardware-iot-hackingSkill
- nrf24-bitbang-spiSkill
- rf-jammer-firmware-portSkill
- rf-multi-protocol-jammerSkill
- smart-card-driver-debugSkill
- smart-card-usb-directSkill
- wifi-deauth-jammerSkill
- wifi-dualband-jammerSkill
- advanced-pentestSkill
- network-pentestSkill
- pentest-quick-checklistSkill
- pentest-tool-referenceSkill
- pentest-tool-setupSkill
- pentest-workflowSkill
- advanced-attack-chainsSkill
- autonomous-pentest-scannerSkill
- intranet-pentest-advancedSkill
- pentest-report-generatorSkill
- post-exploitation-opsSkill
- professional-pentest-guideSkill
- chatgpt-web-relaySkill
- darkweb-researchSkill
- email-osint-investigationSkill
- local-network-reconSkill
- network-device-reconSkill
- osint-frameworkSkill
- osint-recon-modelSkill
- parallel-intel-gatheringSkill
- reconnaissance-opsSkill
- The first 60 shown.
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
- Distinctive: line 28
Use this skill for practical CTF exploitation, not just static reversing.
- Very specific: line 32
Only exploit CTF/lab/authorized binaries and endpoints.
- Distinctive: line 54
Verify reliability: run multiple times locally, then remote with conservative retries.
- Distinctive: line 60
No canary + NX off → shellcode / ret2shellcode.
- Distinctive: line 61
No canary + NX on → ret2win, ROP, ret2libc.
- Very specific: line 68
Write with %n/%hn/%hhn to GOT, return address, hooks, .fini_array, or app variable.
- Distinctive: line 73
Look for UAF, double free, overflow into chunk metadata, off-by-null, largebin/unsorted leaks.
- Distinctive: line 90
Check NX, badchars, seccomp, architecture, syscall ABI.
- Distinctive: line 91
Use staged read if payload length is limited.
- Very specific: line 95
Run seccomp-tools dump ./chall when available.
- Very specific: line 96
If execve blocked but open/read/write allowed, leak flag via ORW ROP/shellcode.
- Distinctive: line 97
If open blocked, check openat, sendfile, writev, mmap/mprotect availability.
- Distinctive: line 110
When a vulnerability's control-flow guard appears unreachable through static analysis alone, binary patching provides runtime confirmation …
- Very specific: line 139
Pitfall: After bypassing a guard, the unchecked code path may have hidden assumptions (e.g., a truncated 32-bit pointer from mov r12d, r14d …
- Distinctive: line 143
When the challenge provides a Dockerfile but no explicit libc, extract the exact libc from the Docker base image's packages:
- Very specific: line 180
When combined with Full RELRO on the binary, GOT overwrite is blocked.
- Very specific: line 187
When you need the exact offset between leaked stack addresses (like lol) and function frames (like main_rbp):
- Very specific: line 236
Place win/system address at a known stack location
- Distinctive: line 241
If you can solve the split-value problem (perhaps through creative use of existing stack data), the attack is:
- Very specific: line 255
Wait for choice> ; if a menu appears, send 1 to start an instance.
- Very specific: line 257
Use menu 3 for status and 2 to stop/restart only when needed.
- Distinctive: line 269
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 …
- Very specific: line 309
Keep local exploit payloads and remote dependencies separate in notes: record the exact payload/offset/address, then explicitly mark …
- Very specific: line 328
stdout_ptr probe: If test_addr == stdout_ptr (PIE+0x4010), stdout is now pointing to 0x41, which is unmapped.
- Very specific: line 348
After *target = value, only printf runs before _exit(0).
- Very specific: line 355
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 …
- Very specific: line 418
Arbitrary write: Use far call (via mmap'd shellcode at fixed address 0x1337000).
- Distinctive: line 436
Use zig as a zero-setup cross-compiler:
- Distinctive: line 470
Use COOP to build arbitrary read/write from existing virtual functions
- Very specific: line 476
When GLIBC_TUNABLES=glibc.malloc.tcache_count=0 disables tcache, all freed chunks go to fastbins.
- Very specific: line 484
Use stack overlap with menu choice byte 'e' (0x65) as fake chunk size 0x60 (validated by argc=1 as PREV_INUSE)
- Very specific: line 523
When GLIBC_TUNABLES=glibc.malloc.tcache_count=0 disables tcache, ALL freed chunks go to fastbins.
- Very specific: line 551
Use MSB of stack return address as chunk's allocated_size (IS_MMAPED bit = 50% probability)
- Distinctive: line 584
Create calc → Delete calc → Create int_arr (overlap)
≈ “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.
Files
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
- Preserve artifacts: binary, libc, ld, Dockerfile, source, remote host/port, challenge description.
- Identify target:
file, arch, stripped/static, libc version, interpreter. - Check protections: NX, PIE, RELRO, stack canary, FORTIFY, seccomp, ASLR assumptions.
- Run safely locally: reproduce prompt, input length, crash, intended interaction.
- Find bug family: stack overflow, format string, heap UAF/double-free/overflow, integer bug, OOB, command injection inside binary, race, sandbox/seccomp.
- Determine exploit primitive:
- control RIP/EIP?
- arbitrary read/write?
- info leak?
- stack pivot?
- heap overlap / tcache poisoning?
- Build exploit incrementally with pwntools:
- local process first
- cyclic offset
- leak phase
- calculate base addresses
- final payload
- remote mode switch
- Verify reliability: run multiple times locally, then remote with conservative retries.
- 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, whileprintf's varargs read from the CALLER'S stack frame. Embedded addresses in a heap-based format string are invisible to%N$p. Detection: register withAAAA%N$pfor increasing N and check if0x4141414141414141ever 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$nwrites 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/%hhnto 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 skipskips whenr13 <= r15(capacity <= index), so the copy fires whencapacity > index— e.g. a\rdelimiter arrives before the buffer is full. This is the opposite ofcmp 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 usesub rcx, r15pre-adjustment so that[rcx+r15]resolves back to the original buffer pointer regardless ofdata_index. This can mask whether the copy source is the heap buffer or a stack scratch buffer — trace both the destination register (oftenrbprestored from[rsp+0x30]) and the copy loop's addressing mode to determine the actual target. Copy loop first-byte pitfall: optimized loops may initializeedxto a constant (e.g.mov edx, 0xdfor CR, orxor edx, edx) thenjmpover the firstmovzx; the first overflow byte is that initialized value, notsrc[0]. Always check the copy loop's entry jump target and recalculate byte-to-offset mapping. Seereferences/network-service-binary-analysis.mdandreferences/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()beforemalloc()/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. Thesystem()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. Seereferences/null-bytes-in-heap-payloads.md. - Convert to leak + arbitrary write, then target
__free_hook/systemon 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, pwntoolsROP. - x86_64 SysV requires stack alignment; add
retbeforesystemif 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) | 0xffto avoid byte-0 null). - cdq; ret for rdx=0: At
libc+0xb5173(glibc 2.41),99 c3=cdq; ret. When eax=0 (set byreturn 0in exit path),cdqsign-extends to edx=0. Use this withexecve()when you need rdx=0 but can't findpop rdx. - execve over system:
system("/bin/sh")may fail silently on some glibc builds. Preferexecve("/bin/sh", NULL, NULL)withcdqfor rdx andpop rsi; pop r15; retfor rsi. - strncat inter-block null pitfall: When filling a buffer via multiple
strncatcalls, 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. Seereferences/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 ./challwhen 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_gadgetOn 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 = 0x8db60Common 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
mainfails with SIGSEGV - The
.datasection 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 writableWhen 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
breakThis 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 successKey implications for exploitation:
- Stack return address overwrite is USELESS if followed by
_exit(0)— the return address is never consumed .fini_arrayoverwrite is USELESS —_exitskips fini functions__exit_funcscorruption is USELESS —_exitdoesn'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:
- Place
win/systemaddress at a known stack location - 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:
scanf: target =stdout_ptr(PIE+0x4010), value =fake_stack_addrprintfuses fake FILE → vtable atfake+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:
- Connect to the advertised host/port and wait for
ctfd token>. - Send the CTFd token.
- Parse
quick usage: python3 pow_solver.py <bits> '<resource>'or equivalent, solve hashcash, send stamp. - Wait for
choice>; if a menu appears, send1to start an instance. - Parse
Connect to: nc <host> <port>and send HTTP/protocol payloads to that instance port, not to the wrapper port. - Use menu
3for status and2to 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:
- Restore the original binary bytes and rerun the exact trigger.
- Trace or log the relevant registers at the guarded block in the original binary.
- Confirm the guard condition permits the dangerous path under attacker-controlled input.
- 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
checksecoutput. - Save crash proof and cyclic offset.
- Record exact libc/ld and remote/local differences.
- Avoid relying on GDB-only addresses.
- Use
context.binary = ELF(...)andargs.REMOTEin 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 throughstrlen()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 ofstrncat-based buffer overflows where the appended data is a SHA256 hash of attacker-controlled input. Covers: null-stopping behavior ofstrncatfor byte-by-byte controlled overwrite, information leak viacounter*32out-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,jbevsjbpitfall, 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 asbuffer_size + saved_rbpwhen no canary is present. In the Echo Escape 1 pattern, the offset was 40 bytes (32 + 8) and the target was a fixedwin()address (0x401256), so the payload wasb'A'*40 + p64(win). Verify locally before remote; the final flag may still require a livencendpoint even when localflag.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 / crashTwo critical pitfalls:
- 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. - 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
.gotentries 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:
- Overwrite
stdoutFILE pointer (BSS, writable) → redirect to fake FILE whose vtable callswin - Overwrite vtable pointer in
_IO_2_1_stdout_(libc.data, may be RO on modern systems) - 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: 0x8db60system: 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: justexit_groupsyscall (0xe7) with no handler calls- RELRO in libc: 0x20cb58–0x210000;
.data(0x210000–0x2116c8)rw-pfrom ELF but may ber--pat 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:
| Symbol | Offset | Type |
__memalign_hook | 0x2171d0 | V (weak) |
__realloc_hook | 0x2171d8 | V |
__malloc_hook | 0x2171e0 | V |
__free_hook | 0x2171e8 | V |
__after_morecore_hook | 0x2171f8 | V |
__malloc_initialize_hook | 0x217200 | B |
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
iretqframe 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. Thefar callpushes 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_cfgDMA 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 viaexecve()of a file with unknown magic bytes. The kernel runs/tmp/xas 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.cProduces 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 addressThis 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:
- Find virtual functions that act as useful primitives (e.g.,
return this->a->b= arbitrary read) - Forge objects with vtable pointing to existing functions
- Chain multiple virtual calls to achieve arbitrary code execution
- With ~200 virtual functions in typical C++ binary, scan with IDA scripts for useful gadgets
Exploitation path (HITCON calc):
- UAF → overlap
int_arrandcalcobject on heap - Forge
calcvtable viaint_arrvalues - Use COOP to build arbitrary read/write from existing virtual functions
- Overwrite
__sF(FILE structures) in libc.bsswith fake structure containingsystem()address +/bin/shpointer - Trigger
system("sh")during program exit viafflush(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:
- Fastbin dup (double-free) → control fastbin freelist
- malloc_consolidate to get libc pointers into chunks (libc leak)
- IS_MMAPED bit bypass: Set
IS_MMAPED(0x2) in chunk header to skipmalloc_usable_size's in-use check —musable()returns(size & ~0x7) - 16directly without validating next chunk - Fake fastbin chain from
__fpu_control: Build fake fastbin headers through writable libc symbols starting at__fpu_control(conveniently after a chunk header with size0x22+IS_MMAPED) - Overwrite
mp_.tcache_bins: Chain fake headers until reachingmp_(malloc_par global struct) and settcache_bins/tcache_countto non-zero values - With tcache re-enabled, allocate near
__libc_argvto leak stack - Use stack overlap with menu choice byte
'e'(0x65) as fake chunk size 0x60 (validated byargc=1as PREV_INUSE) - Overwrite allocated chunks array pointer → control future allocations → overwrite return address
Key mp_ offsets (glibc 2.41):
mp_in.datasection: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)
| Protection | Architecture | Bypass Technique |
| PAC (Pointer Auth) | AArch64 | COOP with existing signed virtual functions; or find autiasp-free code paths |
| BTI (Branch Target ID) | AArch64 | RET not affected; COOP uses valid bti c landing pads via virtual calls |
| Relative Vtables | AArch64/x86_64 | Can't forge vtable → use COOP with existing vtable entries; offset-based resolution |
| CET/IBT | x86_64 | Similar to BTI; COOP or data-only attacks |
| Shadow Stack | x86_64 | Cannot overwrite return address → use data-only attacks or exception handler hijack |
| MTE (Memory Tagging) | AArch64 | Tag mismatch on free → spray matching tags or find tag-aware corruption |
| Safe-Linking | x86_64 glibc 2.32+ | XOR target address with (addr >> 12) — leak heap address first to decode |
| Sealed libc | x86_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:
- LLM reads IDA decompilation via MCP tool calls
- LLM automatically renames functions, identifies protocol, finds flag exfil path
- LLM writes exploit script based on decompilation
- LLM runs script, analyzes output, updates decompilation with findings
- 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) # Bresize(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_consolidateby 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_MMAPEDbit (0x2) in chunk header →musable()skips in-use check, returns(size & ~0x7) - 16directly
Step 4 — Fake fastbin chain from __fpu_control → mp_ overwrite:
__fpu_controlin libc is conveniently after a chunk header with size0x22+ IS_MMAPED- Build fake fastbin headers through writable libc symbols until reaching
mp_struct - Overwrite
mp_.tcache_binsandmp_.tcache_countto 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=1on 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_editfunction'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 callForging 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:
- We still control the vtable POINTER in the object
- We can only point to legitimate vtables in the binary's read-only sections
- Enumerate ALL relative vtables in
.rodataand.data.rel.rovia IDA Python script - Scan ~200 virtual functions for useful primitives (arbitrary read, arbitrary write)
- Build exploit primitives by chaining existing virtual functions
IDA Python enumeration script:
- Scan
.rodataand.data.rel.rofor 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):
- Create calc → Delete calc → Create int_arr (overlap)
- Control
int_0–int_5→ forge entirecalcobject including vtable pointer - Point vtable to legitimate vtable in binary's
.rodata - Use COOP to build arbitrary read/write from existing virtual functions
- Overwrite
__sF(FILE structures) in libc.bsswith fake structure containingsystem()+"sh" - Trigger
system("sh")duringfflush(NULL)on program exit - Constraint: single arbitrary write = 4 bytes only → fake
__sFmust 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.
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
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.

