exploiting-arbitrary-write-to-execution
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.
适合你,如果需要在授权测试中将写漏洞转化为控制流劫持。
npx oh-my-skill add xalgord/xalgorix/exploiting-arbitrary-write-to-executioncurl -fsSL https://oh-my-skill.com/install.sh | bash -s -- xalgord/xalgorix/exploiting-arbitrary-write-to-executionnpx oh-my-skill verify xalgord/xalgorix/exploiting-arbitrary-write-to-execution怎么用
技能原文 SKILL.md
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 onatexithandlers. - 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-pltfails. Confirm RELRO withchecksecfirst 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_hookand__free_hookwere removed in glibc 2.34. On older targets they are a one-write win (point to aone_gadgetthen triggermalloc/free). On newer targets you must pivot to alternatives like FSOP (_IO_FILEvtable),__exit_funcs, or__printf_arginfo_table. - atexit pointers are mangled.
__exit_funcs(theatexit/__cxa_atexitlist) stores function pointers encrypted withPTR_MANGLE(rotate-left by a known amount and XOR withfs:[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_arrayonly fires at clean program exit;__malloc_hookneeds a subsequent allocation; a GOT entry needs the corresponding library call. If nothing calls your target, nothing happens. - Alignment and
movaps. Aone_gadget/libcsystemoften requires 16-byte stack alignment; the constraints printed byone_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
pwn checksec ./vuln # RELRO (Partial/Full), PIE, NX -> decides target viability
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)
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['<known>'].
Step 3: Perform the Arbitrary Write at the Chosen Target
# 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:
# 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
# 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.