0xcafebabe -- per aspera ad astra

Dokument pobrany z: http://www.anadoxin.org/blog/comment/reply/79

Tagi:  •    •    •    •  
Narzędzie objdump okazuje się niezastąpione przy szybkich analizach plików binarnych, lub wszędzie tam, gdzie mamuty typu IDA Pro przestają dawać radę.

Być może ostatnie kilka wyrazów wepchnęło cię w osłupienie ;), ale po prostu czasami nie warto tracić czasu na uruchomienie dedykowanych deasemblerów, nie warto tracić kontekstu pracy przy tworzeniu nowych okien tych programów, nie warto tracić nerwów przy próbie dowiedzenia się, że standard rynkowy inżynierii wstecznej nie obsługuje jakiegoś mniej popularnego formatu wykonywalnego mniej popularnej architektury, czy też zwyczajnie nie warto wytaczać ciężkich dział przy polowaniu na muchy.

Program objdump jest jednym z tych programów, które okazują się szwajcarskim scyzorykiem podczas analiz plików binarnych, których formaty mają jakiś związek z formatami używanymi do enkapsulacji informacji opisujących program; przy czym najczęstsze przypadki jego użycia to wyświetlanie symboli plików wykonywalnych lub obiektowych, wyświetlanie metadanych służących do opisu typów i struktur zmiennych używanych w programie (popularnie zwane debug info), wyświetlanie tablic relokacji programu, oraz deasemblacja, która zawiera kilka ciekawych opcji, które nie są wyszczególnione na stronie man programu objdump.

Notacja asemblera – AT&T lub Intel

Przede wszystkim, opcją -M można wybrać notację asemblera używaną w dead-listingach. Domyślna notacja nazywa się notacją AT&T i niektórych przyprawia o migrenę ;), mimo tego, że jest standardową notacją używaną na systemach UNIX'owych. Turyści z systemu Windows szybko się jednak zniechęcają, ponieważ na Windowsach króluje notacja Intel'owska. Opcja -M intel zmienia sposób reprezentacji notacji, więc każdy powinien poczuć się jak w domu.

Przykład notacji AT&T:

  1. 00000000004005c0 <__do_global_ctors_aux>:
  2.   4005c0:       55                      push   %rbp
  3.   4005c1:       48 89 e5                mov    %rsp,%rbp
  4.   4005c4:       53                      push   %rbx
  5.   4005c5:       48 83 ec 08             sub    $0x8,%rsp
  6.   4005c9:       48 8b 05 58 08 20 00    mov    0x200858(%rip),%rax
  7.   4005d0:       48 83 f8 ff             cmp    $0xffffffffffffffff,%rax
  8.   4005d4:       74 19                   je     4005ef <__do_global_ctors_aux+0x2f>
  9.   4005d6:       bb 28 0e 60 00          mov    $0x600e28,%ebx
  10.   4005db:       eb 03                   jmp    4005e0 <__do_global_ctors_aux+0x20>

Przykład notacji Intel:

  1. 00000000004005c0 <__do_global_ctors_aux>:
  2.   4005c0:       55                      push   rbp
  3.   4005c1:       48 89 e5                mov    rbp,rsp
  4.   4005c4:       53                      push   rbx
  5.   4005c5:       48 83 ec 08             sub    rsp,0x8
  6.   4005c9:       48 8b 05 58 08 20 00    mov    rax,QWORD PTR [rip+0x200858]
  7.   4005d0:       48 83 f8 ff             cmp    rax,0xffffffffffffffff
  8.   4005d4:       74 19                   je     4005ef <__do_global_ctors_aux+0x2f>
  9.   4005d6:       bb 28 0e 60 00          mov    ebx,0x600e28
  10.   4005db:       eb 03                   jmp    4005e0 <__do_global_ctors_aux+0x20>

Podkreślanie różnic pomiędzy tymi dwoma stylami reprezentacji kodu asm jest zupełnie bez celu – wielki Internet zawiera tysiące informacji na ten temat. Sugeruję użycie wielkiego brata, by wskazał prawidłową drogę ku oświeceniu ;).

