← Zurück zum Blog

Innerhalb eines MFT-Datensatzes, Byte für Byte

· 6 Min. Lesezeit

Jeder MFT-Datensatz ist 1.024 Bytes groß. Jeder ist gleich aufgebaut. Wenn du einen in einem Hex-Editor lesen kannst, kannst du alle lesen, und du kannst einen Parser schreiben, der die spröden kommerziellen Tools beim Wiederherstellen beschädigter Datensätze schlägt. Das Layout ist klein genug, dass ein paar Stunden und 010 Editor dich flüssig machen.

Das ist die Byte-Level-Tour, die ich Analysten gebe, die aufhören wollen, MFT-Parser als Black Boxes zu behandeln.

Die FILE-Signatur

46 49 4C 45  ("FILE")

Erste vier Bytes. ASCII FILE. Das ist die Magie, mit der du MFT-Datensätze aus rohen Clustern herausschnitzen kannst, wenn $MFT selbst unlesbar ist. Durchlaufe ein Disk-Image an 512-Byte-Grenzen (älteres NTFS) oder 4096-Byte-Grenzen (Win10 mit 4K-Datensätzen, selten aber vorhanden), suche nach dem Muster 46 49 4C 45, und du kannst meist den Großteil einer MFT wiederherstellen, selbst wenn der eigene Header der Tabelle zerstört ist.

Die andere Signatur, die du in freier Wildbahn sehen wirst, ist BAAD (42 41 41 44). NTFS schreibt das, wenn chkdsk entschied, dass ein Datensatz nicht reparierbar war. Der Slot wird behalten, die Sequenznummer wird bewahrt, aber dem Rest der Bytes sollte nicht vertraut werden. Behandle BAAD-Datensätze als Beweis, dass chkdsk lief, und als Ziele für die Kontextanalyse drumherum. Versuche nicht, ihre Attribute naiv zu parsen.

Der Datensatz-Header (Offsets 0x00 bis 0x37, ungefähr)

Nach den vier Signaturbytes legt NTFS einen Header an, der je nach NTFS-Version leicht variiert. Die Felder, mit Offsets vom Beginn des Datensatzes, die du tatsächlich verwendest:

0x00  4 Bytes  Signatur ("FILE" oder "BAAD")
0x04  2 Bytes  Offset zum Update-Sequence-Array (USA)
0x06  2 Bytes  USA-Größe in 16-Bit-Worten (records=Anzahl von (USN+fixup_entries))
0x08  8 Bytes  $LogFile-Sequenznummer (LSN)
0x10  2 Bytes  Sequenznummer
0x12  2 Bytes  Anzahl harter Links
0x14  2 Bytes  Offset zum ersten Attribut
0x16  2 Bytes  Flags (Bit 0 = IN_USE, Bit 1 = DIRECTORY,
                     Bit 2 = QUOTA_CHARGED, Bit 3 = HAS_VIEW_INDEX)
0x18  4 Bytes  Belegte Größe des Datensatzes
0x1C  4 Bytes  Allozierte Größe (immer 1024 auf Standardvolumes)
0x20  8 Bytes  Basisdateidatensatzreferenz (ungleich null auf Erweiterungsdatensätzen)
0x28  2 Bytes  Nächste Attribut-ID
0x2A  2 Bytes  (Padding/Ausrichtung auf NTFS 3.0)
0x2C  4 Bytes  Datensatznummer (NTFS 3.1+; Selbstreferenz)

Einige davon verdienen besondere Aufmerksamkeit.

Flags bei 0x16. Bit 0 gelöscht bedeutet entfernt. Bit 1 gesetzt bedeutet Verzeichnis (der Datensatz enthält Index-Attribute statt $DATA). Die Kombination dieser beiden bedeutsamen Flags ist, was ein Byte über einen Eintrag viel sagen lässt.

