https://chacker.pl/

Zapoznałem Cię z dynamiczną analizą skażeń, jedną z najskuteczniejszych technik analizy binarnej. DTA pozwala śledzić przepływ danych od źródła skażenia do jego ujścia, co umożliwia zautomatyzowane analizy, od optymalizacji kodu po wykrywanie luk w zabezpieczeniach. Teraz, gdy znasz już podstawy DTA, możesz przejść dalej, gdzie zbudujesz praktyczne narzędzia DTA za pomocą biblioteki libdft.

Ćwiczenie

  1. Projektowanie detektora exploitów dla ciągów formatujących

Luki w zabezpieczeniach ciągów formatujących to dobrze znana klasa błędów oprogramowania podatnych na wykorzystanie w językach programowania podobnych do C. Występują one, gdy funkcja printf zawiera ciąg formatujący kontrolowany przez użytkownika, np. printf(user) zamiast prawidłowego printf(„%s”, user). Dobrym wprowadzeniem do luk w zabezpieczeniach ciągów formatujących jest artykuł „Wykorzystywanie luk w zabezpieczeniach ciągów formatujących” dostępny pod adresem http://julianor.tripod.com/bc/formatstring-1.2.pdf. Zaprojektuj narzędzie DTA, które będzie wykrywać exploity dla ciągów formatujących uruchamiane z sieci lub wiersza poleceń. Jakie powinny być źródła i odbiorniki skażeń oraz jaki rodzaj propagacji i szczegółowości skażenia jest potrzebny? Pod koniec  będziesz w stanie zaimplementować swój detektor exploitów!

Pamięć cienia wielokolorowego

https://chacker.pl/

Silniki skażeń wielokolorowych i systemy x64 wymagają bardziej złożonych implementacji pamięci cienia. Na przykład, spójrzmy na drugi typ pamięci cienia pokazany na rysunku 10-2 (2). Obsługuje on osiem kolorów i wykorzystuje 1 bajt pamięci cienia na bajt pamięci wirtualnej. Ponownie widać, że bajty A, B i D są skażone (odpowiednio kolorami 0x01, 0x04 i 0x20), podczas gdy bajt C jest nieskażony. Należy zauważyć, że aby przechowywać skażenie dla każdego bajtu pamięci wirtualnej w procesie, niezoptymalizowana ośmiokolorowa pamięć cienia musi być tak duża, jak cała przestrzeń pamięci wirtualnej tego procesu! Na szczęście zazwyczaj nie ma potrzeby przechowywania bajtów cienia dla obszaru pamięci, w którym przydzielona jest sama pamięć cienia, więc można pominąć bajty cienia dla tego obszaru pamięci. Mimo to, bez dalszych optymalizacji, pamięć cienia nadal wymaga połowy pamięci wirtualnej. Można to jeszcze bardziej ograniczyć, dynamicznie przydzielając pamięć cienia tylko do części pamięci wirtualnej, które są faktycznie używane (na stosie lub stercie), kosztem dodatkowego narzutu czasu wykonania. Co więcej, strony pamięci wirtualnej, które nie są zapisywalne, nigdy nie mogą zostać skażone, więc można je bezpiecznie mapować na tę samą „wyzerowaną” stronę pamięci cienia. Dzięki tym optymalizacjom wielokolorowe DTA stają się łatwe w obsłudze, choć nadal wymagają dużej ilości pamięci. Ostateczny typ pamięci cienia pokazany na rysunku 10-2 obsługuje 32 kolory (3). Bajty A, B i D są skażone odpowiednio kolorami 0x01000000, 0x00800000 i 0x00000200, podczas gdy bajt C jest nieskażony. Jak widać, wymaga to 4 bajtów pamięci cienia na bajt pamięci, co stanowi dość duży narzut pamięci. Wszystkie te przykłady implementują pamięć cienia jako prostą mapę bitową, tablicę bajtów lub tablicę liczb całkowitych. Dzięki zastosowaniu bardziej złożonych struktur danych możliwe jest obsłużenie dowolnej liczby kolorów. Na przykład, można zaimplementować pamięć cienia, używając zestawu kolorów w stylu C++ dla każdego bajtu pamięci. Jednak takie podejście znacznie zwiększa złożoność i obciążenie systemu DTA w czasie wykonywania.

