Wykrywanie cykli

https://chacker.pl/

Być może zauważyłeś kolejną tylną krawędź na wykresie, prowadzącą od BB7 do BB4. Ta tylna krawędź indukuje cykl, ale nie naturalną pętlę, ponieważ wejście do pętli można wykonać „w środku” w BB6 lub BB7. Z tego powodu BB4 nie dominuje nad BB7, więc cykl nie spełnia definicji pętli naturalnej. Aby znaleźć takie cykle, w tym pętle naturalne, potrzebujesz tylko drzewa CFG, a nie drzewa dominacji. Wystarczy rozpocząć przeszukiwanie w głąb (DFS) od węzła wejściowego CFG, a następnie utworzyć stos, na którym umieszczasz dowolny blok podstawowy, który przechodzi DFS, i „wyciągasz” go z powrotem, gdy DFS się cofa. Jeśli DFS kiedykolwiek natrafi na blok podstawowy, który już znajduje się na stosie, oznacza to znalezienie cyklu. Załóżmy na przykład, że wykonujesz DFS na CFG pokazanym na rysunku . DFS rozpoczyna się w punkcie wejścia, BB1. Listing 6-9 pokazuje, jak ewoluuje stan DFS i w jaki sposób DFS wykrywa oba cykle w CFG (dla zwięzłości nie pokazuję, jak DFS kontynuuje działanie po znalezieniu obu cykli).

Najpierw DFS bada skrajnie lewą gałąź BB1, ale szybko się cofa, docierając do ślepego zaułka. Następnie wchodzi do środkowej gałęzi, prowadzącej od BB1 do BB3 i kontynuuje poszukiwania przez BB5, po czym ponownie trafia do BB3, znajdując w ten sposób cykl obejmujący BB3 i BB5 (1). Następnie wraca do BB5 i kontynuuje poszukiwania ścieżką prowadzącą do BB7, następnie BB4, BB6, aż w końcu ponownie trafia do BB7, znajdując drugi cykl (2).

Wykrywanie pętli

https://chacker.pl/

Jak sama nazwa wskazuje, celem wykrywania pętli jest znajdowanie pętli w kodzie. Na poziomie źródłowym słowa kluczowe, takie jak while lub for, ułatwiają identyfikację pętli. Na poziomie binarnym jest nieco trudniej, ponieważ pętle są implementowane za pomocą tych samych (warunkowych lub bezwarunkowych) instrukcji skoku, które są używane do implementacji rozgałęzień i przełączników if/else. Możliwość znajdowania pętli jest przydatna z wielu powodów. Na przykład, z perspektywy kompilatora, pętle są interesujące, ponieważ większość czasu wykonywania programu jest spędzana w pętlach (często cytowana wartość to 90 procent). Oznacza to, że pętle są interesującym celem optymalizacji. Z perspektywy bezpieczeństwa, analiza pętli jest przydatna, ponieważ luki w zabezpieczeniach, takie jak przepełnienia bufora, często występują w pętlach. Algorytmy wykrywania pętli stosowane w kompilatorach używają innej definicji pętli niż można by się intuicyjnie spodziewać. Algorytmy te szukają pętli naturalnych, czyli pętli o pewnych właściwościach poprawnego sformułowania, które ułatwiają ich analizę i optymalizację. Istnieją również algorytmy, które wykrywają każdy cykl w CFG, nawet te, które nie są zgodne ze ściślejszą definicją pętli naturalnej. Rysunek 6-10 przedstawia przykład CFG zawierającego pętlę naturalną, a także cykl, który nią nie jest.

