Detektor wycieków danych oparty na DTA

https://chacker.pl/

Poprzednie przykładowe narzędzie wymaga tylko jednego koloru skażenia, ponieważ bajty są albo kontrolowane przez atakującego, albo nie. Teraz zbudujmy narzędzie, które używa wielu kolorów skażenia do wykrywania wycieków informacji w plikach, tak aby po odebraniu pliku można było określić, który plik został znaleziony. Idea tego narzędzia jest podobna do obrony opartej na int o nazwie ta przed błędem Heartbleed, który omówiliśmy w rozdziale 10, z tą różnicą, że tutaj narzędzie wykorzystuje odczyty z plików zamiast buforów pamięci jako źródło skażenia. Listing przedstawia pierwszą część tego nowego narzędzia, które nazwę dta -dataleak. Ponownie, dla zwięzłości pomijam pliki nagłówkowe standardowego języka C.

Listing : dta-dataleak.cpp

Podobnie jak w poprzednim narzędziu DTA, dta-dataleak zawiera plik pin.H i wszystkie odpowiednie pliki nagłówkowe libdft (1). Zawiera również znaną deklarację extern tablicy syscall_desc (2), która przechwytuje wywołania systemowe dla źródeł i odbiorników skażeń. Ponadto dta-dataleak definiuje pewne struktury danych, których nie było w dta-execve. Pierwsza z nich, fd2color, to mapa C++, która odwzorowuje deskryptory plików na kolory skażeń (3). Druga, również w C++, o nazwie color2fname, odwzorowuje kolory skażeń na nazwy plików (4). W kolejnych listingach zobaczysz, dlaczego te struktury danych są potrzebne. Istnieje również #define stałej o nazwie MAX_COLOR (5), która jest maksymalną możliwą wartością koloru skażenia, 0x80. Główna funkcja dta-dataleak jest niemal identyczna z funkcją dta-execve, ponieważ inicjuje Pin i libdft, a następnie uruchamia aplikację. Jedyną różnicą jest to, które źródła i odbiorniki taintów definiuje dta-dataleak. Instaluje on dwa programy obsługi post-handlerów, zwane post_open_hook (6) i post_read_hook (7), które są uruchamiane tuż po wywołaniach systemowych open i read. Hak open śledzi, które deskryptory plików są otwarte, podczas gdy hak read jest faktycznym źródłem taintów, który taintuje bajty odczytane z otwartych plików, co wyjaśnię za chwilę. Ponadto dta-dataleak instaluje program obsługi wstępnej dla wywołania systemowego socketcall o nazwie pre_socketcall_hook (8). Pre_socketcall_hook to odbiornik taintów, który przechwytuje wszelkie dane, które mają zostać wysłane przez sieć, aby upewnić się, że dane nie są skażone przed zezwoleniem na wysłanie. Jeśli istnieje ryzyko wycieku jakichkolwiek zanieczyszczonych danych, funkcja pre_socketcall_hook generuje alert za pomocą funkcji o nazwie alert, którą wyjaśnię poniżej. Należy pamiętać, że to przykładowe narzędzie jest uproszczone. W prawdziwym narzędziu należy podłączyć dodatkowe źródła skażenia (takie jak wywołanie systemowe readv) i odbiorniki (takie jak wywołania systemowe write na gnieździe), aby zapewnić kompletność. Należy również zaimplementować reguły określające, które pliki mogą wyciekać przez sieć, a które nie, zamiast zakładać, że wszystkie wycieki plików są złośliwe. Przyjrzyjmy się teraz funkcji alert, pokazanej na listingu, która jest wywoływana, gdy jakiekolwiek zanieczyszczone dane mają wyciekać przez sieć. Ponieważ jest ona podobna do funkcji alert programu dta-execve, opiszę ją tutaj tylko pokrótce.

Listing : dta-dataleak.cpp (ciąg dalszy)

Funkcja alertu rozpoczyna się od wyświetlenia komunikatu ostrzegawczego, szczegółowo określającego, który adres jest skażony i jakimi kolorami (1). Możliwe, że dane wyciekające przez sieć są zanieczyszczone wieloma plikami i dlatego są skażone wieloma kolorami. Dlatego alert przechodzi przez wszystkie możliwe kolory skażenia (2) i sprawdza, które z nich znajdują się w znaczniku skażonego bajtu, który spowodował alert (3). Dla każdego koloru włączonego w znaczniku alert wyświetla kolor i odpowiadającą mu nazwę pliku (4), którą odczytuje ze struktury danych color2fname. Na koniec alert wywołuje exit, aby zatrzymać aplikację i zapobiec wyciekowi danych (5). Następnie przeanalizujmy źródła skażenia dla narzędzia dta-dataleak.

Omijanie DTA za pomocą przepływów niejawnych

https://chacker.pl/

