Pola e_phoff i e_shoff

https://chacker.pl/

Pliki binarne ELF zawierają między innymi tabele nagłówków programów i nagłówków sekcji. Powrócę do znaczenia tych typów nagłówków po zakończeniu omawiania nagłówka wykonywalnego, ale jedną rzeczą, którą mogę już ujawnić, jest to, że tabele nagłówków programu i sekcji nie muszą znajdować się w żadnym konkretnym przesunięciu w pliku binarnym. Jedyną strukturą danych, co do której można założyć, że znajduje się w stałym miejscu w pliku binarnym ELF, jest nagłówek wykonywalny, który zawsze znajduje się na początku. Skąd można wiedzieć, gdzie znaleźć nagłówki programów i nagłówki sekcji? W tym celu nagłówek wykonywalny zawiera dwa dedykowane pola, zwane e_phoff i e_shoff, które wskazują przesunięcia pliku do początku tabeli nagłówków programu i tabeli nagłówków sekcji. W przypadku przykładowego pliku binarnego przesunięcia wynoszą odpowiednio 64 i 6632 bajty (dwie linie w punkcie (7) w Listingu 2-2). Przesunięcia można również ustawić na zero, aby wskazać, że plik nie zawiera nagłówka programu lub tabeli nagłówków sekcji. Ważne jest, aby zauważyć, że te pola są przesunięciami pliku, co oznacza liczbę bajtów, które należy odczytać do pliku, aby dotrzeć do nagłówków. Innymi słowy, w przeciwieństwie do pola e_entry omówionego wcześniej, e_phoff i e_shoff nie są adresami wirtualnymi.

Pole e_entry

https://chacker.pl/

Pole e_entry oznacza punkt wejścia pliku binarnego; jest to wirtualny adres, od którego powinno rozpocząć się wykonywanie (patrz również Sekcja 1.4). W przypadku przykładowego pliku binarnego wykonywanie rozpoczyna się pod adresem 0x400430 (oznaczonym (6) w wyjściu readelf w Liście 2-2). To tutaj interpreter (zwykle ld-linux.so) przekaże kontrolę po zakończeniu ładowania pliku binarnego do pamięci wirtualnej. Punkt wejścia jest również przydatnym punktem początkowym dla rekurencyjnego demontażu.

Pola e_type, e_machine i e_version

https://chacker.pl/

Po tablicy e_ident następuje seria wielobajtowych pól całkowitych. Pierwsze z nich, zwane e_type, określa typ pliku binarnego. Najczęściej spotykane wartości, jakie tutaj napotkasz, to ET_REL (oznaczające plik obiektu relokowalnego), ET_EXEC (plik wykonywalny binarny) i ET_DYN (biblioteka dynamiczna, zwana również plikiem obiektu współdzielonego). W wynikach readelf dla przykładowego pliku binarnego możesz zobaczyć, że masz do czynienia z plikiem wykonywalnym (typ: EXEC (3) w liście 2-2). Następnie pojawia się pole e_machine, które oznacza architekturę, na której plik binarny ma działać (4). W tej książce zwykle będzie ono ustawione na EM_X86_64 (tak jak w wynikach readelf), ponieważ będziesz głównie pracować na 64-bitowych plikach binarnych x86. Inne wartości, na które możesz się natknąć, to EM_386 (32-bit x86) i EM_ARM (dla plików binarnych ARM). Pole e_version pełni tę samą rolę co bajt EI_VERSION w tablicy e_ident; konkretnie wskazuje wersję specyfikacji ELF, która została użyta podczas tworzenia pliku binarnego. Ponieważ pole to ma szerokość 32 bitów, możesz pomyśleć, że istnieje wiele możliwych wartości, ale w rzeczywistości jedyną możliwą wartością jest 1 (EV_CURRENT), co oznacza wersję 1 specyfikacji (5).

Tablica e_ident

https://chacker.pl/