Najpierw pokażę typowy algorytm używany do wykrywania pętli naturalnych. Następnie stanie się dla Ciebie jaśniejsze, dlaczego nie każdy cykl pasuje do tej definicji. Aby zrozumieć, czym jest pętla naturalna, musisz poznać pojęcie drzewa dominacji. Prawa strona rysunku przedstawia przykład drzewa dominacji, które odpowiada CFG przedstawionemu po lewej stronie rysunku. Mówi się, że blok podstawowy A dominuje nad innym blokiem podstawowym B, jeśli jedynym sposobem dotarcia do B z punktu wejścia CFG jest przejście najpierw przez A. Na przykład na rysunku 6-10, BB3 dominuje nad BB5, ale nie nad BB6, ponieważ do BB6 można również dotrzeć przez BB4. Zamiast tego BB6 jest zdominowany przez BB1, który jest ostatnim węzłem, przez który musi przebiegać każda ścieżka z punktu wejścia do BB6. Drzewo dominacji koduje wszystkie relacje dominacji w CFG. Teraz naturalna pętla jest indukowana przez tylną krawędź z bloku podstawowego B do A, gdzie A dominuje nad B. Pętla wynikająca z tej tylnej krawędzi zawiera wszystkie bloki podstawowe zdominowane przez A, z których istnieje ścieżka do B. Konwencjonalnie, sam B jest wykluczony z tego zbioru. Intuicyjnie, ta definicja oznacza, że ​​do pętli naturalnych nie można wejść gdzieś w środku, a jedynie w dobrze zdefiniowanym węźle nagłówkowym. Upraszcza to analizę pętli naturalnych. Na przykład na rysunku 6-10 widoczna jest naturalna pętla obejmująca bloki bazowe BB3 i BB5, ponieważ istnieje tylna krawędź łącząca BB5 z BB3, a BB3 dominuje nad BB5. W tym przypadku BB3 jest węzłem nagłówkowym pętli, BB5 węzłem pętli zwrotnej, a „ciało” pętli (które z definicji nie zawiera węzłów nagłówkowych ani pętli zwrotnej) nie zawiera żadnych węzłów.

Analiza przepływu sterowania

https://chacker.pl/

Celem każdej analizy binarnej jest uzyskanie informacji o właściwościach przepływu sterowania programu, właściwościach przepływu danych lub obu tych właściwościach. Analiza binarna, która analizuje właściwości przepływu sterowania, jest trafnie nazywana analizą przepływu sterowania, natomiast analiza zorientowana na przepływ danych – analizą przepływu danych. Rozróżnienie to opiera się wyłącznie na tym, czy analiza koncentruje się na sterowaniu, czy na przepływie danych; nie mówi nic o tym, czy analiza jest wewnątrzproceduralna czy międzyproceduralna, wrażliwa na przepływ czy niewrażliwa, wrażliwa na kontekst czy niewrażliwa. Zacznijmy od omówienia popularnego typu analizy przepływu sterowania, zwanego wykrywaniem pętli. W następnej sekcji omówimy kilka typowych analiz przepływu danych.

Wrażliwość na kontekst

https://chacker.pl/

Podczas gdy wrażliwość na przepływ bierze pod uwagę kolejność instrukcji, wrażliwość na kontekst bierze pod uwagę kolejność wywołań funkcji. Wrażliwość na kontekst ma znaczenie tylko w przypadku analiz międzyproceduralnych. Analiza międzyproceduralna niewrażliwa na kontekst oblicza pojedynczy, globalny wynik. Z drugiej strony, analiza wrażliwa na kontekst oblicza oddzielny wynik dla każdej możliwej ścieżki przez graf wywołań (innymi słowy, dla każdej możliwej kolejności, w jakiej funkcje mogą pojawić się na stosie wywołań). Należy zauważyć, że oznacza to, że dokładność analizy wrażliwej na kontekst jest ograniczona dokładnością grafu wywołań. Kontekst analizy to stan uzyskany podczas przechodzenia przez graf wywołań. Stan ten przedstawię jako listę wcześniej przebytych funkcji, oznaczoną jako < f1 , f2, … , fn >.