Interpretowanie relokacji przy deasemblacji

Objdump nie interpretuje automatycznie relokacji w plikach. Dzieje się tak dlatego, że jest to dość podstawowy program, który nie ma zamiaru myśleć za użytkownika. Dlatego też domyślna opcja deasemblacji interpretuje opkody w sposób taki, jak są one zapisane w pliku – jeśli pewna część informacji zostanie nadpisana przez relokacje, wtedy faktyczny wygląd kodu w pamięci po załadowaniu programu będzie nieco odbiegał od informacji prezentowanych nam przez program objdump. Przykładowo, podczas deasemblacji pliku .o:

  1. 0000000000000000 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi>:
  2. [...]
  3.       54:       48 89 de                mov    %rbx,%rsi
  4.       57:       48 89 c7                mov    %rax,%rdi
  5.       5a:       e8 00 00 00 00          callq  5f <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x5f>
  6.       5f:       48 8b 85 78 fe ff ff    mov    -0x188(%rbp),%rax
  7.       66:       48 83 c0 28             add    $0x28,%rax
  8.       6a:       48 89 c7                mov    %rax,%rdi
  9.       6d:       e8 00 00 00 00          callq  72 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x72>
  10.       72:       48 8b 85 78 fe ff ff    mov    -0x188(%rbp),%rax
  11.       79:       48 83 c0 48             add    $0x48,%rax
  12.       7d:       48 89 c7                mov    %rax,%rdi
  13.       80:       e8 00 00 00 00          callq  85 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x85>

Offsety 5a, 6d i 80 zawierają instrukcję CALL, która woła pewną funkcję. Zgodnie z listingiem, jej adres to po prostu zero, co wydaje się błędne, ponieważ w tym wypadku funkcja wołałaby różne miejsca samej siebie trzy razy po kolei, co jest po prostu nielogiczne. Zamiast tego, podczas procesu ładowania pliku wykonywalnego do pamięci, niedługo przed rozpoczęciem jego wykonania, zera w argumencie opkodu E8 zostają relokowane przez program ładujący na inne wartości, które można obejrzeć korzystając z podglądu relokacji (przy pomocy argumentu -r):

  1. RELOCATION RECORDS FOR [.text]:
  2. OFFSET           TYPE              VALUE
  3. 000000000000005b R_X86_64_PC32     _ZN3Gtk4HBoxC2Ebi-0x0000000000000004
  4. 000000000000006e R_X86_64_PC32     _ZN13HotkeyInvokerC2Ev-0x0000000000000004
  5. 0000000000000081 R_X86_64_PC32     _ZN16MakeVisibleAwareImEC2Ev-0x0000000000000004

Nie jest to jednak wygodne rozwiązanie, by manualnie szukać offsetów relokowanych i zapamiętywać podstawienia przy odpowiednich adresach, dlatego obie opcje – deasemblacji i wyświetlania relokacji – można połączyć w opcję „relokacyjnej deasemblacji” (-dr):

  1. $ objdump -dr ./src/ui/widgets/hexeditor/HexEditorWidget.o | less
  2. [...]
  3.       5a:       e8 00 00 00 00          callq  5f <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x5f>
  4.                         5b: R_X86_64_PC32       _ZN3Gtk4HBoxC2Ebi-0x4
  5.       5f:       48 8b 85 78 fe ff ff    mov    -0x188(%rbp),%rax
  6.       66:       48 83 c0 28             add    $0x28,%rax
  7.       6a:       48 89 c7                mov    %rax,%rdi
  8.       6d:       e8 00 00 00 00          callq  72 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x72>
  9.                         6e: R_X86_64_PC32       _ZN13HotkeyInvokerC2Ev-0x4
  10.       72:       48 8b 85 78 fe ff ff    mov    -0x188(%rbp),%rax
  11.       79:       48 83 c0 48             add    $0x48,%rax
  12.       7d:       48 89 c7                mov    %rax,%rdi
  13.       80:       e8 00 00 00 00          callq  85 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x85>
  14.                         81: R_X86_64_PC32       _ZN16MakeVisibleAwareImEC2Ev-0x4