Jak dotąd wszystko w porządku: dta-execve pomyślnie wykrył i zatrzymał atak przejęcia kontroli opisany w poprzedniej sekcji. Niestety, dta-execve nie jest całkowicie niezawodny, ponieważ praktyczne systemy DTA, takie jak libdft, nie potrafią śledzić danych propagowanych przez przepływy niejawne. Listing 11-9 przedstawia zmodyfikowaną wersję serwera execve-test-overflow, która zawiera niejawny przepływ uniemożliwiający dta-execve wykrycie ataku. Dla zwięzłości, listing pokazuje tylko te fragmenty kodu, które różnią się od kodu oryginalnego serwera.

Listing : execve-test-overflow-implicit.c

Jedyne zmienione fragmenty kodu znajdują się w funkcji exec_cmd, która zawiera podatną na ataki pętlę for, kopiującą wszystkie bajty z bufora odbiorczego buf do globalnego bufora prefiksu (1). Tak jak poprzednio, pętla nie obsługuje kontroli granic, więc prefiks przepełni się, jeśli wiadomość w buforze buf będzie zbyt długa. Teraz jednak bajty są kopiowane niejawnie w taki sposób, że przepełnienie nie jest wykrywane przez narzędzie DTA! Jak wyjaśniono w rozdziale 10, niejawne przepływy są wynikiem zależności sterujących, co oznacza, że ​​propagacja danych zależy od struktur sterujących, a nie od jawnych operacji na danych. Na listingu  ta struktura sterująca jest pętlą while. Dla każdego bajtu zmodyfikowana funkcja exec_cmd inicjuje znak c zerem (2), a następnie używa pętli while do zwiększania c, aż osiągnie on tę samą wartość co buf[i] (3), skutecznie kopiując buf[i] do c bez jawnego kopiowania jakichkolwiek danych. Na koniec c jest kopiowane do prefiksu (4). Ostatecznie efekt tego kodu jest taki sam, jak w oryginalnej wersji execve-test-overflow: bufor jest kopiowany do prefiksu. Kluczem jest jednak brak jawnego przepływu danych między buforem a prefiksem, ponieważ kopiowanie z bufora [i] do c jest implementowane za pomocą tej pętli while, co pozwala uniknąć jawnego kopiowania danych. Wprowadza to zależność kontrolną między buforem [i] a c (a tym samym, przechodnio, między buforem [i] a prefiksem [i]), której libdft nie jest w stanie śledzić. Gdy ponowisz atak z Listingu 11-8, zastępując execve-test-overflow przez execve-test-overflow-implicit, zobaczysz, że atak teraz kończy się sukcesem pomimo ochrony dta-execve! Możesz zauważyć, że jeśli używasz DTA do zapobiegania atakom na kontrolowany przez siebie serwer, możesz po prostu napisać serwer w taki sposób, aby nie zawierał niejawnych przepływów, które dezorientują libdft. Choć w większości przypadków jest to możliwe (choć nie trywialne), w analizie złośliwego oprogramowania trudno będzie obejść problem niejawnych przepływów, ponieważ nie masz kontroli nad kodem złośliwego oprogramowania, a samo oprogramowanie może zawierać celowo utworzone niejawne przepływy mające na celu utrudnienie analizy skażeń.

Wykrywanie próby przejęcia kontroli za pomocą narzędzia DTA

https://chacker.pl/

Aby sprawdzić, czy narzędzie dta-execve może zatrzymać atak opisany w poprzedniej sekcji, ponownie przeprowadzę ten sam atak. Tym razem jednak narzędzie execve-test-overflow będzie chronione przez narzędzie dta-execve. Wyniki przedstawiono na listingu

Listing : Wykrywanie próby przejęcia kontroli za pomocą narzędzia dta-execve

Ponieważ biblioteka libdft opiera się na narzędziu Pin, należy uruchomić Pin za pomocą dta-execve jako narzędzia Pin (1), aby zabezpieczyć execve-test-overflow za pomocą dta-execve. Jak widać, dodałem opcję -follow_execv do opcji Pin, aby Pin instrumentował wszystkie procesy potomne execve-test-overflow w taki sam sposób, jak proces nadrzędny. Jest to ważne, ponieważ podatny na atak execv jest wywoływany w procesie potomnym. Po uruchomieniu serwera execve-test-overflow chronionego za pomocą dta-execve, ponownie uruchamiam nc, aby połączyć się z serwerem (2). Następnie wysyłam ten sam ciąg exploita, który został użyty w poprzedniej sekcji (3), aby przepełnić bufor prefiksu i zmienić polecenie cmd. Należy pamiętać, że dta-execve wykorzystuje odebrane dane sieciowe jako źródła skażenia. Widać to na Listingu 11-8, ponieważ procedura obsługi wywołań gniazd wyświetla komunikat diagnostyczny informujący o przechwyceniu odebranego komunikatu (4). Następnie procedura obsługi wywołań gniazda zanieczyszcza wszystkie bajty odebrane z sieci (5). Następnie wydruk diagnostyczny z serwera informuje, że zaraz wykona on polecenie echo kontrolowane przez atakującego (6). Na szczęście tym razem dta-execve przechwytuje polecenie execv, zanim będzie za późno (7). Sprawdza skażenie wszystkich argumentów polecenia execv, zaczynając od polecenia execv (8). Ponieważ polecenie to jest kontrolowane przez atakującego za pośrednictwem przepełnienia bufora w sieci, dta-execve zauważa, że ​​polecenie jest zanieczyszczone kolorem 0x01. Generuje alert, a następnie zatrzymuje proces potomny, który ma wykonać polecenie atakującego, skutecznie zapobiegając w ten sposób atakowi (9). Jedynym wyjściem z serwera, które jest zwracane atakującemu, jest ciąg prefiksu, który sam podał (10), ponieważ został on wydrukowany przed poleceniem execv, które spowodowało przerwanie procesu potomnego przez dta-execve.

