
Buffer overflow: Cómo los atacantes vulneran máquinas aprovechando vulnerabilidades en la memoria
El buffer overflow es una de las vulnerabilidades más explotadas del mundo. En 2025, el heap-based buffer overflow (CWE-122) ocupó el noveno puesto en la lista 2025 CWE Top 10 KEV Weaknesses de MITRE, elaborada a partir del catálogo KEV (Known Exploited Vulnerabilities) de CISA. A pesar de las medidas de protección de los sistemas operativos y de los lenguajes con seguridad de memoria, sigue siendo uno de los vectores de ataque más utilizados.
Sin embargo, no es una vulnerabilidad sencilla de comprender, como sí pueden serlo otras de explotación más directa, como el SQL injection o el IDOR. Comprender el buffer overflow requiere entender las vulnerabilidades OOB, fundamentos de arquitectura de procesador, registros, memoria… La complejidad es mucho mayor, tanto en la comprensión como en la explotación.
Las bases del buffer overflow
La memoria y el almacenamiento
A menudo, almacenamiento y memoria son conceptos que se usan de forma intercambiable, sin embargo, hay matices clave para entender el buffer overflow. Los dispositivos de almacenamiento (HDD, SSD…) proporcionan un medio duradero para almacenar información, su propósito es que la información persista en el tiempo.
La responsabilidad de la memoria, por otro lado, no es persistir la información, sino ser rápida. El acceso a la RAM es unas cien mil veces más rápido que el acceso a un disco mecánico. Por esta razón, los sistemas operativos copian información del disco a la RAM, para acceder a la información en la RAM y optimizar el trabajo.
La RAM, desde el punto de vista del SO (sistema operativo), no es más que un montón de direcciones. Normalmente, la mayoría de sticks de RAM agrupan las direcciones por bytes: un módulo de 4 GB tiene unos 4.000 millones de direcciones (2³² ≈ 4.294.967.296). Cuando se ejecuta un programa, el ordenador le asigna un rango de esas direcciones para copiar su contenido y tener acceso rápido a él.
El patrón de cachear información para acceso más rápido va más allá de la RAM, que solo es el primer paso. Por lo general, cuanta menos latencia tiene un dispositivo, de menos almacenamiento dispone, y los SO juegan con los distintos niveles de latencia, posicionando en los dispositivos con menor latencia la información que antes va a ser necesitada por la CPU.

Cifras orientativas; el orden de magnitud coincide con las referencias habituales de latencia, como Latency Numbers Every Programmer Should Know.
Los entresijos del caching y los registros son mucho más complejos, pero la premisa general es simple: prever lo que el procesador va a necesitar próximamente y posicionarlo lo más «cerca» posible de la CPU en función de la urgencia del dato.
Los registers
Los dispositivos que ofrecen menores latencias son los registers, cuyo contenido está disponible para el procesador en tan solo 1 clock cycle, unos 300 picosegundos. Los registers están físicamente en la propia CPU, y en ellos se almacenan valores frecuentemente usados por la CPU. Su tamaño es especialmente reducido, variando entre los 32 y los 64 bits en función de la arquitectura del procesador.
Cabe destacar que los nombres varían en función de la arquitectura. El estándar en ordenadores es x86, manufacturado principalmente por Intel y AMD. Pero existen otras arquitecturas con sintaxis distintas, como ARM, muy utilizado en dispositivos móviles.

