Gadżety

Poniższy artykuł poświęcimy omówieniu pewnej odmiany techniki ROP (Return-oriented programming) używającej tzw. gadżetów.

Motywacją za stosowaniem ROP w znaczącej części współcześnie występujących ataków jest potrzeba obejścia ochrony typu DEP (Data Execution Prevention). Zabezpieczenie to wymusza (po przejęciu kontroli nad licznikiem instrukcji w programie, np. w wyniku udanego ataku przepełnienia bufora) wykonanie skoku do kodu znajdującego się w pamięci wykonywalnej procesu. Może to być na przykład kod biblioteki C, a celem atakującego jest wywołanie odpowiedniej funkcji bibliotecznej umożliwiającej zmianę uprawnień do regionu pamięci tak, by stała się ona wykonywalna. Jeżeli atakującemu udało się uprzednio umieścić w tej pamięci własny shellcode to mógł w ten sposób skutecznie ominąć zabezpieczenie DEP.

Jednak o ile w 32-bitowej wersji architektury x86 ataki z rodziny ret2libc (skok do funkcji z biblioteki C lub pokrewny atak) wystarczają, to ta technika przestaje być skuteczna pod systemami 64-bitowymi. Jest to spowodowane zastosowaniem innych niż cdecl konwencji wywołań funkcji w tych architekturach — konwencji zakładających przekazywanie argumentów do funkcji, również tych bibliotecznych, za pomocą rejestrów, a nie tylko przez odłożenie ich na stosie. Stanowi to pewną techniczną przeszkodę na drodze do przeprowadzenia ataku ret2libc — potrzebujemy bowiem przed wykonaniem skoku do funkcji bibliotecznej umieścić najpierw w odpowiednich rejestrach argumenty do tej funkcji. Moglibyśmy to zrobić przez wykonanie sekwencji instrukcji maszynowych przed wykonaniem skoku do funkcji, ale należy pamiętać, że obecność DEP uniemożliwia wykonanie takich fragmentów kodu, które zostaną wstrzyknięte do programu (znajdą się one w pamięci, która nie jest wykonywalna). Receptą na rozwiązanie tego problemu jest wykonanie skoku do fragmentu kodu istniejącego już w pamięci wykonywalnej, który wypełni stosownymi wartościami wymagane rejestry.


Poniżej zaprezentujemy technikę wyszukiwania i składania gadżetów ROP. Zilustrujemy to przykładem odwołującym się wprawdzie do konstrukcji architektury 32-bitowej (gdzie z powodzeniem sprawdziłby się już zwykły ret2libc), ale ukazującym dobrze ogólną ideę techniki.

Naszym celem będzie wywołanie funkcji systemowej execve i uruchomienie procesu powłoki (w systemie Linux). W klasycznym podejściu (“nie ROP”) sprowadzałoby się to do wykonania następującej sekwencji instrukcji ustalających wartości rejestrów i zgłaszających przerwanie systemowe:

W dalszej części będziemy zakładać, że posiadamy kontrolę nad zawartością stosu procesu i, pośrednio przez modyfikację adresów powrotów, nad licznikiem instrukcji. Możliwość takiej kontroli daje nam dobrze znany program-ofiara podatny na atak z przepełnieniem bufora:

Przyjmujemy więc, że adres powrotu z funkcji foo nie jest chroniony przed nadpisaniem za pomocą kanarka. Dodatkowo, dla ułatwienia zadania atakującemu, wyłączamy globalnie mechanizm ASLR (randomizacja bazowych adresów, m.in., stosu i kodu bibliotek):

$ sudo sysctl -w kernel.randomize_va_space=0

Pozostawiamy jednak domyślnie włączoną ochronę DEP dla programu (kompilujemy bez przełącznika -z execstack).


