Masz plik, którego typ jest tajemnicą? W systemie Linux możesz szybko zidentyfikować jego rodzaj za pomocą polecenia pliku. Jeśli okazuje się, że jest to plik binarny, istnieje wiele narzędzi, które mogą pomóc w jego dalszej analizie. Pokażemy Ci, jak wykorzystać niektóre z tych narzędzi.
Jak rozpoznać typy plików
Pliki zazwyczaj mają cechy, które pozwalają oprogramowaniu na określenie ich typu oraz rodzaju danych, które zawierają. Na przykład otwieranie pliku PNG w programie do odtwarzania MP3 byłoby bezsensowne, dlatego istotne jest, aby plik zawierał jakiś rodzaj identyfikatora.
Może to być kilka bajtów sygnatury na początku pliku, co pozwala na jednoznaczne określenie jego formatu i zawartości. W niektórych przypadkach typ pliku można wywnioskować z wewnętrznej struktury danych, znanej jako architektura pliku.
W przeciwieństwie do systemów operacyjnych takich jak Windows, które polegają na rozszerzeniach plików, Linux wymaga dowodu i zagląda do wnętrza pliku, aby określić jego typ.
Narzędzia opisane w tym artykule są dostępne w dystrybucjach Manjaro 20, Fedora 21 i Ubuntu 20.04. Zaczniemy nasze dochodzenie od użycia polecenia file.
Jak korzystać z polecenia file
W naszym bieżącym katalogu znajduje się zbiór plików różnego typu, w tym dokumentów, plików źródłowych, wykonywalnych i tekstowych.
Użyjemy polecenia ls, aby sprawdzić, co znajduje się w katalogu, z opcją -hl, co umożliwia wyświetlenie rozmiarów plików w formacie przyjaznym dla użytkownika:
ls -hl
Sprawdźmy kilka plików, aby zobaczyć, jakie informacje otrzymamy:
file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu
Wszystkie trzy pliki zostały poprawnie zidentyfikowane, a polecenie file dostarcza dodatkowych informacji, gdzie to możliwe. Na przykład plik PDF jest opisany jako w wersji 1.5.
Nawet jeśli zmienimy rozszerzenie pliku ODT na jakiekolwiek inne, polecenie file nadal poprawnie zidentyfikuje jego typ:
file build_instructions.xyz
Również w przypadku plików multimedialnych, takich jak obrazy czy dźwięki, polecenie file dostarcza szczegółowych informacji na temat formatu, kodowania i rozdzielczości:
file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3
Ciekawostką jest to, że nawet w przypadku plików tekstowych polecenie file nie opiera się wyłącznie na rozszerzeniu. Na przykład, nawet jeśli plik z rozszerzeniem „.c” zawiera zwykły tekst, polecenie file nie uzna go za plik źródłowy C:
file function+headers.h
file makefile
file hello.c
Polecenie file identyfikuje plik nagłówkowy („.h”) jako część zbioru plików źródłowych C, a makefile jako skrypt.
Analiza plików binarnych
Pliki binarne są trudniejsze do analizy niż inne typy plików. Można je otwierać w odpowiednich aplikacjach, ale pliki binarne stanowią większe wyzwanie.
Na przykład pliki „hello” i „wd” to pliki wykonywalne, natomiast plik „wd.o” to plik obiektowy. Podczas kompilacji kodu źródłowego powstają pliki obiektowe, które zawierają kod maszynowy, który jest wykonywany przez komputer, gdy program jest uruchamiany, oraz dodatkowe informacje dla konsolidatora.
Plik „watch.exe” to przykład pliku wykonywalnego przeznaczonego do uruchamiania w systemie Windows:
file wd
file wd.o
file hello
file watch.exe
Polecenie file informuje nas, że plik „watch.exe” jest programem konsolowym PE32+ dla procesorów x86 w systemie Windows. PE to przenośny format wykonywalny, który występuje w wersjach 32- i 64-bitowych.
Pozostałe pliki są identyfikowane jako Format wykonywalny i łączony (ELF). Jest to standard dla plików wykonywalnych oraz współdzielonych plików obiektowych, takich jak biblioteki. Wkrótce przyjrzymy się nagłówkowi ELF.
Ciekawym aspektem jest to, że pliki wykonywalne „wd” i „hello” są identyfikowane jako Linux Standard Base (LSB), podczas gdy plik obiektowy „wd.o” jest oznaczony jako relokowalny LSB. Brak słowa „wykonywalny” w przypadku pliku obiektowego jest oczywisty.
Pliki obiektowe są relokowalne, co oznacza, że ich kod można załadować do pamięci w dowolnym miejscu. Pliki wykonywalne są wymienione jako obiekty współdzielone, ponieważ zostały stworzone przez konsolidator w taki sposób, aby dziedziczyły tę możliwość.
To umożliwia Randomizację układu przestrzeni adresowej (ASLR), która pozwala na ładowanie plików wykonywalnych do pamięci w losowo wybranych adresach. Standardowe pliki wykonywalne mają zakodowany adres ładowania w swoich nagłówkach, który określa miejsce ich załadowania do pamięci.
ASLR to technika bezpieczeństwa, która minimalizuje ryzyko ataków, ponieważ ładowanie plików wykonywalnych pod znane adresy czyni je podatnymi na atak. Pozycjonowanie niezależnych plików wykonywalnych (PIC) umieszcza je pod losowym adresem, co zwiększa bezpieczeństwo.
Jeśli my skompilujemy nasz program za pomocą kompilatora gcc z opcją -no-pie, wygenerujemy plik wykonywalny w tradycyjny sposób.
Opcja -o (plik wyjściowy) pozwala na określenie nazwy pliku wykonywalnego:
gcc -o hello -no-pie hello.c
Następnie użyjemy polecenia file, aby sprawdzić, co się zmieniło:
file hello
Rozmiar pliku wykonywalnego pozostaje taki sam (17 KB):
ls -hl hello
Plik binarny jest teraz identyfikowany jako standardowy plik wykonywalny. Robimy to tylko w celach demonstracyjnych, ponieważ kompilacja aplikacji w ten sposób może prowadzić do utraty zalet ASLR.
Dlaczego plik wykonywalny jest taki duży?
Nasz przykładowy program hello ma 17 KB, co może wydawać się niewielką wielkością, ale wszystko jest względne. Kod źródłowy ma zaledwie 120 bajtów:
cat hello.c
Co powoduje, że plik binarny jest tak duży, skoro jego funkcjonalność ogranicza się do wypisania jednego ciągu tekstowego? Oczywiście wiemy o nagłówku ELF, ale jego długość wynosi tylko 64 bajty dla 64-bitowego pliku binarnego. Musi być coś więcej:
ls -hl hello
Możemy zeskanować plik binarny za pomocą polecenia strings, aby odkryć, co się w nim kryje:
strings hello | less
W pliku binarnym znajdujemy wiele ciągów tekstowych, w tym „Hello, Geek world!”, które pochodzą z naszego kodu źródłowego. Większość z nich to jednak etykiety regionów w pliku binarnym oraz informacje o współdzielonych obiektach, takich jak biblioteki i funkcje, od których zależy plik binarny.
Użycie polecenia ldd ujawnia zależności pliku binarnego:
ldd hello
W wynikach widać trzy wpisy, z których dwa zawierają ścieżki do katalogów (pierwszy nie):
linux-vdso.so: Virtual Dynamic Shared Object (VDSO) to mechanizm jądra, który umożliwia dostęp do zestawu procedur w przestrzeni jądra przez plik binarny przestrzeni użytkownika. To minimalizuje narzuty związane z przełączaniem kontekstu między trybem jądra a trybem użytkownika. Obiekty VDSO są zgodne z formatem ELF i mogą być dynamicznie łączone z plikiem binarnym w trakcie jego działania. VDSO jest przydzielane dynamicznie i korzysta z ASLR. Oferuje bibliotekę GNU C, jeśli jądro wspiera ASLR.
libc.so.6: plik Biblioteka GNU C. obiekt współdzielony.
/lib64/ld-linux-x86-64.so.2: dynamiczny linker, który jest używany przez plik binarny. Dynamiczny linker analizuje plik binarny, aby odkryć jego zależności, wprowadza te zależne obiekty do pamięci, przygotowuje plik do uruchomienia oraz umożliwia dostęp do tych zależności w pamięci. Następnie uruchamia program.
Nagłówek ELF
Możemy zbadać nagłówek ELF przy użyciu narzędzia readelf i opcji -h (nagłówek pliku):
readelf -h hello
Nagłówek jest interpretowany automatycznie.
Pierwszy bajt wszystkich plików ELF to wartość szesnastkowa 0x7F. Kolejne trzy bajty mają wartości 0x45, 0x4C i 0x46, co jednoznacznie identyfikuje plik jako plik ELF. Aby było to przejrzyste, następujące trzy bajty to litery „ELF” w kodzie ASCII.
Klasa: wskazuje, czy plik binarny jest 32- czy 64-bitowy (1 = 32, 2 = 64).
Dane: wskazuje, jakiego typu endianness jest używane. Określa to sposób przechowywania liczb wielobajtowych. W kodowaniu big-endian najważniejsze bity są przechowywane jako pierwsze, podczas gdy w kodowaniu little-endian najpierw są przechowywane najmniej istotne bity.
Wersja: wersja formatu ELF (obecnie 1).
OS / ABI: oznacza typ interfejsu binarnego aplikacji w użyciu, definiując interfejs między różnymi modułami binarnymi, takimi jak program i biblioteka współdzielona.
Wersja ABI: wersja interfejsu ABI.
Typ: typ pliku ELF. Typowe wartości to ET_REL dla plików obiektowych, ET_EXEC dla plików wykonywalnych skompilowanych z flagą -no-pie oraz ET_DYN dla plików obsługujących ASLR.
Maszyna: architektura zestawu instrukcji, która wskazuje na platformę docelową, dla której plik binarny został stworzony.
Wersja: zawsze ustawiona na 1, dla tej wersji ELF.
Adres punktu wejścia: adres pamięci, z którego rozpoczyna się wykonywanie programu.
Pozostałe wpisy dotyczą rozmiarów oraz liczby regionów i sekcji w pliku binarnym, co umożliwia obliczenie ich lokalizacji.
Szybki podgląd pierwszych ośmiu bajtów pliku binarnego za pomocą hexdump pozwala zobaczyć bajt sygnatury oraz ciąg „ELF” w pierwszych czterech bajtach pliku. Opcja -C (kanoniczna) przedstawia bajty w formacie ASCII oraz ich wartości szesnastkowe, a opcja -n (liczba) pozwala określić liczbę bajtów do wyświetlenia:
hexdump -C -n 8 hello
Użycie objdump i szczegółowy widok
Aby zobaczyć szczegóły, możemy użyć polecenia objdump z opcją -d (dezasemblacja):
objdump -d hello | less
To polecenie dezasembluje kod maszynowy i wyświetla go w postaci bajtów szesnastkowych obok odpowiadających mu instrukcji w asemblerze. Adres pierwszego bajtu w każdym wierszu jest pokazany po lewej stronie.
Jest to przydatne tylko dla osób, które potrafią czytać asembler, lub dla tych, którzy są ciekawi, co dzieje się w tle. Wynik jest obszerna, więc umieścimy go w mniej.
Kompilacja i łączenie
Istnieje wiele metod kompilacji plików binarnych. Programiści decydują, czy dołączyć informacje debugowe, a sposób łączenia pliku binarnego wpływa na jego zawartość i rozmiar. Pliki, które korzystają ze współdzielonych obiektów jako zależności zewnętrzne, będą mniejsze niż te, które łączą zależności statycznie.
Większość programistów zna już polecenia, o których mówiliśmy. Dla innych jednak, te informacje oferują prosty sposób na przeszukiwanie i odkrywanie, co znajduje się w tej binarnej „czarnej skrzynce”.
newsblog.pl