Od razu lepiej :)

Deasemblacja plików binarnych

Wybór rozmiaru kodu i rozmiaru adresowania zwykle jest wykonywany przez sam program deasemblujący na podstawie informacji zawartych w pliku kontenera (np. PE dla Windows, ELF dla UNIX'ów). Zwykle jest to informacja, czy zapisany w pliku kod jest kodem 16-bitowym, 32-bitowym, lub 64-bitowym. Problemem okazuje się plik, w którym nie ma formatu, a jedynie jest prostym zrzutem opkodów, np. pliki .com, czy pliki które sami stworzylismy poprzez Copy & Paste, czy inną metodę ;).

Karmiąc takimi plikami program objdump zwykle dostaniemy błąd w postaci nieznanej struktury pliku, jednak da się wymusić dekompilację na nasz wybrany sposób adresowania przy pomocy argumentu -b, -m oraz -M:

  1. $ objdump -D -b binary -mi386 -Maddr16,data16 DN.COM | less
  2.  
  3. DN.COM:     file format binary
  4.  
  5.  
  6. Disassembly of section .data:
  7.  
  8. 00000000 <.data>:
  9.    0:   e9 e9 04                        jmp    0x4ec
  10. […]
  11.  6dc:   ba f7 04                        mov    $0x4f7,%dx
  12.  6df:   b4 09                           mov    $0x9,%ah
  13.  6e1:   cd 21                           int    $0x21
  14.  6e3:   b8 2f 25                        mov    $0x252f,%ax
  15.  6e6:   2e c5 16 48 01                  lds    %cs:0x148,%dx
  16.  6eb:   cd 21                           int    $0x21
  17.  6ed:   b8 01 4c                        mov    $0x4c01,%ax
  18.  6f0:   cd 21                           int    $0x21

Co daje nam w rezultacie czysty 16-bitowy kod programu zapisanego w pliku .com. Adresowanie 32-bitowe można uzyskać modyfikując linię poleceń i zamieniając addr16,data16 na addr32,data32. Kod 64-bit wymagałby zmiany celu z i386 na cel 64-bitowy i zmianę trybu adresowania instrukcji i danych z 32 na 64 bity.

Deasemblacja od konkretnego offsetu

Czasami w kodzie znajduje się skok „w środek instrukcji”, co oczywiście nie jest skokiem w środek per se, ale sugeruje jedynie takie wrażenie, ponieważ zawartość kilku bajtów przed celem skoku może ułożyć się w taki sposób, że zmyli deasembler i zmusi go do kompilacji kodu, którego w rzeczywistości nie ma ;). Przykładowo, posłużę się znowu programem .com:

  1. Disassembly of section .data:
  2.  
  3. 00000000 <.data>:
  4.    0:   e9 e9 04                        jmp    0x4ec
  5. [...]
  6.  498:   65 20 00                        and    %al,%gs:(%bx,%si)
  7.         ...
  8.  4eb:   00 b4 09 ba                     add    %dh,-0x45f7(%si)
  9.  4ef:   91                              xchg   %ax,%cx

Jak widać na powyższym listingu, dekompilator nie załapał się na offset 0x4EC, ponieważ idąc z góry na dół, napotykając zero przy offsecie 0x4EB zinterpretował je jako instrukcję ADD, robiąc z offsetu 0x4EC, 0x4ED i 0x4EE argumenty instrukcji ADD. Jednak z semantycznego punktu widzenia, zero z offsetu 0x4EB stanowi wypełnienie (padding) lub bufor danych o niesprecyzowanym celu, za to bajt spod offsetu 0x4EC stanowi pierwszy bajt nowej instrukcji, co sugerowane jest przez skok w ten offset przy adresie 0. Przy okazji, technika ta (tutaj uzyskana raczej w losowy sposób) dość powszechnie stosowana jest w różnego rodzaju obfuskatorach kodu, które mają za zadanie ukryć prawdziwy kod przed prostymi dekompilatorami, typu objdump właśnie.

