Niezainicjalizowane zmienne

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

Podstawowe informacje

Główna idea tutaj to zrozumieć, co się dzieje z niezainicjalizowanymi zmiennymi, ponieważ będą one miały wartość, która już znajdowała się w przypisanej im pamięci. Przykład:

  • Function 1: initializeVariable: Deklarujemy zmienną x i przypisujemy jej wartość, powiedzmy 0x1234. Ta operacja jest podobna do zarezerwowania miejsca w pamięci i zapisania tam konkretnej wartości.
  • Function 2: useUninitializedVariable: Tutaj deklarujemy kolejną zmienną y, ale nie przypisujemy jej żadnej wartości. W C niezainicjalizowane zmienne nie są automatycznie ustawiane na zero. Zamiast tego zachowują wartość, która ostatnio była zapisana pod ich adresem pamięci.

Kiedy uruchomimy te dwie funkcje sekwencyjnie:

  1. W initializeVariable x otrzymuje wartość (0x1234), która zajmuje określony adres pamięci.
  2. W useUninitializedVariable y jest deklarowane, ale nie otrzymuje wartości, więc zajmuje miejsce w pamięci zaraz po x. Ponieważ y nie zostało zainicjalizowane, kończy „dziedziczyć” wartość z tej samej lokacji pamięci użytej przez x, ponieważ to była ostatnia zapisana tam wartość.

To zachowanie ilustruje kluczową koncepcję w programowaniu niskopoziomowym: zarządzanie pamięcią jest kluczowe, a niezainicjalizowane zmienne mogą prowadzić do nieprzewidywalnego zachowania lub luk bezpieczeństwa, ponieważ mogą niezamierzenie przechowywać wrażliwe dane pozostawione w pamięci.

Niezainicjalizowane zmienne na stosie mogą stwarzać kilka zagrożeń bezpieczeństwa, takich jak:

  • Data Leakage: Wrażliwe informacje, takie jak hasła, klucze szyfrowania lub dane osobowe, mogą zostać ujawnione, jeśli znajdują się w niezainicjalizowanych zmiennych, co pozwala atakującym potencjalnie odczytać te dane.
  • Information Disclosure: Zawartość niezainicjalizowanych zmiennych może ujawnić szczegóły dotyczące układu pamięci programu lub jego wewnętrznych działań, pomagając atakującym w opracowaniu ukierunkowanych exploitów.
  • Crashes and Instability: Operacje z użyciem niezainicjalizowanych zmiennych mogą skutkować niezdefiniowanym zachowaniem, prowadząc do awarii programu lub nieprzewidywalnych rezultatów.
  • Arbitrary Code Execution: W niektórych scenariuszach atakujący mogą wykorzystać te luki do zmiany przepływu wykonania programu, umożliwiając wykonanie dowolnego kodu, co może obejmować zagrożenia związane z remote code execution.

Przykład

#include <stdio.h>

// Function to initialize and print a variable
void initializeAndPrint() {
int initializedVar = 100; // Initialize the variable
printf("Initialized Variable:\n");
printf("Address: %p, Value: %d\n\n", (void*)&initializedVar, initializedVar);
}

// Function to demonstrate the behavior of an uninitialized variable
void demonstrateUninitializedVar() {
int uninitializedVar; // Declare but do not initialize
printf("Uninitialized Variable:\n");
printf("Address: %p, Value: %d\n\n", (void*)&uninitializedVar, uninitializedVar);
}

int main() {
printf("Demonstrating Initialized vs. Uninitialized Variables in C\n\n");

// First, call the function that initializes its variable
initializeAndPrint();

// Then, call the function that has an uninitialized variable
demonstrateUninitializedVar();

return 0;
}

Jak to działa:

  • initializeAndPrint Function: Ta funkcja deklaruje zmienną całkowitą initializedVar, przypisuje jej wartość 100, a następnie wypisuje zarówno adres pamięci, jak i wartość zmiennej. Ten krok jest prosty i pokazuje, jak zachowuje się zainicjalizowana zmienna.
  • demonstrateUninitializedVar Function: W tej funkcji deklarujemy zmienną całkowitą uninitializedVar bez jej inicjalizacji. Gdy próbujemy wypisać jej wartość, wynik może pokazać losową liczbę. Ta liczba reprezentuje dane, które wcześniej znajdowały się w tej lokalizacji pamięci. W zależności od środowiska i kompilatora rzeczywisty output może się różnić, a czasem, ze względów bezpieczeństwa, niektóre kompilatory mogą automatycznie zainicjalizować zmienne zerami — jednak nie należy na to polegać.
  • main Function: Funkcja main wywołuje obie powyższe funkcje jedna po drugiej, pokazując kontrast między zmienną zainicjalizowaną a niezainicjalizowaną.