Pamięć cieni oparta na mapie bitowej

https://chacker.pl/

Prawa część rysunku przedstawia trzy różne typy pamięci cieni i sposób kodowania przez nie informacji o skażeniu dla bajtów A–D. Pierwszy typ pamięci cieni, pokazany w prawym górnym rogu rysunku , to mapa bitowa (1). Przechowuje ona jeden bit informacji o skażeniu na bajt pamięci wirtualnej, dzięki czemu może reprezentować tylko jeden kolor: każdy bajt pamięci jest albo skażony, albo nieskażony. Bajty A–D są reprezentowane przez bity 1101, co oznacza, że ​​bajty A, B i D są skażone, a bajt C nie. Chociaż mapy bitowe mogą reprezentować tylko jeden kolor, mają tę zaletę, że wymagają stosunkowo niewiele pamięci. Na przykład w 32-bitowym systemie x86 całkowity rozmiar pamięci wirtualnej wynosi 4 GB. Mapa bitowa pamięci cienia dla 4 GB pamięci wirtualnej wymaga tylko 4 GB/8 = 512 MB pamięci, pozostawiając pozostałe 7/8 pamięci wirtualnej dostępne do normalnego użytku. Należy pamiętać, że to podejście nie jest skalowalne w systemach 64-bitowych, gdzie przestrzeń pamięci wirtualnej jest znacznie większa.

Pamięć cienia

https://chacker.pl/

Do tej pory pokazałem, że moduły śledzenia skażeń mogą śledzić skażenie dla każdego rejestru lub bajtu pamięci, ale nie wyjaśniłem jeszcze, gdzie przechowują informacje o skażeniach. Aby przechowywać informacje o tym, które części rejestrów lub pamięci są skażone i jakim kolorem, silniki DTA utrzymują dedykowaną pamięć cienia. Pamięć cienia to obszar pamięci wirtualnej przydzielony przez system DTA w celu śledzenia stanu skażenia pozostałej części pamięci. Zazwyczaj systemy DTA przydzielają również specjalną strukturę w pamięci, w której śledzą informacje o skażeniach dla rejestrów procesora. Struktura pamięci cienia różni się w zależności od ziarnistości skażenia i liczby obsługiwanych kolorów skażenia. Rysunek przedstawia przykładowe układy pamięci cienia o ziarnistości bajtowej do śledzenia odpowiednio do 1, 8 lub 32 kolorów na bajt pamięci.

Lewa część rysunku przedstawia pamięć wirtualną programu uruchomionego z DTA. Dokładniej, przedstawia zawartość czterech bajtów pamięci wirtualnej, oznaczonych jako A, B, C i D. Razem bajty te przechowują przykładową wartość szesnastkową 0xde8a421f.

Zależności sterowania

https://chacker.pl/

Przypomnijmy, że śledzenie skażeń służy do śledzenia przepływów danych. Czasami jednak przepływy danych mogą być niejawnie modyfikowane przez struktury sterujące, takie jak rozgałęzienia w tzw. przepływie niejawnym. Na razie przyjrzyjmy się poniższemu przykładowi syntetycznemu:

var = 0;

while(cond–) var++;

W tym przypadku atakujący, który kontroluje warunek pętli cond, może określić wartość zmiennej var. Nazywa się to zależnością sterowania. Chociaż atakujący może kontrolować zmienną var za pomocą zmiennej cond, nie ma jawnego przepływu danych między tymi dwiema zmiennymi. W związku z tym systemy DTA, które śledzą tylko jawne przepływy danych, nie przechwycą tej zależności i pozostawią zmienną var nieskażoną, nawet jeśli cond jest skażona, co skutkuje niedostatecznym skażeniem. W niektórych badaniach próbowano rozwiązać ten problem, propagując skażenie z warunków rozgałęzień i pętli na operacje wykonywane z powodu rozgałęzienia lub pętli. W tym przykładzie oznaczałoby to propagację skażenia z cond do var. Niestety, takie podejście prowadzi do masowego nadmiernego skażenia, ponieważ skażone warunki rozgałęzienia są powszechne, nawet jeśli nie ma ataku. Rozważmy na przykład następujące kontrole sanityzacji danych wprowadzanych przez użytkownika:

if(is_safe(user_input)) funcptr = safe_handler;

else

funcptr = error_handler;

Załóżmy, że skażamy wszystkie dane wprowadzane przez użytkownika, aby sprawdzić je pod kątem ataków, a skażenie user_input rozprzestrzenia się na wartość zwracaną przez funkcję is_safe, która jest używana jako warunek rozgałęzienia. Zakładając, że sanityzacja danych wprowadzanych przez użytkownika jest przeprowadzona poprawnie, listing jest całkowicie bezpieczny pomimo skażonego warunku rozgałęzienia. Jednak systemy DTA, które próbują śledzić zależności sterujące, nie potrafią odróżnić tej sytuacji od niebezpiecznej, pokazanej na poprzednim listingu.

Te systemy zawsze będą skaziły funcptr, wskaźnik do funkcji, który wskazuje na procedurę obsługi danych wprowadzanych przez użytkownika. Może to powodować fałszywe alarmy, gdy później zostanie wywołany zanieczyszczony funcptr. Takie częste fałszywe alarmy mogą całkowicie uniemożliwić korzystanie z systemu. Ponieważ rozgałęzienia na danych wprowadzanych przez użytkownika są powszechne, a niejawne przepływy, z których może skorzystać atakujący, są stosunkowo rzadkie, większość systemów DTA w praktyce nie śledzi zależności sterujących.

Nadmierne i niedomierne skażenie

https://chacker.pl/

W zależności od polityki skażenia, system DTA może cierpieć na niedomierne skażenie, nadmierne skażenie lub oba te zjawiska. Niedomierne skażenie występuje, gdy wartość nie jest skażona, mimo że „powinna być”, co w naszym przypadku oznacza, że ​​atakujący może bezkarnie wpłynąć na tę wartość bez wiedzy użytkownika. Niedomierne skażenie może być wynikiem polityki skażenia, na przykład jeśli system nie obsługuje dodatkowo przypadków skrajnych, takich jak bity przepełnienia, jak wspomniano wcześniej. Może to również wystąpić, gdy skażenie przepływa przez nieobsługiwane instrukcje, dla których nie istnieje mechanizm obsługi propagacji skażenia. Na przykład biblioteki DTA, takie jak libdft, zazwyczaj nie mają wbudowanej obsługi instrukcji x86 MMX lub SSE, więc skażenie przepływające przez takie instrukcje może zostać utracone. Zależności sterujące również mogą powodować niedomierne skażenie, jak wkrótce zobaczysz.

Podobnie jak niedomierne skażenie, nadmierne skażenie oznacza, że ​​wartości zostają skażone, mimo że „nie powinny”. Prowadzi to do fałszywych alarmów, takich jak alerty, gdy nie ma faktycznego ataku. Podobnie jak w przypadku niedostatecznego skażenia, nadmierne skażenie może wynikać z polityki skażenia lub sposobu obsługi zależności kontroli. Chociaż systemy DTA dążą do minimalizacji niedostatecznego i nadmiernego skażenia, generalnie niemożliwe jest całkowite uniknięcie tych problemów przy jednoczesnym zachowaniu rozsądnej wydajności. Obecnie nie ma praktycznej biblioteki DTA, która nie byłaby narażona na niedostateczne lub nadmierne skażenie.

Zasady propagacji skażenia

https://chacker.pl/

