Analiza wyodrębnionego pliku ELF za pomocą polecenia readelf

https://chacker.pl/

Aby wyświetlić szczegóły wyodrębnionego nagłówka ELF, warto skorzystać z polecenia readelf. Ale czy polecenie readelf zadziała w przypadku uszkodzonego pliku ELF zawierającego jedynie nagłówek? Sprawdźmy to na listingu !

Opcja -h (1) nakazuje programowi readelf wydrukować tylko nagłówek pliku wykonywalnego. Nadal wyświetla komunikat, że przesunięcia do tabeli nagłówków sekcji i tabeli nagłówków programu wskazują na obszar poza plikiem, ale to nie stanowi problemu. Ważne jest, aby teraz mieć wygodną reprezentację wyodrębnionego nagłówka ELF. Jak teraz określić rozmiar całego pliku ELF, używając wyłącznie nagłówka pliku wykonywalnego? Na rysunku 2-1 w rozdziale 2 dowiedziałeś się, że ostatnią częścią pliku ELF jest zazwyczaj tabela nagłówków sekcji, a przesunięcie do tabeli nagłówków sekcji jest podane w nagłówku pliku wykonywalnego (2). Nagłówek pliku wykonywalnego informuje również o rozmiarze każdego nagłówka sekcji (3) i liczbie nagłówków sekcji w tabeli (4). Oznacza to, że możesz obliczyć rozmiar całej biblioteki ELF ukrytej w pliku bitmapowym w następujący sposób:

rozmiar = e_shoff + (e_shnum x e_shentsize)

= 8;568 + (27 64)

= 10;296

W tym równaniu rozmiar to rozmiar całej biblioteki, e_shoff to przesunięcie względem tabeli nagłówków sekcji, e_shnum to liczba nagłówków sekcji w tabeli, a e_shentsize to rozmiar każdego nagłówka sekcji. Teraz, gdy wiesz, że rozmiar biblioteki powinien wynosić 10 296 bajtów, możesz użyć dd, aby wyodrębnić ją w całości, w następujący sposób:

$ dd skip=52 count=10296 if=67b8601 (1)of=lib5ae9b7f.so bs=1

10296+0 records in

10296+0 records out

10296 bytes (10 kB, 10 KiB) copied, 0.0287996 s, 358 kB/s

Polecenie dd wywołuje wyodrębniony plik lib5ae9b7f.so (1), ponieważ jest to nazwa brakującej biblioteki, której oczekuje plik binarny ctf. Po uruchomieniu tego polecenia powinieneś mieć w pełni funkcjonalny obiekt współdzielony ELF. Użyjmy readelf, aby sprawdzić, czy wszystko poszło dobrze, jak pokazano na Listingu 5-2. Aby zachować zwięzłość wyników, wydrukujmy tylko nagłówek wykonywalny (-h) i tabele symboli (-s). Te ostatnie powinny dać Ci pojęcie o funkcjonalności biblioteki.

Zgodnie z oczekiwaniami, cała biblioteka wydaje się być poprawnie wyodrębniona. Chociaż jest okrojona, dynamiczna tabela symboli ujawnia kilka interesujących eksportowanych funkcji (od (1) do (5)). Jednak nazwy wydają się być trochę nieczytelne, co utrudnia ich odczytanie. Zobaczmy, czy da się to naprawić.

Przeglądanie zawartości pliku za pomocą xxd

https://chacker.pl/

Aby dokładnie dowiedzieć się, co znajduje się w pliku, nie opierając się na standardowych założeniach dotyczących jego zawartości, należy przeanalizować go na poziomie bajtów. W tym celu można użyć dowolnego systemu liczbowego do wyświetlania bitów i bajtów na ekranie. Na przykład, można użyć systemu binarnego, wyświetlając wszystkie jedynki i zera osobno. Ponieważ jednak prowadzi to do żmudnej analizy, lepiej jest użyć systemu szesnastkowego. W systemie szesnastkowym (znanym również jako system o podstawie 16, w skrócie hex) cyfry mieszczą się w zakresie od 0 do 9 (w standardowym znaczeniu), a następnie od a do f (gdzie a reprezentuje wartość 10, a f reprezentuje 15). Ponadto, ponieważ bajt ma 256 = 16 x 16 możliwych wartości, mieści się dokładnie w dwóch cyfrach szesnastkowych, co czyni to kodowanie wygodnym do kompaktowego wyświetlania bajtów. Aby wyświetlić bajty pliku w systemie szesnastkowym, należy użyć programu do zapisu szesnastkowego. Edytor szesnastkowy to program, który może również edytować bajty w pliku ile. Do edycji szesnastkowej użyjmy prostego programu do zapisu szesnastkowego o nazwie xxd, który jest domyślnie zainstalowany w większości systemów Linux. Oto pierwsze 15 wierszy danych wyjściowych z xxd dla analizowanego pliku bitmapowego:

Jak widać, pierwsza kolumna wyników pokazuje przesunięcie w pliku w formacie szesnastkowym. Kolejne osiem kolumn pokazuje szesnastkowe reprezentacje bajtów w pliku, a po prawej stronie wyników znajduje się reprezentacja ASCII tych samych bajtów. Liczbę bajtów wyświetlanych w wierszu można zmienić za pomocą opcji -c programu xxd. Na przykład, polecenie xxd -c 32 wyświetli 32 bajty w wierszu. Można również użyć opcji -b, aby wyświetlić dane binarne zamiast szesnastkowych, a za pomocą opcji -i wygenerować tablicę w stylu C zawierającą bajty, którą można bezpośrednio uwzględnić w kodzie źródłowym C lub C++. Aby wygenerować tylko część bajtów, można użyć opcji -s (wyszukiwanie), aby określić przesunięcie pliku, od którego ma się rozpocząć, oraz opcji -l (długość), aby określić liczbę bajtów do zrzucenia. W pliku wyjściowym xxd dla pliku bitmapowego magiczne bajty ELF pojawiają się na przesunięciu 0x34 (1), co odpowiada liczbie 52 w systemie dziesiętnym. Wskazuje to, gdzie w pliku zaczyna się podejrzewana biblioteka ELF. Niestety, ustalenie jej końca nie jest takie proste, ponieważ nie ma magicznych bajtów ograniczających koniec pliku ELF. Dlatego przed próbą wyodrębnienia całego pliku ELF, zacznij od wyodrębnienia samego nagłówka ELF. Jest to łatwiejsze, ponieważ wiesz, że 64-bitowe nagłówki ELF zawierają dokładnie 64 bajty. Następnie możesz przeanalizować nagłówek ELF, aby określić rozmiar całego pliku. Aby wyodrębnić nagłówek, użyj dd do skopiowania 64 bajtów z pliku bitmapowego, zaczynając od przesunięcia 52, do nowego pliku wyjściowego o nazwie elf_header.

$ dd skip=52 count=64 if=67b8601 of=elf_header bs=1

64+0 records in

64+0 records out

64 bytes copied, 0.000404841 s, 158 kB/s

Użycie dd jest tutaj kwestią oczywistą, więc nie będę tego szczegółowo wyjaśniał. Jednak dd jest niezwykle wszechstronnym narzędziem1, więc warto przeczytać jego stronę podręcznika, jeśli jeszcze go nie znasz. Użyjmy ponownie xxd, aby sprawdzić, czy zadziałało.

$ xxd elf_header

00000000: Ê7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF…………

00000010: 0300 3e00 0100 0000 7009 0000 0000 0000 ..>…..p…….

00000020: 4000 0000 0000 0000 7821 0000 0000 0000 @…….x!……

00000030: 0000 0000 4000 3800 0700 4000 1b00 1a00 ….@.8…@…..

Wygląda to jak nagłówek ELF! Wyraźnie widać magiczne bajty na początku (1), a także widać, że tablica e_ident i inne pola wyglądają sensownie.

Szybkie wyszukiwanie kodów ASCII

https://chacker.pl/

Podczas interpretowania surowych bajtów jako ASCII często potrzebna jest tabela, która mapuje wartości bajtów w różnych reprezentacjach na symbole ASCII. Aby uzyskać szybki dostęp do takiej tabeli, można skorzystać ze specjalnej strony podręcznika o nazwie man ascii. Oto fragment tabeli z man ascii:

Jak widać, to prosty sposób na wyszukiwanie mapowań z kodowań ósemkowych, dziesiętnych i szesnastkowych na znaki ASCII. To znacznie szybsze niż wyszukiwanie w Google tabeli ASCII!

Używanie ldd do eksploracji zależności

https://chacker.pl/

Chociaż nie zaleca się uruchamiania nieznanych plików binarnych, ponieważ pracujesz na maszynie wirtualnej, spróbujmy uruchomić wyodrębniony plik binarny ctf. Próba uruchomienia pliku nie przyniesie żadnych rezultatów.

$ ./ctf

./ctf: error while loading shared libraries: lib5ae9b7f.so:

cannot open shared object file: No such file or directory

Zanim jakikolwiek kod aplikacji zostanie wykonany, dynamiczny linker zgłasza brak biblioteki o nazwie lib5ae9b7f.so. Nie brzmi to jak biblioteka, którą normalnie można znaleźć w dowolnym systemie. Przed rozpoczęciem poszukiwań tej biblioteki warto sprawdzić, czy ctf ma jakieś nierozwiązane zależności. Systemy Linux zawierają program o nazwie ldd, którego można użyć do sprawdzenia, od których obiektów współdzielonych zależy plik binarny i gdzie (jeśli gdziekolwiek) znajdują się te zależności w systemie. Można nawet użyć ldd wraz z flagą -v, aby dowiedzieć się, jakich wersji bibliotek oczekuje plik binarny, co może być przydatne podczas debugowania. Jak wspomniano w podręczniku ldd, ldd może uruchomić plik binarny w celu ustalenia zależności, dlatego nie jest bezpieczne używanie go w przypadku niezaufanych plików binarnych, chyba że uruchamia się go na maszynie wirtualnej lub w innym odizolowanym środowisku. Oto wynik polecenia ldd dla pliku binarnego ctf:

$ ldd ctf

linux-vdso.so.1 => (0x00007fff6edd4000)

lib5ae9b7f.so => not found

libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f67c2cbe000)

libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f67c2aa7000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f67c26de000)

libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f67c23d5000)

/lib64/ld-linux-x86-64.so.2 (0x0000561e62fe5000)

Na szczęście nie ma żadnych nierozwiązanych zależności poza brakującą biblioteką, zidentyfikowaną wcześniej, lib5ae9b7f.so. Teraz możesz skupić się na ustaleniu, czym jest ta tajemnicza biblioteka i jak ją zdobyć, aby przechwycić flagę! Ponieważ nazwa biblioteki wskazuje, że nie znajdziesz jej w żadnym standardowym repozytorium, musi ona znajdować się gdzieś w plikach, które otrzymałeś do tej pory. Przypomnij sobie z rozdziału 2, że wszystkie pliki binarne i biblioteki ELF zaczynają się od magicznej sekwencji 0x7f ELF. To przydatny ciąg znaków do wyszukania brakującej biblioteki; o ile biblioteka nie jest zaszyfrowana, powinieneś być w stanie znaleźć w ten sposób nagłówek ELF. Spróbujmy prostego wyszukiwania grep dla ciągu „ELF”.

$ grep 'ELF’ *

Binary file 67b8601 matches

Binary file ctf matches

Zgodnie z oczekiwaniami, ciąg „ELF” pojawia się w ctf, co nie jest zaskakujące, ponieważ już wiesz, że to plik binarny ELF. Widać jednak, że ten ciąg znajduje się również w pliku 67b8601, który na pierwszy rzut oka wyglądał jak niewinny plik bitmapowy. Czy w danych pikseli bitmapy może znajdować się biblioteka współdzielona? Z pewnością wyjaśniałoby to te dziwnie kolorowe piksele, które widziałeś na rysunku 5-1b! Przyjrzyjmy się bliżej zawartości pliku 67b8601, aby się tego dowiedzieć.

Rozwiązywanie kryzysów tożsamości za pomocą pliku

https://chacker.pl/

Ponieważ nie otrzymałeś żadnych wskazówek dotyczących zawartości pliku, nie masz pojęcia, co z nim zrobić. W takiej sytuacji (na przykład w scenariuszach inżynierii wstecznej lub kryminalistyki), dobrym pierwszym krokiem jest ustalenie, co możesz wiedzieć o typie pliku i jego zawartości. Narzędzie „plik” zostało zaprojektowane właśnie w tym celu; przyjmuje ono dane wejściowe z kilku plików, a następnie informuje o typie każdego z nich. Możesz je pamiętać z rozdziału 2, gdzie użyłem „plik”, aby dowiedzieć się, jaki jest typ pliku ELF. Zaletą narzędzia „plik” jest to, że nie daje się ono oszukać rozszerzeniami. Zamiast tego, wyszukuje ono inne charakterystyczne wzorce w pliku, takie jak magiczne bajty, takie jak sekwencja ELF 0x7f na początku pliku ELF, aby dowiedzieć się, jaki jest typ pliku. Jest to idealne rozwiązanie, ponieważ plik „ładunek” nie ma rozszerzenia. Oto, co narzędzie „plik” mówi o pliku:

$ file payload

payload: ASCII text

Jak widać, ładunek zawiera tekst ASCII. Aby szczegółowo przeanalizować tekst, można użyć narzędzia head, które zrzuca pierwsze kilka wierszy (domyślnie 10) pliku tekstowego na standardowe wyjście. Istnieje również analogiczne narzędzie o nazwie tail, które wyświetla ostatnie kilka wierszy pliku. Oto, co pokazuje wynik działania narzędzia head:

$ file head

H4sIAKiT61gAA+xaD3RTVZq/Sf9TSKL8aflnn56ioNJJSiktDpqUlL5o0UpbYEVI0zRtI2naSV5K YV0HTig21jqojH9mnRV35syZPWd35ZzZ00XHxWBHYJydXf4ckRldZRUxBRzxz2CFQvb77ru3ee81 AZdZZ92z+XrS733fu993v/v/vnt/bqmVfNNkBlq0cCFyy6KFZiUHKi1buMhMLAvMi0oXWSzlZYtA v2hRWRkRzN94ZEChoOQKCAJp8fdcNt2V3v8fpe9X1y7T63Rjsp7cTlCKGq1UtjL9yPUJGyupIHnw / zoym2SDnKVIZyVWFR9hrjnPZeky4JcJvwq9LFforSo+i6XjXKfgWaoSWFX8mclExQkRxuww1uOz Ze3x2U0qfpDFcUyvttMzuxFmN8LSc054er26fJns18D0DaxcnNtZOrsiPVLdh1ILPudey/xda1Xx MpauTGN3L9hlk69PJsZXsPxS1YvA4uect8N3fN7m8rLv+Frm+7z+UM/8nory+eVlJcHOklIak4ml rbm7kabn9SiwmKcQuQ/g+3n/OJj/byfuqjv09uKVj8889O6TvxXM+G4qSbRbX1TQCZnWPNQVwG86

/F7+4IkHl1a/ ebY91bPemngU8OpI58YNjrWD16u3P3wuzaJ3kh4i6vpuhT6g7rkfs6k0DtS6P8l hf6NFPocfXL9yRTpS0ny+NtJ8vR3p0hfl8J/ bgr9Vyn0b6bQkxTl+ixF+p+m0N+qx743k+wWmlT6

