Panorama#
El capítulo anterior, 3.6 · Stack overflows y shellcode, armó el desbordamiento clásico con las protecciones desactivadas a propósito, para aislar el mecanismo. Este capítulo las vuelve a encender y muestra qué obliga cada una. La idea que ordena todo es la misma que gobierna el exploit-dev moderno: una mitigación no cierra la explotación, la mueve —encarece la técnica y fuerza una nueva—, y el trabajo consiste en encadenar esos rodeos hasta recuperar el control del flujo.
El recorrido tiene una lógica de escalones. Primero, una clase de bug distinta al overflow: el format string, que regala una primitiva de lectura y escritura arbitraria más limpia —y, de paso, la fuga de direcciones que después derrota ASLR—. Segundo, el catálogo de mitigaciones que hoy trae cualquier compilador y sistema operativo, con la cobertura de cada una. Tercero, los bypass: return-to-libc (ret2libc) como primera respuesta a la pila no ejecutable, el Return-Oriented Programming (ROP) como su generalización, y la fuga de direcciones que desmonta ASLR. El capítulo cierra con lo que separa una prueba de concepto de un exploit real: que no tire el servicio y que sobreviva a la inspección.
Dos deslindes con los capítulos vecinos. La profundidad de la explotación de memoria en Linux moderno —los
internals de la GOT/PLT, la reparación del stack canary, el ROP sin returns, la explotación de 64 bits— se
trata en 3.15 · Exploit-dev Linux avanzado, que extiende este capítulo. Los
mecanismos específicos de Windows —el manejo de excepciones SEH, jmp esp con Mona, el patch diffing— van
en 3.8 · Exploit-dev en Windows. Aquí se sientan los conceptos base comunes a ambos.
flowchart LR M1["Pila no ejecutable\n(NX / DEP)"] --> T1["ret2libc → ROP\nreutilizar código ya mapeado"] M2["ASLR / PIE\n(direcciones aleatorias)"] --> T2["Fuga de direcciones\n(format string / memory leak)"] M3["Canario\n(centinela en la pila)"] --> T3["Leak o reparación\n(ver 3.15)"] T2 --> R["Exploit fiable moderno:\ninfo-leak (vs ASLR)\n+ ROP a VirtualProtect (vs DEP)\n+ shellcode"] T1 --> R
El bug de format string: dato usado como formato#
Un format string bug (CWE-134) nace de un error sutil: pasar dato controlado por el usuario directamente
como cadena de formato a printf(), fprintf(), sprintf() y compañía. La forma correcta es
printf("%s", entrada); la vulnerable es printf(entrada). La diferencia es decisiva porque printf es, en el
fondo, un intérprete: recorre la pila tomando un valor por cada especificador % que encuentra, y no tiene
forma de saber cuántos argumentos se le pasaron en realidad. Quien controla la cadena de formato controla, por
lo tanto, cuántos valores lee printf de la pila y qué hace con ellos.
De ahí salen tres primitivas de poder creciente:
- Mapear la pila (lectura de contexto). Repetir
%08xvuelca la pila en hexadecimal, un dword por token, hasta que aparecen los propios bytes de la entrada (0x41414141si se prefijó conAAAA). El número de tokens que hicieron falta es el offset —en el ejemplo del libro, 7— y ubica dónde cae la entrada dentro de la vista deprintf. - Leer memoria arbitraria. Con el offset conocido, se planta una dirección al inicio de la cadena y se usa
%s, que trata ese valor de la pila como puntero e imprime la cadena que hay ahí. El direct parameter access (%7$s) salta directo al parámetro 7 sin arrastrar los intermedios, acortando el payload. Esta lectura es lo que fuga una dirección real y, con ella, derrota ASLR. - Escribir en memoria arbitraria. El especificador
%nguarda en la dirección apuntada cuántos bytes se imprimieron hasta ese punto; controlando el ancho de impresión (con relleno) se controla el valor escrito. Escribir una dirección de 32 bits de un solo golpe exigiría imprimir gigabytes, así que la fórmula mágica (Blaess/Grenier/Raynal) parte la dirección en dos mitades —los dos bytes altos (HOB) y los dos bajos (LOB)— y usa dos escrituras%hnde 2 bytes cada una, ordenadas según cuál mitad sea mayor.
El blanco de la escritura es cualquier puntero de control: el EIP guardado de un frame (que se localiza en
GDB con info frame, sección «Saved registers»), una entrada de la GOT, un function pointer, o un handler
registrado con atexit(). Sobrescribirlo con la dirección del shellcode desvía la ejecución.
# 1) mapear la pila hasta ver 0x41414141 → offset = 7
./fmtstr AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x
# 2) leer una dirección arbitraria (little-endian) con direct parameter access
./fmtstr $(printf "\x84\xfd\xff\xbf")%7\$s # imprime la cadena en 0xbffffd84
# 3) escribir con %hn (fórmula mágica HOB/LOB): dos escrituras de 2 bytes
# blanco típico: el EIP guardado del frame (gdb: info frame → Saved registers)
A diferencia del overflow, el format string es fácil de detectar en el propio código o binario —un
printf con un solo argumento variable es sospechoso—, y por eso decae donde hay análisis estático en el
build (gcc -Wformat -Wformat-security). Sigue apareciendo porque muchas organizaciones publican sin
analizar.
El catálogo de mitigaciones y qué protege cada una#
Antes de rodear las defensas conviene tenerlas ordenadas, porque ninguna cubre todo y el exploit se apoya
justo en el hueco. Se enumeran con la herramienta que las revela —checksec, o objdump/readelf a mano— que
es la misma que el atacante usa para saber qué falta.
| Mitigación | Qué hace | Qué protege | Cómo se rodea |
|---|---|---|---|
| Stack canary (StackGuard, SSP/ProPolice) | Valor centinela entre el buffer y los datos de control; __stack_chk_fail aborta si se daña | Solo la pila | Leak del canario o reparación (→ 3.15) |
| Libsafe | Reemplaza strcpy/gets/… por versiones con bounds-check | Solo la pila | Bugs fuera de esas funciones |
NX / DEP (GNU_STACK, bit NX/XD, /NXCOMPAT) | Marca pila y heap como no ejecutables | Ejecución en pila/heap | ret2libc / ROP (no ejecuta en la pila) |
ASLR / PIE (randomize_va_space, /DYNAMICBASE) | Aleatoriza imágenes, librerías, heap y pila | Direcciones fijas | Fuga de direcciones (info-leak) |
RELRO (-z relro,-z now) | Resuelve la GOT al inicio y la marca de solo lectura | GOT overwrite, ret2plt | Otros punteros escribibles |
El detalle importa. El canary solo interpone un centinela en la pila: no protege el heap, y el SSP además
reordena las variables locales para que un buffer no pise a otras. La pila no ejecutable se apoya en el bit
NX/XD del hardware (AMD lo introdujo en 2004, Intel como XD) y se ve en las flags del segmento ELF
GNU_STACK —RW significa no ejecutable, RWE ejecutable— (gcc -z execstack la vuelve a hacer ejecutable).
ASLR aleatoriza las bases; en Windows se activa por módulo con /DYNAMICBASE, y basta una DLL compilada sin
ese flag para abrir un boquete de dirección estática. La regla general que resume la tabla: ASLR y NX cubren
pila y heap; los canarios y Libsafe, solo la pila.
ret2libc: el primer bypass de la pila no ejecutable#
Si la pila no es ejecutable, saltar a shellcode propio lanza una excepción. Pero el código legítimo ya
mapeado —la libc, que gcc enlaza en todo binario— sí es ejecutable, y a él se puede retornar. El
return-to-libc sobrescribe el EIP guardado con la dirección de system() de glibc y prepara la pila para que
lo que se ejecute sea system("/bin/sh"). El layout es tres valores consecutivos:
[ dirección de system() ][ filler = dirección de exit() ][ puntero a "/bin/sh" ]
Al retornar la función vulnerable, la CPU salta a system(), que encuentra su argumento "/bin/sh" en la pila
y abre una shell con el EUID del proceso —root, si el binario es SUID—. El filler se pone en exit() para que,
cuando system() retorne, el proceso termine limpio en vez de caer en un segfault ruidoso. La cadena "/bin/sh"
suele tomarse de la propia glibc (está embebida), lo que evita depender de variables de entorno.
system() —el bash de algunas distribuciones lo hace— o
hace falta null-terminar un argumento, se encadenan llamadas a libc: por ejemplo printf (usando %n
para escribir un byte nulo en un slot) seguido de execl (que no dropea privilegios). Ese encadenamiento de
llamadas —componer una computación a partir de funciones existentes— es el germen conceptual del ROP.ROP: generalizar ret2libc en micro-secuencias#
Cuando ret2libc no alcanza —porque hace falta preparar registros, o porque las mitigaciones de Windows
(DEP/CFG) complican retornar a funciones completas—, se generaliza. El Return-Oriented Programming, formalizado
por Hovav Shacham y demostrado Turing-completo, reemplaza el retorno a una función por el encadenamiento de
gadgets: secuencias cortas de instrucciones que ya existen en los módulos cargados y terminan en ret.
Se colocan en la pila punteros consecutivos a cada gadget; como cada ret salta a la dirección que la pila tiene
en el tope, la ejecución encadena un gadget tras otro. Es ret2libc llevado al límite: en lugar de reusar una
función, se compone la operación deseada a partir de fragmentos mínimos.
Dos artesanías hacen viable la técnica. La primera: como el conjunto de instrucciones x86 es de longitud
variable, un gadget puede empezar en medio de una instrucción —saltar un byte más adelante reinterpreta los
mismos bytes como otra instrucción que igual termina en ret—, lo que multiplica el repertorio disponible. La
segunda: si un gadget útil arrastra una instrucción parásita (un pop de más), se compensa insertando padding
en la pila para que ese pop consuma basura en vez del puntero al siguiente gadget; los gadgets con parásitos
intolerables se descartan.
El objetivo no suele ser ejecutar el payload entero por ROP —es tedioso— sino solo volver ejecutable la
memoria donde ya está el shellcode, y saltar a él. En Windows la cadena arma los argumentos y llama a
VirtualProtect() para re-marcar la región como PAGE_EXECUTE_READWRITE (0x40); en Linux el equivalente
es mprotect(). Mona automatiza la búsqueda: !mona rop -m <dll> -cp nonull genera las cadenas candidatas y
la lista de gadgets. Rara vez funcionan tal cual: hay que buscar gadgets de compensación y, a veces, un stack
pivot que reubique ESP sobre la cadena. La mecánica profunda de gadgets en Linux (ROP sin returns, JOP,
64 bits) se detalla en 3.15; la variante Windows con SEH, en
3.8.
Derrotar ASLR: la fuga de direcciones#
ROP resuelve la pila no ejecutable, pero sus gadgets viven en direcciones que ASLR aleatoriza en cada ejecución.
Hay dos maneras de lidiar con eso. La fácil no derrota ASLR, solo lo esquiva: reutilizar un módulo compilado
sin /DYNAMICBASE (o un binario no-PIE en Linux), cuyas direcciones no cambian —un parche mientras existan
módulos mal compilados—. La real es filtrar una dirección en tiempo de ejecución.
El patrón es aritmético. Cualquier objeto de posición conocida dentro de un módulo —una vftable, una función— cuya dirección se logre leakear permite recuperar la base aleatorizada del módulo:
base_del_módulo = dirección_filtrada − RVA_offset
donde el RVA offset (el desplazamiento del objeto respecto de la base del módulo) es una constante conocida.
Con la base recuperada, todos los gadgets se recalculan al vuelo (RVA + base) y la cadena ROP funciona aunque
todos los módulos estén rebasados. En Linux la fuga suele venir de un format string (%s sobre una dirección
de la GOT imprime la dirección real de una función de libc); en Windows, de un memory leak propiamente dicho.
El caso canónico es un use-after-free en Internet Explorer 11 (CVE-2017-0059, hallado por Ivan Fratric de
Project Zero): la propiedad textarea.defaultValue filtra un puntero a una vftable de
urlmon!CSecurityManager; restándole el RVA 0x4504 se obtiene la base 0x77250000 de urlmon.dll, y sobre
esa base se arma la ROP.
Fiabilidad y evasión del exploit#
Un exploit que funciona en el laboratorio pero tira el servicio o deja rastro obvio no sirve en una operación real. Dos capas lo endurecen.
No delatarse por pérdida de servicio. La señal de intrusión más inmediata no es el log sino que el daemon
explotado deje de responder: en producción se nota apenas alguien accede al sitio. La evasión consiste en
volver a armar el proceso: reparar el daño colateral del overflow (registros como EBP que quedaron
corrompidos se recalculan desde ESP, que sigue intacto, con lea ebp,[esp+0x68]) y retornar al accept loop
del main, de modo que el servicio siga sirviendo como si nada. Como la shell es interactiva y bloquearía el
servicio, se resuelve con fork(): el proceso hijo entrega la root shell al atacante y el padre restaura
el daemon. El resultado es una shell persistente sobre un servicio que responde con normalidad.
Sobrevivir a la inspección. Aquí la restricción de 3.6 —«sin un solo byte nulo»—
escala. Un IDS de red que busca la cadena /bin/sh se evade codificándola (sumar 5 a cada byte y decodificar
en la pila en tiempo de ejecución con un loop de restas); la firma del NOP sled (0x90 en bloque) se evade
reemplazándolo por instrucciones de un byte que además son ASCII imprimible (inc/dec de registros ya en
cero: los bytes @CABHKIJ). Y cuando el programa valida que la entrada sea solo imprimible, se recurre al
shellcode polimórfico ASCII imprimible: como el repertorio de instrucciones imprimibles es minúsculo, ese
shellcode no hace el trabajo directamente sino que es un loader —construido solo con bytes 0x33–0x7e—
que arma el shellcode real en la pila y salta a él. Los trucos son ingeniosos: poner EAX a cero con dos
AND de valores imprimibles que son inversos bit a bit (ya que xor no es imprimible), y simular la suma
—inexistente entre las instrucciones imprimibles— con el wraparound de 32 bits de varias restas. Es la génesis
del shellcode polimórfico; el tratamiento completo de la evasión de AV/EDR moderna está en
3.11 · Evasión de AV/EDR.
Defensa y detección#
La defensa contra toda esta familia es acumular mitigaciones para que ningún hueco alcance por sí solo, y vigilar las firmas que deja el bypass:
- En compilación y sistema. Canario fuerte (
-fstack-protector-strong), NX/DEP obligatorio, ASLR (randomize_va_space=2) + PIE —y en Windows Force ASLR con todo el árbol de módulos en/DYNAMICBASE, ya que un solo DLL estático abre el info-leak fácil—, Full RELRO (marca la GOT de solo lectura y mata el GOT overwrite del format string y el ret2plt), FORTIFY_SOURCE y-Wformat-securitycontra los format strings. Las protecciones de flujo por hardware/compilador —Intel CET (shadow stack), CFG (/GUARD:CF)— rompen ROP/JOP al validar destinos de retorno y salto; en Windows, Exploit Guard (sucesor de EMET) suma EAF, ACG/CIG y stack pivot protection. - En detección. El abort
*** stack smashing detected ***es unSIGABRTde__stack_chk_fail; un pico de core dumps o de segfaults con el puntero de instrucción en el rango de libc delata ret2libc o un brute-force. La firma más útil del ROP moderno es una llamada anómala aVirtualProtect/VirtualAlloc(Windows) omprotect(Linux) que marca una regiónRWXdesde un proceso que normalmente no lo hace —se ve con el API hooking de un EDR o de Sysmon—, seguida de ejecución desde esa región recién marcada. Y como siempre, un servicio que de golpe abre un puerto de escucha o lanza un/bin/shhijo conectado a un socket es el IOC post-explotación, correlacionable en el SIEM con la ejecución anómala del lado blue (Event ID 4688 / Sysmon 1; ver 4.9 · Defensa de Active Directory).
/dev/null sin error) y el spoof de la IP registrada (inyectar una
sockaddr_in falsa) derrotan cualquier log local. La respuesta es detección out-of-band: logging remoto
append-only a un SIEM que el proceso comprometido no puede tocar, y correlación del log de la aplicación con
telemetría de red independiente (NetFlow, firewall) — si el log dice una IP y el firewall vio otra, hay
manipulación.Referencias#
red-infra/grayhat/cap-03— Advanced Linux Exploits: format strings (%x/%s/%n, direct parameter access, fórmula mágica HOB/LOB), el catálogo de mitigaciones con su cobertura, y ret2libc con encadenamiento de llamadas a libc.red-infra/grayhat/cap-05— Advanced Windows Exploitation: ROP contra DEP haciaVirtualProtectcon Mona, y la fuga de memoria (UAF en IE 11, CVE-2017-0059) que derrota ASLR recalculando la base por RVA.red-infra/art-exploitation/cap-04— Countermeasures: restaurar el daemon yforkpara persistencia, evasión de IDS (encoding de firmas, sled imprimible) y shellcode polimórfico ASCII imprimible.- Hovav Shacham, The Geometry of Innocent Flesh on the Bone (2007) — el paper fundacional del ROP.
- scut / team teso, Exploiting Format String Vulnerabilities (2001) — el tratamiento clásico del bug.
- MITRE ATT&CK — T1203 Exploitation for Client Execution.