Zasada propagacji skażenia systemu DTA opisuje sposób, w jaki system propaguje skażenie i łączy kolory skażenia, jeśli wiele przepływów skażenia przebiega jednocześnie. Tabela 10-1 pokazuje, jak skażenie rozprzestrzenia się przez kilka różnych operacji w przykładowej zasadzie propagacji skażenia dla systemu DTA z dokładnością do bajtów i dwoma kolorami: „czerwonym” (R) i „niebieskim” (B). Wszystkie operandy w przykładach składają się z 4 bajtów. Należy pamiętać, że możliwe są inne zasady propagacji skażenia, szczególnie w przypadku złożonych operacji, które wykonują nieliniowe transformacje na swoich operandach.

Przykłady propagacji skażenia dla systemu DTA o granularności bajtów z dwoma kolorami: czerwonym (R) i niebieskim (B)

W pierwszym przykładzie wartość zmiennej a jest przypisywana zmiennej c (1), co odpowiada instrukcji mov x86. W przypadku prostych operacji, takich jak ta, reguły propagacji skażenia są również proste: ponieważ wyjście c jest po prostu kopią a, informacja o skażeniu dla c jest kopią informacji o skażeniu a. Innymi słowy, operatorem scalania skażeń w tym przypadku jest :=, operator przypisania. Następnym przykładem jest operacja xor, c = a ⊕ b (2). W tym przypadku nie ma sensu po prostu przypisywać skażenia z jednego z operandów wejściowych do wyjścia, ponieważ wyjście zależy od obu wejść. Zamiast tego, powszechną strategią skażenia jest pobieranie sumy bajt po bajcie (∪) skażenia operandów wejściowych. Na przykład, najbardziej znaczący bajt pierwszego operandu jest skażony na czerwono (R), a w drugim operandzie na niebiesko (B). Zatem skazą najbardziej znaczącego bajtu wyjściowego jest suma tych bajtów, oznaczonych kolorem czerwonym i niebieskim (RB). Ta sama polityka sumowania bajt po bajcie jest stosowana do dodawania w trzecim przykładzie (3). Należy zauważyć, że w przypadku dodawania istnieje przypadek skrajny: dodanie 2 bajtów może spowodować przepełnienie bitu, który przepływa do najmniej znaczącego bitu (LSB) sąsiedniego bajtu. Załóżmy, że atakujący kontroluje tylko najmniej znaczący bajt jednego z operandów. Wówczas, w tym przypadku skrajnym, atakujący może spowodować przepełnienie 1 bitu do sąsiedniego bajtu, co pozwoli mu również częściowo wpłynąć na wartość tego bajtu. Można uwzględnić ten przypadek skrajny w polityce skazy, dodając jawne sprawdzenie i skazując sąsiedni bajt w przypadku wystąpienia przepełnienia. W praktyce wiele systemów DTA nie sprawdza tego przypadku skrajnego, aby prostsza i szybsza propagacja skazy była łatwiejsza. Przykład (4) jest szczególnym przypadkiem operacji xor. Biorąc xor operandu z samym sobą (c = a ⊕ a) zawsze daje na wyjściu zero. W tym przypadku, nawet jeśli atakujący kontroluje a, nadal nie będzie miał kontroli nad wyjściem c. Polityka skażenia polega zatem na usunięciu skażenia każdego bajtu wyjściowego poprzez ustawienie go na zbiór pusty (∅). Następnie wykonuje się operację przesunięcia w lewo o stałą wartość, c = a ≪ 6 (5). Ponieważ drugi operand jest stały, atakujący nie zawsze może kontrolować wszystkie bajty wyjściowe, nawet jeśli częściowo kontroluje wejście a. Rozsądną polityką jest propagowanie skażenia wejściowego tylko do tych bajtów wyjścia, które są (częściowo lub całkowicie) pokryte jednym ze skażonych bajtów wejściowych, w efekcie „przesuwając skażenie w lewo”. W tym przykładzie, ponieważ atakujący kontroluje tylko dolny bajt a i jest on przesunięty w lewo o 6 bitów, oznacza to, że skażenie z dolnego bajtu rozprzestrzenia się na dwa dolne bajty wyjścia. Z drugiej strony, w przykładzie (6), zarówno wartość, która jest przesunięta (a), jak i wartość przesunięcia (b), są zmienne. Atakujący, który kontroluje b, jak w przykładzie, może wpłynąć na wszystkie bajty danych wyjściowych. W ten sposób skaza b jest przypisywana do każdego bajtu wyjściowego. Biblioteki DTA, takie jak libdft, mają predefiniowaną politykę skażenia, co oszczędza Ci kłopotu z implementacją reguł dla wszystkich typów instrukcji. Możesz jednak modyfikować reguły dla każdego narzędzia osobno, dla tych instrukcji, dla których domyślna polityka nie do końca odpowiada Twoim potrzebom. Na przykład, jeśli wdrażasz narzędzie przeznaczone do wykrywania wycieków informacji, możesz chcieć poprawić wydajność, wyłączając propagację skazy poprzez instrukcje, które zmieniają dane nie do poznania.

