Podsumowanie

https://chacker.pl/

Poznałeś wszystkie zawiłości formatu ELF. Omówiłem format nagłówka pliku wykonywalnego, tabele nagłówków sekcji i programów oraz zawartość sekcji. To było spore przedsięwzięcie! Warto było, ponieważ teraz, gdy znasz już tajniki plików binarnych ELF, masz solidne podstawy do nauki analizy binarnej. W następnym rozdziale przyjrzysz się szczegółowo formatowi PE, który jest formatem binarnym używanym w systemach Windows.

Ćwiczenia

  1. Ręczna inspekcja nagłówka

Użyj przeglądarki heksadecymalnej, takiej jak xxd, aby wyświetlić bajty w pliku binarnym ELF w formacie szesnastkowym. Na przykład możesz użyć polecenia xxd /bin/ls | head -n 30, aby wyświetlić pierwsze 30 wierszy bajtów dla programu /bin/ls. Czy potrafisz zidentyfikować bajty reprezentujące nagłówek ELF? Spróbuj znaleźć wszystkie pola nagłówka ELF w wynikach xxd i sprawdź, czy zawartość tych pól ma dla Ciebie sens.

  1. Sekcje i segmenty

Użyj readelf, aby wyświetlić sekcje i segmenty w pliku binarnym ELF. W jaki sposób sekcje są mapowane na segmenty? Zrób ilustrację reprezentacji pliku binarnego na dysku w porównaniu z jego reprezentacją w pamięci. Jakie są główne różnice?

  1. Pliki binarne C i C++

Użyj readelf do rozmontowania dwóch plików binarnych, mianowicie pliku binarnego wygenerowanego ze źródła C i jednego wygenerowanego ze źródła C++. Jakie są różnice?

  1. Lazy Binding

Użyj objdump do rozmontowania sekcji PLT pliku binarnego ELF. Z jakich wpisów GOT korzystają stuby PLT? Teraz wyświetl zawartość tych wpisów GOT (ponownie za pomocą objdump) i przeanalizuj ich związek z PLT.

Pole p_align

https://chacker.pl/

Pole p_align jest analogiczne do pola sh_addralign w nagłówku sekcji. Oznacza wymagane wyrównanie pamięci (w bajtach) dla segmentu. Podobnie jak w przypadku sh_addralign, wartość wyrównania 0 lub 1 oznacza, że ​​nie jest wymagane żadne szczególne wyrównanie. Jeśli p_align nie jest ustawione na 0 lub 1, jego wartość musi być potęgą 2, a p_vaddr musi być równe p_offset, modulo p_align.

Pola p_offset, p_vaddr, p_paddr, p_filesz i p_memsz

https://chacker.pl/

Pola p_offset, p_vaddr i p_filesz w Liście 2-11 są analogiczne do pól sh_offset, sh_addr i sh_size w nagłówku sekcji. Określają one odpowiednio przesunięcie pliku, w którym rozpoczyna się segment, adres wirtualny, pod którym ma zostać załadowany, oraz rozmiar pliku segmentu. W przypadku segmentów ładowalnych, p_vaddr musi być równe p_offset, modulo rozmiar strony (który zwykle wynosi 4096 bajtów). W niektórych systemach możliwe jest użycie pola p_paddr w celu określenia, pod którym adresem w pamięci fizycznej załadować segment. W nowoczesnych systemach operacyjnych, takich jak Linux, to pole jest nieużywane i ustawione na zero, ponieważ wykonują wszystkie pliki binarne w pamięci wirtualnej. Na pierwszy rzut oka może nie być oczywiste, dlaczego istnieją oddzielne pola dla rozmiaru pliku segmentu (p_filesz) i rozmiaru w pamięci (p_memsz). Aby to zrozumieć, przypomnij sobie, że niektóre sekcje wskazują tylko na potrzebę przydzielenia niektórych bajtów w pamięci, ale w rzeczywistości nie zajmują tych bajtów w pliku binarnym. Na przykład sekcja .bss zawiera dane zainicjowane zerem. Ponieważ wiadomo, że wszystkie dane w tej sekcji są w każdym razie zerami, nie ma potrzeby faktycznego uwzględniania wszystkich tych zer w pliku binarnym. Jednak podczas ładowania segmentu zawierającego .bss do pamięci wirtualnej wszystkie bajty w .bss powinny zostać przydzielone. Dlatego możliwe jest, że p_memsz będzie większe niż p_filesz. Kiedy tak się stanie, ładowarka dodaje dodatkowe bajty na końcu segmentu podczas ładowania pliku binarnego i inicjuje je zerem.