Udane przejęcie kontroli bez użycia DTA

https://chacker.pl/

Listing przedstawia łagodny przebieg polecenia execve-test-overflow, a następnie przykład wykorzystania przepełnienia bufora do wykonania polecenia wybranego przez atakującego zamiast daty. Niektóre powtarzające się fragmenty wyniku zastąpiłem znakami „…”, aby wiersze kodu nie były zbyt szerokie.

Listing : Przejęcie kontroli w execve-test-overflow

W przypadku łagodnego przebiegu uruchamiam serwer execve-test-overflow jako proces w tle (1), a następnie używam netcat (nc) do połączenia z serwerem (2). W nc wpisuję ciąg „foobar:” (3) i wysyłam go do serwera, który użyje go jako prefiksu wyjściowego. Serwer uruchamia polecenie date i zwraca bieżącą datę z prefiksem „foobar:” (4). Aby zademonstrować lukę w zabezpieczeniach związaną z przepełnieniem bufora, restartuję serwer (5) i łączę się z nim ponownie za pomocą nc (6). Tym razem wysyłany ciąg jest znacznie dłuższy (7), wystarczająco długi, aby przepełnić pole prefiksu w globalnej strukturze cmd. Składa się on z 32 As, które wypełniają 32-bajtowy bufor prefiksu, a następnie 32 B, które przepełniają bufor datefmt i ponownie go całkowicie wypełniają. Ostatnia część ciągu znaków przepełnia bufor cmd i stanowi ścieżkę do programu, który należy uruchomić zamiast daty, czyli ~/code/chapter11/echo. W tym momencie zawartość globalnej struktury cmd wygląda następująco:

Przypomnijmy, że serwer kopiuje zawartość struktury cmd do tablicy argv używanej przez program execv. W rezultacie przepełnienia program execv uruchamia program echo zamiast date! Bufor datefmt jest przekazywany do programu echo jako argument wiersza poleceń, ale ponieważ nie zawiera on końcowego NULL-a, prawdziwym argumentem wiersza poleceń, który widzi program echo, jest datefmt połączony z buforem cmd. Na koniec, po uruchomieniu programu echo, serwer zapisuje dane wyjściowe z powrotem do gniazda (8), które składają się z połączenia prefiksu, datefmt i cmd jako prefiksu, a następnie danych wyjściowych polecenia echo. Teraz, gdy wiesz, jak nakłonić program execve-test-overflow do wykonania niezamierzonego polecenia, dostarczając mu złośliwe dane wejściowe z sieci, sprawdźmy, czy narzędzie dta-execve zdoła powstrzymać ten atak!

Wykrywanie próby przejęcia kontroli nad przepływem

https://chacker.pl/

Aby przetestować zdolność programu dta-execve do wykrywania ataków przejęcia kontroli nad siecią, użyję programu testowego o nazwie execve-test-overflow. Listing  przedstawia pierwszą część kodu źródłowego, zawierającą funkcję główną. Aby zaoszczędzić miejsce, pomijam kod sprawdzający błędy i nieistotne funkcje w listach programów testowych. Jak zwykle, pełne wersje programów można znaleźć na maszynie wirtualnej.

Listing :  execve-test-overflow.c

Jak widać, execve-test-overflow to prosty program serwerowy, który otwiera gniazdo sieciowe (używając funkcji open_socket pominiętej w listingu) i nasłuchuje na localhost na porcie 9999 (1). Następnie odbiera komunikat z gniazda (2) i przekazuje go do funkcji o nazwie exec_cmd (3). Jak wyjaśnię w następnym listingu, exec_cmd to podatna na ataki funkcja, która wykonuje polecenie za pomocą execv i może zostać zaatakowana przez atakującego, który wysyła złośliwą wiadomość do serwera. Po zakończeniu działania exec_cmd zwraca deskryptor pliku, którego serwer używa do odczytania wyniku wykonanego polecenia (4). Na koniec serwer zapisuje wynik polecenia do gniazda sieciowego (5). Zwykle funkcja exec_cmd uruchamia program o nazwie date, aby uzyskać bieżącą godzinę i datę, a serwer następnie przesyła ten wynik przez sieć, poprzedzając go komunikatem otrzymanym wcześniej z gniazda. Jednak exec_cmd zawiera lukę w zabezpieczeniach, która pozwala atakującym na uruchomienie dowolnego polecenia, jak pokazano na Listingu.