Bardziej zaawansowane dekompilatory typu IDA Pro potrafią automatycznie poradzić sobie z takimi problemami, jednak z uwagi na niskopoziomowość objdump tutaj nie mamy tego szczęścia i musimy poradzić sobie ręcznie, przy pomocy argumentu --start-address:

  1. $ objdump -D -b binary -mi386 -Maddr16,data16 plik.com --start-address=0x4ec | less
  2.  
  3. Disassembly of section .data:
  4.  
  5. 000004ec <.data+0x4ec>:
  6.  4ec:   b4 09                           mov    $0x9,%ah
  7.  4ee:   ba 91 04                        mov    $0x491,%dx
  8.  4f1:   cd 21                           int    $0x21
  9.  4f3:   b4 52                           mov    $0x52,%ah

Po raz kolejny, kod stał się czytelny i można analizować go dalej ;).

Demanglement ;), lub też usuwanie dekoracji nazw symboli

Pod enigmatycznym określeniem „mangling” kryje się proces enkodowania zapisu typu symbolu z postaci języka programowania, do prostej, jednoznacznej metody, zaimplementowanej za pomocą prostego algorytmu. Innymi słowy, zmienne, funkcje, w klasach, czy nie, muszą mieć jednoznaczny identyfikator, który pomoże linkerowi w odróżnieniu, gdzie znajduje się dany symbol, do którego odwołuje się np. któraś funkcja. Linker nie może po prostu traktować symboli po samych ich nazwach, ponieważ pogubiłby się przy napotkaniu np. dwóch zmiennych nazywających się tak samo, ale istniejących w dwóch różnych namespace'ach, czy przy napotkaniu dwóch funkcji z przeciążonymi argumentami. Stąd konieczność uwzględniania w nazwie symbolu całej sygnatury danego obiektu, nie tylko nazwy; czyli razem z miejscem, gdzie dany obiekt się znajduje, typem obiektu, listą argumentów, jeśli obiekt jest funkcją, etc.

Kiedyś pisałem na ten temat krótką notkę, która brała na „ruszt” sposób enkodowania sygnatur w języku Java. Proces enkodowania nazwy obiektu na sygnaturę nazywa się więc mangling'iem, proces dekodowania – demanglingiem (niestety nie znam polskich nazw). Nie są to prawdopodobnie oficjalne nazwy, ponieważ nie mają za dużo wspólnego z oficjalnym stwierdzeniem „dekoracji nazw”, ale jakie to ma znaczenie ;).

Każdy kompilator ma swoją technikę manglowania nazw i nie ma jednej metody na odczytanie ich wszystkich. Objdump posiada kilka algorytmów, adresujących różne kompilatory (m.in. Java), dlatego opcja ta jest dość użyteczna.

Przykład domyślnych ustawień:

  1. 0000000000410120 <_ZN3Gtk6Widget12on_drag_dropERKN4Glib6RefPtrIN3Gdk11DragContextEEEiij@plt>:
  2.   410120:       ff 25 12 df 2a 00       jmpq   *0x2adf12(%rip)        # 6be038 <_GLOBAL_OFFSET_TABLE_+0x50>
  3.   410126:       68 07 00 00 00          pushq  $0x7
  4.   41012b:       e9 70 ff ff ff          jmpq   4100a0 <_init+0x18>
  5.  
  6. 0000000000410170 <_ZN3Gtk6Widget16on_state_changedENS_9StateTypeE@plt>:
  7.   410170:       ff 25 ea de 2a 00       jmpq   *0x2adeea(%rip)        # 6be060 <_GLOBAL_OFFSET_TABLE_+0x78>
  8.   410176:       68 0c 00 00 00          pushq  $0xc
  9.   41017b:       e9 20 ff ff ff          jmpq   4100a0 <_init+0x18>
  10.  
  11. 0000000000410190 <_ZThn16_N3Gtk9ContainerD0Ev@plt>:
  12.   410190:       ff 25 da de 2a 00       jmpq   *0x2adeda(%rip)        # 6be070 <_GLOBAL_OFFSET_TABLE_+0x88>
  13.   410196:       68 0e 00 00 00          pushq  $0xe
  14.   41019b:       e9 00 ff ff ff          jmpq   4100a0 <_init+0x18>