Pole p_flags

https://chacker.pl/

Flagi określają uprawnienia dostępu do segmentu w czasie wykonywania. Istnieją trzy ważne typy flag: PF_X, PF_W i PF_R. Flaga PF_X wskazuje, że segment jest wykonywalny i jest ustawiony dla segmentów kodu (readelf wyświetla ją jako E, a nie X w kolumnie Flg w Liście 2-12). Flaga PF_W oznacza, że ​​segment jest zapisywalny i zwykle jest ustawiana tylko dla zapisywalnych segmentów danych, nigdy dla segmentów kodu. Na koniec PF_R oznacza, że ​​segment jest czytelny, co zwykle ma miejsce zarówno w przypadku segmentów kodu, jak i danych.

Pole p_type

https://chacker.pl/

Pole p_type identyfikuje typ segmentu. Ważne wartości tego pola obejmują PT_LOAD, PT_DYNAMIC i PT_INTERP. Segmenty typu PT_LOAD, jak sama nazwa wskazuje, są przeznaczone do załadowania do pamięci podczas konfigurowania procesu. Rozmiar ładowalnego fragmentu i adres, pod który należy go załadować, są opisane w pozostałej części nagłówka programu. Jak widać w wynikach readelf, zwykle występują co najmniej dwa segmenty PT_LOAD — jeden obejmujący sekcje niezapisywalne i jeden zawierający sekcje danych zapisywalnych. Segment PT_INTERP zawiera sekcję .interp, która zawiera nazwę interpretera, który ma zostać użyty do załadowania pliku binarnego. Z kolei segment PT_DYNAMIC zawiera sekcję .dynamic, która informuje interpreter, jak analizować i przygotowywać plik binarny do wykonania. Warto również wspomnieć o segmencie PT_PHDR, który obejmuje tabelę nagłówków programu.

Nagłówki programu

https://chacker.pl/

Tabela nagłówków programu zapewnia widok segmentu pliku binarnego, w przeciwieństwie do widoku sekcji zapewnianego przez tabelę nagłówków sekcji. Widok sekcji pliku binarnego ELF, który omówiłem wcześniej, jest przeznaczony wyłącznie do celów łączenia statycznego. Natomiast widok segmentu, który omówię później, jest używany przez system operacyjny i dynamiczny linker podczas ładowania pliku ELF do procesu w celu wykonania, aby zlokalizować odpowiedni kod i dane oraz zdecydować, co załadować do pamięci wirtualnej. Segment ELF obejmuje zero lub więcej sekcji, zasadniczo łącząc je w jeden fragment. Ponieważ segmenty zapewniają widok wykonania, są potrzebne tylko w przypadku wykonywalnych plików ELF, a nie w przypadku plików niewykonywalnych, takich jak obiekty relokowalne. Tabela nagłówków programu koduje widok segmentu przy użyciu nagłówków programu typu struct Elf64_Phdr. Każdy nagłówek programu zawiera pola pokazane w Listingu 2-11.

Listing 2-11: Definition of Elf64_Phdr in /usr/include/elf.h

typedef struct {

uint32_t p_type; /* Segment type */

uint32_t p_flags; /* Segment flags */

uint64_t p_offset; /* Segment file offset */

uint64_t p_vaddr; /* Segment virtual address */

uint64_t p_paddr; /* Segment physical address */

uint64_t p_filesz; /* Segment size in file */

uint64_t p_memsz; /* Segment size in memory */

uint64_t p_align; /* Segment alignment */

} Elf64_Phdr;

Opiszę każde z tych pól w kilku następnych sekcjach. Listing 2-12 pokazuje tabelę nagłówków programu dla przykładowego pliku binarnego, wyświetlaną przez readelf.

Listing 2-12: A typical program header as shown by readelf

$ readelf –wide –segments a.out

Elf file type is EXEC (Executable file)