W praktyce kontekst jest zazwyczaj ograniczony, ponieważ bardzo duże konteksty sprawiają, że analiza wrażliwa na przepływ jest zbyt kosztowna obliczeniowo. Na przykład analiza może obliczać wyniki tylko dla kontekstów pięciu (lub dowolnej liczby) kolejnych funkcji, zamiast dla kompletnych ścieżek o nieokreślonej długości. Jako przykład korzyści płynących z analizy kontekstowej, spójrz na rysunek .

Rysunek pokazuje, jak wrażliwość na kontekst wpływa na wynik analizy wywołań pośrednich w opensshd v3.5. Celem analizy jest określenie możliwych celów pośredniego wywołania w funkcji channel_handler (wiersz, który zawiera (*ftab[c->type])(c, readset, writeset);). Miejsce pośredniego wywołania pobiera swój cel z tabeli wskaźników do funkcji, która jest przekazywana jako argument o nazwie ftab do funkcji channel_handler. Funkcja channel_handler jest wywoływana z dwóch innych funkcji: channel_prepare_select i channel_after_select. Każda z nich przekazuje swoją własną tabelę wskaźników do funkcji jako argument ftab. Analiza wywołań pośrednich niewrażliwych na kontekst prowadzi do wniosku, że pośrednie wywołanie w channel_handler może dotyczyć dowolnego wskaźnika do funkcji w tabeli channel_pre (przekazanej z channel_prepare_select) lub tabeli channel_post (przekazanej z channel_after_select). W efekcie stwierdza się, że zbiór możliwych celów jest sumą wszystkich możliwych zestawów w dowolnej ścieżce przez program (1). Natomiast analiza kontekstowa określa inny zbiór celów dla każdego możliwego kontekstu poprzednich wywołań. Jeśli channel_handler został wywołany przez channel_prepare_select, to jedynymi prawidłowymi celami są te z tabeli channel_pre, którą przekazuje do channel_handler (2). Z drugiej strony, jeśli channel_handler został wywołany z channel_after_select, to możliwe są tylko cele z tabeli channel_post (3). W tym przykładzie omówiłem tylko kontekst o długości 1, ale ogólnie kontekst może być dowolnie długi (o ile jest to najdłuższa możliwa ścieżka przez graf wywołań). Podobnie jak w przypadku wrażliwości na przepływ, zaletą wrażliwości na kontekst jest większa precyzja, a wadą większa złożoność obliczeniowa. Ponadto analizy kontekstowe muszą uwzględniać dużą liczbę stanów, które muszą być przechowywane, aby śledzić wszystkie różne konteksty. Co więcej, jeśli istnieją jakiekolwiek funkcje rekurencyjne, liczba możliwych kontekstów jest nieskończona, dlatego konieczne jest zastosowanie specjalnych środków, aby poradzić sobie z takimi przypadkami. Często stworzenie skalowalnej, kontekstowej wersji analizy może być niemożliwe bez uciekania się do kompromisów kosztów i korzyści, takich jak ograniczenie rozmiaru kontekstu.

Wrażliwość na przepływ

https://chacker.pl/

Analiza binarna może być wrażliwa na przepływ lub niewrażliwa na przepływ. Wrażliwość na przepływ oznacza, że analiza uwzględnia kolejność instrukcji. Aby to wyjaśnić, spójrz na poniższy przykład w pseudokodzie.

Kod pobiera liczbę całkowitą bez znaku z danych wprowadzonych przez użytkownika, a następnie wykonuje na niej pewne obliczenia. W tym przykładzie załóżmy, że chcesz przeprowadzić analizę, która próbuje określić potencjalne wartości, jakie może przyjąć każda zmienna; nazywa się to analizą zbiorów wartości. Wersja tej analizy niewrażliwa na przepływ po prostu ustaliłaby, że x może zawierać dowolną wartość, ponieważ otrzymuje ją z danych wprowadzonych przez użytkownika. Chociaż ogólnie rzecz biorąc, x może przyjąć dowolną wartość w pewnym momencie programu, nie dotyczy to wszystkich punktów programu.

