GDB i analiza programów

GDB i analiza programów


Instalacja nakładki PEDA

Korzystanie z GDB bez dodatkowych narzędzi jest dosyć kłopotliwe. Narzędzie to oferuje bardzo nieporęczny, niskopoziomowy interfejs, którego opanowanie wymaga wiele wysiłku, a po opanowaniu używanie jest pracochłonne. Dlatego powstało wiele nakładek na GDB, które wspomagają różnego rodzaju działania. Na naszych zajęciach przyjrzymy się bliżej nakładce PEDA (ang. Python Exploit Development Assistance), która jest używana do wykazywania podatności aplikacji na ataki.

Instalacja nakładki PEDA sprowadza się do ściągnięcia jej kodu z repozytorium:

# git clone https://github.com/longld/peda.git ~/peda

i wpisaniu do pliku konfiguracyjnego GDB (.gdbinit) kodu ładującego procedury PEDA:

# echo "source ~/peda/peda.py" >> ~/.gdbinit

Pierwsze kroki w GDB (z PEDA)

Na zajęciach będziemy pracowali nad kodem następującego programu w C (plik sum.c).

#include 

int process(int a, int len)
{
  int ar[8];
  for (int i=0; i < len; i++)
    ar[i]=a+i;
  int sum = 0;
  for (int i=0; i < len; i++)
    sum+=ar[i];
  
  return sum;
}

int main(void)
{
    int n = 10;
    int i = 1;
    int sum = 0;
    for(;i <=n;i++)
      sum+=process(i, 8);
    printf("\n Sum is : [%d]\n",sum);
    return 0;
}

Dla ułatwienia naszej pracy skompilujemy go poleceniem:

# gcc -g sum.c -o sum

Możemy teraz uruchomić debugger na tak skompilowanym kodzie:

# gdb sum

Otrzymamy dłuższe powitanie, po którym pojawi się znak zachęty:

gdb-peda$

Możemy teraz uruchomić załadowany program, pisząc run lub po prostu r. Otrzymamy wtedy informacje o ładowaniu otoczenia wykonywanego programu, jego wyjście oraz raport z zamykania jego procesów:

Starting program: /home/alx/Praca/Zajecia/BSK/gdb/sum 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

 Sum is : [720]
[Inferior 1 (process 1141169) exited normally]
Warning: not running

Jednak ogromna siła GDB polega na tym, że możemy działanie programu zatrzymać we wskazanym punkcie i począwszy od tego punktu rozpocząć wykonanie programu i ewentualnie wpłynąć na jego działanie przez podmianę danych w trakcie działania programu.

Punkt zatrzymania biegu programu ustalamy, ustanawiając tzw. punkt przerwania (ang. breakpoint). Robimy to poleceniem

break  <wskazanie punktu>

lub

b <wskazanie punktu>

Często pierwszą rzeczą, jaką robimy po wejściu do GDB jest napisanie:

gdb-peda$ b main

W ten sposób ustawiamy sobie punkt przerwania na pierwszej instrukcji funkcji main. Jak widać możemy wstawiać symbole z tablicy symboli za punkty przerwania. W takim razie po poprzednich zajęciach możemy mieć ochotę napisać też:

gdb-peda$ b _start

Jednak pech chce, że po załadowaniu programu z informacjami dotyczącymi debugowania do pamięci, symbol _start jest niejednoznaczny, gdyż występuje on nie tylko w skompilowanym programie, ale i w dynamicznie ładowanych bibliotekach. Dlatego bezpieczniej jest założyć punkt przerwania na adresie wyczytanym za pomocą objdump.

gdb-peda$ b *0x0000000000401040

Wtedy będziemy rzeczywiście mogli prześledzić wykonanie kodu spod symbolu _start pochodzącego z naszego programu.

Wróćmy jednak do wykonywania funkcji main. PEDA oferuje tutaj pewne usprawnienie, bo zamiast przechodzić przez sekwencje ustawienia punktu przerwania na main i uruchomienia run, wystarczy, że uruchomimy polecenie

gdb-peda$ start

które automatyzuje te kroki.

