Ochrona stosu

Michał mina86 Nazarewicz, https://mina86.com/

Stronę najlepiej oglądać w Operze w trybie pełnoekranowym.

Czym jest błąd przepełnienia bufora?

Błąd przepełnienia bufora występuje, gdy proces usiłuje zapisać dane poza przydzielonym dla nich buforem.

bufor zmienna
00000 00000 420
'a''l''a'' ''m' 'a'' ''k''o''t' 'a'0

Po wpisaniu ciągu ala ma kota do bufora, zmienna zmienia wartość.

Przykład błędu przepełnienia bufora

Błąd przepełnienia bufora może się wiązać z wykorzystaniem funkcji takich jak strcpy(), sprintf(), czy gets(), które nie sprawdzają długości docelowego bufora.

#include <stdio.h>
#include <string.h>

int validate(const char *pass) {
    char buf[10];
    strcpy(buf, pass);
    return !strcmp(buf, "password");
}

int main(int argc, char **argv) {
    if (argc != 2 || !validate(argv[1])) {
        puts("Sorry.");
        return 1;
    }
    puts("Welcome!");
    return 0;
}
#include <stdio.h>
#include <string.h>

static int validate(char *pass) {
    char buf[10];
    int valid = 0;
    strcpy(buf, pass);
    if (!strcmp(buf, "password")) {
        valid = 1;
    }
    return valid;
}

int main(int argc, char **argv) {
    if (argc != 2 || !validate(argv[1])) {
        puts("Sorry.");
        return 1;
    }
    puts("Welcome!");
    return 0;
}
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

static void do_job(int valid, char *pass) {
    char buf[10];
    strcpy(buf, pass);
    if (!valid && !strcmp(buf, "password")) {
        valid = 1;
    }

    if (valid) {
        puts("Welcome!");
        exit(0);
    } else {
        puts("Sorry.");
        exit(1);
    }
}

int main(int argc, char **argv) {
    if (argc != 2) {
        puts("Sorry.");
        return 1;
    } else {
        do_job(0, argv[1]);
    }
}

Błąd ten może również wynikać z błędnego wyliczenia długości bufora (np. integer overflow), nieumiejętnego wykorzystania funkcji strncpy(), ale i z różnych innych powodów.

Co się dzieje przy wołaniu funkcji?

Przestrzeń rodzica
Argumenty
Adres powrotu
Stary ebp
Zmienne lokalne

Funkcja wołająca zapisuje na stosie argumenty wywołania po czym wykonuje instrukcję call, która wykonuje tzw. skok ze śladem, czyli skacze pod podany adres jednocześnie zapisując adres powrotu na stosie. W innych architekturach niż x86 może się to odbywać odrobinę inaczej, przykładowo adres powrotu może być zapisany do rejestru (przeróżne procesory RISC) i dopiero funkcja wołana ewentualnie go zapisze na stosie lub wręcz nie będzie ona zapisywana na stosie jawnie przez kod użytkownika, gdy stosowany jest mechanizm okien rejestrów (SPARC).

Funkcja wołana zapisuje na stosie starą wartość wskaźnika ramki stosu (rejestr ebp) i ustawia go na wartość równą wskaźnikowi wierzchołka stosu (rejestr esp). Dzięki temu, bez konieczności śledzenia zmian stosu, można odwoływać się do argumentów oraz zmiennych lokalnych.

Na końcu funkcja rezerwuje na stosie przestrzeń potrzebną na zmienne lokalne.

Co się dzieje przy powrocie z funkcji?

Przestrzeń rodzica
Argumenty
Adres powrotu
Stary ebp
Zmienne lokalne

Przy powrocie, funkcja zwalnia przestrzeń zarezerwowaną na zmienne lokalne oraz przywraca ze stosu wartość wskaźnika ramki stosu (rejestr ebp), po czym wykonuje instrukcję ret, która odczytuje adres powrotu ze stosu i wykonuje skok.

Nadpisanie adresu powrotu umożliwia osobie atakującej wykonanie dowolnego fragmentu programu, do którego normalnie nie miałby dostępu. Umożliwia także, wykonanie skoku na stos, gdzie został wpisany dowolny kod maszynowy.

Jak działa ochrona stosu?

Przestrzeń rodzica
Argumenty
Adres powrotu
Stary ebp
Kanarek
Lokalne bufory
Inne zmienne lokalne
Kopia argumentów

Aby zapobiec tego typu atakom, funkcja wołana może za starym wskaźnikiem ramki stosu wstawić pewną wartość, której poprawność sprawdzi przy powrocie z funkcji. Jeżeli okaże się, że została zmieniona mamy pewność, iż coś poszło nie tak i teraz jedynym sensownym wyjściem jest bezwarunkowe zakończenie działania programu.

Dodatkową ochronę daje skopiowanie wartości argumentów funkcji gdzieś za zmiennymi lokalnymi. Ponadto, można zmienne lokalne tak poprzestawiać, aby wszelkie bufory znajdowały się pod wyższymi adresami niż inne zmienne, które w ten sposób nie zostaną nadpisane, gdy nastąpi przepełnienie któregoś z buforów.

Przed czym ochrona stosu nie chroni?

Co oczywiste, ochrona stosu chroni przed błędami przepełnienia buforów występujących jedynie na stosie. Dla buforów statycznych lub dynamicznych należałoby zastosować inne formy zabezpieczeń.

Mechanizm wprowadza minimalną ochronę, gdy zastosowana wartość kanarka jest stała lub prosta do przewidzenia.

Ochrona stosu nie daje żadnych mechanizmów przywracania poprawności stanu stosu, czyli jeżeli zmiana zostanie wykryta program nie może polegać na zawartości stosu.

Mechanizm zawodzi przed zapobieganiem nadpisania zmiennych lokalnych jeżeli mamy do czynienia ze strukturą, w której po buforze znajduje się zmienna. Przykładowo w poniższym kodzie, zmienna foo.size będzie pod adresem wyższym niż bufor foo.data przez co będzie podatna na błędy przepełnienia bufora:

struct foo {
    char data[12];
    unsigned size;
};

void foo(void) {
    struct foo f;
    ...
}

Wady i zalety

Wady?

Wolniejszy kod.

Większe zużycie stosu.

Większy kod.

Zalety

Ochrona przed niektórymi atakami.

Włączenie ochrony stosu

Aby GCC kompilowało programy z włączoną ochroną stosu należy użyć opcji -fstack-protector lub -fstack-protector-all. Ta pierwsza opcja stosuje pewną heurystykę i stosuje ochronę jedynie dla tablic znaków, a ta druga również dla innych typów.

gcc -fstack-protector     -o example example.c
gcc -fstack-protector-all -o example example.c

OpenBSD posiada zmodyfikowaną wersję tego kompilatora, w której, opcja ta jest włączona domyślnie i aby ją wyłączyć należy użyć przełącznika -fno-stack-protector.

W Linuksie istnieje możliwość włączenia tego mechanizmu dla architektury x86_64. Aby to uczynić należy włączyć opcję Procesor type features -> Enable -fstack-protector buffer overflow detection.

Inne sposoby ochrony stosu

Ustawienie odpowiednio uprawnień do stron - uniemożliwia wykonanie kodu ze stosu.

Losowe adresy bazowe i losowa kolejność ładowania bibliotek - utrudnia ustawienie odpowiedniego adresu powrotu (przy niektórych atakach)