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.
Cómo funciona GOT/PLT
Sección titulada «Cómo funciona GOT/PLT»# el programa no llama directamente a printf de libc; llama a printf@pltcall 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 directoLa 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.
El ataque: sobrescribir una entrada
Sección titulada «El ataque: sobrescribir una entrada»# si tienes una primitiva de escritura arbitraria (write-what-where):# escribir en system@libc la entrada de printf@gotprintf@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") -> shellfrom 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/overflowObjetivos típicos
Sección titulada «Objetivos típicos»# 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 modernoputs@got, atoi@got, strlen@got ... según el flujo del programa# también: sobrescribir una entrada para saltar a win()/one_gadgetEl 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.
Encontrar las direcciones
Sección titulada «Encontrar las direcciones»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)RELRO: la mitigación clave
Sección titulada «RELRO: la mitigación clave»La GOT es escribible por defecto, pero RELRO lo cambia:
No RELRO GOT totalmente escribible -> ataque fácilPartial 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 atacableFull RELRO toda la GOT de solo lectura (resolución eager al inicio) -> GOT overwrite IMPOSIBLE# comprobar:checksec --file=./vuln # muestra el nivel de RELROCon Full RELRO, este ataque no funciona y hay que buscar otra primitiva (hooks de libc, retaddr, FSOP).
Para la defensa
Sección titulada «Para la defensa»- 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).
Checklist de prueba
Sección titulada «Checklist de prueba»- ¿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