Explicando el stack y el heap
El buffer overflow es una de las vulnerabilidades más explotadas, en 2025, el heap-based overflow ocupó el puesto 9 en la lista de KEV (Known Exploited Vulnerabilities). Sin embargo, ¿Qué es el heap? ¿Qué es un overflow? ¿Cómo se producen y cómo se encuentran estas vulnerabilidades? ¿Cómo nos protegen los sistemas operativos de ellas? Todas estas preguntas quedarán respondidas en este artículo.
Comenzando como a mí me gusta, por lo más fundamental. La arquitectura de un ordenador es una extremadamente compleja e imposible de abarcar en condiciones brevemente, pero, sobresimplificando el flujo de ejecución de un programa, sucedería algo como lo siguiente.
Imaginemos que queremos abrir un programa llamado program.exe, que reside en el disco duro. El procesador, junto con el sistema operativo, se encargarán de consultar en qué sección del disco duro se encuentra el programa, para ejecutarlo. Dependiendo de varios factores, como la memoria disponible y el OS (sistema operativo), una parte o la totalidad del programa se cargará en la memoria volátil (RAM). Esto se hace porque el procesador accede muchísimo más rápido a la memoria de la RAM que a la de las unidades de almacenamiento. En la RAM se pueden llegar a almacenar varios MB o GB de información de un solo programa.
Sin embargo, hay algo todavía más rápido que la RAM: los registros. El procesador tarda unos 150–250 ciclos (la unidad de tiempo del procesador) en leer información de la RAM, mientras que solo tarda 1 ciclo en leer del registro. ¿Por qué es tan rápido? Porque los registros están físicamente en la propia CPU, y aunque tienen una capacidad mínima de almacenamiento, permiten lecturas casi instantáneas. Por esta razón, la información más urgente para el procesador se almacena aquí, y en esa categoría entran cosas como la dirección de la próxima instrucción a ejecutar, los elementos de la próxima operación…
Los registros le "pasan" a la CPU variables locales, argumentos de funciones y lo más importante para los overflow, la dirección de retorno (que le dice a la CPU cuál es la dirección de la próxima instrucción a ejecutar). Por poner una analogía, es como si un ayudante preparase todos los ingredientes a un chef antes de comenzar una receta, para que el cocinero solo tenga que preocuparse por cocinar y no perder tiempo en buscar por los armarios.