Praktyczne wzorce eksploatacji (2024–2025)

Klasyczny “read-before-write” bug pozostaje istotny, ponieważ nowoczesne mechanizmy (ASLR, canaries) często opierają się na poufności. Typowe powierzchnie ataku:

  • Partially initialized structs copied to userland: Kernel or drivers frequently memset only a length field and then copy_to_user(&u, &local_struct, sizeof(local_struct)). Padding and unused fields leak stack canary halves, saved frame pointers or kernel pointers. If the struct contains a function pointer, leaving it uninitialized may also allow controlled overwrite when later reused.
  • Uninitialized stack buffers reused as indexes/lengths: An uninitialized size_t len; used to bound read(fd, buf, len) may give attackers out-of-bounds reads/writes or allow bypassing size checks when the stack slot still contains a large value from a prior call.
  • Compiler-added padding: Even when individual members are initialized, implicit padding bytes between them are not. Copying the whole struct to userland leaks padding that often contains prior stack content (canaries, pointers).
  • ROP/Canary disclosure: If a function copies a local struct to stdout for debugging, uninitialized padding can reveal the stack canary enabling subsequent stack overflow exploitation without brute-force.

Minimal PoC pattern to detect such issues during review:

struct msg {
char data[0x20];
uint32_t len;
};

ssize_t handler(int fd) {
struct msg m;              // never fully initialized
m.len = read(fd, m.data, sizeof(m.data));
// later debug helper
write(1, &m, sizeof(m));   // leaks padding + stale stack
return m.len;
}

Środki zaradcze i opcje kompilatora (pamiętaj przy omijaniu)

  • Clang/GCC auto-init: Nowsze toolchainy udostępniają -ftrivial-auto-var-init=zero lub -ftrivial-auto-var-init=pattern, wypełniając każdą automatyczną (stack) zmienną przy wejściu do funkcji zerami lub wzorcem poison (0xAA / 0xFE). To zamyka większość uninitialized-stack info leaks i utrudnia exploitację przez przekształcenie sekretów w znane wartości.
  • Linux kernel hardening: Jądra zbudowane z CONFIG_INIT_STACK_ALL lub nowszym CONFIG_INIT_STACK_ALL_PATTERN zerują/wzorcują każdą pozycję stosu przy wejściu do funkcji, wymazując canaries/wskaźniki które w przeciwnym razie leak. Szukaj dystrybucji dostarczających jądra zbudowane Clangiem z tymi opcjami włączonymi (częste w konfiguracjach hardening 6.8+).
  • Opt-out attributes: Clang pozwala teraz na __attribute__((uninitialized)) na określonych lokalnych/structs, aby utrzymać obszary krytyczne wydajnościowo nieinicjalizowane nawet gdy globalne auto-init jest włączone. Przejrzyj takie adnotacje uważnie — często oznaczają celowy attack surface dla side channels.

Z perspektywy atakującego, wiedza czy binarka została zbudowana z tymi flagami decyduje, czy stack-leak primitives są wykonalne, czy musisz pivotować do heap/data-section disclosures.

Szybkie znajdowanie błędów uninitialized-stack

  • Compiler diagnostics: Kompiluj z -Wall -Wextra -Wuninitialized (GCC/Clang). Dla kodu C++ clang-tidy -checks=cppcoreguidelines-init-variables automatycznie naprawi wiele przypadków do zero-init i jest przydatny do wykrywania pominiętych lokalnych zmiennych podczas audytu.
  • Dynamic tools: -fsanitize=memory (MSan) w Clang lub Valgrind z --track-origins=yes niezawodnie oznaczają odczyty nieinicjalizowanych bajtów stosu podczas fuzzingu. Instrumentuj test harnessy tymi narzędziami, aby ujawnić subtelne padding leaks.
  • Grepping patterns: W przeglądach szukaj wywołań copy_to_user / write całych struktur, lub memcpy/send danych ze stosu, gdzie tylko część struktury jest ustawiona. Zwróć szczególną uwagę na ścieżki błędów, gdzie inicjalizacja jest pomijana.

ARM64 Przykład

To w ARM64 w ogóle się nie zmienia, ponieważ zmienne lokalne też są zarządzane na stosie — możesz sprawdzić ten przykład, gdzie to jest pokazane.

Referencje

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