Dateisysteme · Lektion 6 von 8
ext4: Inodes, Extents, Journal und Zeitstempel
11 Min. Lesezeit
ext4 ist das Dateisystem, dem Sie auf einem Linux-Server, einem Linux-Desktop oder einem älteren Android-Telefon am häufigsten begegnen werden. Sein Aufbau unterscheidet sich von NTFS. Namen befinden sich ausschließlich in Verzeichnissen, andere Metadaten in Inodes fester Größe, Inhalte werden über Extents gefunden, und ein Journal kann ältere Metadaten bewahren. Diese Lektion zeigt, wie Sie diese Strukturen lesen, ihre Zeitstempel in UTC umrechnen und beurteilen, was nach einer Löschung zurückbleibt.
Wo ext4 vorkommt
ext4 ist das Standarddateisystem in Debian, Ubuntu und vielen anderen Linux-Distributionen, auf Servern, virtuellen Maschinen in der Cloud und Desktops. Es wird nicht überall eingesetzt: Red Hat Enterprise Linux verwendet seit Version 7 standardmäßig XFS, und einige Distributionen nutzen Btrfs. Unter Android war die Benutzerdatenpartition (/data) viele Jahre lang auf den meisten Geräten mit ext4 formatiert, obwohl viele neuere Geräte F2FS verwenden. ext4 findet sich auch auf Geräten wie NAS-Systemen, Videorekordern und Routern.
Struktur auf dem Datenträger
Ein ext4-Volume ist in Blöcke unterteilt, die üblicherweise 4.096 Bytes groß sind (auf kleinen Volumes sind 1 KiB und 2 KiB möglich). Die Blöcke werden zu Blockgruppen zusammengefasst. Bei 4-KiB-Blöcken umfasst eine Gruppe 32.768 Blöcke (entsprechend der Anzahl der Bits in einem Bitmap-Block), also 128 MiB.
- Superblock. Der Superblock beginnt unabhängig von der Blockgröße immer bei Byte 1.024 des Volumes und ist 1.024 Bytes lang. Seine magische Zahl 0xEF53 befindet sich am Offset 56 (0x38) innerhalb des Superblocks, also bei Byte 1.080 des Volumes. Da ext4 Little-Endian verwendet, lauten die Bytes auf dem Datenträger
53 EF. Weitere nützliche Felder:s_log_block_size(0x18; Blockgröße = 1.024 << Wert),s_inodes_per_group(0x28), Zeitpunkt des letzten Einhängenss_mtime(0x2C), Zeitpunkt des letzten Schreibzugriffss_wtime(0x30),s_inode_size(0x58), Volume-UUID (0x68), Bezeichnung (0x78) unds_last_mounted(0x88), das Verzeichnis, in dem es zuletzt eingehängt war. Gruppe 0 enthält den primären Superblock; Sicherungskopien befinden sich in Gruppe 1 und in Gruppen, deren Nummern Potenzen von 3, 5 und 7 sind (sparse_super). - Gruppendeskriptortabelle. Diese Tabelle folgt auf den Superblock (bei 4-KiB-Blöcken ab Block 1). Sie enthält einen Deskriptor pro Gruppe: 32 Bytes oder 64 Bytes, wenn die Funktion
64bitaktiviert ist. Jeder Deskriptor enthält die Blocknummern der Block-Bitmap, der Inode-Bitmap und der Inode-Tabelle dieser Gruppe. - Block-Bitmap und Inode-Bitmap. Jede enthält ein Bit pro Block bzw. Inode: 1 bedeutet belegt, 0 bedeutet frei. Mit der Funktion
flex_bgwerden die Bitmaps und Inode-Tabellen mehrerer Gruppen (standardmäßig 16) zusammengelegt, sodass sich die Inode-Tabelle einer Gruppe nicht unbedingt innerhalb dieser Gruppe befindet. - Inode-Tabelle. Dies ist ein Array von Inodes. Unter ext4 ist jeder Inode standardmäßig 256 Bytes groß; auf alten oder sehr kleinen Dateisystemen gibt es Inodes mit 128 Bytes.
Der Inode
Ein Inode enthält alle Informationen über eine Datei mit Ausnahme ihres Namens. Wichtige Felder und ihre Offsets:
| Offset | Feld | Bedeutung |
|---|---|---|
| 0x00 | i_mode | Dateityp (reguläre Datei, Verzeichnis, symbolischer Link …) und Berechtigungsbits |
| 0x02 / 0x18 | i_uid / i_gid | Benutzer- und Gruppen-ID des Eigentümers (untere 16 Bits; obere Bits werden weiter hinten gespeichert) |
| 0x04 | i_size_lo | Dateigröße in Bytes (untere 32 Bits; obere Bits bei 0x6C) |
| 0x08 | i_atime | Letzter Zugriff |
| 0x0C | i_ctime | Inode-Änderung (Metadatenänderung) |
| 0x10 | i_mtime | Letzte Inhaltsänderung |
| 0x14 | i_dtime | Löschzeitpunkt (0, solange der Inode verwendet wird) |
| 0x1A | i_links_count | Anzahl der Verzeichniseinträge (Hardlinks), die hierher verweisen |
| 0x28 | i_block | 60 Bytes: Extent-Header und Extents oder die alte Blockzuordnung |
| 0x84–0x8C | i_ctime_extra, i_mtime_extra, i_atime_extra | Nanosekunden und zusätzliche Epochenbits |
| 0x90 / 0x94 | i_crtime / i_crtime_extra | Erstellungszeitpunkt |
Die häufigste Fehlinterpretation in der Linux-Forensik besteht darin, ctime als „Erstellungszeitpunkt“ zu verstehen. Es handelt sich um den Zeitpunkt der Inode-Änderung, der bei jeder Metadatenänderung aktualisiert wird: Berechtigungen, Eigentümer, Linkanzahl, Umbenennungen oder Schreibzugriffe auf den Inhalt. Der tatsächliche Erstellungszeitpunkt ist crtime, den ext4 nur in Inodes mit mindestens 256 Bytes speichert. Werkzeuge bezeichnen ihn als „Birth“ (stat) oder „File Created“ (TSK istat). Behandeln Sie atime mit Vorsicht: Die standardmäßige Einhängeoption relatime aktualisiert diesen Wert nur, wenn er älter als mtime oder ctime oder mehr als 24 Stunden alt ist.
Alle vier Zeitstempel geben Sekunden seit der Unix-Epoche (1970-01-01T00:00:00Z) in UTC an und werden als vorzeichenbehaftete 32-Bit-Werte gespeichert. Ein vorzeichenbehafteter 32-Bit-Wert erreicht am 2038-01-19T03:14:07Z seine Grenze. ext4 löst dies mit den Feldern _extra. Jedes davon umfasst 32 Bits: Die unteren 2 Bits sind Epochenbits, die den Sekundenwert erweitern (Epoche × 2³² addieren), und die oberen 30 Bits sind Nanosekunden. Dies ermöglicht Nanosekundengenauigkeit und Datumswerte bis zum Jahr 2446. Dateisystemen mit 128-Byte-Inodes fehlen die zusätzlichen Felder und crtime; bei ihnen bleibt die Grenze von 2038 bestehen.
Die ersten zehn Inodes sind reserviert. Inode 2 ist das Wurzelverzeichnis, und Inode 8 ist das Journal. Der erste reguläre Inode ist 11 und wird normalerweise von lost+found verwendet. Die Inode-Nummerierung beginnt bei 1, nicht bei 0.
Extents im Vergleich zur alten Blockzuordnung
ext2 und ext3 verwendeten i_block als Blockzuordnung: 12 direkte Zeiger sowie einfach, doppelt und dreifach indirekte Zeiger (15 × 4 Bytes). ext4 verwendet Extents, die jeweils einen zusammenhängenden Bereich von Blöcken beschreiben.
Die 60 Bytes von i_block enthalten einen 12 Byte großen Extent-Header, gefolgt von bis zu vier jeweils 12 Byte großen Extents. Der Header beginnt mit der magischen Zahl 0xF30A (auf dem Datenträger 0A F3), gefolgt von eh_entries (verwendete Einträge), eh_max (Kapazität, 4 innerhalb des Inodes) und eh_depth. Jeder Extent enthält den ersten logischen Block, den er abdeckt (ee_block, 4 Bytes), eine Länge (ee_len, 2 Bytes) und einen physischen Startblock als 48-Bit-Wert (ee_start_hi 2 Bytes + ee_start_lo 4 Bytes). Ein initialisierter Extent deckt höchstens 32.768 Blöcke ab (128 MiB bei 4-KiB-Blöcken); ein ee_len über 32.768 kennzeichnet einen unbeschriebenen (vorab zugewiesenen) Extent. Wenn eine Datei mehr als vier Extents benötigt, ist eh_depth größer als 0, und die Einträge innerhalb des Inodes werden zu Indexeinträgen, die auf Extent-Baumblöcke an anderer Stelle des Volumes verweisen. Dateien auf Volumes, die von ext3 aktualisiert wurden, können weiterhin eine Blockzuordnung verwenden. Prüfen Sie daher zuerst auf 0xF30A.
Verzeichniseinträge
Ein Verzeichnis ist eine Datei, deren Datenblöcke Einträge im Format ext4_dir_entry_2 enthalten:
| Offset | Größe | Feld |
|---|---|---|
| 0 | 4 | Inode-Nummer |
| 4 | 2 | Datensatzlänge (rec_len, Abstand zum nächsten Eintrag) |
| 6 | 1 | Namenslänge |
| 7 | 1 | Dateityp (1 = reguläre Datei, 2 = Verzeichnis, 7 = symbolischer Link) |
| 8 | n | Name (nicht nullterminiert; Datensatz auf ein Vielfaches von 4 aufgefüllt) |
Für report.pdf ist der Datensatz 8 + 10 = 18 Bytes lang und wird auf 20 aufgefüllt. Große Verzeichnisse verwenden einen hashbasierten B-Baum (htree), die Funktion dir_index. Der erste Block enthält einen Hash-Index, der auf Blattblöcke verweist; die Blattblöcke enthalten normale Einträge. Die Namen sind daher nach Hashwert und nicht nach Erstellungsreihenfolge angeordnet. Wenn ein Eintrag entfernt wird, wird rec_len des vorherigen Eintrags üblicherweise so vergrößert, dass es den entfernten Eintrag mit abdeckt. Bei Kerneln vor 5.13 bleiben der alte Name und die Inode-Nummer in dieser Lücke häufig lesbar; auf diese Weise listen Werkzeuge gelöschte Namen auf. Seit Linux 5.13 setzt der Kernel den entfernten Eintrag auf null, sodass unter neueren Kerneln gelöschte Namen normalerweise nicht mehr vorhanden sind.
Durchgerechnetes Beispiel: Einen Inode lokalisieren und seinen Zeitstempel lesen
fsstat meldet 8.192 Inodes pro Gruppe und 4.096 Byte große Blöcke. Sie suchen Inode 393.231.
- Gruppe = (393.231 − 1) / 8.192 = 393.230 / 8.192 = 48 (Ganzzahldivision).
- Index = (393.231 − 1) mod 8.192 = 393.230 − 48 × 8.192 = 393.230 − 393.216 = 14.
- Byte-Offset innerhalb der Inode-Tabelle = 14 × 256 = 3.584.
- Wenn Gruppendeskriptor 48 eine Inode-Tabelle bei Block 1.572.896 angibt, liegt der Inode bei 1.572.896 × 4.096 + 3.584 = 6.442.585.600 Bytes ab dem Beginn des Volumes.
debugfs kann dies mit imap <393231> bestätigen; der Befehl gibt den Block und den Offset eines Inodes aus.
Am Inode-Offset 0x10 (i_mtime) lesen Sie 80 B4 1D 67. In Little-Endian entspricht das 0x671DB480 = 1.730.000.000 Sekunden. 1.730.000.000 / 86.400 = 20.023 Tage mit einem Rest von 12.800 Sekunden. Tag 20.023 nach dem 1970-01-01 ist der 2024-10-27, und 12.800 s entsprechen 03:33:20. Bei 0x88 (i_mtime_extra) lesen Sie 54 34 6F 1D = 0x1D6F3454. Die unteren 2 Bits sind 00 (Epoche 0, keine Anpassung), und 0x1D6F3454 >> 2 = 123.456.789 ns. Die mtime lautet 2024-10-27T03:33:20.123456789Z.
Das jbd2-Journal
ext4 verwendet das jbd2-Journal, das normalerweise in Inode 8 gespeichert ist. Anders als der Rest von ext4 verwenden jbd2-Strukturen Big-Endian, und die eigenen Steuerblöcke des Journals (Superblock, Deskriptor-, Commit- und Revoke-Blöcke) beginnen mit der magischen Zahl 0xC03B3998. Das Journal ist ein zirkuläres Transaktionsprotokoll. Jede Transaktion enthält Deskriptorblöcke, dann vollständige Kopien der Dateisystemblöcke, die geändert werden, und anschließend einen Commit-Block. Die Einhängeoption data= legt fest, was im Journal protokolliert wird:
| Modus | Was im Journal protokolliert wird |
|---|---|
data=ordered (Standard) | Nur Metadaten; Dateidaten werden an ihren endgültigen Speicherort geschrieben, bevor die Metadaten festgeschrieben werden |
data=writeback | Nur Metadaten, keine Garantie für die Schreibreihenfolge der Daten |
data=journal | Metadaten und Dateidaten |
Da vollständige Metadatenblöcke protokolliert werden, kann das Journal ältere Versionen von Inode-Tabellen-, Verzeichnis- und Bitmap-Blöcken enthalten. Nach einer kürzlich erfolgten Löschung kann eine frühere Kopie des Blocks, der den Inode enthält, noch dessen Extents zeigen. Werkzeuge wie ext4magic und extundelete automatisieren diese Suche; mit debugfs logdump -i <inode> und TSK jls/jcat können Sie das Journal manuell untersuchen. Die Grenzen sind jedoch erheblich. Das Journal ist klein und zirkulär, sodass alte Kopien auf einem stark genutzten System schnell überschrieben werden. Im Ordered-Modus wird der Inhalt nicht im Journal protokolliert; eine wiederhergestellte Extent-Zuordnung hilft daher nur, wenn diese Datenblöcke nicht erneut verwendet wurden. Beide Werkzeuge sind alte Projekte, die neuere ext4-Funktionen möglicherweise nicht korrekt verarbeiten. Überprüfen Sie daher ihre Ausgaben. Beachten Sie außerdem, dass beim Einhängen eines nicht sauber ausgehängten ext4-Volumes das Journal auch bei schreibgeschütztem Einhängen erneut angewendet wird, sofern nicht noload angegeben wird.
Löschen unter ext4
Wenn eine Datei gelöscht wird, wird ihr Verzeichniseintrag entfernt. Die Linkanzahl des Inodes sinkt auf 0, i_size wird auf 0 gesetzt, i_dtime wird gesetzt, und die Bits in der Inode- und Block-Bitmap werden gelöscht. Entscheidend ist, dass die Extent-Einträge in i_block auf null gesetzt werden und mtime und ctime sich normalerweise auf den Löschzeitpunkt ändern, während crtime erhalten bleibt. Der Inode gibt nicht mehr an, wo sich der Inhalt befand. ext2 ließ Blockzeiger unverändert, sodass gelöschte Dateien anhand der Metadaten leicht wiederhergestellt werden konnten; ext3 löschte seine Zeiger, und ext4 löscht seine Extents. Die realistischen Möglichkeiten unter ext4 sind die Journalanalyse und das signaturbasierte Carving nicht zugewiesener Blöcke (Lektion 1.8). Der gelöschte Inode zeigt weiterhin Modus, Eigentümer, dtime und crtime, und bei Kerneln vor 5.13 verknüpft der alte Verzeichniseintrag den Namen häufig noch mit dem Inode.
ext4 mit gängigen Werkzeugen lesen
Beispiele mit The Sleuth Kit (-o ist der Partitionsoffset in Sektoren) und debugfs, das den Offset in Bytes als Teil des Dateinamens entgegennimmt (2.048 × 512 = 1.048.576):
| Aufgabe | Beispiel |
|---|---|
| Superblock, Blockgröße, Inodes pro Gruppe, Gruppenbereiche | fsstat -o 2048 image.dd |
Namen rekursiv auflisten, gelöschte mit * markiert | fls -r -p -o 2048 image.dd |
| Inode-Details: Zeitstempel, Größe, Extents, Löschzeitpunkt | istat -o 2048 image.dd 393231 |
| Inhalt anhand des Inodes extrahieren | icat -o 2048 image.dd 393231 > out.bin |
| Inode-Status mit crtime und Nanosekunden | debugfs -R "stat <393231>" "image.dd?offset=1048576" |
| Journal-Kopien des Blocks eines Inodes | debugfs -R "logdump -i <393231>" "image.dd?offset=1048576" |
debugfs öffnet schreibgeschützt, sofern nicht -w angegeben wird. Wenn ein Wert entscheidend ist, gleichen Sie die Werkzeugausgaben mit einer direkten Hex-Auslesung des Inodes ab.
Prüfungsschwerpunkt: Rechnen Sie damit, einen Inode anhand seiner Nummer lokalisieren zu müssen (Gruppe, Index und Offset), einen ext4-Zeitstempel einschließlich seiner _extra-Nanosekunden in einen UTC-Wert nach ISO 8601 umzurechnen und crtime von ctime zu unterscheiden. Fragen können auch die Signaturen 0xEF53 (Superblock, Byte 1.080) und 0xF30A (Extent-Header), die speziellen Inodes 2 und 8 sowie den ersten regulären Inode 11 behandeln und prüfen, warum gelöschte ext4-Dateien normalerweise eine Journalanalyse oder Carving statt einer Wiederherstellung anhand der Metadaten erfordern.
Kernpunkte
- Der Superblock beginnt bei Byte 1.024; die magische Zahl 0xEF53 befindet sich darin am Offset 56. Gruppendeskriptoren geben die Position der Bitmaps und der Inode-Tabelle jeder Gruppe an.
- Gruppe = (Inode − 1) / Inodes_pro_Gruppe; Index = (Inode − 1) mod Inodes_pro_Gruppe; Offset = Index × Inode-Größe.
- ctime ist der Zeitpunkt der Inode-Änderung. Der Erstellungszeitpunkt ist crtime, der nur in Inodes mit mindestens 256 Bytes vorhanden ist. Die Felder
_extraergänzen Nanosekunden und die Epochenbits, die Datumswerte über 2038 hinaus ermöglichen. i_blockenthält einen Extent-Header (0xF30A) und bis zu vier Extents; größere Dateien verwenden einen Extent-Baum.- Beim Löschen werden die Extents im Inode auf null gesetzt, sodass die Wiederherstellung von älteren Metadatenkopien im jbd2-Journal oder von Carving abhängt.
- Inode 2 ist das Wurzelverzeichnis, Inode 8 das Journal, und Inode 11 ist üblicherweise
lost+found.