Nagłówek wykonywalny (i plik ELF) zaczyna się od 16-bajtowej tablicy o nazwie e_ident. Tablica e_ident zawsze zaczyna się od 4-bajtowej „magicznej wartości” identyfikującej plik jako plik binarny ELF. Magiczna wartość składa się z liczby szesnastkowej 0x7f, po której następują kody znaków ASCII dla liter E, L i F. Umieszczenie tych bajtów na samym początku jest wygodne, ponieważ pozwala narzędziom takim jak file, a także specjalistycznym narzędziom takim jak binarny loader, szybko odkryć, że mają do czynienia z plikiem ELF. Po magicznej wartości następuje szereg bajtów, które podają bardziej szczegółowe informacje o specyfice typu pliku ELF. W elf.h indeksy tych bajtów (indeksy od 4 do 15 w tablicy e_ident) są symbolicznie określane odpowiednio jako EI_CLASS, EI_DATA, EI_VERSION, EI_OSABI, EI_ABIVERSION i EI_PAD. Rysunek 2-1 przedstawia ich wizualną reprezentację. Pole EI_PAD w rzeczywistości zawiera wiele bajtów, mianowicie indeksy od 9 do 15 w e_ident. Wszystkie te bajty są obecnie oznaczone jako wypełnienie; są zarezerwowane do możliwego przyszłego użycia, ale obecnie ustawione na zero. Bajt EI_CLASS oznacza to, co specyfikacja ELF nazywa „klasą” pliku binarnego. Jest to trochę mylące, ponieważ słowo klasa jest tak ogólne, że może oznaczać niemal wszystko. Bajt tak naprawdę oznacza, czy plik binarny jest przeznaczony dla architektury 32-bitowej czy 64-bitowej. W pierwszym przypadku bajt EI_CLASS jest ustawiony na stałą ELFCLASS32 (równą 1), podczas gdy w drugim przypadku jest ustawiony na ELFCLASS64 (równą 2). Z szerokością bitową architektury związana jest kolejność bajtów architektury. Innymi słowy, czy wartości wielobajtowe (takie jak liczby całkowite) są uporządkowane w pamięci od najmniej znaczącego bajtu jako pierwszego (little-endian) czy od najbardziej znaczącego bajtu jako pierwszego (big-endian)? Bajt EI_DATA wskazuje kolejność bajtów pliku binarnego. Wartość ELFDATA2LSB (równa 1) wskazuje na little-endian, podczas gdy ELFDATA2MSB (równa 2) oznacza big-endian. Następny bajt, zwany EI_VERSION, wskazuje wersję specyfikacji ELF używanej podczas tworzenia pliku binarnego. Obecnie jedyną prawidłową wartością jest EV_CURRENT, która jest zdefiniowana jako równa 1. Na koniec bajty EI_OSABI i EI_ABIVERSION oznaczają informacje dotyczące interfejsu binarnego aplikacji (ABI) i systemu operacyjnego (OS), dla którego skompilowano plik binarny. Jeśli bajt EI_OSABI jest ustawiony na wartość różną od zera, oznacza to, że w pliku ELF używane są pewne rozszerzenia specyficzne dla ABI lub systemu operacyjnego; może to zmienić znaczenie niektórych innych pól w pliku binarnym lub może sygnalizować obecność niestandardowych sekcji. Domyślna wartość zero oznacza, że ​​plik binarny jest skierowany do UNIX System V ABI. Bajt EI_ABIVERSION oznacza konkretną wersję ABI wskazaną w bajcie EI_OSABI, do której skierowany jest plik binarny. Zwykle zobaczysz to ustawione na zero, ponieważ nie jest konieczne określanie żadnych informacji o wersji, gdy używany jest domyślny EI_OSABI. Możesz sprawdzić tablicę e_ident dowolnego pliku binarnego ELF, używając readelf do wyświetlenia nagłówka pliku binarnego. Na przykład Listing 2-2 pokazuje dane wyjściowe dla pliku binarnego compilation_example z rozdziału 1 (będę się również odwoływał do tych danych wyjściowych, omawiając inne pola w nagłówku pliku wykonywalnego).