Zatem informacje dostarczane przez analizę niewrażliwą na przepływ nie są zbyt precyzyjne, ale analiza ta jest stosunkowo tania pod względem złożoności obliczeniowej. Wersja analizy wrażliwa na przepływ dałaby dokładniejsze wyniki. W przeciwieństwie do wariantu niewrażliwego na przepływ, zapewnia ona oszacowanie możliwego zbioru wartości x w każdym punkcie programu, biorąc pod uwagę poprzednie instrukcje. W (1) analiza stwierdza, że x może mieć dowolną wartość bez znaku, ponieważ jest pobierana z danych wejściowych użytkownika i nie było jeszcze żadnych instrukcji ograniczających wartość x. Jednak w (2) można doprecyzować oszacowanie: ponieważ wartość 5 jest dodawana do x, wiadomo, że od tego momentu x może mieć wartość co najmniej 5. Podobnie, po instrukcji w (3) wiadomo, że x jest co najmniej równe 15. Oczywiście, w rzeczywistości sprawy nie są tak proste, gdzie trzeba radzić sobie ze złożonymi konstrukcjami, takimi jak rozgałęzienia, pętle i (rekurencyjne) wywołania funkcji, zamiast prostego kodu liniowego. W rezultacie analizy wrażliwe na przepływ są zazwyczaj znacznie bardziej złożone i wymagają większych nakładów obliczeniowych niż analizy niewrażliwe na przepływ.

Analiza międzyproceduralna i wewnątrzproceduralna

https://chacker.pl/

Przypomnijmy, że funkcje są jedną z podstawowych struktur kodu, które deasemblery próbują odtworzyć, ponieważ analiza kodu na poziomie funkcji jest bardziej intuicyjna. Innym powodem stosowania funkcji jest skalowalność: niektóre analizy są po prostu niewykonalne w przypadku zastosowania do całego programu. Liczba możliwych ścieżek w programie rośnie wykładniczo wraz z liczbą przeniesień sterowania (takich jak skoki i wywołania) w programie. W programie z zaledwie 10 rozgałęzieniami if/else istnieje do 210 = 1;024 możliwych ścieżek w kodzie. W programie ze stu takimi rozgałęzieniami istnieje do 1,27 1030 możliwych ścieżek, a tysiąc rozgałęzień daje do 1,07 x 10301 ścieżek! Wiele programów ma znacznie więcej rozgałęzień, więc analiza każdej możliwej ścieżki w nietrywialnym programie nie jest obliczeniowo wykonalna. Dlatego kosztowne obliczeniowo analizy binarne często mają charakter wewnątrzproceduralny: uwzględniają kod tylko w obrębie jednej funkcji na raz. Zazwyczaj analiza wewnątrzproceduralna analizuje CFG każdej funkcji po kolei. Jest to w przeciwieństwie do analizy międzyproceduralnej, która traktuje cały program jako całość, zazwyczaj łącząc wszystkie CFG funkcji za pomocą grafu wywołań. Ponieważ większość funkcji zawiera zaledwie kilkadziesiąt instrukcji przekazania sterowania, złożone analizy są wykonalne obliczeniowo na poziomie funkcji. Jeśli indywidualnie przeanalizujesz 10 funkcji z 1024 możliwymi ścieżkami każda, przeanalizujesz łącznie 10  x 1024 = 10240 ścieżek; to znacznie więcej niż 102410 ≈  1,27 x  1030 ścieżek, które musiałbyś przeanalizować, gdybyś rozpatrywał cały program naraz. Wadą analizy wewnątrzproceduralnej jest to, że jest niekompletna. Na przykład, jeśli program zawiera błąd, który pojawia się dopiero po wykonaniu ściśle określonej kombinacji wywołań funkcji, narzędzie do wykrywania błędów wewnątrzproceduralnych go nie znajdzie. Po prostu rozważy każdą funkcję osobno i stwierdzi, że wszystko jest w porządku. Natomiast narzędzie międzyproceduralne wykryje błąd, ale może to zająć tak dużo czasu, że wyniki nie będą miały już znaczenia. Jako inny przykład, rozważmy, jak kompilator mógłby zoptymalizować kod pokazany na Listingu , w zależności od tego, czy stosuje optymalizację wewnątrzproceduralną, czy międzyproceduralną.