Entry point 0x400430

There are 9 program headers, starting at offset 64

Program Headers:

Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align

PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8

INTERP 0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R 0x1

[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x00070c 0x00070c R E 0x200000

LOAD 0x000e10 0x0000000000600e10 0x0000000000600e10 0x000228 0x000230 RW 0x200000

DYNAMIC 0x000e28 0x0000000000600e28 0x0000000000600e28 0x0001d0 0x0001d0 RW 0x8

NOTE 0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R 0x4

GNU_EH_FRAME 0x0005e4 0x00000000004005e4 0x00000000004005e4 0x000034 0x000034 R 0x4

GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10

GNU_RELRO 0x000e10 0x0000000000600e10 0x0000000000600e10 0x0001f0 0x0001f0 R 0x1

(1) Section to Segment mapping:

Segment Sections…

00

01 .interp

02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version

.gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata

.eh_frame_hdr .eh_frame

03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss

04 .dynamic

05 .note.ABI-tag .note.gnu.build-id

06 .eh_frame_hdr

07

08 .init_array .fini_array .jcr .dynamic .got

Zwróć uwagę na mapowanie sekcji na segment na dole wyjścia readelf, które wyraźnie pokazuje, że segmenty to po prostu zbiór sekcji połączonych razem (1). To konkretne mapowanie sekcji na segment jest typowe dla większości plików binarnych ELF, z którymi się spotkasz.

Sekcje .shstrtab, .symtab, .strtab, .dynsym i .dynstr

https://chacker.pl/

Jak wspomniano podczas dyskusji na temat nagłówków sekcji, sekcja .shstrtab jest po prostu tablicą ciągów zakończonych zerem, które zawierają nazwy wszystkich sekcji w pliku binarnym. Jest indeksowana przez nagłówki sekcji, aby umożliwić narzędziom takim jak readelf znalezienie nazw sekcji. Sekcja .symtab zawiera tabelę symboli, która jest tabelą struktur Elf64_Sym, z których każda kojarzy nazwę symboliczną z fragmentem kodu lub danymi w innym miejscu pliku binarnego, takim jak funkcja lub zmienna. Faktyczne ciągi zawierające nazwy symboliczne znajdują się w sekcji .strtab. Na te ciągi wskazują struktury Elf64_Sym. W praktyce pliki binarne, na które natrafisz podczas analizy binarnej, będą często usuwane, co oznacza, że ​​tabele .symtab i .strtab są usuwane. Sekcje .dynsym i .dynstr są analogiczne do sekcji .symtab i .strtab, z tym wyjątkiem, że zawierają symbole i ciągi potrzebne do dynamicznego łączenia, a nie do statycznego łączenia. Ponieważ informacje w tych sekcjach są potrzebne podczas dynamicznego łączenia, nie można ich usunąć. Należy zauważyć, że statyczna tabela symboli ma typ sekcji SHT_SYMTAB, podczas gdy dynamiczna tabela symboli ma typ SHT_DYNSYM. Dzięki temu narzędzia takie jak strip mogą łatwo rozpoznać, które tabele symboli można bezpiecznie usunąć podczas usuwania pliku binarnego, a których nie.

Sekcje .init_array i .fini_array

https://chacker.pl/

Sekcja .init_array zawiera tablicę wskaźników do funkcji, które mają być używane jako konstruktory. Każda z tych funkcji jest wywoływana po kolei, gdy plik binarny jest inicjowany, przed wywołaniem funkcji main. Podczas gdy wspomniana sekcja .init zawiera pojedynczą funkcję startową, która wykonuje pewne kluczowe inicjalizacje potrzebne do uruchomienia pliku wykonywalnego, .init_array jest sekcją danych, która może zawierać dowolną liczbę wskaźników do funkcji, w tym wskaźniki do własnych niestandardowych konstruktorów. W gcc możesz oznaczyć funkcje w plikach źródłowych C jako konstruktory, dekorując je za pomocą __attribute__((constructor)). W przykładowym pliku binarnym .init_array zawiera tylko jeden wpis. Jest to wskaźnik do innej domyślnej funkcji inicjalizacji, zwanej frame_dummy, jak widać w wynikach objdump pokazanych na listingu 2-10.

Listing 2-10: Contents of the .init_array section

(1) $ objdump -d –section .init_array a.out

a.out: file format elf64-x86-64

Disassembly of section .init_array:

0000000000600e10 <__frame_dummy_init_array_entry>:

600e10: (2) 00 05 40 00 00 00 00 00 ..@…..

(3) $ objdump -d a.out | grep '<frame_dummy>’

0000000000400500 <frame_dummy>:

Pierwsze wywołanie objdump pokazuje zawartość .init_array (1). Jak widać, istnieje pojedynczy wskaźnik funkcji (zacieniowany na wyjściu), który zawiera bajty 00 05 40 00 00 00 00 00 (2). To po prostu little-endian-speak dla adresu 0x400500 (uzyskanego przez odwrócenie kolejności bajtów i usunięcie wiodących zer). Drugie wywołanie objdump pokazuje, że jest to rzeczywiście adres początkowy funkcji frame_dummy (3). Jak zapewne już zgadłeś, .fini_array jest analogiczne do .init_array, z tą różnicą, że .fini_array zawiera wskaźniki do destruktorów, a nie konstruktorów. Wskaźniki zawarte w .init_array i .fini_array są łatwe do zmiany, co czyni je wygodnymi miejscami do wstawiania haków, które dodają kod inicjalizacji lub finalizacji do pliku binarnego w celu modyfikacji jego zachowania. Należy pamiętać, że pliki inaries generowane przez starsze wersje gcc mogą zawierać sekcje o nazwach .ctors i .dtors zamiast .init_array i .fini_array.

Sekcja .dynamic

https://chacker.pl/

Sekcja .dynamic działa jako „mapa drogowa” dla systemu operacyjnego i dynamicznego linkera podczas ładowania i konfigurowania pliku binarnego ELF do wykonania. Jeśli zapomniałeś, jak działa proces ładowania, możesz zapoznać się z sekcją 1.4. Sekcja .dynamic zawiera tabelę struktur Elf64_Dyn (określonych w /usr/include/elf.h), zwanych również tagami. Istnieją różne typy tagów, z których każdy ma skojarzoną wartość. Jako przykład przyjrzyjmy się zawartości .dynamic w przykładowym pliku binarnym, pokazanym w Listingu2-9.

Listing  2.9

Contents of the .dynamic section

$ readelf –dynamic a.out

Dynamic section at offset 0xe28 contains 24 entries:

Tag Type Name/Value

(1) 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

0x000000000000000c (INIT) 0x4003c8

0x000000000000000d (FINI) 0x4005c4

0x0000000000000019 (INIT_ARRAY) 0x600e10

0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)