W Listingu 2-2 tablica e_ident jest pokazana w wierszu oznaczonym Magic (1). Zaczyna się od znanych czterech bajtów magicznych, po których następuje wartość 2 (oznaczająca ELFCLASS64), następnie 1 (ELFDATA2LSB) i na końcu kolejne 1 (EV_CURRENT). Pozostałe bajty są zerowane, ponieważ bajty EI_OSABI i EI_ABIVERSION mają wartości domyślne; bajty wypełnienia są również ustawione na zero. Informacje zawarte w niektórych bajtach są wyraźnie powtarzane w dedykowanych wierszach oznaczonych odpowiednio Class, Data, Version, OS/ABI i ABI Version (2)

Nagłówek wykonywalny

https://chacker.pl/

Każdy plik ELF zaczyna się od nagłówka wykonywalnego, który jest po prostu ustrukturyzowaną serią bajtów informujących, że jest to plik ELF, jaki to rodzaj pliku ELF i gdzie w pliku można znaleźć całą pozostałą zawartość. Aby dowiedzieć się, jaki jest format nagłówka wykonywalnego, możesz wyszukać jego definicję typu (oraz definicje innych typów i stałych związanych z ELF) w /usr/include/elf.h lub w specyfikacji ELF.1 Listing 2-1 pokazuje definicję typu dla 64-bitowego nagłówka wykonywalnego ELF.

typedef struct {

unsigned char e_ident[16]; /* Magic number and other info */

uint16_t e_type; /* Object file type */

uint16_t e_machine; /* Architecture */

uint32_t e_version; /* Object file version */

uint64_t e_entry; /* Entry point virtual address */

uint64_t e_phoff; /* Program header table file offset */

uint64_t e_shoff; /* Section header table file offset */

uint32_t e_flags; /* Processor-specific flags */

uint16_t e_ehsize; /* ELF header size in bytes */

uint16_t e_phentsize; /* Program header table entry size */

uint16_t e_phnum; /* Program header table entry count */

uint16_t e_shentsize; /* Section header table entry size */

uint16_t e_shnum; /* Section header table entry count */

uint16_t e_shstrndx; /* Section header string table index */

} Elf64_Ehdr;

Nagłówek wykonywalny jest tutaj reprezentowany jako struktura C o nazwie Elf64 _Ehdr. Jeśli poszukasz jej w /usr/include/elf.h, możesz zauważyć, że podana tam definicja struktury zawiera typy takie jak Elf64_Half i Elf64_Word. Są to po prostu definicje typu dla typów całkowitych, takich jak uint16_t i uint32_t.

FORMAT ELF

https://chacker.pl/

Teraz, gdy masz ogólne pojęcie o tym, jak wyglądają pliki binarne i jak działają, jesteś gotowy, aby zanurzyć się w prawdziwym formacie binarnym. W tym rozdziale zbadamy Executable and Linkable Format (ELF), który jest domyślnym formatem binarnym w systemach opartych na systemie Linux i tym, z którym będziesz pracować w tej książce. ELF jest używany do plików wykonywalnych, plików obiektowych, bibliotek współdzielonych i zrzutów pamięci. Tutaj skupię się na plikach wykonywalnych ELF, ale te same koncepcje dotyczą innych typów plików ELF. Ponieważ w tej książce będziesz mieć do czynienia głównie z plikami binarnymi 64-bitowymi, skupię dyskusję na 64-bitowych plikach ELF. Jednak format 32-bitowy jest podobny, różniąc się głównie rozmiarem i kolejnością niektórych pól nagłówka i innych struktur danych. Nie powinieneś mieć żadnych problemów z uogólnieniem omawianych tutaj koncepcji na 32-bitowe pliki binarne ELF. Rysunek  ilustruje format i zawartość typowego 64-bitowego pliku wykonywalnego ELF.

Gdy po raz pierwszy zaczniesz szczegółowo analizować pliki binarne ELF, wszystkie zawiłości mogą wydawać się przytłaczające. Ale w istocie pliki binarne ELF składają się tylko z czterech typów komponentów: nagłówka pliku wykonywalnego, serii (opcjonalnych) nagłówków programu, szeregu sekcji i serii (opcjonalnych) nagłówków sekcji, po jednym na sekcję. Omówię każdy z tych komponentów w dalszej części.Jak widać na Rysunku , nagłówek wykonywalny pojawia się jako pierwszy w standardowych plikach binarnych ELF, następnie nagłówki programów, a sekcje i nagłówki sekcji na końcu. Aby ułatwić śledzenie poniższej dyskusji, użyję nieco innej kolejności i omówię sekcje i nagłówki sekcji przed nagłówkami programów. Zacznijmy od nagłówka wykonywalnego.