Możemy wreszcie przystąpić do opracowywania ataku. Interesuje nas znalezienie w pamięci procesu istniejącego kodu, który ustawi zawartość rejestrów EAX, EBX, ECX oraz EDX na odpowiednie wartości, po czym wywoła przerwanie 0x80. Należy jednak oczekiwać, że dokładnie taka sekwencja instrukcji w pamięci po prostu nie istnieje. Kluczowa do dalszego działania będzie obserwacja, że szukany przez nas ciąg instrukcji nie musi zajmować spójnego obszaru w pamięci — potrzebne nam instrukcje mogą znajdować się w różnych miejscach kodu, a naszym zadaniem będzie takie pokierowanie sterowaniem programu, by zostały one wykonane kolejno po sobie.

Termin “gadżet” odnosi się więc do fragmentu kodu zawierającego kilka potrzebnych nam do ataku instrukcji maszynowych (często spotyka się również pojęcie “borrowed code chunks”). Przez umiejętne złożenie kilku gadżetów w dłuższy ciąg będziemy mogli “skleić” pełną funkcjonalność ataku. By takie składanie było możliwe gadżet musi zawierać instrukcję, która wykona skok do kolejnego gadżetu. Stąd kandydatami na gadżety ROP będą fragmenty kodu zlokalizowane bezpośrednio przed instrukcjami RET (lub ewentualnie call, z argumentem zapisanym w rejestrze którego zawartość można kontrolować, np. CALL EAX). Przypomnijmy — kontrolujemy zawartość stosu procesu, a więc możemy ją spreparować w ten sposób, by po przed wykonaniem ostatniej instrukcji RET z danego gadżetu, wierzchołek stosu zawierał adres kolejnego gadżetu. W ten sposób zbudujemy łańcuch wywołań gadżetów.

Zastanówmy się teraz jakie instrukcje powinny zawierać nasze gadżety. Potrzebujemy na przykład fragmentu, który umieści wartość 11 w rejestrze EAX. Należy jednak oczekiwać, że instrukcja MOV EAX, 0xb zawierająca tę właśnie stałą będzie rzadko spotykana. Zamiast tego użyjemy więc instrukcji POP EAX, która powinna być dużo powszechniejsza, a wartość 11 umieścimy na przygotowanym przez nas stosie.

Podobnie, wartości pozostałych, wymaganych w wywołaniu systemowym execve, rejestrów odłożymy na stosie. Jak przekonamy się za chwilę, odnajdziemy w pamięci gadżet zawierający sekwencję instrukcji POP EDX; POP ECX; POP EBX.

Łącznie do ataku użyjemy 3 gadżetów:

  1. zdejmującego wartość rejestru EAX ze stosu
  2. zdejmującego wartość kolejnych rejestrów: EDX, ECX oraz EBX ze stosu
  3. zgłaszającego przerwanie programowe 0x80

Wykonując przepełnienie bufora na stosie nadpiszemy adres powrotu z funkcji foo oraz zawartość stosu poniżej tego adresu tak, by odpowiadała ona następującemu układowi:

Pozostaje nam odszukać adresy potrzebnych nam gadżetów (w tym przypadku mamy szczęście i dokładnie takie gadżety w pamięci odnajdziemy — w ogólności może zaistnieć konieczność wybrania gadżetu zawierającego nieco inną sekwencję, ale równoważnych lub prawie równoważnych instrukcji, np. jeśli nie dysponujemy gadżetem POP EAX; RET zdejmującym wartość 11 ze stosu to możemy go zastąpić gadżetem XOR EAX, EAX; RET i następującym po nim 11-krotnym złożeniem gadżetów INC EAX; RET). Nieocenioną pomoc stanowić tu będzie rozszerzenie PEDA do debuggera gdb i komenda ropsearch:

Za jej pomocą możemy wyszukać gadżet zawierający podany ciąg instrukcji, w zadanym (opcjonalnie) regionie pamięci działającego procesu. Oczywiście rozmiar naszego programu-ofiary jest niewielki, co drastycznie zmniejsza szanse na odnalezienie stosownych gadżetów. Przez wzgląd na to przeszukiwanie “bazy” kodu wykonamy na bibliotece C, która ładowana jest do pamięci dynamicznie przy starcie procesu. W tym celu ustawimy breakpoint w funkcji main programu, uruchomimy go, po czym wyszukamy komendą ropsearch gadżet zawierający instrukcję POP EAX pochodzący z biblioteki C:

$ gdb victim
...
Reading symbols from victim...
gdb-peda$ break main
Breakpoint 1 at 0x80484b0
gdb-peda$ run
Starting program: /home/bo/victim
...
gdb-peda$ ropsearch "pop eax" libc
Searching for ROP gadget: 'pop eax' in: libc ranges
0xf7f96382: (b'58c3') pop eax; ret
[i wiele innych]
...

Nasz pierwszy gadżet zlokalizowany będzie pod adresem 0xf7f96382 (polegamy tutaj istotnie na wyłączonej randomizacji adresów).

Podobnie znajdujemy drugi gadżet:

gdb-peda$ ropsearch "pop edx; pop ecx; pop ebx" libc
Searching for ROP gadget: 'pop edx; pop ecx; pop ebx' in: libc ranges
0xf7ef9091: (b'5a595bc3') pop edx; pop ecx; pop ebx; ret

Tym razem taka sekwencja instrukcji występuje jedynie raz, w funkcji bibliotecznej o enigmatycznej nazwie:

Dodajmy, że to częścią której funkcji jest dany gadżet nie będzie mieć dalej żadnego znaczenia.

Wreszcie, pora na znalezienie trzeciego gadżetu:

gdb-peda$ ropsearch "int 0x80" libc
Searching for ROP gadget: 'int 0x80' in: libc ranges
0xf7eebf01: (b'cd805d5f5e5bc3') int 0x80; pop ebp; pop edi; pop esi; pop ebx; ret

Jest to najdłuższy ze znalezionych przez nas gadżetów, ale w tym konkretnym zastosowaniu instrukcje następujące po wywołaniu przerwania nie będą istotne — po udanym wywołaniu funkcji execve sterowanie nie wróci już do naszego programu. Polecenie ropsearch wyszukuje jednak tylko te gadżety, w których RET występuje wkrótce po zadanej przez nas instrukcji.

Ostatnim adresem, który musimy ustalić jest adres napisu ze ścieżką do programu /bin/bash. Napis ten umieścimy na początku przepełnianego bufora, a jego adres zostanie nam usłużnie wypisany przez sam program:

$ ./victim
0xffffd168

Za pomocą prostego skryptu w łączymy wymienione adresy w pakiet, którym nadpisany zostanie stos, zgodnie z podaną wyżej ilustracją:

Przy kompilacji programu-ofiary poinstruowaliśmy kompilator, by stosował wyrównanie stosu do 4 bajtów, przez co tuż pod 8-bajtowym buforem w funkcji foo znajduje się zapisana zawartość rejestru EBP, a następnie adres powrotu. Natomiast 12 pierwszych bajtów przekazanych na wejście programu to “/bin/bash\0aa” (dopełniamy sztucznie “/bin/bash\0” do 12 bajtów); kolejnymi 4 bajtami nadpisujemy adres powrotu.

Uruchomienie programu na tak spreparowanym wejściu daje nam oczekiwany efekt:

$ (./ropgadget.py; cat) | ./victim
0xffffd168
/bin/bash
whoami
bo

Altenatywnym rozwiązaniem dla wyszukiwarki ropsearch wbudowanej w zestaw PEDA jest skrypt ROPgadget, który instalowany jest jako część pakietu pwntools i może być uruchamiany bezpośrednio z konsoli. W odróżnieniu od ropsearch skrypt ROPgadget operuje na pliku binarnym programu, a nie na pamięci działającego procesu. W efekcie nie odnajdziemy wymienionych wcześniej gadżetów znajdujących się w bibliotece dzielonej poza programem. By temu zaradzić, na potrzeby zajęć laboratoryjnych zlinkujemy bibliotekę C statycznie do naszego programu-ofiary (kod biblioteki zostanie włączony do pliku ELF z programem):

gcc -m32 -mpreferred-stack-boundary=2 -fno-stack-protector -static victim.c -o victim

(Uwaga: ROPgadget obsługuje również dla pliki PE w systemie Windows)