Gdy już program nam się zatrzyma na początku funkcji main, możemy zacząć wykonywać go krok po kroku, używając poleceń

  • step (w skrócie s) do wykonania jednej instrukcji z granulacją określoną wierszami w kodzie źródłowym,
  • stepi (w skrócie si) do wykonania jednej instrukcji z granulacją wskazaną przez kod maszynowy,
  • next (w skrócie n) do wykonania jednej instrukcji z granulacją wskazaną w kodzie źródłowym, ale bez wchodzenia do wywołań,
  • nexti (w skrócie ni) do wykonania jednej instrukcji z granulacją wskazaną przez kod maszynowy, ale bez wchodzenia do wywołań.

Oczywiście wpisywanie w kółko nawet powyższych skrótów jest niewygodne, więc warto pamiętać, że naciśnięcie klawisza przejścia do nowego wiersza powoduje wykonanie ostatnio wpisanej instrukcji. Dodatkowo powyższe instrukcje wykonane z parametrem liczbowym (np. step 5) pozwalają na wykonanie wskazanej w parametrze liczby instrukcji. Zwykle też wygodne jest użycie

  • xuntil <punkt>
    prowadzące do wykonania kodu aż do osiągnięcia wskazanego punktu w kodzie.

Co PEDA wyświetla?

Po przejściu przez każdy krok wykonania GDB z nakładką PEDA wypisuje nam najważniejsze elementy kontekstu wykonania programu, są to w kolejności wyświetlania:

  • aktualna zawartość rejestrów,
  • okolice właśnie wykonywanej instrukcji,
  • okolice szczytu stosu.

Zawartość rejestrów jest przedstawiana w formie ciągu wierszy, z których każdy zaczyna się nazwą rejestru, po której następuje jego aktualna wartość, a dalej, jeśli wartość da się zinterpretować jako adres w aktualnej przestrzeni adresowej procesu, to wartość, jaka znajduje się pod adresem z rejestru. Rejestr flag ($eflags) opatrzony jest dodatkowo słownym opisem tego, jakie flagi są ustawione.

Okolice właśnie wykonywanej instrukcji zawierają zapisy postaci

   0x401183 <main>:     push   rbp
   0x401184 <main+1>:   mov    rbp,rsp
   0x401187 <main+4>:   sub    rsp,0x10
=> 0x40118b <main+8>:   mov    DWORD PTR [rbp-0xc],0xa
   0x401192 <main+15>:  mov    DWORD PTR [rbp-0x4],0x1
   0x401199 <main+22>:  mov    DWORD PTR [rbp-0x8],0x0
   0x4011a0 <main+29>:  jmp    0x4011b8 <main+53>
   0x4011a2 <main+31>:  mov    eax,DWORD PTR [rbp-0x4]

Instrukcja, która w następnym kroku ma być wykonana jest wyróżniona za pomocą znacznika =>. W każdym wierszu widzimy najpierw adres instrukcji, po którym występuje symboliczne wskazanie, jakiemu miejscu której procedury adres odpowiada (np. <main+8>), a następnie widzimy mnemonik instrukcji maszynowej wraz z jego argumentami.

Trzeci blok to blok stosu wygląda tak

0000| 0x7fffffffd628 --> 0x7ffff7dc4560 (<__libc_start_call_main+128>:	mov    edi,eax)
0008| 0x7fffffffd630 --> 0x400040 --> 0x400000006 
0016| 0x7fffffffd638 --> 0x40112a (
: sub rsp,0x8) 0024| 0x7fffffffd640 --> 0x1000006f0 0032| 0x7fffffffd648 --> 0x7fffffffd758 --> 0x7fffffffdb75 ("/irgendwie/irgendwo/sum") 0040| 0x7fffffffd650 --> 0x0 0048| 0x7fffffffd658 --> 0x7ea1c88705a356f0 0056| 0x7fffffffd660 --> 0x7fffffffd758 --> 0x7fffffffdb75 ("/irgendwie/irgendwo/sum")

Jego zawartość jest podobna w formie i znaczeniu do zawartości bloku rejestrów, ale informacje zaczynają się nie od nazwy rejestru, a od adresu na stosie. Widzimy tutaj dwa pola - w pierwszym znajduje się wielkość przesunięcia danej pozycji adresowej względem szczytu stosu, dalej znajduje się rzeczywisty adres pozycji. Po adresie, za strzałką --> znajduje się zawartość pozycji na stosie.

Dodatkowo wszystkie adresy są pokolorowane, dzięki czemu w miarę szybko można się zorientować, do czego one prowadzą. Adresy czerwone są wskaźnikami do kodu, adresy niebieskie do danych zmienialnych, adresy zielone to adresy do danych przeznaczonych tylko do odczytu.