Podsumowanie

https://chacker.pl/

Teraz, gdy znasz już ogólną anatomię i cykl życia pliku binarnego, czas zagłębić się w szczegóły konkretnego formatu binarnego. Zacznijmy od szeroko rozpowszechnionego formatu ELF, który jest tematem następnego rozdziału.

Ćwiczenia

  1. Lokalizacja funkcji

Napisz program w C, który zawiera kilka funkcji i skompiluj go odpowiednio do pliku asemblera, pliku obiektu i pliku wykonywalnego. Spróbuj zlokalizować funkcje, które napisałeś w pliku asemblera oraz w zdeasemblowanym pliku obiektu i pliku wykonywalnym. Czy widzisz związek między kodem C a kodem asemblera? Na koniec rozbierz plik wykonywalny i spróbuj ponownie zidentyfikować funkcje.

  1. Sekcje

Jak widziałeś, pliki binarne ELF (i inne typy plików binarnych) są podzielone na sekcje. Niektóre sekcje zawierają kod, a inne dane. Dlaczego Twoim zdaniem istnieje rozróżnienie między sekcjami kodu i danych? Jak myślisz, w jaki sposób proces ładowania różni się dla sekcji kodu i danych? Czy konieczne jest kopiowanie wszystkich sekcji do pamięci, gdy plik binarny jest ładowany do wykonania?

Ładowanie i wykonywanie pliku binarnego

https://chacker.pl/

Teraz wiesz, jak działa kompilacja, a także jak wyglądają pliki binarne w środku. Nauczyłeś się również, jak statycznie deasemblować pliki binarne za pomocą objdump. Jeśli śledziłeś, powinieneś mieć nawet swój własny, błyszczący nowy plik binarny na dysku twardym. Teraz dowiesz się, co się dzieje, gdy ładujesz i wykonujesz plik binarny, co będzie pomocne, gdy omówię koncepcje analizy dynamicznej w późniejszych rozdziałach. Chociaż dokładne szczegóły różnią się w zależności od platformy i formatu pliku binarnego, proces ładowania i wykonywania pliku binarnego zazwyczaj obejmuje szereg podstawowych kroków. Rysunek  pokazuje, jak załadowany plik binarny ELF (taki jak ten właśnie skompilowany) jest reprezentowany w pamięci na platformie opartej na systemie Linux. Na wysokim poziomie ładowanie pliku binarnego PE w systemie Windows jest dość podobne.

Ładowanie pliku binarnego to skomplikowany proces, który wymaga dużo pracy ze strony systemu operacyjnego. Ważne jest również, aby zauważyć, że reprezentacja pliku binarnego w pamięci niekoniecznie odpowiada jednoznaczności jego reprezentacji na dysku. Na przykład duże obszary danych inicjowanych zerami mogą zostać ściśnięte w pliku binarnym na dysku (aby zaoszczędzić miejsce na dysku), podczas gdy wszystkie te zera zostaną rozszerzone w pamięci. Niektóre części pliku binarnego na dysku mogą być inaczej uporządkowane w pamięci lub w ogóle nie zostać załadowane do pamięci.  Na razie trzymajmy się ogólnego przeglądu tego, co dzieje się podczas procesu ładowania. Gdy zdecydujesz się uruchomić plik binarny, system operacyjny rozpoczyna od skonfigurowania nowego procesu, w którym program ma zostać uruchomiony, w tym wirtualnej przestrzeni adresowej. Następnie system operacyjny mapuje interpreter do wirtualnej pamięci procesu. Jest to program przestrzeni użytkownika, który wie, jak załadować plik binarny i wykonać niezbędne relokacje. W systemie Linux interpreter jest zazwyczaj biblioteką współdzieloną o nazwie ld-linux.so. W systemie Windows funkcjonalność interpretera jest implementowana jako część pliku ntdll.dll. Po załadowaniu interpretera jądro przekazuje mu kontrolę, a interpreter rozpoczyna pracę w przestrzeni użytkownika. Pliki binarne ELF systemu Linux zawierają specjalną sekcję o nazwie .interp, która określa ścieżkę do interpretera, który ma zostać użyty do załadowania pliku binarnego, jak widać w przypadku readelf, jak pokazano na listingu 1-12.