Listing: execve-test-overflow.c (ciąg dalszy)

Serwer używa globalnej struktury o nazwie cmd do śledzenia polecenia i powiązanych z nim parametrów (1). Zawiera ona prefiks dla wyniku polecenia (komunikat wcześniej odebrany z gniazda) (2), a także ciąg formatu daty i bufor zawierający samo polecenie date. Chociaż Linux posiada domyślne narzędzie date, zaimplementowałem własne na potrzeby tego testu, które znajdziesz w pliku ~/code/chapter11/date. Jest to konieczne, ponieważ domyślne narzędzie date na maszynie wirtualnej jest 64-bitowe, czego biblioteka libdft nie obsługuje. Przyjrzyjmy się teraz funkcji exec_cmd, która rozpoczyna się od skopiowania komunikatu odebranego z sieci (przechowywanego w buforze) do pola prefiksu polecenia cmd (3). Jak widać, kopia nie posiada odpowiednich kontroli powiązań, co oznacza, że ​​atakujący mogliby wysłać złośliwą wiadomość, która spowodowałaby przepełnienie prefiksu, umożliwiając im nadpisanie sąsiednich pól w cmd, zawierających format daty i ścieżkę polecenia. Następnie exec_cmd kopiuje argument polecenia i formatu daty ze struktury cmd do tablicy argv, która ma być używana przez execv (4). Następnie otwiera potok (5) i używa forka (6), aby uruchomić proces potomny (7), który wykona polecenie i przekaże dane wyjściowe procesowi nadrzędnemu. Proces potomny przekierowuje stdout przez potok (8), aby proces nadrzędny mógł odczytać dane wyjściowe execv z potoku i przesłać je przez gniazdo. Na koniec proces potomny wywołuje execv z potencjalnie kontrolowanym przez atakującego poleceniem i argumentami jako danymi wejściowymi (9). Uruchommy teraz execve-test-overflow, aby zobaczyć, jak atakujący może wykorzystać lukę w zabezpieczeniach związaną z przepełnieniem prefiksu do przejęcia kontroli w praktyce. Najpierw uruchomię go bez ochrony narzędzia dta-execve, aby zobaczyć, czy atak się powiódł. Następnie włączę dta-execve, aby zobaczyć, jak wykrywa i zatrzymuje atak.

Usuwanie skażeń: sprawdzanie argumentów execve

https://chacker.pl/

Na koniec przyjrzyjmy się funkcji pre_execve_hook, hakowi wywołania systemowego, który uruchamia się tuż przed każdym wywołaniem execve i upewnia się, że dane wejściowe execve nie są skażone. Listing  przedstawia kod funkcji pre_execve_hook.

Listing : dta-execve.cpp (ciąg dalszy)

Pierwszą czynnością, którą wykonuje funkcja pre_execve_hook, jest analiza danych wejściowych funkcji execve na podstawie jej parametru ctx. Dane wejściowe to nazwa pliku programu, który ma zostać uruchomiony przez funkcję xecve (1), a następnie tablica argumentów (2) i tablica środowiskowa (3) przekazane do funkcji execve. Jeśli którekolwiek z tych danych wejściowych są skażone, funkcja pre_execve_hook wygeneruje alert. Aby sprawdzić, czy każde dane wejściowe są skażone, funkcja pre_execve_hook używa funkcji check_string_taint, którą opisałem wcześniej w listingu 11-2. Najpierw funkcja ta używa tej funkcji do weryfikacji, czy parametr nazwy pliku funkcji execve jest nieskażony (4). Następnie funkcja przechodzi przez pętlę po wszystkich argumentach funkcji execve (5) i sprawdza każdy z nich pod kątem skażenia (6). Na koniec funkcja pre_execve_hook przechodzi przez pętlę po tablicy środowiskowej (7) i sprawdza, czy każdy parametr środowiskowy jest nieskażony (8). Jeśli żaden z danych wejściowych nie jest zanieczyszczony, pre_execve_hook wykonuje się do końca, a wywołanie systemowe execve kontynuuje działanie bez żadnego alertu. Z drugiej strony, jeśli zostanie wykryty jakikolwiek zanieczyszczony plik wejściowy, program zostaje przerwany i wyświetlony zostaje komunikat o błędzie. To cały kod narzędzia dta-execve! Jak widać, biblioteka libdft pozwala na zwięzłą implementację narzędzi DTA. W tym przypadku przykładowe narzędzie składa się z zaledwie 165 wierszy kodu, wliczając wszystkie komentarze i wydruki diagnostyczne. Skoro już poznałeś cały kod dta-execve, przetestujmy, jak dobrze potrafi ono wykrywać ataki.