Kolory skażenia

https://chacker.pl/

We wszystkich dotychczasowych przykładach zakładaliśmy, że wartość jest skażona lub nie. Wracając do naszej analogii z rzeką, było to dość proste do zrobienia przy użyciu tylko jednego koloru barwnika. Czasami jednak może zaistnieć potrzeba jednoczesnego śledzenia wielu rzek przepływających przez ten sam system jaskiń. Jeśli zabarwisz wiele rzek tylko jednym kolorem, nie będziesz dokładnie wiedzieć, jak się one łączą, ponieważ zabarwiona woda może pochodzić z dowolnego źródła. Podobnie w systemach DTA czasami chcesz wiedzieć nie tylko, że wartość jest skażona, ale skąd pochodzi skażenie. Możesz użyć wielu kolorów skażenia, aby zastosować inny kolor do każdego źródła skażenia, tak aby po dotarciu skażenia do ujścia można było dokładnie określić, które źródło wpływa na ten ujścia. W systemie DTA z dokładnością do bajtów i tylko jednym kolorem skażenia, potrzebujesz tylko jednego bitu do śledzenia skażenia dla każdego bajtu pamięci. Aby obsługiwać więcej niż jeden kolor, musisz przechowywać więcej informacji o skażeniu na bajt. Na przykład, aby obsługiwać osiem kolorów, potrzebny jest 1 bajt informacji o skażeniu na bajt pamięci. Na pierwszy rzut oka można by pomyśleć, że w 1 bajcie informacji o skażeniu można zapisać 255 różnych kolorów, ponieważ jeden bajt może przechowywać 255 różnych wartości niezerowych. Jednak takie podejście nie pozwala na mieszanie różnych kolorów. Bez możliwości mieszania kolorów nie będzie można rozróżnić przepływów skażeń, gdy dwa przepływy skażeń działają razem: jeśli wartość jest dotknięta przez dwa różne źródła skażeń, każde z własnym kolorem, nie będzie można zarejestrować obu kolorów w informacjach o skażeniach dotkniętej wartości. Aby obsługiwać mieszanie kolorów, należy użyć dedykowanego bitu na kolor skażenia. Na przykład, jeśli masz 1 bajt informacji o skażeniu, możesz obsługiwać kolory 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40 i 0x80. Następnie, jeśli konkretna wartość jest skażona zarówno kolorami 0x01, jak i 0x02, łączna informacja o skażeniu dla tej wartości wynosi 0x03, co jest bitowym OR dwóch kolorów. Możesz myśleć o różnych kolorach skażeń w kategoriach rzeczywistych kolorów, aby ułatwić sobie sprawę. Na przykład, możesz określić 0x01 jako „czerwony”, 0x02 jako „niebieski”, a połączony kolor 0x03 jako „fioletowy”.

Granularność skażenia

https://chacker.pl/