To zdecydowanie nie wygląda na czytelne dla człowieka. Przyglądając się bliżej alfabetowi użytemu w pliku, widać, że składa się on wyłącznie ze znaków alfanumerycznych oraz znaków + i /, uporządkowanych w równych wierszach. Widząc plik wyglądający w ten sposób, zazwyczaj można bezpiecznie założyć, że jest to plik Base64.

Base64 to powszechnie stosowana metoda kodowania danych binarnych jako tekstu ASCII. Jest ona powszechnie stosowana między innymi w poczcie e-mail i internecie, aby zapobiec przypadkowemu zniekształceniu danych binarnych przesyłanych przez sieć przez usługi obsługujące wyłącznie tekst. Co ciekawe, systemy Linux zawierają narzędzie o nazwie base64 (zazwyczaj w pakiecie GNU coreutils), które umożliwia kodowanie i dekodowanie Base64. Domyślnie base64 koduje wszystkie pliki lub dane wejściowe przesyłane do standardu stdin. Można jednak użyć flagi -d, aby nakazać base64 dekodowanie. Zdekodujmy dane i zobaczmy, co otrzymasz!

$ base64 -d ładunek > zdekodowany_ładunek

To polecenie dekoduje ładunek, a następnie zapisuje zdekodowaną zawartość w nowym pliku o nazwie decoded_payload. Teraz, gdy zdekodowałeś ładunek, użyjmy ponownie polecenia file, aby sprawdzić typ zdekodowanego pliku.

$ file decoded_payload

decoded_payload: gzip compressed data, last modified: Tue Oct 22 15:46:43 2019, from Unix

No to zaczynasz coś rozumieć! Okazuje się, że za warstwą kodowania Base64, tajemniczy plik jest w rzeczywistości skompresowanym archiwum, które używa gzip jako zewnętrznej warstwy kompresji. To okazja, aby wprowadzić kolejną przydatną funkcję pliku: możliwość zaglądania do wnętrza skompresowanych plików. Możesz przekazać opcję -z do pliku, aby zobaczyć, co znajduje się w archiwum bez jego rozpakowywania. Oto, co powinieneś zobaczyć:

file -z decoded_payload

decoded_payload: POSIX tar archive (GNU) (gzip compressed data, last modified:

Tue Oct 22 19:08:12 2019, from Unix)

Widać, że masz do czynienia z wieloma warstwami, które musisz wyodrębnić, ponieważ warstwa zewnętrzna to warstwa kompresji gzip, a wewnątrz niej znajduje się archiwum tar, które zazwyczaj zawiera pakiet plików. Aby wyświetlić pliki przechowywane w środku, użyj tar do rozpakowania i wyodrębnienia pliku decoded_payload, w następujący sposób:

$ tar xvzf decoded_payload

ctf

67b8601

Jak widać w logu tar, z archiwum wypakowano dwa pliki: ctf i 67b8601. Użyjmy ponownie polecenia file, aby sprawdzić, z jakimi typami plików mamy do czynienia.

$ file ctf

ctf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,

BuildID[sha1]=29aeb60bcee44b50d1db3a56911bd1de93cd2030, stripped

Pierwszy plik, ctf, to dynamicznie linkowany 64-bitowy plik wykonywalny ELF z uproszczoną wersją kodu. Drugi plik, o nazwie 67b8601, to plik bitmapowy (BMP) o rozmiarze 512 x 512 pikseli. Ponownie, można to zobaczyć, używając polecenia file w następujący sposób:

$ file 67b8601

67b8601: PC bitmap, Windows 3.x format, 512 x 512 x 24

Ten plik BMP przedstawia czarny kwadrat, jak widać na rysunku .

Jeśli przyjrzysz się uważnie, powinieneś zauważyć kilka pikseli o nieregularnych kolorach u dołu rysunku. Rysunek 2 przedstawia powiększony fragment tych pikseli.

Zanim przejdziemy do wyjaśnienia, co to wszystko oznacza, przyjrzyjmy się bliżej plikowi ctf, który właśnie wyodrębniłeś.

PODSTAWOWA ANALIZA BINARNA W SYSTEMIE LINUX

https://chacker.pl/