Przykład użycia opcji --demangle (-C):

  1. 0000000000410120 <Gtk::Widget::on_drag_drop(Glib::RefPtr<Gdk::DragContext> const&, int, int, unsigned int)@plt>:
  2.   410120:       ff 25 12 df 2a 00       jmpq   *0x2adf12(%rip)        # 6be038 <_GLOBAL_OFFSET_TABLE_+0x50>
  3.   410126:       68 07 00 00 00          pushq  $0x7
  4.   41012b:       e9 70 ff ff ff          jmpq   4100a0 <_init+0x18>
  5.  
  6. 0000000000410170 <Gtk::Widget::on_state_changed(Gtk::StateType)@plt>:
  7.   410170:       ff 25 ea de 2a 00       jmpq   *0x2adeea(%rip)        # 6be060 <_GLOBAL_OFFSET_TABLE_+0x78>
  8.   410176:       68 0c 00 00 00          pushq  $0xc
  9.   41017b:       e9 20 ff ff ff          jmpq   4100a0 <_init+0x18>
  10.  
  11. 0000000000410190 <non-virtual thunk to Gtk::Container::~Container()@plt>:
  12.   410190:       ff 25 da de 2a 00       jmpq   *0x2adeda(%rip)        # 6be070 <_GLOBAL_OFFSET_TABLE_+0x88>
  13.   410196:       68 0e 00 00 00          pushq  $0xe
  14.   41019b:       e9 00 ff ff ff          jmpq   4100a0 <_init+0x18>

Inne procesory

Dla innych procesorów, nawet takich z którymi IDA Pro nie potrafi sobie poradzić, stosuje się inne buildy programu objdump. Przykładowo, asembler wykorzystany w mikrokontrolerach attiny13 może być zdekompilowany przy użyciu programu avr-objdump:

  1. test:     file format elf32-avr
  2.  
  3. Disassembly of section .text:
  4.  
  5. [...]
  6. 00000022 <main>:
  7.   22:   81 e0           ldi     r24, 0x01       ; 1
  8.   24:   87 bb           out     0x17, r24       ; 23
  9.   26:   88 bb           out     0x18, r24       ; 24
  10.   28:   41 e0           ldi     r20, 0x01       ; 1
  11.   2a:   2f ef           ldi     r18, 0xFF       ; 255
  12.   2c:   3f ef           ldi     r19, 0xFF       ; 255
  13.   2e:   88 b3           in      r24, 0x18       ; 24
  14.   30:   84 27           eor     r24, r20
  15.   32:   88 bb           out     0x18, r24       ; 24
  16.   34:   c9 01           movw    r24, r18
  17.   36:   01 97           sbiw    r24, 0x01       ; 1
  18.   38:   f1 f7           brne    .-4             ; 0x36 <__CCP__+0x2>
  19.   3a:   f9 cf           rjmp    .-14            ; 0x2e <main+0xc>

Część opcji innych buildów jest kompatybilna z oryginalnym programem objdump, jednak wiadomo, że jeśli w grę wchodzi nowy procesor, część konceptów może być po prostu z nim niekompatybilna (np. adresowanie 64bit w attiny13), dlatego przed użyciem polecam jednak przeczytać tą j...ą instrukcję ;) danego builda objdump.

Komentuj

Zawartość tego pola nie będzie udostępniana publicznie.
  • Adresy internetowe są automatycznie zamieniane w klikalne odnośniki.
  • Use <!--pagebreak--> to create page breaks.
  • You may post block code using <blockcode [type="language"]>...</blockcode> tags. You may also post inline code using <code [type="language"]>...</code> tags.
  • Use <fn>...</fn> to insert automatically numbered footnotes.

Więcej informacji na temat formatowania