Źródła skażenia: skażenie odebranych bajtów

https://chacker.pl/

Skoro już wiesz, jak sprawdzić kolor skażenia dla danego adresu pamięci, omówmy teraz, jak w ogóle skażać bajty. Listing  przedstawia kod funkcji post_socketcall_hook, która jest źródłem skażenia wywoływanym zaraz po każdym wywołaniu systemowym socketcall i skazuje bajty odebrane z sieci.

W bibliotece libdft, haki wywołań systemowych, takie jak post_socketcall_hook, to funkcje typu void, które przyjmują syscall_ctx_t* jako jedyny argument wejściowy. W listingu 11-3 nazwałem ten argument wejściowy ctx i działa on jako deskryptor wywołania systemowego, które właśnie miało miejsce. Zawiera on między innymi argumenty przekazane do wywołania systemowego oraz wartość zwracaną przez to wywołanie. Hak sprawdza ctx, aby określić, które bajty (jeśli w ogóle) mają zostać skażone. Wywołanie systemowe socketcall przyjmuje dwa argumenty, które można zweryfikować, odczytując polecenie man socketcall. Pierwszym z nich jest int o nazwie call, który informuje, jakiego rodzaju jest to wywołanie gniazda, na przykład, czy jest to recv, czy recvfrom. Drugi, o nazwie args, zawiera blok argumentów dla wywołania gniazda w postaci unsigned long*. Funkcja post_socketcall_hook rozpoczyna się od analizy wywołania call (1) i args (2) z wywołania systemowego ctx. Aby uzyskać argument z wywołania systemowego ctx, należy odczytać odpowiedni wpis z jego pola arg (na przykład ctx->arg[SYSCALL_ARG0]) i rzutować go na właściwy typ. Następnie dta-execve używa przełącznika, aby rozróżnić możliwe typy wywołań. Jeśli call wskazuje, że jest to zdarzenie SYS_RECV lub SYS_RECVFROM (3), dta-execve analizuje je dokładniej, aby dowiedzieć się, które bajty zostały odebrane i wymagają skażenia. W przypadku domyślnym po prostu ignoruje wszystkie inne zdarzenia. Jeśli bieżącym zdarzeniem jest odbiór, następną rzeczą, jaką robi dta-execve, jest sprawdzenie wartości zwracanej przez wywołanie funkcji socketcall poprzez inspekcję ctx->ret (4). Jeśli jest ona mniejsza lub równa zeru, oznacza to, że nie odebrano żadnych bajtów, więc nic nie jest zanieczyszczone, a wywołanie systemowe po prostu zwraca wynik. Inspekcja wartości zwracanej jest możliwa tylko w programie obsługi końcowej, ponieważ w programie obsługi wstępnej wywołanie systemowe, które przechwytujesz, jeszcze nie nastąpiło. Jeśli odebrano bajty, należy przeanalizować tablicę args, aby uzyskać dostęp do argumentu recv lub recvfrom i znaleźć adres bufora odbioru. Tablica args zawiera argumenty w tej samej kolejności, co funkcja socket odpowiadająca typowi wywołania. W przypadku recv i recvfrom oznacza to, że args[0] zawiera numer deskryptora pliku gniazda (5), a args[1] zawiera adres bufora odbioru (6). Pozostałe argumenty nie są tutaj potrzebne, więc funkcja post_socketcall_hook ich nie analizuje. Biorąc pod uwagę adres bufora odbiorczego i wartość zwracaną funkcji socketcall (która wskazuje liczbę odebranych bajtów (7)), funkcja post_socketcall_hook może teraz zanieczyścić wszystkie odebrane bajty. Po wykonaniu kilku diagnostycznych wydruków odebranych bajtów, funkcja post_socketcall_hook ostatecznie zanieczyszcza odebrane bajty, wywołując tagmap_setn (8), funkcję biblioteki libdft, która może zanieczyścić dowolną liczbę bajtów jednocześnie. Jako pierwszy parametr przyjmuje ona uintptr_t reprezentujący adres pamięci, który jest pierwszym adresem, który zostanie zanieczyszczony. Następnym parametrem jest size_t, który określa liczbę bajtów do zanieczyszczenia, a następnie uint8_t zawierający kolor zanieczyszczenia. W tym przypadku ustawiłem kolor zanieczyszczenia na 0x01. Teraz wszystkie odebrane bajty są skażone, więc jeśli kiedykolwiek wpłyną na którykolwiek z danych wejściowych execve, dta-execve to zauważy i wygeneruje alert. Aby skazić tylko niewielką, stałą liczbę bajtów, libdft udostępnia również funkcje o nazwach tagmap_setb, tagmap_setw i tagmap_setl, które skazują odpowiednio jeden, dwa lub cztery kolejne bajty. Mają one argumenty równoważne tagmap_setn, z tym wyjątkiem, że pomijają parametr długości.