Nawet w najbardziej złożonej analizie binarnej można osiągnąć zaskakująco zaawansowane wyczyny, łącząc zestaw podstawowych narzędzi w odpowiedni sposób. Może to zaoszczędzić godziny pracy na samodzielnym wdrażaniu równoważnych funkcji. Tu poznasz podstawowe narzędzia potrzebne do przeprowadzania analizy binarnej w systemie Linux. Zamiast po prostu pokazać listę narzędzi i wyjaśnić, do czego służą, posłużę się wyzwaniem „Capture the Flag” (CTF), aby zilustrować ich działanie. W bezpieczeństwie komputerowym i hakowaniu wyzwania CTF często rozgrywane są jako konkursy, w których celem jest zazwyczaj analiza lub wykorzystanie danego pliku binarnego (lub działającego procesu lub serwera), aż do momentu przechwycenia ukrytej w nim flagi. Flaga jest zazwyczaj ciągiem szesnastkowym, którego można użyć do udowodnienia ukończenia wyzwania, a także do odblokowania nowych wyzwań. W tym CTF zaczynasz od tajemniczego pliku o nazwie „payload”, który znajdziesz na maszynie wirtualnej w katalogu. Celem jest odkrycie sposobu na wyodrębnienie ukrytej flagi z ładunku. Analizując ładunek i szukając flagi, nauczysz się korzystać z szerokiej gamy podstawowych narzędzi do analizy binarnej, dostępnych praktycznie w każdym systemie Linux (większość z nich jest częścią pakietu GNU coreutils lub binutils).  Większość narzędzi, które zobaczysz, oferuje szereg przydatnych opcji, ale jest ich zbyt wiele, aby omówić je wyczerpująco. Dlatego warto zapoznać się ze stroną podręcznika każdego narzędzia, używając polecenia man na maszynie wirtualnej. Na końcu  użyjesz odzyskanej flagi, aby odblokować nowe wyzwanie, które możesz ukończyć samodzielnie!

Podsumowanie

https://chacker.pl/

Poznałeś wszystkie formaty binarne. Nauczyłeś się, jak ładować te pliki binarne, aby przygotować je do późniejszej analizy binarnej. Przy okazji poznałeś również bibliotekę libbfd, powszechnie używaną do ładowania plików binarnych. Teraz, gdy masz już działający program ładujący pliki binarne, możesz przejść do technik analizy plików binarnych. Po wprowadzeniu do podstawowych technik analizy binarnej w c, użyjesz programu ładującego do wdrożenia własnych narzędzi do analizy binarnej.

Ćwiczenia

  1. Zrzucanie zawartości sekcji

Dla zwięzłości, obecna wersja programu loader_demo nie wyświetla zawartości sekcji. Rozszerz ją o możliwość przyjęcia pliku binarnego i nazwy sekcji jako danych wejściowych. Następnie zrzuć zawartość tej sekcji na ekran w formacie szesnastkowym.

  1. Nadpisywanie słabych symboli

Niektóre symbole są słabe, co oznacza, że ich wartość może zostać nadpisana przez inny symbol, który nie jest słaby. Obecnie program ładujący binarne nie bierze tego pod uwagę i po prostu przechowuje wszystkie symbole. Rozszerz program ładujący binarne tak, aby w przypadku późniejszego nadpisania słabego symbolu przez inny symbol, zachowywana była tylko najnowsza wersja. Zajrzyj do pliku /usr/include/bfd.h, aby dowiedzieć się, które flagi należy sprawdzić.

  1. Wyświetlanie symboli danych

Rozszerz program ładujący binarne i program loader_demo tak, aby mogły obsługiwać lokalne i globalne symbole danych, a także symbole funkcji. Musisz dodać obsługę symboli danych w programie ładującym, dodać nowy typ SymbolType w klasie Symbol oraz dodać kod do programu loader_demo, aby wyświetlać symbole danych na ekranie. Koniecznie przetestuj swoje modyfikacje na pliku binarnym bez pasków, aby upewnić się, że występują tam symbole danych. Pamiętaj, że elementy danych nazywane są obiektami w terminologii symboli. Jeśli nie masz pewności co do poprawności danych wyjściowych, użyj polecenia readelf, aby je zweryfikować.

Testowanie programu ładującego pliki binarne

https://chacker.pl/

Utwórzmy prosty program do testowania nowego programu ładującego pliki binarne. Program będzie pobierał nazwę pliku binarnego jako dane wejściowe, używał programu ładującego do załadowania tego pliku, a następnie wyświetlał informacje diagnostyczne dotyczące załadowanego pliku. Listing  przedstawia kod programu testowego.

Listing : loader_demo.cc

#include <stdio.h>

#include <stdint.h>

#include <string>

#include „../inc/loader.h”

int

main(int argc, char *argv[])

{

size_t i;

Binary bin;

Section *sec;

Symbol *sym;

std::string fname;

if(argc < 2) {

printf(„Usage: %s <binary>\n”, argv[0]);

return 1;

}

fname.assign(argv[1]);

(1) if(load_binary(fname, &bin, Binary::BIN_TYPE_AUTO) < 0) {

return 1;

}

(2) printf(„loaded binary '%s’ %s/%s (%u bits) entry@0x%016jx\n”,

bin.filename.c_str(),

bin.type_str.c_str(), bin.arch_str.c_str(),

bin.bits, bin.entry);

(3) for(i = 0; i < bin.sections.size(); i++) {

sec = &bin.sections[i];

printf(” 0x%016jx %-8ju %-20s %s\n”,

sec->vma, sec->size, sec->name.c_str(),

sec->type == Section::SEC_TYPE_CODE ? „CODE” : „DATA”);

}

(4)Í if(bin.symbols.size() > 0) {

printf(„scanned symbol tables\n”);

for(i = 0; i < bin.symbols.size(); i++) {

sym = &bin.symbols[i];

printf(” %-40s 0x%016jx %s\n”,

sym->name.c_str(), sym->addr,

(sym->type & Symbol::SYM_TYPE_FUNC) ? „FUNC” : „”);

}

}

(5) unload_binary(&bin);

return 0;

}