En el diagrama se observa algo como lo descrito, en la pila, los datos entran en orden LIFO(last-in-first-out), es decir, el último dato en entrar es el primero en salir.
La CPU, "trabaja" con lenguaje assembly, que ya es prácticamente código máquina, un simple hello world, que podría escribirse en Python como print("hello world"), tiene este aspecto.
section .text
extern printf
global _start ; Entry point for the program
_start: ; Start of the program
mov edx,len ; Calculate message length
mov ecx,msg ; Load address of message
mov ebx,1 ; File descriptor (stdout)
mov eax,4 ; System call number (sys_write)
int 0x80 ; Call kernel to display message
mov eax,1 ; System call number (sys_exit)
int 0x80 ; Call kernel to exit program
section .data
msg db 'Hello World!',10 ; Our message with a newline
len equ $ - msg ; Calculate length of messageSi se analizan los datos que pasan por la pila, veremos que se trabaja con direcciones virtuales en la memoria (esto es importante para el buffer overflow y se explicará más adelante). Esto no dejan de ser datos inmediatos para la CPU, por ejemplo, en la línea mov ecx,msg, lo que se le está diciendo al procesador es: "Mueve el contenido de 'msg' a la dirección ECX.
El stack es una región de la RAM "gestionada" por estos registros. Es decir, los registros apuntan a la región de la RAM en la que existe el stack, en él se almacena información estática, constantes, variables de funciones… Podría decirse que los registros leen y escriben en el stack para guardar información entre operaciones de la CPU, esto es clave para el buffer overflow, ya que si conseguimos manipular los datos del stack (en la RAM), y que los registros "transmitan" estos datos que hemos inyectado a la CPU, esta los ejecutará.
El heap, por otro lado, está totalmente gestionado por el software, no por los registros. Es una abstracción que existe en la RAM. En el heap reside la información que requiere dinamismo, es decir, estructuras de datos cambiantes, que aumentan, disminuyen o mutan conforme la aplicación se ejecuta.
Tanto el heap como el stack pueden utilizarse como vectores para explotar un overflow (aunque explotar un heap-based overflow es más complejo), pero tienen sus diferencias.
¿Qué es un buffer overflow?
En esta sección no quiero explayarme demasiado, ya que sobre esto sí hay mucho contenido en castellano (a diferenia de lo explicado en la sección anterior) y no me gusta la redundancia, sin embargo, este artículo estaría incompleto sin una mención y breve explicación.
Stack-based overflows
Como se explicó arriba, el stack existe en la RAM y guarda información crítica, a la cual los registros acceden para pasarle dicha información a la CPU. Imaginemos que nuestro programa contiene una función como esta.
#include <stdio.h>
#include <string.h>
void funcion_vulnerable(char *entrada_usuario) {
// Reservamos 8 bytes
char buffer[8];
// ¡PELIGRO! strcpy no comprueba si 'entrada_usuario' cabe en 'buffer'
strcpy(buffer, entrada_usuario);
printf("Contenido del buffer: %s\n", buffer);
}
int main() {
// Una entrada que claramente supera los 8 bytes
char *input_malicioso = "ESTE_TEXTO_ES_MUY_LARGO_Y_VA_A_DESBORDAR_EL_STACK";
funcion_vulnerable(input_malicioso);
return 0;
}No es necesario entender todo, lo realmente importante es lo siguiente: char buffer[8] declara una variable de 8 bytes. Justo después, strcpy(buffer, entrada_usuario) copia el input del usuario a la variable buffer que acabamos de declarar, pero no se limita el input del usuario a 8 bytes.
Ahora, cuando se llama a la función vulnerable dentro de main() si el input proporcionado por el usuario es mayor a 8 bytes, lo que no cabe en la variable buffer se desborda (desbordamiento de búfer/buffer overflow).
Esto, ya de por sí es un problema, pero para explotar esto se requiere algo más. El objetivo es conseguir desbordar la memoria hasta encontrar la dirección en la RAM en la que el registro EIP ha guardado información. Es decir, queremos conseguir (con métodos que no voy a explicar en este artículo), que el desbordamiento alcance la dirección donde el EIP está apuntando (recordemos que los registros apuntan a direcciones de la RAM), si conseguimos sobreescribir el contenido de esa dirección, estaremos diciéndole a la CPU, a través del registro EIP, qué es lo próximo que debe ejecutar. Si eso es nuestro propio código malicioso, y la aplicación está ejecutándose con privilegios de administrador, habríamos ganado control absoluto de la máquina.
Para simplificar, imaginemos que la dirección a la que apunta el EIP es la contigua a la que estaba siendo reservada para buffer, si el input era de 16 bytes, y los 8 bytes que desbordan el buffer son código malicioso, EIP los llevaría de aquí directamente a la CPU.

De esta manera estaríamos consiguiendo "inyectar" instrucciones directamente a la CPU con los permisos de la aplicación, habilitando todo tipo de fechorías como escalada de privilegios o acceso remoto.
Hay mucha más complejidad y entresijos para explotar algo así, tanto a la hora de encontrar el desbordamiento, como a la hora de encontrar dónde está guardado el EIP, como a la hora de pasarle el código a la CPU, por no hablar de las medidas de protección como DEP o ASLR… Pero eso lo dejo como tarea para el lector. Al final el artículo ha quedado más largo de lo que esperaba, así que quizá deje la explicación del heap-based overflow, así como de las medidas de protección, para otro día.