0x000000000000001a (FINI_ARRAY) 0x600e18

0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)

0x000000006ffffef5 (GNU_HASH) 0x400298

0x0000000000000005 (STRTAB) 0x400318

0x0000000000000006 (SYMTAB) 0x4002b8

0x000000000000000a (STRSZ) 61 (bytes)

0x000000000000000b (SYMENT) 24 (bytes)

0x0000000000000015 (DEBUG) 0x0

0x0000000000000003 (PLTGOT) 0x601000

0x0000000000000002 (PLTRELSZ) 48 (bytes)

0x0000000000000014 (PLTREL) RELA

0x0000000000000017 (JMPREL) 0x400398

0x0000000000000007 (RELA) 0x400380

0x0000000000000008 (RELASZ) 24 (bytes)

0x0000000000000009 (RELAENT) 24 (bytes)

(2) 0x000000006ffffffe (VERNEED) 0x400360

(3) 0x000000006fffffff (VERNEEDNUM) 1

0x000000006ffffff0 (VERSYM) 0x400356

0x0000000000000000 (NULL) 0x0

Jak widać, typ każdego znacznika w sekcji .dynamic jest pokazany w drugiej kolumnie wyjściowej. Znaczniki typu DT_NEEDED informują dynamiczny linker o zależnościach pliku wykonywalnego. Na przykład plik binarny używa funkcji puts z biblioteki współdzielonej libc.so.6 (1), więc musi zostać załadowany podczas wykonywania pliku binarnego. Znaczniki DT_VERNEED (2) i DT_VERNEEDNUM (3) określają adres początkowy i liczbę wpisów tabeli zależności wersji, co wskazuje oczekiwaną wersję różnych zależności pliku wykonywalnego. Oprócz listy zależności, sekcja .dynamic zawiera również wskaźniki do innych ważnych informacji wymaganych przez dynamiczny linker (na przykład dynamiczna tabela ciągów, dynamiczna tabela symboli, sekcja .got.plt i sekcja dynamicznej relokacji wskazywana przez znaczniki typu DT_STRTAB, DT_SYMTAB, DT_PLTGOT i DT_RELA, odpowiednio).