Sprawdzanie informacji o skażeniu

https://chacker.pl/

W poprzedniej sekcji pokazano, jak główna funkcja narzędzia dta-execve wykonuje wszystkie niezbędne inicjalizacje, konfiguruje odpowiednie haki wywołań systemowych, które będą służyć jako źródła i odbiorniki skażeń, a następnie uruchamia aplikację. W tym przypadku odbiornikiem skażeń jest hak wywołania systemowego o nazwie pre_execve_hook, który sprawdza, czy którykolwiek z argumentów execve jest skażony, co wskazuje na atak polegający na przejęciu kontroli. Jeśli tak, generuje alert i zatrzymuje atak, przerywając działanie aplikacji. Ponieważ sprawdzanie skażeń jest powtarzane dla każdego argumentu execve, zaimplementowałem je w osobnej funkcji o nazwie check_string_taint. Najpierw omówię check_string_taint, a następnie przejdę do kodu pre_execve_hook . Listing  przedstawia funkcję check_string_taint, a także funkcję alert, która jest wywoływana w przypadku wykrycia ataku.

Funkcja alert (1) po prostu wyświetla komunikat alertu ze szczegółami dotyczącymi skażonego adresu, a następnie wywołuje exit, aby zatrzymać aplikację i zapobiec atakowi. Faktyczna logika sprawdzania skażonych adresów jest zaimplementowana w funkcji check_string_taint (2), która przyjmuje dwa ciągi znaków jako dane wejściowe. Pierwszy ciąg (str) jest tym, który ma zostać sprawdzony pod kątem skażonych adresów, a drugi (source) jest ciągiem diagnostycznym przekazywanym do funkcji alert i przez nią wyświetlanym, określającym źródło pierwszego ciągu, którym jest ścieżka execve, parametr execve lub parametr środowiskowy. Aby sprawdzić skażoną strukturę str, funkcja check_string_taint wykonuje pętlę po wszystkich bajtach struktury str (3). Dla każdego bajtu sprawdza status skażonych adresów za pomocą funkcji tagmap_getb biblioteki libdft (4). Jeśli bajt jest skażony, wywoływana jest funkcja alert, która wyświetla błąd i kończy działanie (5). Funkcja tagmap_getb przyjmuje adres pamięci bajtu (w formie uintptr_t) jako dane wejściowe i zwraca bajt cienia zawierający kolor skażenia dla tego adresu. Kolor skażenia (nazywany tag w Listingu 11-2) to uint8_t, ponieważ biblioteka libdft przechowuje jeden bajt cienia na bajt pamięci. Jeśli tag jest równy zero, bajt pamięci jest nieskażony. Jeśli nie jest równy zero, bajt jest skażony, a kolor tagu może posłużyć do ustalenia źródła skażenia. Ponieważ to narzędzie DTA ma tylko jedno źródło skażenia (odbiór sieciowy), używa tylko jednego koloru skażenia. Czasami może zaistnieć potrzeba pobrania tagu skażenia wielu bajtów pamięci jednocześnie. W tym celu biblioteka libdft udostępnia funkcje tagmap_getw i tagmap_getl, które są analogiczne do tagmap_getb, ale zwracają dwa lub cztery kolejne bajty cienia naraz, odpowiednio w formie uint16_t lub uint32_t.

Wykorzystanie DTA do wykrywania zdalnego przejęcia kontroli

https://chacker.pl/

Pierwsze narzędzie DTA, które zobaczysz, zostało zaprojektowane do wykrywania niektórych typów ataków zdalnego przejęcia kontroli. Dokładniej, wykrywa ono ataki, w których dane otrzymane z sieci są wykorzystywane do kontrolowania argumentów wywołania execve. Zatem źródłami skażenia będą sieciowe funkcje odbiorcze recv i recvfrom, a wywołanie systemowe execve będzie odbiornikiem skażenia. Jak zwykle, pełny kod źródłowy można znaleźć na maszynie wirtualnej w pliku ~/code/chapter11. Starałem się, aby to przykładowe narzędzie było jak najprostsze, aby ułatwić zrozumienie tematu. Oznacza to, że z konieczności opiera się ono na założeniach upraszczających i nie wykryje wszystkich typów ataków przejęcia kontroli. W prawdziwym, pełnoprawnym narzędziu DTA należy zdefiniować dodatkowe źródła i odbiorniki skażenia, aby zapobiec większej liczbie typów ataków. Na przykład, oprócz danych otrzymanych za pomocą recv i recvfrom, należy uwzględnić dane odczytane z sieci za pomocą wywołania systemowego read. Co więcej, aby zapobiec zanieczyszczaniu nieszkodliwych odczytów plików, należy śledzić, które deskryptory plików odczytują dane z sieci, przechwytując wywołania sieciowe, takie jak accept. Po zrozumieniu działania poniższego przykładowego narzędzia, będziesz w stanie samodzielnie je udoskonalić. Ponadto libdft zawiera bardziej rozbudowane przykładowe narzędzie DTA, które implementuje wiele z tych udoskonaleń w celach referencyjnych. Można je znaleźć w pliku tools/libdft-dta.c w katalogu libdft, jeśli jesteś zainteresowany. Wiele narzędzi DTA opartych na bibliotece libdft przechwytuje wywołania systemowe, które służą jako źródła i odbiorniki taintów. W systemie Linux każde wywołanie systemowe ma swój własny numer, którego libdft używa do indeksowania tablicy syscall_desc. Listę dostępnych wywołań systemowych i powiązanych z nimi numerów można znaleźć w pliku /usr/include/x86_64-linux-gnu/asm/unistd_32.h dla architektury x86 (32-bitowej) lub w pliku /usr/include/asm-generic/unistd.h dla architektury x64.3. Przyjrzyjmy się teraz przykładowemu narzędziu o nazwie dta-execve. Listing  przedstawia pierwszą część kodu źródłowego.

