GOT/PLT Overwrite
When a binary uses functions from a shared library (like libc’s printf or system), it doesn’t know their addresses at compile time: they’re resolved at runtime via two tables, the GOT (Global Offset Table) and the PLT (Procedure Linkage Table). The GOT stores the real addresses of those functions, and by default it’s writable. If an attacker gets an arbitrary write (via format string, heap, overflow), they can overwrite a GOT entry so that, when the program calls a function, it jumps wherever the attacker wants —typically system.
How GOT/PLT works
Section titled “How GOT/PLT works”# the program doesn't call libc's printf directly; it calls printf@pltcall printf@plt# printf@plt jumps to the address stored in printf@got# the first time, the linker resolves printf and writes its real address to printf@got (lazy binding)# subsequent times, printf@got already has the address -> direct jumpThe GOT is, in practice, a table of function pointers the program consults on each call. Overwriting a pointer = hijacking that function.
The attack: overwrite an entry
Section titled “The attack: overwrite an entry”# if you have an arbitrary-write primitive (write-what-where):# write system@libc into printf@gotprintf@got = &system# from now on, each printf(x) actually runs system(x)# if you control the argument (e.g. printf(user) with user="/bin/sh") -> shellfrom pwn import *e = ELF('./vuln')got_printf = e.got['printf'] # address of printf's GOT entry# with a format string (see pwn-format):payload = fmtstr_payload(offset, {got_printf: addr_system})# or with a heap/overflow writeTypical targets
Section titled “Typical targets”# functions the program calls LATER and whose argument you control:printf@got -> system (if there's a later printf with controlled data)free@got -> system (if there's a later free(chunk) with "/bin/sh" inside) -> modern __free_hookputs@got, atoi@got, strlen@got ... depending on the program flow# also: overwrite an entry to jump to win()/one_gadgetThe trick is choosing a GOT entry of a function that (a) is called after your write and (b) receives an argument you control, so system(YOUR_ARG) gives you the shell.
Find the addresses
Section titled “Find the addresses”objdump -R ./vuln # relocations (GOT entries)readelf -r ./vuln# in pwntools: e.got['printf'], e.plt['system'], e.symbols[...]# system's address: from libc (with a leak if ASLR, see pwn-rop)RELRO: the key mitigation
Section titled “RELRO: the key mitigation”The GOT is writable by default, but RELRO changes that:
No RELRO fully writable GOT -> easy attackPartial RELRO (default today) part of the GOT is read-only, but the GOT of lazy-resolved functions (.got.plt) stays writable -> still attackableFull RELRO whole GOT read-only (eager resolution at start) -> GOT overwrite IMPOSSIBLE# check:checksec --file=./vuln # shows the RELRO levelWith Full RELRO, this attack doesn’t work and you must find another primitive (libc hooks, retaddr, FSOP).
For the defense
Section titled “For the defense”- Full RELRO (
-Wl,-z,relro,-z,now): makes the GOT read-only → closes the attack entirely. - Avoid the source arbitrary-write primitives (fix format strings, overflows, UAF).
- ASLR/PIE (force leaking addresses before writing), FORTIFY_SOURCE.
- In general: defenses against the vulnerabilities that give the write-what-where (Format String/heap/stack).
Testing checklist
Section titled “Testing checklist”- Do you have an arbitrary-write primitive? (format/heap/overflow)
- checksec: RELRO level (writable GOT?)
- Locate GOT entries (objdump -R / e.got)
- Choose a later-called function with a controlled argument
- Overwrite its GOT entry with system/one_gadget
- Trigger the call → shell
- If Full RELRO: find another primitive (hooks/retaddr/FSOP)
- Shell/flag locally → remote