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:
- 00000000004005c0 <__do_global_ctors_aux>:
- 4005c0: 55 push %rbp
- 4005c1: 48 89 e5 mov %rsp,%rbp
- 4005c4: 53 push %rbx
- 4005c5: 48 83 ec 08 sub $0x8,%rsp
- 4005c9: 48 8b 05 58 08 20 00 mov 0x200858(%rip),%rax
- 4005d0: 48 83 f8 ff cmp $0xffffffffffffffff,%rax
- 4005d4: 74 19 je 4005ef <__do_global_ctors_aux+0x2f>
- 4005d6: bb 28 0e 60 00 mov $0x600e28,%ebx
- 4005db: eb 03 jmp 4005e0 <__do_global_ctors_aux+0x20>
Przykład notacji Intel:
- 00000000004005c0 <__do_global_ctors_aux>:
- 4005c0: 55 push rbp
- 4005c1: 48 89 e5 mov rbp,rsp
- 4005c4: 53 push rbx
- 4005c5: 48 83 ec 08 sub rsp,0x8
- 4005c9: 48 8b 05 58 08 20 00 mov rax,QWORD PTR [rip+0x200858]
- 4005d0: 48 83 f8 ff cmp rax,0xffffffffffffffff
- 4005d4: 74 19 je 4005ef <__do_global_ctors_aux+0x2f>
- 4005d6: bb 28 0e 60 00 mov ebx,0x600e28
- 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:
- 0000000000000000 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi>:
- [...]
- 54: 48 89 de mov %rbx,%rsi
- 57: 48 89 c7 mov %rax,%rdi
- 5a: e8 00 00 00 00 callq 5f <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x5f>
- 5f: 48 8b 85 78 fe ff ff mov -0x188(%rbp),%rax
- 66: 48 83 c0 28 add $0x28,%rax
- 6a: 48 89 c7 mov %rax,%rdi
- 6d: e8 00 00 00 00 callq 72 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x72>
- 72: 48 8b 85 78 fe ff ff mov -0x188(%rbp),%rax
- 79: 48 83 c0 48 add $0x48,%rax
- 7d: 48 89 c7 mov %rax,%rdi
- 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):
- RELOCATION RECORDS FOR [.text]:
- OFFSET TYPE VALUE
- 000000000000005b R_X86_64_PC32 _ZN3Gtk4HBoxC2Ebi-0x0000000000000004
- 000000000000006e R_X86_64_PC32 _ZN13HotkeyInvokerC2Ev-0x0000000000000004
- 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):
- $ objdump -dr ./src/ui/widgets/hexeditor/HexEditorWidget.o | less
- [...]
- 5a: e8 00 00 00 00 callq 5f <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x5f>
- 5b: R_X86_64_PC32 _ZN3Gtk4HBoxC2Ebi-0x4
- 5f: 48 8b 85 78 fe ff ff mov -0x188(%rbp),%rax
- 66: 48 83 c0 28 add $0x28,%rax
- 6a: 48 89 c7 mov %rax,%rdi
- 6d: e8 00 00 00 00 callq 72 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x72>
- 6e: R_X86_64_PC32 _ZN13HotkeyInvokerC2Ev-0x4
- 72: 48 8b 85 78 fe ff ff mov -0x188(%rbp),%rax
- 79: 48 83 c0 48 add $0x48,%rax
- 7d: 48 89 c7 mov %rax,%rdi
- 80: e8 00 00 00 00 callq 85 <_ZN15HexEditorWidgetC2ER16CharsetConverterR6BufferR12BufferTraitsi+0x85>
- 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:
- $ objdump -D -b binary -mi386 -Maddr16,data16 DN.COM | less
- DN.COM: file format binary
- Disassembly of section .data:
- 00000000 <.data>:
- 0: e9 e9 04 jmp 0x4ec
- […]
- 6dc: ba f7 04 mov $0x4f7,%dx
- 6df: b4 09 mov $0x9,%ah
- 6e1: cd 21 int $0x21
- 6e3: b8 2f 25 mov $0x252f,%ax
- 6e6: 2e c5 16 48 01 lds %cs:0x148,%dx
- 6eb: cd 21 int $0x21
- 6ed: b8 01 4c mov $0x4c01,%ax
- 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:
- Disassembly of section .data:
- 00000000 <.data>:
- 0: e9 e9 04 jmp 0x4ec
- [...]
- 498: 65 20 00 and %al,%gs:(%bx,%si)
- ...
- 4eb: 00 b4 09 ba add %dh,-0x45f7(%si)
- 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:
- $ objdump -D -b binary -mi386 -Maddr16,data16 plik.com --start-address=0x4ec | less
- Disassembly of section .data:
- 000004ec <.data+0x4ec>:
- 4ec: b4 09 mov $0x9,%ah
- 4ee: ba 91 04 mov $0x491,%dx
- 4f1: cd 21 int $0x21
- 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ń:
- 0000000000410120 <_ZN3Gtk6Widget12on_drag_dropERKN4Glib6RefPtrIN3Gdk11DragContextEEEiij@plt>:
- 410120: ff 25 12 df 2a 00 jmpq *0x2adf12(%rip) # 6be038 <_GLOBAL_OFFSET_TABLE_+0x50>
- 410126: 68 07 00 00 00 pushq $0x7
- 41012b: e9 70 ff ff ff jmpq 4100a0 <_init+0x18>
- 0000000000410170 <_ZN3Gtk6Widget16on_state_changedENS_9StateTypeE@plt>:
- 410170: ff 25 ea de 2a 00 jmpq *0x2adeea(%rip) # 6be060 <_GLOBAL_OFFSET_TABLE_+0x78>
- 410176: 68 0c 00 00 00 pushq $0xc
- 41017b: e9 20 ff ff ff jmpq 4100a0 <_init+0x18>
- 0000000000410190 <_ZThn16_N3Gtk9ContainerD0Ev@plt>:
- 410190: ff 25 da de 2a 00 jmpq *0x2adeda(%rip) # 6be070 <_GLOBAL_OFFSET_TABLE_+0x88>
- 410196: 68 0e 00 00 00 pushq $0xe
- 41019b: e9 00 ff ff ff jmpq 4100a0 <_init+0x18>
Przykład użycia opcji --demangle (-C):
- 0000000000410120 <Gtk::Widget::on_drag_drop(Glib::RefPtr<Gdk::DragContext> const&, int, int, unsigned int)@plt>:
- 410120: ff 25 12 df 2a 00 jmpq *0x2adf12(%rip) # 6be038 <_GLOBAL_OFFSET_TABLE_+0x50>
- 410126: 68 07 00 00 00 pushq $0x7
- 41012b: e9 70 ff ff ff jmpq 4100a0 <_init+0x18>
- 0000000000410170 <Gtk::Widget::on_state_changed(Gtk::StateType)@plt>:
- 410170: ff 25 ea de 2a 00 jmpq *0x2adeea(%rip) # 6be060 <_GLOBAL_OFFSET_TABLE_+0x78>
- 410176: 68 0c 00 00 00 pushq $0xc
- 41017b: e9 20 ff ff ff jmpq 4100a0 <_init+0x18>
- 0000000000410190 <non-virtual thunk to Gtk::Container::~Container()@plt>:
- 410190: ff 25 da de 2a 00 jmpq *0x2adeda(%rip) # 6be070 <_GLOBAL_OFFSET_TABLE_+0x88>
- 410196: 68 0e 00 00 00 pushq $0xe
- 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:
- test: file format elf32-avr
- Disassembly of section .text:
- [...]
- 00000022 <main>:
- 22: 81 e0 ldi r24, 0x01 ; 1
- 24: 87 bb out 0x17, r24 ; 23
- 26: 88 bb out 0x18, r24 ; 24
- 28: 41 e0 ldi r20, 0x01 ; 1
- 2a: 2f ef ldi r18, 0xFF ; 255
- 2c: 3f ef ldi r19, 0xFF ; 255
- 2e: 88 b3 in r24, 0x18 ; 24
- 30: 84 27 eor r24, r20
- 32: 88 bb out 0x18, r24 ; 24
- 34: c9 01 movw r24, r18
- 36: 01 97 sbiw r24, 0x01 ; 1
- 38: f1 f7 brne .-4 ; 0x36 <__CCP__+0x2>
- 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.