Listing 1-12: Contents of the .interp section

$ readelf -p .interp a.out

String dump of section ’.interp’:

[ 0] /lib64/ld-linux-x86-64.so.2

Jak wspomniano, interpreter ładuje plik binarny do swojej wirtualnej przestrzeni adresowej (tej samej przestrzeni, w której załadowany jest interpreter). Następnie analizuje plik binarny, aby dowiedzieć się (między innymi), z których bibliotek dynamicznych korzysta plik binarny. Interpreter mapuje je do wirtualnej przestrzeni adresowej (używając funkcji mmap lub równoważnej), a następnie wykonuje wszelkie niezbędne relokacje w ostatniej chwili w sekcjach kodu pliku binarnego, aby wypełnić prawidłowe adresy dla odniesień do bibliotek dynamicznych. W rzeczywistości proces rozwiązywania odniesień do funkcji w bibliotekach dynamicznych jest często odkładany na później. Innymi słowy, zamiast rozwiązywać te odniesienia natychmiast w momencie ładowania, interpreter rozwiązuje odniesienia tylko wtedy, gdy są one wywoływane po raz pierwszy. Jest to znane jako leniwe wiązanie, które wyjaśnię bardziej szczegółowo później. Po zakończeniu relokacji interpreter wyszukuje punkt wejścia pliku binarnego i przekazuje mu kontrolę, rozpoczynając normalne wykonywanie pliku binarnego.

Badanie kompletnego pliku wykonywalnego binarnego

https://chacker.pl/

Teraz, gdy zobaczyłeś wnętrze pliku obiektowego, czas rozmontować kompletny plik binarny. Zacznijmy od przykładowego pliku binarnego z symbolami, a następnie przejdźmy do jego pozbawionego odpowiednika, aby zobaczyć różnicę w wynikach dezasemblacji. Istnieje duża różnica między dezasemblacją pliku obiektowego a plikiem wykonywalnym binarnym, jak widać w wynikach objdump w Listingu 1-10

Listing 1-10: Disassembling an executable with objdump

$ objdump -M intel -d a.out

a.out: file format elf64-x86-64

Disassembly of section (1).init:

00000000004003c8 <_init>:

4003c8: 48 83 ec 08 sub rsp,0x8

4003cc: 48 8b 05 25 0c 20 00 mov rax,QWORD PTR [rip+0x200c25]

4003d3: 48 85 c0 test rax,rax

4003d6: 74 05 je 4003dd <_init+0x15>

4003d8: e8 43 00 00 00 call 400420 <__libc_start_main@plt+0x10>

4003dd: 48 83 c4 08 add rsp,0x8

4003e1: c3 ret

Disassembly of section (2).plt:

00000000004003f0 <puts@plt-0x10>:

4003f0: ff 35 12 0c 20 00 push QWORD PTR [rip+0x200c12]

4003f6: ff 25 14 0c 20 00 jmp QWORD PTR [rip+0x200c14]

4003fc: 0f 1f 40 00 nop DWORD PTR [rax+0x0]

0000000000400400 <puts@plt>:

400400: ff 25 12 0c 20 00 jmp QWORD PTR [rip+0x200c12]

400406: 68 00 00 00 00 push 0x0

40040b: e9 e0 ff ff ff jmp 4003f0 <_init+0x28>

Disassembly of section (3).text:

0000000000400430 <_start>:

400430: 31 ed xor ebp,ebp

400432: 49 89 d1 mov r9,rdx

400435: 5e pop rsi

400436: 48 89 e2 mov rdx,rsp

