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
Na zajęciach będziemy pracowali nad kodem następującego programu w C (plik sum.c).
#include <stdio.h> 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>
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:
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 (<main>: 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:
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ć
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:
Na przykład wypisanie 5 instrukcji, począwszy od szczytu stosu można uzyskać tak:
x /5i $rsp
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.
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.
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.
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); } 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:
root_me
po załadowaniu do pamięci.
bad_function
.
# echo "odpowiedni napis" | ./wlam
exit
, przyda się też lektura podręcznika man dla polecenia echo).
Załącznik | Wielkość |
---|---|
sum.c | 349 bajtów |
wlam.c | 296 bajtów |