W tym przykładzie istnieje funkcja o nazwie „dead”, która przyjmuje pojedynczy parametr całkowity x i nic nie zwraca (1). Wewnątrz funkcji znajduje się gałąź, która wydrukuje komunikat tylko wtedy, gdy x jest równe 5 (2). Tak się składa, że funkcja „dead” jest wywoływana tylko z jednej lokalizacji, ze stałą wartością 4 jako argumentem (3). Zatem gałąź w punkcie (2) nigdy nie jest pobierana i żaden komunikat nie jest drukowany. Kompilatory używają optymalizacji zwanej eliminacją martwego kodu, aby znaleźć wystąpienia kodu, do których w praktyce nigdy nie można dotrzeć, dzięki czemu mogą pominąć taki bezużyteczny kod w skompilowanym pliku binarnym. W tym przypadku jednak czysto wewnątrzproceduralny przebieg eliminacji martwego kodu nie wyeliminowałby bezużytecznej gałęzi w punkcie (2). Dzieje się tak, ponieważ gdy przebieg optymalizuje funkcję „dead”, nie wie o żadnym kodzie w innych funkcjach, więc nie wie, gdzie i jak funkcja „dead” jest wywoływana. Podobnie, optymalizując funkcję main, nie może zajrzeć do funkcji dead, aby zauważyć, że konkretny argument przekazany do funkcji dead w punkcie (3) powoduje, że funkcja dead nie wykonuje żadnej czynności. Wymaga to analizy międzyproceduralnej, aby stwierdzić, że funkcja dead jest wywoływana z funkcji main tylko z wartością 4, co oznacza, że gałąź w punkcie (2) nigdy nie zostanie wykonana. Zatem wewnątrzproceduralna eliminacja martwego kodu spowoduje wyświetlenie całej funkcji dead (i jej wywołań) w skompilowanym pliku binarnym, mimo że nie ma ona żadnego celu, podczas gdy przejście międzyproceduralne pominie całą bezużyteczną funkcję.

Metody analizy fundamentalnej

https://chacker.pl/

Techniki dezasemblacji, które poznałeś do tej pory e, stanowią fundament analizy binarnej. Wiele zaawansowanych technik omówionych w kolejnych rozdziałach, takich jak instrumentacja binarna i wykonywanie symboliczne, opiera się na tych podstawowych metodach dezasemblacji. Zanim jednak przejdziemy do tych technik, chciałbym omówić kilka „standardowych” analiz, ponieważ mają one szerokie zastosowanie. Należy pamiętać, że nie są to samodzielne techniki analizy binarnej, ale można je wykorzystać jako składniki bardziej zaawansowanych analiz binarnych. O ile nie zaznaczę inaczej, wszystkie te techniki są zazwyczaj implementowane jako analizy statyczne, choć można je również modyfikować, aby działały w przypadku dynamicznych śladów wykonania.

Reprezentacje pośrednie

https://chacker.pl/

