La Pila: Stack Frames, Convenciones de Llamada y Buffer Overflows
Anatomia del stack en x86/x64: stack frames, prologo y epilogo de funciones, convenciones de llamada (cdecl, stdcall, fastcall, x64), y como los buffer overflows explotan la estructura de la pila para redirigir la ejecucion.
La pila: estructura fundamental de la ejecucion
La pila (stack) es la estructura de datos mas importante para entender como se ejecuta un programa y como el malware explota vulnerabilidades. Cada llamada a funcion crea un nuevo marco (stack frame) en la pila, cada retorno lo destruye. La pila contiene variables locales, parametros de funciones, registros guardados y direcciones de retorno.
Entender el stack es esencial para tres areas del analisis de malware: seguir el flujo de ejecucion entre funciones, analizar exploits de buffer overflow y comprender como el malware manipula la cadena de handlers de excepciones (SEH).
Como funciona la pila en x86
La pila crece hacia direcciones bajas de memoria. PUSH decrementa ESP y escribe el valor en la nueva cima. POP lee el valor de la cima e incrementa ESP.
Direcciones altas (0xBFFFFFFF)
+---------------------------+
| Parametros del caller | [EBP+0x08] primer param, [EBP+0x0C] segundo, etc.
+---------------------------+
| Direccion de retorno | [EBP+0x04] (colocada por CALL)
+---------------------------+
| EBP del caller guardado | [EBP+0x00] (colocado por PUSH EBP)
+---------------------------+ <- EBP apunta aqui
| Variables locales | [EBP-0x04], [EBP-0x08], etc.
| ... |
+---------------------------+
| Registros guardados | PUSH EBX, PUSH ESI, etc.
+---------------------------+ <- ESP apunta aqui
Direcciones bajas (0x00000000)
ESP siempre apunta al ultimo valor colocado en la pila (la cima). EBP apunta a la base del frame actual y sirve como referencia fija para acceder a parametros y variables locales.
Prologo y epilogo de funcion
Prologo estandar
Casi todas las funciones compiladas comienzan con el mismo patron:
push ebp ; guarda el EBP del caller en la pila
mov ebp, esp ; EBP = ESP (establece la base del nuevo frame)
sub esp, 0x20 ; reserva 32 bytes para variables locales
Despues del prologo:
- EBP apunta a la base del frame actual
- ESP apunta a la cima de la pila (32 bytes mas abajo)
- Las variables locales estan en [EBP-0x04] a [EBP-0x20]
- Los parametros del caller estan en [EBP+0x08], [EBP+0x0C], etc.
- La direccion de retorno esta en [EBP+0x04]
Epilogo estandar
El epilogo deshace el prologo:
mov esp, ebp ; restaura ESP (libera variables locales)
pop ebp ; restaura el EBP del caller
ret ; saca la direccion de retorno y salta a ella
O alternativamente con la instruccion LEAVE:
leave ; equivale a MOV ESP, EBP + POP EBP
ret
Frame Pointer Omission (FPO)
Los compiladores modernos pueden omitir el uso de EBP como frame pointer (flag /Oy en MSVC, -fomit-frame-pointer en GCC). En ese caso, todas las referencias a variables locales y parametros usan ESP directamente:
; Sin frame pointer (FPO)
sub esp, 0x20 ; reserva espacio
mov [esp+0x1C], eax ; variable local
mov eax, [esp+0x24] ; primer parametro
; ...
add esp, 0x20 ; libera espacio
ret
El analisis con FPO es mas dificil porque ESP cambia con cada PUSH/POP, haciendo que los offsets a variables locales y parametros cambien dinamicamente. Ghidra e IDA manejan esto automaticamente en la mayoria de los casos, pero a veces fallan y necesitas reconstruir el stack frame manualmente.
Convenciones de llamada
Una convencion de llamada define tres cosas: como se pasan los parametros (stack o registros), quien limpia el stack despues de la llamada (caller o callee) y que registros se preservan entre llamadas.
cdecl (C declaration)
La convencion por defecto de C en x86. Los parametros se empujan al stack de derecha a izquierda. El caller limpia el stack despues del CALL.
; Llamada: resultado = funcion(1, 2, 3)
push 3 ; tercer parametro (ultimo primero)
push 2 ; segundo parametro
push 1 ; primer parametro
call funcion
add esp, 12 ; caller limpia 3 parametros * 4 bytes = 12
mov [resultado], eax ; valor de retorno en EAX
El add esp, 12 despues del CALL es la marca distintiva de cdecl. Si ves un ADD ESP despues de un CALL, la funcion usa cdecl.
cdecl permite funciones con numero variable de argumentos (como printf) porque el caller sabe cuantos parametros paso.
stdcall (Standard Call)
La convencion de las APIs de Windows. Los parametros van al stack de derecha a izquierda (igual que cdecl), pero el callee limpia el stack al retornar con ret N.
; Llamada a API Windows (stdcall)
push 0 ; cuarto parametro
push offset buffer ; tercer parametro
push 100 ; segundo parametro
push handle ; primer parametro
call ReadFile
; NO hay ADD ESP aqui: ReadFile limpia el stack con RET 16
test eax, eax
jz error
La marca distintiva de stdcall es la ausencia de ADD ESP despues del CALL. Si miras el final de la funcion llamada, veras ret 0x10 (16 bytes = 4 parametros * 4 bytes).
fastcall (Microsoft)
Los dos primeros parametros enteros van en ECX y EDX. El resto va al stack. El callee limpia el stack.
; fastcall: func(a, b, c, d)
push d ; cuarto parametro al stack
push c ; tercer parametro al stack
mov edx, b ; segundo parametro en EDX
mov ecx, a ; primer parametro en ECX
call func
fastcall es menos comun en APIs publicas de Windows pero aparece en funciones internas del kernel y en malware que invoca syscalls directamente.
thiscall (C++ Microsoft)
Igual que stdcall pero con el puntero this en ECX:
; obj->metodo(param1, param2)
push param2
push param1
mov ecx, puntero_obj ; this en ECX
call [vtable+offset] ; llamada a metodo virtual
Si ves un MOV ECX seguido de un CALL indirecto a traves de una tabla de punteros, es casi seguro una llamada a un metodo virtual de C++.
Windows x64 Calling Convention
En Windows de 64 bits hay una unica convencion de llamada. Los primeros 4 parametros enteros van en RCX, RDX, R8, R9. Los parametros de punto flotante van en XMM0-XMM3. El resto va al stack.
; CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurity, ...)
mov r9, 0 ; lpSecurityAttributes (4to param)
mov r8d, 1 ; dwShareMode (3er param)
mov edx, 80000000h ; dwDesiredAccess (2do param)
lea rcx, [filename] ; lpFileName (1er param)
; 5to, 6to, 7mo parametros irian en [RSP+0x28], [RSP+0x30], [RSP+0x38]
sub rsp, 0x28 ; shadow space (32 bytes) + alineacion
call CreateFileW
add rsp, 0x28
El "shadow space" es una particularidad de Windows x64: el caller siempre reserva 32 bytes en el stack antes de cualquier CALL, incluso si la funcion no los necesita. Este espacio permite al callee guardar los parametros de registro en el stack si lo desea. El shadow space mas la alineacion a 16 bytes hacen que veas sub rsp, 0x28 o sub rsp, 0x38 constantemente en codigo de 64 bits.
System V AMD64 ABI (Linux/macOS)
Linux y macOS x64 usan una convencion diferente. Los parametros enteros van en RDI, RSI, RDX, RCX, R8, R9 (orden diferente a Windows). No hay shadow space. Si analizas malware de Linux en 64 bits, los parametros estan en registros diferentes.
Registros preservados (callee-saved vs caller-saved)
No todos los registros se preservan entre llamadas a funciones. Esto es critico para entender el estado de los registros despues de un CALL:
Callee-saved (preservados por la funcion llamada):
- x86: EBX, ESI, EDI, EBP
- x64 Windows: RBX, RBP, RDI, RSI, R12-R15
Si una funcion modifica estos registros, debe guardarlos al inicio (PUSH) y restaurarlos al final (POP).
Caller-saved (pueden ser modificados por la funcion llamada):
- x86: EAX, ECX, EDX
- x64 Windows: RAX, RCX, RDX, R8-R11
Despues de un CALL, asume que EAX/RAX, ECX/RCX y EDX/RDX contienen valores diferentes a los de antes de la llamada. EAX/RAX tendra el valor de retorno.
Buffer overflow en el stack
Un buffer overflow en el stack es la vulnerabilidad clasica que explota la estructura de la pila. Ocurre cuando una funcion escribe mas datos en un buffer local de los que caben.
Anatomia del overflow
Considera una funcion con un buffer local de 16 bytes:
push ebp
mov ebp, esp
sub esp, 0x10 ; char buffer[16] en [EBP-0x10]
; Copia insegura: si el input tiene mas de 16 bytes...
push dword [ebp+0x08] ; fuente (parametro de la funcion)
lea eax, [ebp-0x10]
push eax ; destino (buffer local)
call strcpy ; copia sin verificar longitud
La pila antes del overflow:
+-----------------------+
| parametro (fuente) | [EBP+0x08]
+-----------------------+
| direccion de retorno | [EBP+0x04]
+-----------------------+
| EBP guardado | [EBP+0x00] <- EBP
+-----------------------+
| buffer[12..15] | [EBP-0x04]
+-----------------------+
| buffer[8..11] | [EBP-0x08]
+-----------------------+
| buffer[4..7] | [EBP-0x0C]
+-----------------------+
| buffer[0..3] | [EBP-0x10] <- inicio del buffer
+-----------------------+ <- ESP
strcpy copia bytes desde la fuente al buffer empezando en [EBP-0x10] y avanzando hacia direcciones altas. Si la fuente tiene mas de 16 bytes:
- Los bytes 17-20 sobrescriben el EBP guardado
- Los bytes 21-24 sobrescriben la direccion de retorno
Cuando la funcion ejecuta RET, salta a la direccion controlada por el atacante.
Explotacion clasica
En el exploit clasico (sin protecciones modernas), el atacante coloca shellcode en el buffer y sobrescribe la direccion de retorno con la direccion del buffer. Cuando RET se ejecuta, el procesador salta al shellcode del atacante.
+-----------------------+
| AAAA (padding) | parametro (irrelevante)
+-----------------------+
| 0xBFFF0040 | direccion de retorno -> apunta al shellcode
+-----------------------+
| AAAA (padding) | EBP sobrescrito
+-----------------------+
| \x31\xc0\x50\x68... | shellcode (las instrucciones del atacante)
| ... |
+-----------------------+
Protecciones modernas
Las protecciones actuales hacen que la explotacion sea mucho mas compleja, pero el principio subyacente (sobrescribir la direccion de retorno) sigue siendo relevante para entender malware:
Stack Canaries (/GS en MSVC): un valor aleatorio (cookie) se coloca entre las variables locales y la direccion de retorno. Antes de RET, la funcion verifica que el canary no fue modificado. Si lo fue, termina el proceso.
; Prologo con canary
push ebp
mov ebp, esp
sub esp, 0x20
mov eax, [__security_cookie] ; valor aleatorio global
xor eax, ebp ; XOR con EBP para hacerlo unico por frame
mov [ebp-0x04], eax ; canary en la primera variable local
; ... cuerpo de la funcion ...
; Epilogo con verificacion
mov ecx, [ebp-0x04]
xor ecx, ebp
call __security_check_cookie ; si no coincide, termina el proceso
mov esp, ebp
pop ebp
ret
DEP/NX (No-Execute): marca el stack como no ejecutable. Aunque el atacante sobrescriba la direccion de retorno, no puede ejecutar shellcode directamente en el stack. Los exploits modernos usan ROP (Return-Oriented Programming) para sortear esta proteccion.
ASLR (Address Space Layout Randomization): aleatoriza las direcciones base de modulos, stack y heap. El atacante no puede predecir la direccion del shellcode o de los gadgets ROP. Los exploits necesitan un information leak para vencer ASLR.
SafeSEH / SEHOP: protegen la cadena de Structured Exception Handlers contra sobrescritura.
SEH y su relacion con el stack
La cadena de Structured Exception Handlers (SEH) de Windows se almacena en el stack. Cada frame SEH contiene un puntero al siguiente handler y un puntero a la funcion manejadora.
El malware abusa de SEH de dos formas:
Anti-debug via SEH: el malware genera una excepcion intencionalmente. Si hay un debugger, el debugger intercepta la excepcion. Si no hay debugger, el SEH handler del malware se ejecuta y el malware sabe que no esta siendo depurado.
; Instalar SEH handler
push handler_antidebug ; direccion del handler
push dword fs:[0x00] ; puntero al handler anterior
mov fs:[0x00], esp ; registrar nuevo handler en la cadena SEH
; Generar excepcion
xor eax, eax
mov [eax], eax ; access violation (escribe en direccion 0)
; Si llegamos aqui, habia un debugger que manejo la excepcion
jmp detectado_debugger
handler_antidebug:
; Si llegamos aqui, no hay debugger
; Limpiar y continuar ejecucion normal
SEH overflow: en versiones antiguas de Windows sin SafeSEH, un buffer overflow podia sobrescribir el handler SEH en el stack. El atacante generaba una excepcion y el sistema operativo llamaba al handler controlado por el atacante.
Stack pivoting
El stack pivoting es una tecnica avanzada donde el malware o un exploit cambia ESP para que apunte a una region de memoria controlada (heap, seccion de datos). Esto convierte esa region en el "nuevo stack":
; Stack pivot: mover ESP a una region controlada
mov esp, eax ; EAX contiene una direccion en el heap con datos controlados
ret ; ahora RET lee la "direccion de retorno" del heap controlado
El stack pivoting se usa en exploits ROP cuando el espacio disponible en el stack real es insuficiente para la cadena ROP completa. Tambien aparece en malware que quiere operar con un stack propio para dificultar el analisis.
Identificar la convencion de llamada en Ghidra/IDA
Cuando analizas un binario, necesitas identificar la convencion de llamada para interpretar correctamente los parametros:
- Mira si hay ADD ESP despues del CALL: cdecl
- Mira si la funcion termina con RET N: stdcall (N indica bytes de parametros limpiados)
- Mira si se usan ECX/EDX antes del CALL sin PUSHes correspondientes: fastcall o thiscall
- Mira si se usan RCX, RDX, R8, R9: Windows x64
- Mira si se usan RDI, RSI, RDX, RCX, R8, R9: Linux x64
Ghidra e IDA generalmente identifican la convencion correcta automaticamente, pero en funciones con calling conventions no estandar (callbacks, funciones generadas por compiladores exoticos o shellcode manual) puede fallar.
Depurar el stack en x64dbg
Ejercicio practico con x64dbg:
- Abre un sample en x64dbg y pon un breakpoint en el entry point
- Observa la ventana de stack: muestra el contenido de la pila con anotaciones
- Avanza instruccion a instruccion (F7) por el prologo de una funcion
- Observa como ESP cambia con cada PUSH y SUB ESP
- Pon un breakpoint en un CALL a una API de Windows
- Cuando se detenga, lee los parametros: en x86, estan en el stack ([ESP], [ESP+4], [ESP+8], [ESP+0xC]). En x64, estan en RCX, RDX, R8, R9
- Deja que la funcion retorne (Ctrl+F9) y lee RAX para el valor de retorno
El stack view de x64dbg muestra las direcciones de retorno resaltadas, lo que te permite ver rapidamente la cadena de llamadas (call stack) sin necesidad de la ventana de call stack separada.
Resumen
La pila crece hacia direcciones bajas. Cada CALL crea un stack frame con la direccion de retorno, EBP guardado y variables locales. EBP es la referencia fija dentro del frame. ESP apunta a la cima y cambia constantemente.
Las convenciones de llamada definen como se pasan parametros (stack en 32 bits, registros en 64 bits), quien limpia el stack (caller en cdecl, callee en stdcall) y que registros se preservan.
Los buffer overflows explotan la proximidad entre buffers locales y la direccion de retorno en el stack. Las protecciones modernas (canaries, DEP, ASLR) dificultan la explotacion pero no la eliminan.
El siguiente articulo cubre el flujo de control: como los compiladores traducen if/else, for/while y switch/case a patrones de ensamblador que reconoceras en malware.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.