Gdyby przytłaczał nas nadmiar informacji ze wszystkich trzech sekcji, możemy sobie wyświetlić tylko jedną z nich za pomocą odpowiednio:

  • context reg
  • context code
  • context stack

Jeśli interesuje nas kod źródłowy programu, który badamy (niestety ta możliwość jest dostępna tylko, gdy program został skompilowany z opcją -g, czego się w zasadzie nie robi dla kodu produkcyjnego), to możemy napisać

  • list

które polecenie wypisze nam fragment kodu źródłowego z okolic właśnie wykonywanej instrukcji maszynowej.

Wielkość kontekstu w razie potrzeby można regulować za pomocą parametru liczbowego, po poleceniu (po code lub stack).

Gdyby powyższe udogodnienia PEDA nam nie wystarczały, to możemy spróbować użyć podstawowego, służącego do wyświetlania danych polecenia GDB. Ma ono postać:

  • print <nazwa zmiennej>

Spowoduje to wypisanie zmiennej zadeklarowanej w kodzie źródłowym zgodnie z jej typem. Można używać też formy skróconej tego polecenia - p, a i też jako zmiennej użyć identyfikatora rejestru. Wreszcie można podpowiedzieć poleceniu nieco, w jakiej formie ma wypisać wynik

  • p/x  <nazwa zmiennej> - wypisze wartość zmiennej szesnastkowo,
  • p/d  <nazwa zmiennej> - wypisze wartość zmiennej dziesiętnie,
  • p/s <nazwa zmiennej> - wypisze wartość zmiennej jako napis.

Jeszcze jedna forma wypisywania danych możliwa jest do uzyskania za pomocą polecenia

x /nfu <adres>

Litera x jest tutaj skrótem od angielskiego examine. Literki nfu reprezentują tutaj symbolicznie różne opcjonalne parametry wypisywanej wartości:

  • n - wskazuje za pomocą liczby dziesiętnej, ile kolejnych wartości z pamięci ma zostać wypisane,
  • f - wskazuje, w jakim formacie mają być wypisywane wartości (dziesiętnie, szesnastkowo, jako napis, jako instrukcje asemblera i.in.),
  • u - rozmiar wypisywanej jednostki pamięci (b to bajty 8-bitowe, h to półsłowa 16-bitowe, w to słowa 32-bitowe, g to gigantyczne słowa 64-bitowe).

Na przykład wypisanie 5 instrukcji, począwszy od szczytu stosu można uzyskać tak:

x /5i $rsp

Przejście przez program

Spróbujmy teraz przejść się trochę po danym nam kodzie programu. Zaczniemy od wykonania

gdb-peda$ xuntil 0x4011a0

(0x4011a0 jest podejrzanym przez nas za pomocą objdump adresem początka pętli). Możemy się teraz zabawić w podmianę danych na gorąco:

gdb-peda$ set sum = 100

spowoduje, że zaczniemy sumowanie nie od zera, ale od 100, co da nam wyraźnie inny wynik końcowy, o czym się przekonamy, prosząc GDB o kontynuację wykonywania kodu za pomocą continue (lub po prostu c).

Trochę bardziej kłopotliwe jest zmienianie wartości zmiennych n czy i, gdyż te są uznawane za skróty poleceń GDB. Dlatego dla nich lepiej jest pisać od razu

gdb-peda$ set var n = 100

Możemy się też w każdej chwili przekonać, jaka jest wartość zmiennej sum w danym momencie

gdb-peda$ print sum

czy zmiennej n.

gdb-peda$ p n

lub jakiegoś rejestru

gdb-peda$ p $rcx

Ciekawy efekt uzyskamy, gdy wykonamy

gdb-peda$ xuntil 0x4011ac

(tu dostaniemy się tuż przed wywołanie process), a następnie zmienimy zawartość rejestru odpowiadającego za drugi argument wywołania:

gdb-peda$ set $esi = 13

Gdy liczbę 13 zamienimy na 14, efekt będzie jeszcze ciekawszy.

Poszukiwanie wzorców

Uruchommy nasz program od początku poleceniem start.

Ważnym narzędziem pozwalającym na stosunkowo szybkie trafienie w miejsce przetwarzania programu, jakie nas interesuje, jest wyszukanie jakiegoś napisu, który nam się pojawia w interfejsie użytkownika. W naszym przykładowym programie moglibyśmy poszukać napisu "Sum". Można to zrobić za pomocą:

gdb-peda$ searchmem "Sum"

Uzyskamy wtedy mniej więcej taki wynik:

Searching for 'Sum' in: None ranges
Found 2 results, display max 2 items:
sum : 0x402012 ("Sum is : [%d]\n")
sum : 0x403012 ("Sum is : [%d]\n")

Poszukiwanie to może być znacznie bardziej skomplikowane i poszukiwane mogą być wystąpienia pasujące do wyrażenia regularnego, a także może ono być ograniczone tylko do jakichś obszarów (np. możemy szukać jakiegoś wzorca w kodzie biblioteki dzielonej).

Jak już znajdziemy interesujący nas napis, to możemy poprosić GDB, aby zatrzymało się przy próbie odwołania do tego adresu:

rwatch *0x402013

(niekoniecznie musi to być początek znalezionego regionu). Gdy teraz uruchomimy wykonanie za pomocą c, nasz program zatrzyma się w jakimś miejscu. Polecenie bt wyświetli nam stos wywołanych do tej pory funkcji. Dzięki temu możemy się zorientować, jakie wywołanie biblioteczne powoduje odwołanie do tego naszego adresu (ta wiedza jest bardzo nieoczywista, gdy nie mamy do dyspozycji kodu źródłowego programu). W naszym wyniku zobaczymy, że jesteśmy gdzieś w środku wywołania funkcji __printf. Możemy teraz kilka razy użyć polecenia finish, aby wyjść z wywołania tej funkcji. Wtedy zaś możemy za pomocą odpowiedniego polecenia context rozejrzeć się, co doprowadziło do wypisania danego napisu.

Zabawa ze stosem

Jeszcze raz wystartujmy nasz program od początku. Następnie po uruchomieniu za pomocą start ustawmy punkt zatrzymania programu na funkcji process i puśćmy działanie programu do momentu trafienia na tę funkcję.

Po przyjrzeniu się stosowi możemy zauważyć, że adres powrotu z procedury process znajduje się tam pod adresem 0x7fffffffd5f8. Spróbujmy pod ten adres wpisać jakąś inną wartość:

gdb-peda$ set *0x7fffffffd5f8=0

Jeśli teraz poprosimy GDB o kontynuację działania programu (c), to okaże się, że program zakończy działanie na SIGSEGV. To jest łatwe do wytłumaczenia, bo przecież kazaliśmy mu skoczyć pod adres zerowy. Może ciekawsze jednak będzie, jeśli przed wykonaniem polecenia set nieco lepiej dobierzemy adres powrotu. Spróbujmy za pomocą

gdb-peda$ p _exit

dowiedzieć się, pod jakim adresem znajduje się funkcja _exit (uwaga: koniecznie z podkreśleniem na początku) i wpisać na stosie zamiast adresu powrotu z process właśnie ten adres:

set *(int64_t*)0x7fffffffd5f8=0x7ffff7e67520

(tutaj trochę staranności wymaga to, żeby pod wskazany adres trafiła wartość 64-bitowa, a nie 32-bitowa, powyższa forma zagwarantuje nam właśnie takie działanie).

Okazuje się, że tym razem funkcja wychodzi całkiem zgrabnie, bez niepotrzebnego alarmu w postaci błędu segmentacji.

Dalsze lektury

  • Dokumentacja GDB jest tutaj
  • Strona PEDA jest tutaj.

Ćwiczenie

Rozważmy program

#include 
#include 

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

void root_me(void) {
  printf("Ha, ha, mam Cię!!!\n");
}

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

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

gcc -g wlam.c -o wlam

Dla uzyskanego pliku wykonywalnego wykonaj następujące czynności:

  • Ustal, jaki adres ma kod funkcji root_me po załadowaniu do pamięci.
  • Ustal, za pomocą GDB, jakiej długości napis spowoduje nadpisanie adresu powrotu z funkcji bad_function.
  • Spreparuj za pomocą polecenia postaci
    # echo "odpowiedni napis" | ./wlam
    eksploita, który spowoduje wypisanie Ha, ha, mam Cię!!! i poprawne zakończenie programu. (Podpowiedź: do tego ostatniego użyj funkcji exit, przyda się też lektura podręcznika man dla polecenia echo).
  • Skrypt zawierający powyżej spreparowane polecenie prześlij do Moodle.
ZałącznikWielkość
sum.c349 bajtów
wlam.c296 bajtów