Granularność skażenia to jednostka informacji, za pomocą której system DTA śledzi skażenie. Na przykład, system bitowy śledzi, czy każdy pojedynczy bit w rejestrze lub pamięci jest skażony, podczas gdy system bajtowy śledzi informacje o skażeniu tylko dla każdego bajtu. Jeśli nawet 1 bit w danym bajcie jest skażony, system bajtowy oznaczy cały bajt jako skażony. Podobnie, w systemie słów, informacje o skażeniu są śledzone dla każdego słowa pamięci itd. Aby zobrazować różnicę między systemami DTA bitowymi a bajtowymi, rozważmy, jak skażenie rozprzestrzenia się poprzez operację bitową AND (&) na dwóch operandach o rozmiarze bajtu, z których jeden jest skażony. W poniższym przykładzie pokażę wszystkie bity każdego operandu osobno. Każdy bit jest otoczony polem. Białe pola reprezentują bity nieskażone, a szare bity skażone. Po pierwsze, oto jak skażenie rozprzestrzeniałoby się w systemie o dużej granularności bitowej:

Jak widać, wszystkie bity w pierwszym operandzie są skażone, podczas gdy w drugim operandzie nie ma żadnych bitów. Ponieważ jest to operacja bitowa AND, każdy bit wyjściowy może być ustawiony na 1 tylko wtedy, gdy oba operandy wejściowe mają wartość 1 na odpowiadającej im pozycji. Innymi słowy, jeśli atakujący kontroluje tylko pierwszy operand wejściowy, to jedynymi pozycjami bitów w wyjściu, na które może wpływać, są te, w których drugi operand ma wartość 1. Wszystkie pozostałe bity wyjściowe będą zawsze ustawione na 0. Dlatego w tym przykładzie tylko jeden bit wyjściowy jest skażony. Jest to jedyna pozycja bitowa, którą atakujący może kontrolować, ponieważ tylko ta pozycja jest ustawiona na 1 w drugim operandzie. W efekcie nieskażony drugi operand działa jak „filtr” skazy pierwszego operandu. Porównajmy to teraz z odpowiednią operacją w systemie DTA z granularnością bajtów. Dwa operandy wejściowe są takie same jak poprzednio.

Ponieważ system DTA z granularnością bajtową nie może rozpatrywać każdego bitu indywidualnie, całe wyjście jest oznaczane jako skażone. System po prostu widzi skażony bajt wejściowy i niezerowy drugi operand i dlatego wnioskuje, że atakujący może wpłynąć na operand wyjściowy. Jak widać, granularność systemu DTA jest ważnym czynnikiem wpływającym na jego dokładność: system z granularnością bajtową może być mniej dokładny niż system z granularnością bitową, w zależności od danych wejściowych. Z drugiej strony, granularność skażenia jest również ważnym czynnikiem wpływającym na wydajność systemu DTA. Kod instrumentacji wymagany do śledzenia skażenia indywidualnie dla każdego bitu jest złożony, co prowadzi do dużego obciążenia wydajności. Chociaż systemy z granularnością bajtową są mniej dokładne, pozwalają na prostsze reguły propagacji skażenia, wymagające jedynie prostego kodu instrumentacji. Ogólnie rzecz biorąc, oznacza to, że systemy z granularnością bajtową są znacznie szybsze niż systemy z granularnością bitową. W praktyce większość systemów DTA wykorzystuje granularność bajtową, aby osiągnąć rozsądny kompromis między dokładnością a szybkością.

Czynniki projektowe DTA: Granularność skażenia, kolory skażenia i zasady dotyczące skażenia

https://chacker.pl/

W poprzedniej sekcji DTA wymagało jedynie prostych reguł propagacji skażenia, a samo skażenie również było proste: bajt pamięci albo jest skażony, albo nie. W bardziej złożonych systemach DTA istnieje wiele czynników, które decydują o równowadze między wydajnością a wszechstronnością systemu. W tej sekcji poznasz trzy najważniejsze wymiary projektowe systemów DTA: granularność skażenia, liczbę kolorów i zasady dotyczące propagacji skażenia. Należy pamiętać, że DTA można wykorzystać do wielu różnych celów, w tym do wykrywania błędów, zapobiegania eksfiltracji danych, automatycznej optymalizacji kodu, analiz kryminalistycznych i innych. W każdym z tych zastosowań stwierdzenie, że wartość jest skażona, oznacza coś innego. Aby uprościć poniższą dyskusję, gdy wartość jest skażona, będę konsekwentnie interpretował to jako „atakujący może wpłynąć na tę wartość”.