Sekcje .rel.* i .rela.*

https://chacker.pl/

ak widać w zrzucie readelf nagłówków sekcji przykładowego pliku binarnego, istnieje kilka sekcji o nazwach w formie rela.*. Te sekcje są typu SHT_RELA, co oznacza, że ​​zawierają informacje używane przez linker do wykonywania relokacji. Zasadniczo każda sekcja typu SHT_RELA jest tabelą wpisów relokacji, przy czym każdy wpis szczegółowo opisuje konkretny adres, pod którym należy zastosować relokację, a także instrukcje dotyczące sposobu rozwiązania konkretnej wartości, która musi zostać podłączona pod tym adresem. Wypis 2-8 przedstawia zawartość sekcji relokacji w przykładowym pliku binarnym.

Listing 2-8: Sekcje relokacji w przykładowym pliku binarnym

$ readelf –relocs a.out

Sekcja relokacji „.rela.dyn” w przesunięciu 0x380 zawiera 1 wpis:

Informacje o przesunięciu Typ Sym. Wartość Sym. Nazwa + Dodatek

(1)0000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

Sekcja relokacji „.rela.plt” w przesunięciu 0x398 zawiera 2 wpisy:

Informacje o przesunięciu Typ Sym. Wartość Sym. Nazwa + Dodatek

(2) 0000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0

(3) 0000601020 000200000007 R_X86_64_JUMP_SLO 00000000000000000 __libc_start_main@GLIBC_2.2.5 + 0

Jak widać, pozostają tylko relokacje dynamiczne (które mają zostać wykonane przez linker dynamiczny), ponieważ wszystkie relokacje statyczne, które istniały w pliku obiektu, zostały już rozwiązane podczas łączenia statycznego. W dowolnym pliku binarnym w świecie rzeczywistym (w przeciwieństwie do tego prostego przykładu) byłoby oczywiście znacznie więcej relokacji dynamicznych.Istnieją dwa typy relokacji, zwane R_X86_64_GLOB_DAT i R_X86_64_JUMP_SLO. Chociaż w naturze można spotkać o wiele więcej typów, te są jednymi z najczęstszych i najważniejszych. Wszystkie typy relokacji mają to do siebie, że określają przesunięcie, przy którym należy zastosować relokację. Szczegóły dotyczące sposobu obliczania wartości do wstawienia przy tym przesunięciu różnią się w zależności od typu relokacji i czasami są dość skomplikowane. Wszystkie te szczegóły można znaleźć w specyfikacji ELF, chociaż w przypadku normalnych zadań analizy binarnej nie trzeba ich znać. Pierwsza relokacja pokazana na Liście 2-8, typu R_X86_64_GLOB_DAT, ma swoje przesunięcie w sekcji .got (1), jak można stwierdzić, porównując przesunięcie z adresem bazowym .got pokazanym w wyjściu readelf na Liście 2-5. Ogólnie rzecz biorąc, ten typ relokacji jest używany do obliczania adresu symbolu danych i wstawiania go do prawidłowego przesunięcia w .got. Wpisy R_X86_64_JUMP_SLO nazywane są slotami skoku (2)(3); mają one swoje przesunięcie w sekcji .got.plt i reprezentują sloty, w których można wstawić adresy funkcji bibliotecznych. Jeśli spojrzysz wstecz na zrzut PLT przykładowego pliku binarnego w Liście 2-7, zobaczysz, że każdy z tych slotów jest używany przez jeden z szczątkowych PLT do pobrania jego pośredniego celu skoku. Adresy slotów skoku (obliczone z względnego przesunięcia do rejestru rip) pojawiają się po prawej stronie wyjścia w Liście 2-7, tuż za symbolem #.