Los registers son críticos para el óptimo funcionamiento del ordenador, con tipos tan importantes como las FLAGS, que contienen los bits de condición resultantes de operaciones —por ejemplo, lo que permite diferenciar un resultado positivo de uno negativo—. Sin embargo, el que resulta realmente relevante para el buffer overflow es el EIP/RIP (en arquitectura x86) o simplemente IP, un register que contiene la dirección de la próxima instrucción a ejecutar.
Esto es el centro del buffer overflow, ya que conseguir inyectar valores arbitrarios en esta dirección implica pasar instrucciones directamente a la CPU para su ejecución. Si un atacante controla el EIP, controla la máquina. El objetivo de un hacker que busca vulnerabilidades de este tipo es llegar a manipular el EIP.
Memory safety
Otra clave importante del buffer overflow es que es una vulnerabilidad inherentemente ligada a los lenguajes memory-unsafe. Muchos lenguajes populares hoy en día (Python, Java, JavaScript, C#, Go) cuentan con garbage collector (GC). El GC es una implementación que asume la responsabilidad de controlar, aprovisionar y liberar memoria: el compilador o el intérprete asumen la carga y liberan de ella al programador. Estos lenguajes se consideran memory-safe. Rust también lo es, pero sin recolector: lo consigue con su sistema de propiedad (ownership).
Es un sacrificio: se pierde control sobre el hardware y el rendimiento, pero se gana en seguridad. De todas formas, muchos otros lenguajes (algunos de ellos, muy populares) sí permiten, o directamente fuerzan, la gestión manual de memoria. En la gestión manual es donde reside el problema. El error humano a la hora de gestionar la memoria es lo que permite que un atacante pueda «escapar» del programa, e inyectar directamente al procesador sus propias instrucciones.
La siguiente tabla recoge algunos de los más populares, con su posición aproximada en el índice TIOBE de popularidad (los valores fluctúan cada mes):

Para alcanzar el procesador, el atacante debe pasar antes por una data structure, hay dos en lo que respecta al buffer overflow, y por eso los tipos de buffer overflow se dividen en dos: heap-based y stack-based.
El stack
¿Cómo es posible llegar a la CPU? Para comprenderlo, es necesario entender el stack. El stack es una estructura lineal de información, en forma de pila LIFO (last in, first out). Es decir, el primer dato en entrar es el primero en salir. Está controlado por 3 registros, que cambian de nombre en función de la arquitectura (de ahora en adelante se usará el nombre base).
| SP | ESP | RSP |
| BP | EBP | RBP |
| IP | EIP | RIP |
El BP (base pointer) apunta al fondo —el principio— del stack. El SP (stack pointer) apunta al «techo» del stack —el final—, y el IP (instruction pointer), a la próxima instrucción a ser ejecutada. Estos registros contienen punteros que apuntan a direcciones virtuales de la RAM.
El stack crece «hacia abajo», el BP apunta a una dirección inicial en la RAM mayor que 0, por ejemplo, 1300, y crece hacia el 0 —modificando el SP— conforme necesita más espacio, hasta alcanzar la dirección ocupada por el heap. Si el heap terminase en la dirección 1200, el stack tendría una capacidad de 100 bytes, desde la dirección 1300 hasta la 1200.
Direcciones altas
1300 ┤ ← BP: base del stack
│ Stack → crece hacia abajo (el SP baja hacia 0)
│ ▼
│ ▲
1200 ┤ ← tope del heap (límite con el stack)
│ Heap
0 ┤
Direcciones bajas
📝 Nota
Esto es más complejo en realidad, ya que en la explicación no se tienen en cuenta conceptos como la memoria virtual o la paginación. Se ha explicado así para mayor simplicidad y brevedad.
Assembly y los registers
Al analizar la versión compilada de un programa sencillo en C, se puede observar claramente cómo la CPU opera los registers. En este código de ejemplo, se define una función sum() que recibe tres parámetros y los suma, luego, se llama esa función sum() desde main() para sumar 2, 4 y 8.

Para poder entender assembly, hay que entender lo básico de las instrucciones:
push: es una especie de inicializador, posiciona el parámetro en el techo del stack.mov: de move, literalmente, se está moviendo el contenido del parámetro a la derecha al de la izquierda.mov rbp, rspmueve el contenido dersparbp.add: como su nombre indica,addsuma los parámetros.pop: el «opuesto» depush, quita el parámetro del techo del stack.ret: saca de la pila la dirección de retorno y la carga en el IP, devolviendo la ejecución a quien hizo la llamada. Es la contrapartida decall.
"sum":
push rbp ;Guarda en la pila el BP del caller (main)
mov rbp, rsp ;Fija el SP en el BP (cambia la dirección base del stack por la del "techo")
mov DWORD PTR [rbp-4], edi ;Guarda en la pila el parámetro 'a' (llega en EDI)
mov DWORD PTR [rbp-8], esi ;Guarda en la pila el parámetro 'b' (llega en ESI)
mov DWORD PTR [rbp-12], edx ;Guarda en la pila el parámetro 'c' (llega en EDX)
mov edx, DWORD PTR [rbp-4] ;Guarda el valor de 'a' en EDX
mov eax, DWORD PTR [rbp-8] ;Guarda el valor de 'b' en EAX
add edx, eax ;Suma EDX y EAX ( a + b )
mov eax, DWORD PTR [rbp-12] ;Guarda el valor de 'c' en EAX
add eax, edx ;Suma EAX y EDX ( ( a + b ) +c )
pop rbp ;Restaura el BP del caller
ret ;Saca la dirección de retorno de la pila a RIP y salta
"main":
push rbp ;Guarda el BP del caller
mov rbp, rsp ;Establece el marco de main() fijando el SP en el BP (el puntero que apuntaba al techo ahora apunta a la base)
mov edx, 8 ;Guarda el valor de 'c' (8) en EDX
mov esi, 4 ;Guarda el valor de 'b' (4) en ESI
mov edi, 2 ;Guarda el valor de 'a' (2) en EDI
call "sum" ;Invoca la función sum()
mov eax, 0 ;main() devuelve 0 (salida exitosa)
pop rbp ;Restaura el BP del caller
ret
Lo importante de entender es lo siguiente: las variables locales de un programa se guardan en el stack antes de que la CPU las utilice. Esto es la clave del buffer overflow, cuya piedra angular es «escapar» del espacio de una variable hasta sobrescribir la dirección de retorno guardada en la pila. El ejemplo sirve para visualizar cómo los registers se cargan con los valores de las variables y las instrucciones van pasando por la CPU.
El heap
El stack no es la única forma de explotar una vulnerabilidad de este tipo. El otro escenario es el heap: la región de memoria de reserva dinámica, la que se pide en tiempo de ejecución con malloc, new, etc. No es un árbol; el allocator la organiza en bloques (chunks), cada uno con sus metadatos. Es una región gestionada por el programador, así que un error de tamaño permite el overflow.
OOB read/write
El buffer overflow es un tipo específico de OOB write. OOB significa out of bounds (fuera de límites), y hay dos tipos de OOB. Las lecturas OOB y las escrituras OOB.
En ambas, la premisa es la misma. Un búfer es una región de memoria que retiene información temporalmente antes de moverla de un origen a un destino. Cuando una operación de lectura o escritura sobre ese búfer no comprueba bien sus límites, se puede acceder fuera de él y llegar a información no contemplada por diseño.
Un ejemplo famoso de OOB read es la vulnerabilidad Heartbleed, una vulnerabilidad en OpenSSL que permitía leer la memoria de los sistemas afectados.

¿Por qué es esto importante? Porque el buffer overflow es un OOB write. Hay un búfer (los registers o el heap) cuyo propósito es «escribir» información a la CPU. Si se puede escribir fuera de los límites establecidos en el diseño, se pueden alcanzar otros búfers que permiten ejecutar instrucciones arbitrarias.

Buffer overflow es un término amplio, y abarca todas las anomalías en las que un programa escribe fuera de los límites de un búfer, escribiendo direcciones contiguas. Normalmente, se explota a través de una aplicación vulnerable, y es un vector común para escaladas de privilegios o apertura de reverse shells o backdoors.
Como se ha mencionado anteriormente, hay dos tipos principales de buffer overflow, clasificados en función de la estructura utilizada para lograr la explotación.
Stack-based (CWE-121)
Dentro de su complejidad, la explotación a través del stack es la más sencilla. Al ser una explotación que pasa por el procesador, naturalmente el método varía en función de la arquitectura; no se explota igual el buffer overflow en ARM que en x86, ya que cambian muchas cosas muy relevantes para la explotación. En este artículo se trata x86, pero los fundamentos son los mismos.
#include <stdio.h>
#include <string.h>
void vulnerable(const char *input) {
// Se declara una variable de tipo char llamada buffer, con un tamaño de 64 bytes
char buffer[64];
// Se copia el contenido del parámetro input a la variable buffer sin controlar el tamaño de input
strcpy(buffer, input);
printf("Copied: %s\n", buffer);
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "usage: %s <input>\n", argv[0]);
return 1;
}
vulnerable(argv[1]);
return 0;
}
En el código de ejemplo (escrito en C), la función vulnerable() recibe como parámetro una variable de tipo char* llamada input. El input del usuario se pasa a la función a través de dicha variable. Dentro de la función, se define una variable llamada buffer, de tipo char y —la clave de la vulnerabilidad— un tamaño de 64 bytes.
"vulnerable":
push rbp
mov rbp, rsp
sub rsp, 80
mov QWORD PTR [rbp-72], rdi
mov rdx, QWORD PTR [rbp-72]
lea rax, [rbp-64]
mov rsi, rdx
mov rdi, rax
call "strcpy"
lea rax, [rbp-64]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call "printf"
nop
leave
ret
El código de arriba es el assembly equivalente a la función vulnerable. Se aprecia un concepto explicado anteriormente: las variables locales se reservan en el stack (aquí buffer queda en [rbp-64]). Como no se trunca ni procesa la variable input en ningún momento, si supera los 64 bytes los bytes sobrantes «rebasan» el espacio de buffer y sobrescriben la memoria contigua de la pila. Esto es el overflow.
Si un atacante es capaz de identificar el punto de una aplicación en el que se produce un overflow, puede afinar probando distintos inputs hasta sobrescribir la dirección de retorno guardada en la pila. Cuando la función ejecuta ret, esa dirección se carga en el RIP, el register que contiene la dirección de la próxima instrucción a ejecutar. Si el atacante controla ese valor, controla la máquina con los privilegios de la aplicación vulnerable, ya que puede apuntar a una dirección que contenga su propio código malicioso.
En la práctica, la explotación es más compleja, y las medidas de protección son cada vez más efectivas. Sin embargo, la base del buffer overflow es esa: encontrar un fallo en la gestión de memoria, desbordar el búfer para sobrescribir la dirección de retorno y ajustar los valores hasta controlar el contenido del RIP. Una vez el atacante controla el RIP, controla la CPU —con los privilegios del programa vulnerable—.