400439: 48 83 e4 f0 and rsp,0xfffffffffffffff0

40043d: 50 push rax

40043e: 54 push rsp

40043f: 49 c7 c0 c0 05 40 00 mov r8,0x4005c0

400446: 48 c7 c1 50 05 40 00 mov rcx,0x400550

40044d: 48 c7 c7 26 05 40 00 mov rdi,0x400526

400454: e8 b7 ff ff ff call 400410 <__libc_start_main@plt>

400459: f4 hlt

40045a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]

0000000000400460 <deregister_tm_clones>:

0000000000400526 (4)<main>:

400526: 55 push rbp

400527: 48 89 e5 mov rbp,rsp

40052a: 48 83 ec 10 sub rsp,0x10

40052e: 89 7d fc mov DWORD PTR [rbp-0x4],edi

400531: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi

400535: bf d4 05 40 00 mov edi,0x4005d4

40053a: e8 c1 fe ff ff call 400400 (5)<puts@plt>

40053f: b8 00 00 00 00 mov eax,0x0

400544: c9 leave

400545: c3 ret

400546: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]

40054d: 00 00 00

0000000000400550 <__libc_csu_init>:

Disassembly of section .fini:

00000000004005c4 <_fini>:

4005c4: 48 83 ec 08 sub rsp,0x8

4005c8: 48 83 c4 08 add rsp,0x8

4005cc: c3 ret

Możesz zobaczyć, że plik binarny ma o wiele więcej kodu niż plik obiektowy. Nie jest to już tylko funkcja główna ani nawet pojedyncza sekcja kodu. Teraz jest wiele sekcji o nazwach takich jak .init (1), .plt (2) i .text (3). Wszystkie te sekcje zawierają kod obsługujący różne funkcje, takie jak inicjalizacja programu lub stuby do wywoływania bibliotek współdzielonych. Sekcja .text jest główną sekcją kodu i zawiera funkcję główną (4). Zawiera również szereg innych funkcji, takich jak _start, które są odpowiedzialne za zadania takie jak konfigurowanie argumentów wiersza poleceń i środowiska wykonawczego dla main i czyszczenie po main. Te dodatkowe funkcje są standardowymi funkcjami, obecnymi w każdym pliku binarnym ELF wygenerowanym przez gcc. Możesz również zobaczyć, że wcześniej niekompletny kod i odwołania do danych zostały teraz rozwiązane przez linker. Na przykład wywołanie puts (5) wskazuje teraz na właściwy stub (w sekcji .plt) dla biblioteki współdzielonej, która zawiera puts. Tak więc pełny plik wykonywalny binarny zawiera znacznie więcej kodu (i danych, choć tego nie pokazałem) niż odpowiadający mu plik obiektowy. Ale jak dotąd wynik nie jest o wiele trudniejszy do zinterpretowania. To się zmienia, gdy plik binarny jest rozbierany, jak pokazano w Liście 1-11, gdzie używa się objdump do rozbierania rozbieranej wersji przykładowego pliku binarnego.

Listing 1-11: Disassembling a stripped executable with objdump

$ objdump -M intel -d ./a.out.stripped

./a.out.stripped: file format elf64-x86-64

Disassembly of section (1).init:

00000000004003c8 <.init>:

4003c8: 48 83 ec 08 sub rsp,0x8

4003cc: 48 8b 05 25 0c 20 00 mov rax,QWORD PTR [rip+0x200c25]

4003d3: 48 85 c0 test rax,rax

4003d6: 74 05 je 4003dd <puts@plt-0x23>

4003d8: e8 43 00 00 00 call 400420 <__libc_start_main@plt+0x10>

4003dd: 48 83 c4 08 add rsp,0x8

4003e1: c3 ret

Disassembly of section (2).plt:

Disassembly of section (3).text:

0000000000400430 <.text>:

(4) 400430: 31 ed xor ebp,ebp

400432: 49 89 d1 mov r9,rdx

400435: 5e pop rsi

400436: 48 89 e2 mov rdx,rsp

400439: 48 83 e4 f0 and rsp,0xfffffffffffffff0

40043d: 50 push rax

40043e: 54 push rsp