Ten program testowy ładuje plik binarny podany jako pierwszy argument (1), a następnie wyświetla podstawowe informacje o pliku binarnym, takie jak nazwa pliku, typ, architektura i punkt wejścia (2). Następnie drukuje adres bazowy, rozmiar, nazwę i typ każdej sekcji (3), a na koniec wyświetla wszystkie znalezione symbole (4). Następnie rozładowuje plik binarny i zwraca (5). Spróbuj uruchomić program loader_demo na maszynie wirtualnej! Powinieneś zobaczyć wynik podobny do Listingu.

Listing : Example output of the loader test program

$ loader_demo /bin/ls

loaded binary '/bin/ls’ elf64-x86-64/i386:x86-64 (64 bits) entry@0x4049a0

0x0000000000400238 28 .interp DATA

0x0000000000400254 32 .note.ABI-tag DATA

0x0000000000400274 36 .note.gnu.build-id DATA

0x0000000000400298 192 .gnu.hash DATA

0x0000000000400358 3288 .dynsym DATA

0x0000000000401030 1500 .dynstr DATA

0x000000000040160c 274 .gnu.version DATA

0x0000000000401720 112 .gnu.version_r DATA

0x0000000000401790 168 .rela.dyn DATA

0x0000000000401838 2688 .rela.plt DATA

0x00000000004022b8 26 .init CODE

0x00000000004022e0 1808 .plt CODE

0x00000000004029f0 8 .plt.got CODE

0x0000000000402a00 70281 .text CODE

0x0000000000413c8c 9 .fini CODE

0x0000000000413ca0 27060 .rodata DATA

0x000000000041a654 2060 .eh_frame_hdr DATA

0x000000000041ae60 11396 .eh_frame DATA

0x000000000061de00 8 .init_array DATA

0x000000000061de08 8 .fini_array DATA

0x000000000061de10 8 .jcr DATA

0x000000000061de18 480 .dynamic DATA

0x000000000061dff8 8 .got DATA

0x000000000061e000 920 .got.plt DATA

0x000000000061e3a0 608 .data DATA

scanned symbol tables

_fini 0x0000000000413c8c FUNC

_init 0x00000000004022b8 FUNC

free 0x0000000000402340 FUNC

_obstack_memory_used 0x0000000000412960 FUNC

_obstack_begin 0x0000000000412780 FUNC

_obstack_free 0x00000000004128f0 FUNC

localtime_r 0x00000000004023a0 FUNC

_obstack_allocated_p 0x00000000004128c0 FUNC

_obstack_begin_1 0x00000000004127a0 FUNC

_obstack_newchunk 0x00000000004127c0 FUNC

malloc 0x0000000000402790 FUNC

Ładowanie sekcji

https://chacker.pl/

Po załadowaniu symboli pozostaje tylko jedna rzecz do zrobienia, choć prawdopodobnie najważniejsza: załadowanie sekcji pliku binarnego. Listing  pokazuje, jak load_sections_bfd implementuje tę funkcjonalność.

Listing : inc/loader.cc (continued)

static int

load_sections_bfd(bfd *bfd_h, Binary *bin)

{

int bfd_flags;

uint64_t vma, size;

const char *secname;

(1) asection* bfd_sec;

Section *sec;

Section::SectionType sectype;

(2) for(bfd_sec = bfd_h->sections; bfd_sec; bfd_sec = bfd_sec->next) {

(3) bfd_flags = bfd_get_section_flags(bfd_h, bfd_sec);

sectype = Section::SEC_TYPE_NONE;

(4) if(bfd_flags & SEC_CODE) {

sectype = Section::SEC_TYPE_CODE;

} else if(bfd_flags & SEC_DATA) {

sectype = Section::SEC_TYPE_DATA;

} else {

continue;

}

(5) vma = bfd_section_vma(bfd_h, bfd_sec);

(6) size = bfd_section_size(bfd_h, bfd_sec);

(7) secname = bfd_section_name(bfd_h, bfd_sec);

if(!secname) secname = „<unnamed>”;

(8) bin->sections.push_back(Section());

sec = &bin->sections.back();

sec->binary = bin;

sec->name = std::string(secname);

sec->type = sectype;

sec->vma = vma;

sec->size = size;

(9) sec->bytes = (uint8_t*)malloc(size);

if(!sec->bytes) {

fprintf(stderr, „out of memory\n”);

return -1;

}

(10) if(!bfd_get_section_contents(bfd_h, bfd_sec, sec->bytes, 0, size)) {

fprintf(stderr, „failed to read section '%s’ (%s)\n”,

secname, bfd_errmsg(bfd_get_error()));

return -1;

}

}

return 0;

}

