Cum să aruncați o privire în interiorul fișierelor binare din linia de comandă Linux

Ai întâmpinat vreodată un fișier misterios? Comanda `file` din Linux te ajută să identifici rapid tipul acestuia. Chiar și în cazul fișierelor binare, poți obține informații suplimentare cu ajutorul unor instrumente specializate. În acest articol, vom explora modul de utilizare a acestor instrumente pentru a analiza fișierele în detaliu.

Identificarea Naturii Fișierelor

În general, fișierele includ caracteristici care permit aplicațiilor să determine tipul și structura datelor pe care le conțin. De exemplu, deschiderea unui fișier imagine PNG într-un player audio MP3 nu ar funcționa corect. Prin urmare, este esențial ca un fișier să poarte cu el un fel de identificator.

Acest identificator poate fi o secvență de octeți, o așa-numită semnătură, plasată chiar la începutul fișierului. Aceasta specifică formatul și conținutul său. În unele cazuri, tipul fișierului este dedus din organizarea internă a datelor, numită arhitectura fișierului.

Sistemele de operare, cum ar fi Windows, se bazează pe extensia fișierului pentru a identifica tipul acestuia. De exemplu, un fișier cu extensia DOCX este considerat a fi un fișier de procesare de text. Spre deosebire de Windows, Linux nu se bazează doar pe extensie. Analizează conținutul fișierului pentru a determina tipul său real.

Instrumentele prezentate în acest articol sunt disponibile în distribuțiile Manjaro 20, Fedora 21 și Ubuntu 20.04, pe care le-am folosit pentru cercetare. Vom începe analiza cu comanda `file`.

Utilizarea Comenzii `file`

Avem o varietate de fișiere în directorul curent, inclusiv documente, cod sursă, executabile și fișiere text.

Comanda `ls` ne afișează conținutul directorului, iar opțiunea `-hl` (dimensiuni lizibile, listare detaliată) ne oferă dimensiunea fiecărui fișier:

ls -hl

Să analizăm câteva fișiere cu comanda `file`:

file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu

Comanda `file` identifică corect formatele celor trei fișiere. În plus, oferă informații suplimentare, cum ar fi versiunea formatului PDF: format versiunea 1.5.

Chiar dacă redenumim fișierul ODT cu o extensie arbitrară, cum ar fi XYZ, comanda `file` îl identifică corect, la fel ca și browserul de fișiere.

Browserul de fișiere afișează pictograma corectă. Comanda `file` ignoră extensia și analizează conținutul fișierului pentru a determina tipul:

file build_instructions.xyz

În cazul fișierelor imagine sau audio, comanda `file` oferă informații despre format, codificare, rezoluție și altele:

file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3

Chiar și pentru fișierele text, comanda `file` nu se bazează pe extensie. De exemplu, un fișier cu extensia „.c” care conține text simplu, dar nu cod sursă, nu va fi confundat cu un fișier cod sursă:

file function+headers.h
file makefile
file hello.c

Comanda `file` identifică corect fișierul antet („.h”) ca parte a codului sursă C și recunoaște `makefile` ca script.

Analiza Fișierelor Binare

Fișierele binare sunt mai dificil de analizat decât alte tipuri de fișiere. Fișierele imagine, audio sau documente pot fi deschise cu aplicații specifice, dar fișierele binare sunt mai opace.

De exemplu, fișierele „hello” și „wd” sunt executabile binare, adică programe. Fișierul „wd.o” este un fișier obiect, rezultat al compilării codului sursă. Acesta conține cod mașină și informații pentru linker, care combină fișierele obiect cu bibliotecile necesare, generând un fișier executabil.

Fișierul „watch.exe” este un executabil binar compilat pentru a rula pe Windows:

file wd
file wd.o
file hello
file watch.exe

Comanda `file` identifică fișierul „watch.exe” ca un program executabil PE32+, de consolă, pentru arhitectura x86 pe Microsoft Windows. PE înseamnă format Portabil Executabil, disponibil în versiuni pe 32 și 64 de biți. PE32 este versiunea pe 32 de biți, iar PE32+ este versiunea pe 64 de biți.

Celelalte trei fișiere sunt identificate ca având format Executable and Linkable Format (ELF), standard pentru executabile și biblioteci partajate. Vom analiza formatul antetului ELF în curând.

Observăm că executabilele („wd” și „hello”) sunt identificate ca obiecte partajate Linux Standard Base (LSB), iar fișierul obiect „wd.o” este identificat ca un obiect LSB relocabil. Termenul „executabil” lipsește din descrierea obiectelor partajate.

Fișierele obiect sunt relocabile, ceea ce înseamnă că codul lor poate fi încărcat în memorie în orice locație. Executabilele sunt listate ca obiecte partajate deoarece, după legarea cu bibliotecile, moștenesc această proprietate.

Această abordare permite Randomizarea aspectului spațiului de adrese (ASMR), care încarcă executabilele în memorie la adrese alese aleatoriu. Executabilele standard au o adresă de încărcare fixă, ceea ce le face vulnerabile la atacuri. Executabilele independente de poziție (PIE), încărcate aleatoriu, elimină această vulnerabilitate.