Zestawy instrukcji, takie jak x86 i ARM, zawierają wiele różnych instrukcji o złożonej semantyce. Na przykład, w x86, nawet pozornie proste instrukcje, takie jak add, mają skutki uboczne, takie jak ustawianie flag statusu w rejestrze eflags. Ogromna liczba instrukcji i skutków ubocznych utrudnia automatyczne rozumowanie programów binarnych. Na przykład, jak zobaczysz w rozdziałach od 10 do 13, dynamiczna analiza skażeń i silniki wykonywania symbolicznego muszą implementować jawne procedury obsługi, które przechwytują semantykę przepływu danych wszystkich analizowanych ijnstrukcji. Dokładna implementacja wszystkich tych procedur obsługi jest trudnym zadaniem. Reprezentacje pośrednie (IR), znane również jako języki pośrednie, zostały zaprojektowane w celu usunięcia tego obciążenia. IR to prosty język, który służy jako abstrakcja od języków maszynowych niskiego poziomu, takich jak x86 i ARM. Popularne IR to Reverse Engineering Intermediate Language (REIL) i VEX IR (IR używany w frameworku instrumentacji Valgrind12). Istnieje nawet narzędzie o nazwie McSema, które tłumaczy pliki binarne na kod bitowy LLVM (znany również jako LLVM IR). Idea jjjęzyków IR polega na automatycznym tłumaczeniu rzeczywistego kodu maszynowego, takiego jak kod x86, na kod IR, który uwzględnia całą semantykę kodu maszynowego, ale jest znacznie prostszy w analizie. Dla porównania, REIL zawiera tylko 17 różnych instrukcji, w przeciwieństwie do setek instrukcji w x86. Co wijęcej, języki takie jak REIL, VEX i LLVM IR jawnie wyrażają wszystkie operacje, bez ukrytych efektów ubocznych instrukcji. Implementacja etapu translacji z kodu maszynowego niskiego poziomu na kod IR nadal wymaga dużo pracy, ale po jej wykonaniu znacznie łatwiej jest zaimplementować nowe analizy binarne na podstawie przetłumaczonego kodu. Zamiast pisać procedury obsługi poszczególnych instrukcji dla każdej analizy binarnej, w przypadku języków IR wstarczy to zrobić tylko raz, aby zaimplementować etap translacji. Co więcej, można pisać translatory dla wielu ISA, takich jak x86, ARM i MIPS, i mapować je wszystkie na ten sam IR. W ten sposób każde narzędzie do analizy binarnej, które działa na tym IR, automatycznie dziedziczy obsługę wszystkich ISA obsługiwanych przez IR. Kompromisem związanym z tłumaczeniem złożonego zestawu instrukcji, takiego jak x86, na prosty język, taki jak REIL, VEX lub LLVM IR, jest to, że języki IR są znacznie mniej zwięzłe. Jest to nieodłączna konsekwencja wyrażania złożonych operacji, wraz ze wszystkimi efektami ubocznymi, za pomocą ograniczonej liczby prostych instrukcji. Zazwyczaj nie stanowi to problemu w przypadku analiz automatycznych, ale zazwyczaj utrudnia ludziom odczytanie pośrednich reprezentacji. Aby dać ci wyobrażenie o tym, jak wygląda IR, spójrz na Listing , który pokazuje, jak instrukcja x86-64 add rax,rdx jest tłumaczona na VEX IR.