Sequenznummer bei 0x10. Wird bei jeder Wiederverwendung des Slots inkrementiert. Die 64-Bit-Dateireferenz (Datensatznummer in den unteren 48 Bit, Sequenznummer in den oberen 16) ist der eigentliche eindeutige Identifikator für die Existenz einer bestimmten Datei. Referenzen in anderen Attributen ($FILE_NAME-Eltern-Referenz, $ATTRIBUTE_LIST-Einträge) verwenden diese 64-Bit-Form. Eine Referenz, deren Sequenz nicht zum aktuellen Datensatz passt, verweist auf einen früheren Bewohner; meist eine gelöschte Datei. So durchläuft Sleuth Kit gelöschte Verzeichnisketten.

Basisdateidatensatzreferenz bei 0x20. Null bei einem Basisdatensatz; ungleich null bei einem Erweiterungsdatensatz (wenn die Attribute einer Datei einen einzelnen Slot überlaufen). Der Nicht-Null-Wert ist die 64-Bit-Referenz des Basisdatensatzes, zu dem diese Erweiterung gehört. Parser müssen $ATTRIBUTE_LIST-Ketten folgen, um die vollständige Datei zusammenzusetzen.

$LogFile-Sequenznummer bei 0x08. Verweist in $LogFile. Nützlich für die Wiederherstellung auf Transaktionsebene; weniger nützlich für die Routineanalyse. Es ist gut zu wissen, dass es existiert.

Das Fixup-Array (Update Sequence)

NTFS schützt vor zerrissenen Schreibvorgängen mit einem kleinen Trick. Jeder 1.024-Byte-Datensatz wird in zwei 512-Byte-Sektoren geteilt. Vor dem Schreiben:

  1. Wählt NTFS eine 16-Bit-Update-Sequence-Nummer (USN, keine Verbindung zur $UsnJrnl-USN trotz des geteilten Akronyms).
  2. Legt die ursprünglichen letzten zwei Bytes jedes Sektors in einem Array ab, das direkt nach dem Header liegt.
  3. Ersetzt die letzten zwei Bytes jedes Sektors durch die USN selbst.

Beim Lesen prüft NTFS, dass die letzten zwei Bytes jedes 512-Byte-Sektors der gewählten USN entsprechen. Wenn sie es tun, war der Schreibvorgang atomar; ziehe die Originalbytes aus dem Fixup-Array heraus und setze sie wieder ein. Wenn das Ende eines Sektors nicht passt, war der Schreibvorgang zerrissen und der Datensatz ist verdächtig.

Das Array ist als ein USN-Wort gefolgt von N Fixup-Worten angelegt, wobei N die Anzahl der Sektoren ist. Für einen 1.024-Byte-Datensatz auf einem Volume mit 512-Byte-Sektoren ist N = 2. Das Array belegt daher insgesamt 6 Bytes (USN, fixup_for_sector_0, fixup_for_sector_1). Sein Offset ist der 2-Byte-Wert bei 0x04 des Datensatzes (typischerweise 0x2A oder 0x30 je nach NTFS-Version).

Die praktische Konsequenz: Wenn du rohe 1.024-Byte-Brocken von $MFT ohne Anwendung von Fixups liest, wird jeder Datensatz Müll an Offsets 510 und 1022 haben. Resident $DATA, das diese Offsets kreuzt, wird beschädigt sein. Parser wie MFTECmd, omerbenamram/mft, analyzeMFT und Sleuth Kits fls/istat wenden Fixups als ersten Schritt an. Wenn du deinen eigenen Parser schreibst (was eine gute Übung ist; siehe Parsing von $MFT in Python), tu dies vor allem anderen.

Der Attributstrom

Nach dem Header und dem Fixup-Array enthält jeder Datensatz eine Reihe typisierter Attribute, hintereinander gepackt, an 8-Byte-Grenzen ausgerichtet, beendet durch den Sentinel-Wert 0xFFFFFFFF, wo der Typcode des nächsten Attributs stünde.

Jedes Attribut beginnt mit einem kleinen standardisierten Header:

0x00  4 Bytes  Attributtyp-Code (0x10 = $STANDARD_INFORMATION usw.)
0x04  4 Bytes  Länge dieses Attributs (Header + Daten)
0x08  1 Byte   Non-Resident-Flag (0 = resident, 1 = nicht-resident)
0x09  1 Byte   Namenslänge (in Zeichen; 0 wenn unbenannt)
0x0A  2 Bytes  Offset zum Namen (in Zeichen vom Anfang des Attributs)
0x0C  2 Bytes  Flags (komprimiert/verschlüsselt/sparse)
0x0E  2 Bytes  Attribut-ID

Für residente Attribute sind die nächsten Felder Inhaltlänge und Inhaltsoffset; für nicht-residente sind sie Start-/End-VCN, Runlist-Offset, komprimierte/allozierte/echte Größen. Nach all dem die eigentlichen Daten. Namen (falls vorhanden) sind Unicode und nicht ausgerichtet; erwarte, dass die Implementierung fummelig ist.

Ein minimaler Datensatz enthält drei Attribute:

  • $STANDARD_INFORMATION (0x10): Zeitstempel, DOS-Flags, Sicherheits-ID.
  • $FILE_NAME (0x30): Name, Eltern-Referenz, zweites Set Zeitstempel, allozierte/echte Größen ab dem Zeitpunkt, an dem der Name gesetzt wurde. Datensätze können viele davon tragen (eines pro hartem Link plus den 8.3-Kurznamen auf Volumes, auf denen 8.3-Generierung aktiviert ist).
  • $DATA (0x80): Inhalt der Datei, resident wenn er passt, sonst Runlist. Datensätze können mehrere $DATA-Attribute tragen; das unbenannte ist der Primärstream, benannte sind Alternate Data Streams.

Für den vollständigen Attributkatalog und wo jeder lebt, siehe die Master-File-Table-Referenz.

Warum das wichtig ist, wenn Datensätze beschädigt sind

Die Kombination aus stabilem 1.024-Byte-Layout, der FILE-Signatur, dem Fixup-Mechanismus und selbstbeschreibenden Attribut-Headern ist es, was das Carving gelöschter NTFS-Datensätze möglich macht. Selbst wenn $MFT selbst weg ist (Korruption, Ransomware-Kritzeleien, partielles Wipe), holt ein Signatur-Scan des rohen Volumes nach 46 49 4C 45-Grenzen die Datensätze wieder, solange die zugrunde liegenden Cluster nicht überschrieben wurden. Die Fixup-Verifikation gibt dir eine Integritätsprüfung pro Sektor; Datensätze, die sie nicht bestehen, sollten markiert werden, aber ihre Attributdaten sind manchmal noch teilweise lesbar.

Das ist die Grundlage unter Tools wie Sleuth Kits mmls+fls, R-Studios "Deep Scan" und den verschiedenen kommerziellen Recovery-Suiten. Sie laufen alle dasselbe Byte-Layout ab. Es selbst zu kennen, ermöglicht es dir, ihre Ausgabe einer Plausibilitätsprüfung zu unterziehen.

Einen von Hand lesen

Öffne eine extrahierte $MFT in 010 Editor mit dem NTFS-MFT-Record-Template. Wähle Datensatz 5 (Wurzelverzeichnis; Offset 5 * 1024 = 5120 vom Anfang der Datei). Bestätige:

  • Bytes 0x00 bis 0x03: 46 49 4C 45.
  • Flags bei 0x16: 0x03 (IN_USE und DIRECTORY beide gesetzt).
  • Erster Attributtyp am Offset, der von 0x14 angegeben wird: 0x10 ($STANDARD_INFORMATION).

Wenn das passt, wurde der Datensatz sauber geparst. Wenn nicht, ist entweder die Datei beschädigt oder das Fixup-Array wurde nicht angewendet (010 Editors Template wendet es für dich an).

Sobald du das einmal gemacht hast, wird der Rest der MFT lesbar. Die Felder hören auf, kryptisch zu sein, und werden zu einem Datensatz, den du ohne Hilfe lesen kannst.

Weiterführende Literatur

Externe Ressourcen