Tutaj pokazuję tylko pliki nagłówkowe specyficzne dla narzędzi DTA opartych na libdft, ale jeśli jesteś zainteresowany, możesz zobaczyć pominięty kod w kodzie źródłowym na maszynie wirtualnej. Pierwszym plikiem nagłówkowym jest pin.H (1), ponieważ wszystkie narzędzia libdft to po prostu narzędzia Pin powiązane z biblioteką libdft. Następnie znajduje się kilka plików nagłówkowych, które razem zapewniają dostęp do API libdft (2). Pierwszy z nich, branch_pred.h, zawiera makra likely i likely, których można użyć do dostarczenia kompilatorowi wskazówek dotyczących przewidywania rozgałęzień, co wyjaśnię za chwilę. Następnie libdft_api.h, syscall_desc.h i tagmap.h zapewniają dostęp odpowiednio do podstawowego API libdft, interfejsu przechwytywania wywołań systemowych i tagmap (pamięci cienia). Po dołączeniu następuje deklaracja zewnętrzna tablicy syscall_desc (3), która jest strukturą danych używaną przez bibliotekę libdft do śledzenia haków wywołań systemowych. Dostęp do niej będzie potrzebny do podpięcia źródeł i ujść taintów. Rzeczywista definicja syscall_desc znajduje się w pliku źródłowym biblioteki libdft syscall_desc.c. Przyjrzyjmy się teraz głównej funkcji narzędzia dta-execve. Rozpoczyna ona od zainicjowania przetwarzania symboli Pin (4) w przypadku, gdy symbole są obecne w pliku binarnym, a następnie samego Pin (5). Kod inicjalizacji Pinu został przedstawiony w rozdziale 9, ale tym razem wartość zwracana przez PIN_Init jest sprawdzana za pomocą zoptymalizowanej gałęzi, oznaczonej makrem „prophemy”, aby poinformować kompilator, że mało prawdopodobne jest niepowodzenie PIN_Init. Ta wiedza może pomóc kompilatorowi w przewidywaniu gałęzi, co może pozwolić mu na nieco szybsze generowanie kodu. Następnie funkcja main inicjuje samą bibliotekę libdft za pomocą funkcji libdft_init (6), ponownie z zoptymalizowanym sprawdzeniem wartości zwracanej. Ta inicjalizacja pozwala bibliotece libdft skonfigurować kluczowe struktury danych, takie jak mapa tagów. Jeśli ta konfiguracja się nie powiedzie, funkcja libdft_init zwraca wartość różną od zera. W takim przypadku należy wywołać funkcję libdft_die, aby zwolnić wszelkie zasoby przydzielone przez bibliotekę libdft (7). Po zainicjowaniu funkcji Pin i libdft można zainstalować haki yscall, które pełnią funkcję źródeł i odbiorników skażeń. Należy pamiętać, że odpowiedni hak zostanie wywołany za każdym razem, gdy zinstrumentowana aplikacja (program, który chronisz narzędziem DTA) wykona odpowiednie wywołanie systemowe. W tym przypadku dta-execve instaluje dwa haki: funkcję obsługi końcowej (post-handler) o nazwie post_socketcall_hook, która uruchamia się zaraz po każdym wywołaniu systemowym socketcall (8) oraz funkcję obsługi wstępnej (pre-handler) uruchamianą przed wywołaniami systemowymi execve, o nazwie pre_execve_hook (9). Funkcja obsługi gniazd (socketcall) przechwytuje wszystkie zdarzenia związane z gniazdami w systemie Linux x86-32, w tym zdarzenia recv i recvfrom. Funkcja obsługi gniazd (post_socketcall_hook) rozróżnia różne typy zdarzeń gniazd, co wyjaśnię za chwilę. Aby zainstalować funkcję obsługi wywołań systemowych, należy wywołać funkcję syscall_set_post (dla funkcji obsługi końcowej) lub syscall_set_pre (dla funkcji obsługi wstępnej). Obie te funkcje przyjmują wskaźnik do wpisu w tablicy syscall_desc biblioteki libdft, w której ma zostać zainstalowany moduł obsługi, oraz wskaźnik do funkcji obsługującej, którą należy zainstalować. Aby uzyskać odpowiedni wpis w pliku syscall_desc, należy zindeksować go numerem wywołania systemowego, które chcesz przechwycić. W tym przypadku odpowiednie numery wywołań systemowych są reprezentowane przez nazwy symboliczne __NR_socketcall i __NR_execve, które można znaleźć w pliku /usr/include/i386-linux-gnu/asm/unistd_32.h dla architektury x86-32. Na koniec wywołujesz PIN_StartProgram, aby uruchomić zinstrumentowaną aplikację (10). Przypomnijmy z rozdziału 9, że PIN_StartProgram nigdy nie zwraca wartości, więc wartość return 0 na końcu funkcji main nigdy nie jest osiągana. Chociaż nie używam jej w tym przykładzie, biblioteka libdft umożliwia przechwytywanie instrukcji w niemal taki sam sposób, jak wywołania systemowe, jak pokazano na poniższym listingu:

Aby przechwycić instrukcje, należy globalnie zadeklarować zewnętrzną tablicę ins_desc (1) (analogicznie do syscall_desc) w narzędziu DTA, a następnie użyć ins_set_pre lub ins_set_post (2), aby zainstalować odpowiednio procedury obsługi pre- lub post-instrukcji. Zamiast numerów wywołań systemowych, indeksuje się tablicę ins_desc za pomocą nazw symbolicznych dostarczonych przez bibliotekę kodera/dekodera x86 firmy Intel (XED), dołączoną do Pin. XED definiuje te nazwy w wyliczeniu o nazwie xed_iclass_enum_t, a każda nazwa oznacza klasę instrukcji, taką jak X86_ICLASS_RET_NEAR. Nazwy klas odpowiadają mnemonikom instrukcji. Listę wszystkich nazw klas instrukcji można znaleźć online pod adresem https://intelxed.github.io/ref-manual/ lub w pliku nagłówkowym xed-iclass-enum.h dołączonym do Pin.

Polityka dotycząca skażenia

https://chacker.pl/

Polityka propagacji skażenia biblioteki libdft definiuje pięć następujących klas instrukcji.2 Każda z tych klas propaguje i scala skażenie w inny sposób.

ALU

Są to instrukcje arytmetyczne i logiczne z dwoma lub trzema operandami, takie jak add, sub, and, xor, div i imul. W przypadku tych operacji biblioteka libdft scala skażenie w taki sam sposób, jak w przykładach add i xor w tabeli 10-1 na stronie 273 — skażenie wyjściowe jest sumą (∪) skażenia operandów. Biblioteka libdft uważa wartości bezpośrednie za nieskażone, ponieważ atakujący nie ma możliwości, aby na nie wpłynąć.

XFER

Klasa XFER zawiera wszystkie instrukcje, które kopiują wartość do innego rejestru lub komórki pamięci, takie jak instrukcja mov. Ponownie, jest ona obsługiwana jak przykład mov w tabeli 10-1, za pomocą operacji przypisania (:=). W przypadku tych instrukcji biblioteka libdft po prostu kopiuje skazę z operandu źródłowego do operandu docelowego.

CLR

Jak sama nazwa wskazuje, instrukcje w tej klasie zawsze powodują, że ich operandy wyjściowe stają się nieskażone. Innymi słowy, biblioteka libdft ustawia skazę wyjściową na zbiór pusty (∅). Ta klasa zawiera pewne szczególne przypadki instrukcji z innych klas, takie jak operacja XOR operandu z samym sobą lub odejmowanie operandu od samego siebie. Obejmuje również instrukcje takie jak cpuid, w przypadku których atakujący nie ma kontroli nad wynikami.

SPECJALNE

Są to instrukcje wymagające specjalnych reguł propagacji skazy, nieobjętych przez inne klasy. Do tej klasy należą między innymi xchg i cmpxchg (gdzie skaza dwóch operandów jest zamieniana) oraz lea (gdzie skaza wynika z obliczenia adresu pamięci).

FPU, MMX, SSE

Ta klasa zawiera instrukcje, których biblioteka libdft obecnie nie obsługuje, takie jak instrukcje FPU, MMX i SSE. Gdy skażenie przepływa przez takie instrukcje, libdft nie może go śledzić, więc informacja o skażeniu nie jest propagowana do operandów wyjściowych instrukcji, co skutkuje niedoskażeniem.

Teraz, gdy znasz już libdft, zbudujmy narzędzia DTA z libdft!