Jak widać, pojedyncza instrukcja dodawania generuje 10 instrukcji VEX plus kilka metadanych. Po pierwsze, są tam metadane mówiące, że jest to superblok IR (IRSB) (1) odpowiadający jednej instrukcji maszynowej. IRSB zawiera cztery wartości tymczasowe oznaczone od t0 do t3, wszystkie typu Ity_I64 (64-bitowa liczba całkowita) (2). Następnie mamy IMark (3), czyli metadane określające między innymi adres i długość instrukcji maszynowej. Następnie mamy rzeczywiste instrukcje IR modelujące dodawanie. Po pierwsze, są to dwie instrukcje GET, które pobierają 64-bitowe wartości z rax i rdx do pamięci tymczasowych odpowiednio t2 i t1 (4). Należy zauważyć, że w tym przypadku rax i rdx to jedynie symboliczne nazwy części stanu VEX używanych do modelowania tych rejestrów — instrukcje VEX nie pobierają danych z rzeczywistych rejestrów rax lub rdx, lecz ze stanu lustrzanego tych rejestrów w VEX. Aby wykonać faktyczne dodawanie, IR używa instrukcji Add64 VEX, dodając dwie 64-bitowe liczby całkowite t2 i t1 i zapisując wynik w t0 (5). Po dodaniu występują pewne instrukcje PUT, które modelują efekty uboczne instrukcji add, takie jak aktualizacja flag stanu x86 (6). Następnie kolejna instrukcja PUT zapisuje wynik dodawania w stanie VEX reprezentującym rax (7). Na koniec IR VEX modeluje aktualizację licznika programu do następnej instrukcji (8). Ijk_Boring (Jump Kind Boring) (9) to wskazówka dotycząca przepływu sterowania, która mówi, że instrukcja add nie wpływa na przepływ sterowania w żaden interesujący sposób; ponieważ add nie jest żadnym rodzajem rozgałęzienia, sterowanie po prostu „przechodzi” do następnej instrukcji w pamięci. Natomiast instrukcje rozgałęzienia mogą być oznaczone wskazówkami takimi jak Ijk_Call lub Ijk_Ret, aby poinformować analizę, na przykład, że ma miejsce wywołanie lub powrót. Wdrażając narzędzia na bazie istniejącego frameworka analizy binarnej, zazwyczaj nie trzeba zajmować się reakcją na analizę binarną (IR). Framework sam zajmie się wszystkimi kwestiami związanymi z IR. Warto jednak wiedzieć o reakcjach na analizę binarną, jeśli planujesz wdrożyć własny framework analizy binarnej lub zmodyfikować istniejący.

Dekompilacja

https://chacker.pl/

Jak sama nazwa wskazuje, dekompilatory to narzędzia, które próbują „odwrócić proces kompilacji”. Zazwyczaj zaczynają od zdeasemblowanego kodu i tłumaczą go na język wyższego poziomu, zazwyczaj pseudokod podobny do C. Dekompilatory są przydatne przy odwracaniu dużych programów, ponieważ zdekompilowany kod jest łatwiejszy w odczycie niż wiele instrukcji asemblera. Dekompilatory ograniczają się jednak do ręcznego odwracania, ponieważ proces dekompilacji jest zbyt podatny na błędy, aby stanowić wiarygodną podstawę do jakiejkolwiek automatycznej analizy. Chociaż w tej książce nie będziemy korzystać z dekompilacji, spójrzmy na Listing 6-6, aby dać Ci wyobrażenie o tym, jak wygląda zdekompilowany kod. Najczęściej używanym dekompilatorem jest Hex-Rays, wtyczka dostarczana z IDA Pro. Listing 6-6 przedstawia wynik działania Hex-Rays.

Jak widać na listingu, zdekompilowany kod jest znacznie łatwiejszy do odczytania niż surowy kod asemblera. Dekompilator zgaduje sygnaturę funkcji (1) i zmienne lokalne (2). Co więcej, zamiast mnemoników asemblera, operacje arytmetyczne i logiczne są wyrażane bardziej intuicyjnie, za pomocą operatorów standardowych języka C (3). Dekompilator próbuje również zrekonstruować konstrukcje sterowania przepływem, takie jak rozgałęzienia if/else (4), pętle (5) i wywołania funkcji (6). Dostępna jest również instrukcja return w stylu języka C, ułatwiająca zobaczenie wyniku końcowego funkcji (7). Choć to wszystko jest przydatne, należy pamiętać, że dekompilacja to nic więcej niż narzędzie pomagające zrozumieć działanie programu. Zdekompilowany kod jest daleki od oryginalnego kodu źródłowego w języku C, może jawnie zawieść i jest obarczony wszelkimi niedokładnościami w samym procesie dekompilacji, a także w samym procesie dekompilacji. Dlatego też nakładanie bardziej zaawansowanych analiz na dekompilację nie jest generalnie dobrym pomysłem.