40043f: 49 c7 c0 c0 05 40 00 mov r8,0x4005c0

400446: 48 c7 c1 50 05 40 00 mov rcx,0x400550

40044d: 48 c7 c7 26 05 40 00 mov rdi,0x400526

(5) 400454: e8 b7 ff ff ff call 400410 <__libc_start_main@plt>

400459: f4 hlt

40045a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]

(6) 400460: b8 3f 10 60 00 mov eax,0x60103f

400520: 5d pop rbp

400521: e9 7a ff ff ff jmp 4004a0 <__libc_start_main@plt+0x90>

(7) 400526: 55 push rbp

400527: 48 89 e5 mov rbp,rsp

40052a: 48 83 ec 10 sub rsp,0x10

40052e: 89 7d fc mov DWORD PTR [rbp-0x4],edi

400531: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi

400535: bf d4 05 40 00 mov edi,0x4005d4

40053a: e8 c1 fe ff ff call 400400 <puts@plt>

40053f: b8 00 00 00 00 mov eax,0x0

400544: c9 leave

(8) 400545: c3 ret

400546: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]

40054d: 00 00 00

400550: 41 57 push r15

400552: 41 56 push r14

Disassembly of section .fini:

00000000004005c4 <.fini>:

4005c4: 48 83 ec 08 sub rsp,0x8

4005c8: 48 83 c4 08 add rsp,0x8

4005cc: c3 ret

Najważniejszym wnioskiem z Listingu 1-11 jest to, że podczas gdy różne sekcje są nadal wyraźnie rozróżnialne (oznaczone (1), (2) i (3)), funkcje nie. Zamiast tego wszystkie funkcje zostały połączone w jeden duży fragment kodu. Funkcja _start zaczyna się w (4), a deregister_tm_clones zaczyna się w (6). Funkcja main zaczyna się w (7) i kończy w (8), ale we wszystkich tych przypadkach nie ma nic szczególnego, co wskazywałoby, że instrukcje przy tych znacznikach reprezentują uruchomienia funkcji. Jedynymi wyjątkami są funkcje w sekcji .plt, które nadal mają swoje nazwy jak poprzednio (jak widać w wywołaniu __libc_start_main w (5)). Poza tym musisz sam spróbować zrozumieć wynik demontażu. Nawet w tym prostym przykładzie wszystko jest już mylące; wyobraź sobie próbę zrozumienia większego pliku binarnego zawierającego setki różnych funkcji połączonych ze sobą! Właśnie dlatego dokładne automatyczne wykrywanie funkcji jest tak ważne w wielu obszarach analizy binarnej.

Zaglądanie do pliku obiektowego

https://chacker.pl/

Na razie użyję narzędzia objdump, aby pokazać, jak wykonać cały deasembling (omówię inne narzędzia do deasemblacji w rozdziale 6). Jest to prosty, łatwy w użyciu deasembler dołączony do większości dystrybucji Linuksa i doskonale nadaje się do szybkiego zapoznania się z kodem i danymi zawartymi w pliku binarnym. Listing 1-8 pokazuje zdeasemblowaną wersję przykładowego pliku obiektowego, compilation_example.o.

Listing 1-8: Disassembling an object file

$ (1)objdump -sj .rodata compilation_example.o

compilation_example.o: file format elf64-x86-64

Contents of section .rodata:

0000 48656c6c 6f2c2077 6f726c64 2100 Hello, world!.

$ (2)objdump -M intel -d compilation_example.o

compilation_example.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 (3)<main>:

0: 55 push rbp

1: 48 89 e5 mov rbp,rsp

4: 48 83 ec 10 sub rsp,0x10

8: 89 7d fc mov DWORD PTR [rbp-0x4],edi

b: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi

f: bf 00 00 00 00 mov edi,(4)0x0

14: e8 00 00 00 00 (5)call 19 <main+0x19>

19: b8 00 00 00 00 mov eax,0x0

1e: c9 leave

1f: c3 ret