Heap-based (CWE-122)
En lo que respecta a la explotación, el heap-based buffer overflow está en otra categoría respecto al stack-based. El stack es una estructura sencilla, una pila LIFO en la que, una vez se desborda un búfer, resulta relativamente sencillo alcanzar la dirección de retorno (no es tan sencillo con las protecciones modernas).
El heap, en cambio, es una estructura más compleja. Tanto la pila como el heap residen en memoria, pero el heap se aprovisiona de forma dinámica, está más fragmentado y lo gestionan mecanismos y metadatos mucho más complejos.
La memoria heap se divide en chunks, cada uno con sus metadatos. Cuando se aprovisiona memoria (malloc(size)), el allocator devuelve un puntero a (al menos) size bytes, más los metadatos. Si un programador comete un error y permite un input mayor que size en el chunk aprovisionado, el input desborda el heap, lo cual puede provocar (de menor a mayor complejidad en la explotación):
- Corrupción del heap: crashes o comportamiento inesperado del programa.
- Manipulación de metadatos del heap.
- Escritura arbitraria: un atacante experimentado podría aprovechar el desbordamiento para escribir a regiones contiguas del heap.
Medidas de prevención/protección
Al ser una vulnerabilidad tan popular y crítica, existen numerosas medidas de prevención y protección, tanto a nivel de SO, como de los propios lenguajes o la CPU en sí.
A nivel de lenguaje
A nivel de lenguaje, la implementación más popular es el ya mencionado garbage collector. Sin embargo, muchos lenguajes no renuncian a la gestión manual de memoria, y hacen falta otras medidas menos drásticas.
Funciones seguras y buenas prácticas
La primera y más ambigua sería seguir las buenas prácticas y adherirse a los métodos de desarrollo seguro. Revisar bien el código, auditarlo, realizar input tests… Además de esto, algunos lenguajes memory-unsafe implementan «versiones seguras de funciones inseguras». Por ejemplo, strncpy requiere como parámetro un límite de tamaño, a diferencia de strcpy.
De esta manera, se puede copiar de forma segura desde un input, asegurando siempre que este nunca supera en tamaño la variable a la cual se está copiando, y por ende no la puede desbordar.
#include <string.h>
#include <stdio.h>
void vulnerable(const char *input) {
char buffer[16]; // Buffer de 16 bytes
strcpy(buffer, input); // Si input > buffer, hay overflow
printf("Stored: %s\n", buffer);
}
#include <string.h>
#include <stdio.h>
void safer(const char *input) {
char buffer[16];
strncpy(buffer, input, sizeof(buffer) - 1); // strncpy copia como MÁXIMO 15 bytes (el tamaño del buffer)
buffer[sizeof(buffer) - 1] = '\0'; // No hay overflow, porque input siempre se trunca a 15 bytes
printf("Stored: %s\n", buffer);
}
Esto es tan solo un ejemplo de muchos, pero sirve para ilustrar cómo se implementan medidas de seguridad desde los propios lenguajes memory-unsafe.
Bounds checking (en runtime)
Lenguajes como Rust, Go o Java cancelan la ejecución con un panic o una excepción cuando detectan que van a escribir fuera de límites.
Stack canaries
El compilador coloca un valor centinela —el canary— entre el buffer y la dirección de retorno guardada. Antes de que la función retorne, comprueba que ese valor no ha cambiado; si un desbordamiento lo ha sobrescrito, no coincide y el programa aborta. En GCC/Clang se activa con -fstack-protector.
A nivel de CPU
Las protecciones implementadas a nivel de CPU son variadas y dependen del fabricante, por lo que los matices varían entre los distintos proveedores. Sin embargo, el patrón es común, protección basada en autenticación criptográfica. Por ejemplo, PAC (Pointer Authentication, en ARM) firma criptográficamente los punteros: calcula un código a partir del valor del puntero y una clave secreta, y lo guarda en los bits altos que el puntero de 64 bits no utiliza. Antes de usar el puntero —por ejemplo, la dirección de retorno—, el procesador verifica esa firma; si alguien lo ha manipulado, la verificación falla.
Otro ejemplo sería el de CET Shadow Stack (x86), que implementa un stack secundario contenido en memoria, que no puede ser modificado por aplicaciones. Cuando el procesador ejecuta un call, guarda la dirección de retorno en el Shadow Stack además de en el stack normal; al ejecutar ret, compara ambas copias para verificar su integridad.
A nivel de sistema operativo
ASLR
Address Space Layout Randomization aleatoriza en cada ejecución las direcciones base de la pila, el heap y las bibliotecas —y, con binarios PIE, del propio ejecutable—. Al no ser predecibles, el atacante no sabe a qué dirección saltar. Se debilita con fugas de información (info leaks), poca entropía (sobre todo en 32 bits) o binarios compilados sin PIE.
DEP / NX
Marca regiones como la pila y el heap como no ejecutables: aunque el atacante logre escribir su código ahí, la CPU se niega a ejecutarlo. Es la protección que obligó a idear técnicas como return-oriented programming (ROP), que reutilizan código ya presente en lugar de inyectar el suyo.
Conclusión
El buffer overflow es una vulnerabilidad compleja, y su explotación a día de hoy requiere un alto nivel de experiencia y comprensión de varios conceptos low level de informática, como arquitectura de procesadores. Evadir las protecciones modernas es realmente complejo, pero la recompensa por encontrar una vulnerabilidad de este tipo puede ser gigante.
Entenderla, al menos a un nivel básico, viene muy bien para comprender estos conceptos complejos. A tono personal, espero que este artículo haya servido para esclarecer algunos de ellos, como la memoria, los registers y los fundamentos de arquitectura de procesadores y assembly.