Do przechowywania sekcji biblioteka libbfd używa struktury danych o nazwie asection, znanej również jako struct bfd_section. Wewnętrznie biblioteka libbfd przechowuje listę powiązaną struktur asection reprezentujących wszystkie sekcje. Moduł ładujący rezerwuje asection* do iterowania po tej liście (1). Aby iterować po wszystkich sekcjach, należy zacząć od pierwszej (wskazywanej przez bfd_h->sections, nagłówek listy sekcji biblioteki libbfd), a następnie podążać za kolejnym wskaźnikiem zawartym w każdym obiekcie asection (2). Gdy kolejny wskaźnik będzie równy NULL, dotarto do końca listy. Dla każdej sekcji moduł ładujący najpierw sprawdza, czy w ogóle powinna zostać załadowana. Ponieważ moduł ładujący ładuje tylko sekcje kodu i danych, rozpoczyna od pobrania flag sekcji, aby sprawdzić jej typ. Aby pobrać flagi, używa funkcji bfd_get_section_flags (3). Następnie sprawdza, czy ustawiona jest flaga EC_CODE lub SEC_DATA (4). Jeśli nie, pomija tę sekcję i przechodzi do następnej. Jeśli któraś z flag jest ustawiona, program ładujący ustawia typ sekcji dla odpowiadającego jej obiektu sekcji i kontynuuje ładowanie sekcji. Oprócz typu sekcji, program ładujący kopiuje adres wirtualny, rozmiar (w bajtach), nazwę i surowe bajty każdej sekcji kodu lub danych. Aby znaleźć wirtualny adres bazowy sekcji libbfd, należy użyć bfd_section_vma (5). Podobnie, należy użyć bfd_section_size (6) i bfd_section_name (7), aby uzyskać odpowiednio rozmiar i nazwę sekcji. Możliwe jest, że sekcja nie ma nazwy, w takim przypadku bfd_section_name zwróci NULL. Program ładujący kopiuje teraz rzeczywistą zawartość sekcji do obiektu sekcji. Aby to osiągnąć, rezerwuje sekcję w obiekcie binarnym (8) i kopiuje wszystkie pola, które właśnie odczytał. Następnie przydziela wystarczającą ilość miejsca w elemencie bytes elementu sekcji, aby pomieścić wszystkie bajty w sekcji (9). Jeśli operacja malloc się powiedzie, kopiuje wszystkie bajty sekcji z obiektu sekcji libbfd do sekcji za pomocą funkcji bfd_get_section_contents (10). Argumentami są: uchwyt bfd, wskaźnik do interesującego obiektu sekcji, tablica docelowa zawierająca zawartość sekcji, przesunięcie, od którego należy rozpocząć kopiowanie, oraz liczba bajtów do skopiowania do tablicy docelowej. Aby skopiować wszystkie bajty, przesunięcie początkowe wynosi 0, a liczba bajtów do skopiowania jest równa rozmiarowi sekcji. Jeśli kopiowanie się powiedzie, funkcja bfd_get_section_contents zwraca wartość true; w przeciwnym razie zwraca wartość false. Jeśli wszystko przebiegło pomyślnie, proces ładowania jest zakończony!

Ładowanie symboli

https://chacker.pl/

Listing  przedstawia kod funkcji load_symbols_bfd, która ładuje statyczną tabelę symboli.

Listing : inc/loader.cc (continued)

static int

load_symbols_bfd(bfd *bfd_h, Binary *bin)

{

int ret;

long n, nsyms, i;

(1) asymbol **bfd_symtab;

Symbol *sym;

bfd_symtab = NULL;

(2) n = bfd_get_symtab_upper_bound(bfd_h);

if(n < 0) {

fprintf(stderr, „failed to read symtab (%s)\n”,

bfd_errmsg(bfd_get_error()));

goto fail;

} else if(n) {

(3) bfd_symtab = (asymbol**)malloc(n);

if(!bfd_symtab) {

fprintf(stderr, „out of memory\n”);

goto fail;

}

(4) nsyms = bfd_canonicalize_symtab(bfd_h, bfd_symtab);

if(nsyms < 0) {

fprintf(stderr, „failed to read symtab (%s)\n”,

bfd_errmsg(bfd_get_error()));

goto fail;

}

(5) for(i = 0; i < nsyms; i++) {

(6) if(bfd_symtab[i]->flags & BSF_FUNCTION) {

bin->symbols.push_back(Symbol());

sym = &bin->symbols.back();

(7) sym->type = Symbol::SYM_TYPE_FUNC;

(8) sym->name = std::string(bfd_symtab[i]->name);

(9) sym->addr = bfd_asymbol_value(bfd_symtab[i]);

}

}

}

ret = 0;

goto cleanup;

fail:

ret = -1;

cleanup:

(10) if(bfd_symtab) free(bfd_symtab);

return ret;

}

