Haben Sie eine rätselhafte Datei gefunden? Der Linux-Befehl `file` kann Ihnen rasch Auskunft darüber geben, um welchen Dateityp es sich handelt. Falls es sich um eine Binärdatei handelt, sind weitere Details erforschbar. Es existiert eine Reihe verwandter Tools, die Ihnen bei der Analyse behilflich sein können. Wir zeigen Ihnen, wie Sie einige dieser Werkzeuge einsetzen können.
Dateitypen erkennen
Dateien verfügen in der Regel über charakteristische Merkmale, die es Softwareanwendungen ermöglichen, den Dateityp und die darin enthaltenen Daten zu identifizieren. Es wäre unlogisch, eine PNG-Datei in einem MP3-Player zu öffnen, daher ist es sinnvoll und praktisch, dass Dateien eine Art Kennzeichnung tragen.
Diese Kennzeichnung kann aus einigen Signatur-Bytes am Anfang der Datei bestehen. Dadurch kann eine Datei ihr Format und ihren Inhalt eindeutig deklarieren. Manchmal wird der Dateityp aus einem spezifischen Aspekt der internen Struktur der Daten abgeleitet, der als Dateisignatur bekannt ist.
Einige Betriebssysteme, wie Windows, verlassen sich vollständig auf die Dateierweiterung. Man könnte dies als naiv oder vertrauensselig bezeichnen, aber Windows geht davon aus, dass jede Datei mit der Erweiterung DOCX tatsächlich eine DOCX-Textverarbeitungsdatei ist. Linux hingegen ist nicht so leichtgläubig, wie Sie gleich feststellen werden. Es verlangt Beweise und durchsucht die Datei danach.
Die hier beschriebenen Werkzeuge waren bereits in den von uns zur Recherche verwendeten Distributionen Manjaro 20, Fedora 21 und Ubuntu 20.04 vorinstalliert. Beginnen wir unsere Untersuchung mit dem Befehl `file`.
Der `file`-Befehl im Einsatz
In unserem aktuellen Verzeichnis befindet sich eine Sammlung verschiedener Dateitypen. Diese umfasst eine Mischung aus Dokument-, Quellcode-, ausführbaren und Textdateien.
Der Befehl `ls` zeigt uns den Inhalt des Verzeichnisses an, während die Option `-hl` (für menschenlesbare Größen, lange Auflistung) die Größe jeder Datei angibt:
ls -hl
Lassen Sie uns den `file`-Befehl auf einige dieser Dateien anwenden, um zu sehen, welche Informationen wir erhalten:
file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu
Die drei Dateiformate werden korrekt erkannt. Falls möglich, liefert der Befehl `file` zusätzliche Details. So wird beispielsweise angegeben, dass die PDF-Datei im Version 1.5-Format vorliegt.
Selbst wenn wir die ODT-Datei umbenennen und ihr eine beliebige Erweiterung wie XYZ geben, wird die Datei weiterhin korrekt identifiziert, sowohl im Dateibrowser als auch über die Kommandozeile mit dem Befehl `file`.
Im Dateibrowser „Dateien“ wird ihr das passende Symbol zugewiesen. In der Kommandozeile ignoriert `file` die Dateiendung und untersucht die Datei selbst, um ihren Typ zu bestimmen:
file build_instructions.xyz
Die Anwendung des Befehls `file` auf Mediendateien wie Bild- und Musikdateien liefert gewöhnlich Informationen zum Format, zur Kodierung, zur Auflösung usw.:
file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3
Interessanterweise beurteilt `file` selbst bei reinen Textdateien die Datei nicht aufgrund ihrer Erweiterung. Wenn Sie beispielsweise eine Datei mit der Erweiterung „.c“ haben, die Standardtext, aber keinen Quellcode enthält, wird diese nicht mit einer echten C- Quellcodedatei verwechselt:
file function+headers.h
file makefile
file hello.c
Der Befehl `file` identifiziert die Header-Datei („*.h“) korrekt als Teil einer C-Quellcodesammlung und erkennt, dass die `makefile` ein Skript ist.
`file` bei Binärdateien
Binärdateien sind oft eine größere Herausforderung als andere Dateitypen. Bilddateien können angezeigt, Tondateien wiedergegeben und Dokumentdateien mit der entsprechenden Software geöffnet werden. Binärdateien stellen jedoch eine größere Herausforderung dar.
Beispielsweise sind die Dateien „hello“ und „wd“ binäre ausführbare Dateien, also Programme. Die Datei „wd.o“ ist eine Objektdatei. Wenn Quellcode von einem Compiler übersetzt wird, entstehen eine oder mehrere Objektdateien. Diese enthalten den Maschinencode, den der Computer schließlich ausführt, wenn das fertige Programm gestartet wird, zusammen mit Informationen für den Linker. Der Linker überprüft jede Objektdatei auf Funktionsaufrufe an Bibliotheken und verknüpft diese mit allen Bibliotheken, die das Programm verwendet. Das Ergebnis dieses Vorgangs ist eine ausführbare Datei.
Die Datei „watch.exe“ ist eine binäre ausführbare Datei, die für die Ausführung unter Windows kompiliert wurde:
file wd
file wd.o
file hello
file watch.exe
Betrachten wir zunächst die letzte Datei: `file` teilt uns mit, dass es sich bei „watch.exe“ um ein ausführbares PE32+-Konsolenprogramm für die x86-Prozessorfamilie unter Microsoft Windows handelt. PE steht für Portable Executable Format, von dem es 32- und 64-Bit-Versionen gibt. PE32 ist die 32-Bit-Version und PE32+ ist die 64-Bit-Version.
Die anderen drei Dateien werden alle als Executable and Linkable Format (ELF)-Dateien identifiziert. Dies ist ein Standard für ausführbare Dateien und Dateien mit gemeinsam genutzten Objekten, wie z. B. Bibliotheken. Wir werden uns in Kürze das ELF-Header-Format ansehen.
Was Ihnen möglicherweise auffällt, ist, dass die beiden ausführbaren Dateien („wd“ und „hello“) als Linux Standard Base (LSB) Shared Objects identifiziert werden, während die Objektdatei „wd.o“ als LSB relocatable angegeben wird. Das Wort „ausführbar“ fehlt hier offensichtlich.
Objektdateien sind verschiebbar, was bedeutet, dass der darin enthaltene Code an beliebiger Stelle im Speicher geladen werden kann. Die ausführbaren Dateien werden als gemeinsam genutzte Objekte gelistet, da sie vom Linker aus den Objektdateien mit dieser Eigenschaft erstellt wurden.
Dies ermöglicht dem Address Space Layout Randomization (ASMR)-System, die ausführbaren Dateien an beliebigen Adressen im Speicher zu laden. Standardmäßige ausführbare Dateien haben in ihren Headern eine Ladeadresse, die vorgibt, wo sie im Speicher abgelegt werden sollen.
ASMR ist eine Sicherheitstechnik. Das Laden ausführbarer Dateien an vorhersagbaren Adressen macht sie anfällig für Angriffe, da Angreifer ihre Einstiegspunkte und die Positionen ihrer Funktionen kennen. Positionsunabhängige ausführbare Dateien (PIE), die an einer zufälligen Adresse geladen werden, beseitigen diese Schwachstelle.
Wenn wir unser Programm mit dem gcc-Compiler und der Option `-no-pie` kompilieren, generieren wir eine herkömmliche ausführbare Datei.
Mit der Option `-o` (Ausgabedatei) können wir einen Namen für unsere ausführbare Datei angeben:
gcc -o hello -no-pie hello.c
Wir verwenden `file` für die neue ausführbare Datei und sehen, was sich geändert hat:
file hello
Die Größe der ausführbaren Datei ist die gleiche wie zuvor (17 KB):
ls -hl hello
Die Binärdatei wird nun als standardmäßige ausführbare Datei identifiziert. Wir tun dies nur zu Demonstrationszwecken. Wenn Sie Anwendungen auf diese Weise kompilieren, verlieren Sie alle Vorteile von ASMR.
Warum ist eine ausführbare Datei so groß?
Unser Beispiel-Hello-Programm ist 17 KB groß, was man kaum als groß bezeichnen kann, aber alles ist relativ. Der Quellcode ist 120 Byte groß:
cat hello.c
Was füllt die Binärdatei auf, wenn sie doch nur eine Zeichenkette auf der Konsole ausgibt? Wir wissen, dass es einen ELF-Header gibt, der bei einer 64-Bit-Binärdatei aber nur 64 Byte groß ist. Offensichtlich gibt es noch etwas anderes:
ls -hl hello
Lassen Sie uns die Binärdatei mit dem Befehl `strings` durchsuchen, um herauszufinden, was darin enthalten ist. Wir leiten die Ausgabe an `less` weiter:
strings hello | less
Es gibt viele Zeichenketten in der Binärdatei, neben „Hello, Geek world!“ aus unserem Quellcode. Die meisten sind Bezeichnungen für Bereiche innerhalb der Binärdatei sowie die Namen und Verknüpfungsinformationen von gemeinsam genutzten Objekten. Dazu gehören die Bibliotheken und Funktionen in diesen Bibliotheken, von denen die Binärdatei abhängt.
Der Befehl `ldd` zeigt uns die Abhängigkeiten von gemeinsam genutzten Objekten einer Binärdatei:
ldd hello
Die Ausgabe enthält drei Einträge, von denen zwei einen Verzeichnispfad enthalten (der erste nicht):
linux-vdso.so: Virtual Dynamic Shared Object (VDSO) ist ein Kernel-Mechanismus, der einer Benutzerbereich-Binärdatei den Zugriff auf eine Reihe von Kernelbereich-Routinen ermöglicht. Dies vermeidet den Aufwand eines Kontextwechsels zwischen Benutzer- und Kernelmodus. Gemeinsam genutzte VDSO-Objekte folgen dem Executable and Linkable Format (ELF), sodass sie zur Laufzeit dynamisch mit der Binärdatei verknüpft werden können. Das VDSO wird dynamisch zugewiesen und nutzt ASMR. Die VDSO-Funktionalität wird von der standardmäßigen GNU C-Bibliothek bereitgestellt, wenn der Kernel das ASMR-Schema unterstützt.
libc.so.6: Die gemeinsame Objektdatei der GNU C-Bibliothek.
/lib64/ld-linux-x86-64.so.2: Dies ist der dynamische Linker, den die Binärdatei verwenden möchte. Der dynamische Linker fragt die Binärdatei ab, um ihre Abhängigkeiten zu ermitteln. Er lädt diese gemeinsam genutzten Objekte in den Speicher. Er bereitet die Binärdatei darauf vor, ausgeführt zu werden, findet ihre Abhängigkeiten im Speicher und greift darauf zu. Dann startet er das Programm.
Der ELF-Header
Wir können den ELF-Header untersuchen und dekodieren, indem wir das Hilfsprogramm `readelf` mit der Option `-h` (Dateiheader) verwenden:
readelf -h hello
Der Header wird für uns interpretiert.
Das erste Byte aller ELF-Binärdateien ist auf den hexadezimalen Wert 0x7F gesetzt. Die nächsten drei Bytes sind auf 0x45, 0x4C und 0x46 gesetzt. Das erste Byte ist ein Flag, das die Datei als ELF-Binärdatei identifiziert. Um es ganz deutlich zu machen, schreiben die nächsten drei Bytes „ELF“ in ASCII:
Klasse: Gibt an, ob die Binärdatei eine ausführbare 32- oder 64-Bit-Datei ist (1=32, 2=64).
Daten: Gibt die verwendete Endianität an. Die Endian-Kodierung definiert, wie Multibyte-Zahlen gespeichert werden. Bei der Big-Endian-Kodierung wird eine Zahl mit ihren höchstwertigen Bits zuerst gespeichert. Bei der Little-Endian-Kodierung wird die Zahl mit ihren niedrigstwertigen Bits zuerst gespeichert.
Version: Die Version von ELF (derzeit ist es 1).
OS/ABI: Repräsentiert den Typ der verwendeten Application Binary Interface. Dies definiert die Schnittstelle zwischen zwei binären Modulen, z. B. einem Programm und einer gemeinsam genutzten Bibliothek.
ABI-Version: Die Version der ABI.
Typ: Der Typ der ELF-Binärdatei. Übliche Werte sind ET_REL für eine verschiebbare Ressource (wie eine Objektdatei), ET_EXEC für eine ausführbare Datei, die mit dem Flag -no-pie kompiliert wurde, und ET_DYN für eine ASMR-fähige ausführbare Datei.
Maschine: Die Befehlssatzarchitektur. Dies gibt die Zielplattform an, für die die Binärdatei erstellt wurde.
Version: Für diese Version von ELF immer auf 1 gesetzt.
Einstiegspunktadresse: Die Speicheradresse innerhalb der Binärdatei, an der die Ausführung beginnt.
Die anderen Einträge sind Größen und Anzahlen von Regionen und Abschnitten innerhalb der Binärdatei, um deren Positionen zu berechnen.
Ein kurzer Blick auf die ersten acht Bytes der Binärdatei mit hexdump zeigt das Signatur-Byte und die „ELF“-Zeichenkette in den ersten vier Bytes der Datei. Die Option `-C` (kanonisch) gibt uns die ASCII-Darstellung der Bytes zusammen mit ihren hexadezimalen Werten, und die Option `-n` (Zahl) lässt uns angeben, wie viele Bytes wir sehen wollen:
hexdump -C -n 8 hello
`objdump` und die detaillierte Ansicht
Wenn Sie die Details sehen möchten, können Sie den Befehl `objdump` mit der Option `-d` (Disassemblierung) verwenden:
objdump -d hello | less
Dadurch wird der ausführbare Maschinencode zerlegt und in hexadezimalen Bytes neben dem Assemblersprache-Äquivalent angezeigt. Die Adressposition des ersten Bytes in jeder Zeile wird ganz links angezeigt.
Dies ist nur sinnvoll, wenn Sie Assembler lesen können oder neugierig sind, was hinter den Kulissen geschieht. Es gibt eine große Menge an Ausgaben, deshalb haben wir die Ausgabe in `less` geleitet.
Kompilieren und Verlinken
Es gibt viele Möglichkeiten, eine Binärdatei zu kompilieren. Beispielsweise entscheidet der Entwickler, ob Debuginformationen hinzugefügt werden sollen. Die Art und Weise, wie die Binärdatei verlinkt ist, spielt auch eine Rolle für ihren Inhalt und ihre Größe. Wenn die Binärdatei auf Objekte als externe Abhängigkeiten verweist, ist sie kleiner als eine, in die die Abhängigkeiten statisch einkompiliert sind.
Die meisten Entwickler sind mit den hier besprochenen Befehlen vertraut. Für andere bieten sie jedoch eine einfache Möglichkeit, herumzustöbern und zu sehen, was sich in der binären Blackbox befindet.