Jeśli uważnie przyjrzysz się Listingowi 1-8, zobaczysz, że wywołałem objdump dwa razy. Najpierw, w (1) mówię objdump, aby pokazał zawartość sekcji .rodata. Oznacza to „dane tylko do odczytu” i jest to część pliku binarnego, w której przechowywane są wszystkie stałe, w tym ciąg „Hello, world!”. Wrócę do bardziej szczegółowej dyskusji na temat .rodata i innych sekcji w plikach binarnych ELF w rozdziale 2, który obejmuje format pliku binarnego ELF. Na razie zauważ, że zawartość .rodata składa się z kodowania ASCII ciągu, pokazanego po lewej stronie wyjścia. Po prawej stronie możesz zobaczyć czytelną dla człowieka reprezentację tych samych bajtów. Drugie wywołanie objdump w (2) rozkłada cały kod w pliku obiektu na składnię Intel. Jak widać, zawiera on tylko kod funkcji głównej (3), ponieważ jest to jedyna funkcja zdefiniowana w pliku źródłowym. W większości przypadków dane wyjściowe są dość ściśle zgodne z kodem asemblera wygenerowanym wcześniej przez fazę kompilacji (z pewnymi wyjątkami). Co ciekawe, wskaźnik do ciągu „Hello, world!” (w (4)) jest ustawiony na zero. Następne wywołanie (5), które powinno wydrukować ciąg na ekranie za pomocą puts, również wskazuje na bezsensowną lokalizację (przesunięcie 19, w środku main). Dlaczego wywołanie, które powinno odwoływać się do puts, wskazuje zamiast tego na środek main? Wcześniej wspomniałem, że odwołania do danych i kodu z plików obiektów nie są jeszcze w pełni rozwiązane, ponieważ kompilator nie wie, pod jakim adresem bazowym plik zostanie ostatecznie załadowany. Dlatego wywołanie puts nie jest jeszcze poprawnie rozwiązane w pliku obiektu. Plik obiektu czeka, aż linker wypełni poprawną wartość dla tego odwołania. Możesz to potwierdzić, prosząc readelf o pokazanie wszystkich symboli relokacji obecnych w pliku obiektu, jak pokazano na Liście 1-9.

Listing 1-9: Relocation symbols as shown by readelf

$ readelf –relocs compilation_example.o

Relocation section ’.rela.text’ at offset 0x210 contains 2 entries:

Offset Info Type Sym. Value Sym. Name + Addend

(1)000000000010 00050000000a R_X86_64_32 0000000000000000 .rodata + 0

(2) 000000000015 000a00000002 R_X86_64_PC32 0000000000000000 puts – 4

Symbol relokacji w (1) mówi linkerowi, że powinien rozwiązać odwołanie do ciągu tak, aby wskazywał na dowolny adres, na którym kończy się w sekcji .rodata. Podobnie, linia oznaczona (2) mówi linkerowi, jak rozwiązać wywołanie puts. Możesz zauważyć, że wartość 4 jest odejmowana od symbolu puts. Możesz to na razie zignorować; sposób, w jaki linker oblicza relokacje, jest nieco skomplikowany, a dane wyjściowe readelf mogą być mylące, więc po prostu pominę tutaj szczegóły relokacji i skupię się na szerszym obrazie deasemblacji pliku binarnego. Więcej informacji o symbolach relokacji podam w rozdziale 2. Lewa kolumna każdego wiersza w wyjściu readelf (zacieniowana) w liście 1-9 to przesunięcie w pliku obiektu, w którym należy wypełnić rozwiązane odwołanie. Jeśli uważnie się przyjrzysz, możesz zauważyć, że w obu przypadkach jest ono równe przesunięciu instrukcji, którą należy naprawić, plus 1. Na przykład wywołanie puts znajduje się w przesunięciu kodu 0x14 w wyjściu objdump, ale symbol relokacji wskazuje na przesunięcie 0x15. Dzieje się tak, ponieważ chcesz tylko nadpisać operand instrukcji, a nie kod operacji instrukcji. Tak się składa, że ​​w przypadku obu instrukcji, które wymagają naprawy, kod operacji ma długość 1 bajtu, więc aby wskazać operand instrukcji, symbol relokacji musi pominąć bajt kodu operacji.