W bibliotece libbfd symbole są reprezentowane przez strukturę asymbol, która jest po prostu skróconą nazwą struktury bfd_symbol. Z kolei tablica symboli to po prostu asymbol**, czyli tablica wskaźników do symboli. Zatem zadaniem funkcji load_symbols_bfd jest wypełnienie tablicy wskaźników asymbol zadeklarowanych w punkcie (1), a następnie skopiowanie interesujących informacji do obiektu binarnego. Parametrami wejściowymi funkcji load_symbols_bfd są uchwyt bfd i obiekt binarny, w którym przechowywane są informacje symboliczne. Zanim będzie można załadować jakiekolwiek wskaźniki do symboli, należy przydzielić wystarczająco dużo miejsca na ich przechowywanie. Funkcja bfd_get_symtab_upper_bound (2) informuje, ile bajtów należy w tym celu przydzielić. W przypadku błędu liczba bajtów jest ujemna i może również wynosić zero, co oznacza brak tablicy symboli. Jeśli nie ma tablicy symboli, funkcja load_symbols_bfd wykonuje zadanie i po prostu zwraca wartość. Jeśli wszystko jest w porządku i tablica symboli zawiera dodatnią liczbę bajtów, przydzielasz wystarczająco dużo miejsca, aby pomieścić wszystkie wskaźniki asymbol w (3). Jeśli operacja malloc się powiedzie, możesz wreszcie poprosić libbfd o wypełnienie tablicy symboli! Robisz to za pomocą funkcji bfd_canonicalize_symtab (4), która przyjmuje jako dane wejściowe Twój uchwyt bfd i tablicę symboli, którą chcesz wypełnić (Twój asymbol**). Zgodnie z żądaniem, libbfd należycie wypełnia tablicę symboli i zwraca liczbę symboli umieszczonych w tablicy (ponownie, jeśli ta liczba jest ujemna, wiesz, że coś poszło nie tak). Teraz, gdy masz wypełnioną tablicę symboli, możesz przejść przez wszystkie zawarte w niej symbole (5). Pamiętaj, że w przypadku programu ładującego binarne interesują Cię tylko symbole funkcji. Zatem dla każdego symbolu sprawdzasz, czy ustawiona jest flaga BSF_FUNCTION, co oznacza, że jest to symbol funkcji (6). Jeśli tak, rezerwujesz miejsce na Symbol (przypomnijmy, że jest to klasa własna modułu ładującego, w której przechowywane są symbole) w obiekcie Binary, dodając wpis do wektora zawierającego wszystkie załadowane symbole. Oznaczasz nowo utworzony Symbol jako symbol funkcji (7), kopiujesz nazwę symbolu (8) i ustawiasz adres Symbolu (9). Aby uzyskać wartość symbolu funkcji, czyli adres początkowy funkcji, używasz funkcji bfd_asymbol_value udostępnianej przez bibliotekę libbfd. Teraz, gdy wszystkie interesujące symbole zostały skopiowane do obiektów Symbol, moduł ładujący nie potrzebuje już reprezentacji biblioteki libbfd. Dlatego po zakończeniu funkcji load_symbols_bfd zwalnia ona wszelkie miejsce zarezerwowane do przechowywania symboli libbfd (10). Następnie funkcja kończy działanie, a proces ładowania symboli jest ukończony. Tak właśnie ładuje się symbole ze statycznej tabeli symboli za pomocą biblioteki libbfd. Ale jak to się robi w przypadku dynamicznej tabeli symboli? Na szczęście proces jest niemal identyczny, jak widać na listingu poniżej

Listing : inc/loader.cc (continued)

static int

load_dynsym_bfd(bfd *bfd_h, Binary *bin)

{

int ret;

long n, nsyms, i;

(1) asymbol **bfd_dynsym;

Symbol *sym;

bfd_dynsym = NULL;

 (2) n = bfd_get_dynamic_symtab_upper_bound(bfd_h);

if(n < 0) {

fprintf(stderr, „failed to read dynamic symtab (%s)\n”,

bfd_errmsg(bfd_get_error()));

goto fail;

} else if(n) {

bfd_dynsym = (asymbol**)malloc(n);

if(!bfd_dynsym) {

fprintf(stderr, „out of memory\n”);

goto fail;

}

(3) nsyms = bfd_canonicalize_dynamic_symtab(bfd_h, bfd_dynsym);

if(nsyms < 0) {

fprintf(stderr, „failed to read dynamic symtab (%s)\n”,

bfd_errmsg(bfd_get_error()));

goto fail;

}

for(i = 0; i < nsyms; i++) {

if(bfd_dynsym[i]->flags & BSF_FUNCTION) {

bin->symbols.push_back(Symbol());

sym = &bin->symbols.back();

sym->type = Symbol::SYM_TYPE_FUNC;

sym->name = std::string(bfd_dynsym[i]->name);

sym->addr = bfd_asymbol_value(bfd_dynsym[i]);

}

}

}

ret = 0;

goto cleanup;

fail:

ret = -1;

cleanup:

if(bfd_dynsym) free(bfd_dynsym);

return ret;

}

Funkcja pokazana na Listingu , służąca do ładowania symboli z dynamicznej tablicy symboli, trafnie nazywa się load_dynsym_bfd. Jak widać, biblioteka libbfd używa tej samej struktury danych (asymbolu) do reprezentowania symboli statycznych i dynamicznych (1). Jedyne różnice w stosunku do wcześniej pokazanej funkcji load_symbols_bfd są następujące. Po pierwsze, aby znaleźć liczbę bajtów, które należy zarezerwować na wskaźniki symboli, wywołujemy funkcję bfd_get_dynamic_symtab_upper_bound (2) zamiast bfd_get_symtab_upper_bound. Po drugie, aby zapełnić tablicę symboli, używamy funkcji bfd_canonicalize_dynamic_symtab (3) zamiast bfd_canonicalize_symtab. To wszystko! Reszta procesu dynamicznego ładowania symboli jest taka sama jak w przypadku symboli statycznych.

.