Dacă compilăm programul nostru cu gcc folosind opțiunea -no-pie, generăm un executabil convențional.

Opțiunea -o (fișier de ieșire) ne permite să specificăm numele executabilului:

gcc -o hello -no-pie hello.c

Analizând noul executabil cu comanda `file`, observăm schimbările:

file hello

Dimensiunea executabilului este aceeași (17 KB):

ls -hl hello

Binarul este acum identificat ca un executabil standard. Facem această modificare doar pentru demonstrație. Compilarea aplicațiilor în acest mod elimină beneficiile oferite de ASMR.

De Ce Este Executabilul Atât de Mare?

Programul nostru „hello” are 17 KB. Deși nu este enorm, este semnificativ mai mare decât codul sursă, care are doar 120 de octeți:

cat hello.c

Ce anume mărește dimensiunea binarului? Știm că există un antet ELF, dar acesta are doar 64 de octeți pentru un binar pe 64 de biți.

ls -hl hello

Vom analiza binarul cu comanda `strings`, care identifică șirurile de text din interior:

strings hello | less

Pe lângă „Hello, Geek world!”, binarul conține numeroase șiruri, etichete pentru regiuni, nume și informații despre legăturile cu obiecte partajate, inclusiv biblioteci și funcțiile lor de care depinde binarul.

Comanda `ldd` ne arată dependențele de obiecte partajate ale unui binar:

ldd hello

Rezultatul include trei intrări, două dintre ele conținând căi de director:

linux-vdso.so: Obiectul virtual dinamic partajat (VDSO) este un mecanism al nucleului care permite accesul la rutinele spațiului nucleului de către un binar din spațiul utilizatorului. Acest mecanism evită costul comutării de context între modul utilizator și modul nucleu. Obiectele partajate VDSO au format ELF și pot fi legate dinamic la binar în timpul execuției. VDSO este alocat dinamic și beneficiază de ASMR. VDSO este oferit de Biblioteca GNU C dacă nucleul suportă ASMR.
libc.so.6: Obiectul partajat al Bibliotecii GNU C.
/lib64/ld-linux-x86-64.so.2: Acesta este linkerul dinamic pe care binarul îl folosește. Linkerul dinamic interoghează binarul pentru a determina dependențele, încarcă obiectele partajate în memorie, pregătește binarul pentru execuție și lansează programul.

Antetul ELF

Putem analiza și decoda antetul ELF cu utilitarul `readelf` și opțiunea `-h` (antetul fișierului):

readelf -h hello

Antetul este interpretat pentru noi.

Primul octet al tuturor binarelor ELF este 0x7F. Următorii trei octeți sunt 0x45, 0x4C și 0x46, reprezentând „ELF” în ASCII.

Clasa: indică dacă binarul este pe 32 sau 64 de biți (1=32, 2=64).
Date: indică endianness folosită. În big-endian, numerele sunt stocate cu biții cei mai semnificativi primii, iar în little-endian, cu biții cei mai puțin semnificativi.
Versiune: versiunea ELF (în prezent, 1).
OS/ABI: tipul de interfață binară a aplicației. Aceasta definește interfața dintre două module binare, cum ar fi un program și o bibliotecă partajată.
Versiunea ABI: versiunea ABI.
Tip: tipul de binar ELF. Valorile comune sunt ET_REL (relocabil, cum ar fi un fișier obiect), ET_EXEC (executabil compilat cu opțiunea -no-pie) și ET_DYN (executabil compatibil ASMR).
Mașină: arhitectura setului de instrucțiuni. Indică platforma țintă.
Versiune: Întotdeauna setată la 1 pentru această versiune ELF.
Adresa punctului de intrare: adresa din memorie la care începe execuția binarului.

Celelalte intrări reprezintă dimensiunile și numerele regiunilor din binar, pentru a putea calcula locațiile lor.

O privire rapidă asupra primilor opt octeți ai binarului cu comanda `hexdump` afișează octetul de semnătură și șirul „ELF”. Opțiunea `-C` (canonică) afișează reprezentarea ASCII a octeților, iar opțiunea `-n` (număr) specifică numărul de octeți:

hexdump -C -n 8 hello

objdump și vizualizarea granulară

Pentru a vedea detalii mai precise, puteți utiliza comanda `objdump` cu opțiunea `-d` (dezasamblare):

objdump -d hello | less

Această comandă dezasamblează codul mașină și îl afișează în hexazecimal, alături de echivalentul în limbaj de asamblare. Adresa de memorie a fiecărei instrucțiuni este afișată în stânga.

Această metodă este utilă pentru cei care știu limbajul de asamblare sau doresc să înțeleagă funcționarea binarului. Rezultatul fiind lung, a fost afișat cu `less`.

Compilarea și Legarea

Există multe moduri de a compila un binar. Dezvoltatorul poate alege dacă să includă informații de depanare. Modul de legare a binarului influențează, de asemenea, conținutul și dimensiunea sa. Un binar care folosește obiecte partajate ca dependențe externe va fi mai mic decât unul care include dependențele static.

Mulți dezvoltatori cunosc deja aceste comenzi. Totuși, pentru alții, ele reprezintă o modalitate ușoară de a investiga și a înțelege structura fișierelor binare.