Redo- und Undo-Operationen im NTFS-$LogFile erklärt
Der NTFS-Log-Header Byte für Byte, die 38 Redo-/Undo-Opcodes, welche forensisch zählen und wie Anlegen, Löschen, Umbenennen und Zeitstempeländerungen aussehen.
TL;DR. Ein NTFS-Logeintrag besteht aus einem LFS-Header (LSN, vorherige LSN, Transaktions-ID…) und einem darauf folgenden NTFS-Header: Redo-Opcode, Undo-Opcode, wo die Redo- und Undo-Bytes liegen, auf welches geöffnete Attribut sie zielen und an welcher Stelle darin. Es gibt 38 Opcodes (0x00–0x25), aber eine Handvoll trägt den Großteil des forensischen Werts: InitializeFileRecordSegment, DeallocateFileRecordSegment, CreateAttribute / DeleteAttribute, UpdateResidentValue und die vier Operationen Add/DeleteIndexEntry*. Alles andere ist Verwaltungsarbeit, die Sie trotzdem brauchen, um Ziele aufzulösen und Einträge zu gruppieren.
Wenn Ihnen das $LogFile undurchschaubar vorkommt, liegt das daran, dass aus einer einzigen Benutzeraktion — „diese Datei umbenennen“ — mehrere Einträge werden, die jeweils nur mit dem richtigen Kontext Sinn ergeben: welches Attribut unter welchem Index geöffnet ist, welche Clustergröße das Volume nutzt, wie der FILE-Record vorher aussah. Dieser Artikel ist der Schlüssel zum Entziffern. Das Gesamtbild steht in NTFS-$LogFile-Forensik: der vollständige Leitfaden.
Zwei Header pro Eintrag
Jeder Logeintrag beginnt mit dem LFS-Eintragsheader, gefolgt von den Client-Daten. Beim NTFS-Client beginnen die Client-Daten mit einem eigenen Header. Offsets aus fslog.c von Linux ntfs3:
LFS-Eintragsheader (0x30 Bytes)
| Offset | Größe | Feld |
|---|---|---|
0x00 | 8 | Diese LSN |
0x08 | 8 | Vorherige LSN des Clients (gleiche Transaktion) |
0x10 | 8 | Undo-Next-LSN des Clients |
0x18 | 4 | Länge der Client-Daten |
0x1C | 4 | Client-ID |
0x20 | 4 | Eintragstyp: 1 = Client-Eintrag, 2 = Client-Restart |
0x24 | 4 | Transaktions-ID |
0x28 | 2 | Flags (0x1 = Eintrag geht auf der nächsten Seite weiter) |
NTFS-Log-Header (Beginn der Client-Daten)
| Offset | Größe | Feld | Zweck |
|---|---|---|---|
0x00 | 2 | Redo-Operation | Was zu tun ist |
0x02 | 2 | Undo-Operation | Wie es rückgängig gemacht wird |
0x04 / 0x06 | 2 + 2 | Redo-Offset / -Länge | Wo die Redo-Bytes liegen |
0x08 / 0x0A | 2 + 2 | Undo-Offset / -Länge | Wo die Undo-Bytes liegen |
0x0C | 2 | Zielattribut | Index in die Open Attribute Table |
0x0E | 2 | Anzahl folgender LCNs | Anzahl der Clusternummern ab 0x20 |
0x10 | 2 | Eintrags-Offset | Offset des Attributs im FILE-Record (oder des Eintrags in einem Indexpuffer) |
0x12 | 2 | Attribut-Offset | Offset der Änderung innerhalb dieses Attributs |
0x14 | 2 | Cluster-Block-Offset | In Einheiten von 512 Bytes, innerhalb des Zielclusters |
0x18 | 8 | Ziel-VCN | Virtueller Cluster im Zielattribut |
0x20 | 8 × n | Ziel-LCNs | Physische Cluster |
Einträge vom Typ 2 sind Checkpoints (Client-Restart-Bereiche), keine Änderungen. Sie werden in Restart Area und LSN im $LogFile erklärt behandelt.
Den MFT-Eintrag finden, den ein Logeintrag betrifft
Einträge sagen nicht „MFT-Eintrag 322“. Sie sagen „Attribut #N in der Open Attribute Table, VCN v, Cluster-Block-Offset c“. Ist das Zielattribut $MFT:$DATA, ergibt sich der Eintrag so:
entry = (VCN × cluster_size + cluster_block_offset × 512) / mft_record_size
Dafür werden zwei Volume-Parameter benötigt. Ein Parser kann sie aus dem Bootsektor entnehmen oder, wie der $LogFile-Parser im Browser, aus dem Log selbst kalibrieren: Einträge vom Typ InitializeFileRecordSegment enthalten ein vollständiges Abbild eines FILE-Records, dessen Header die eigene Eintragsnummer angibt. Es gewinnt die Kombination aus Clustergröße und Eintragsgröße, die die meisten davon korrekt abbildet.
Die Open Attribute Table wird aus OpenNonresidentAttribute-Einträgen sowie aus den bei jedem Checkpoint geschriebenen OpenAttributeTableDump und AttributeNamesDump wiederaufgebaut. Das Layout ihrer Einträge unterscheidet sich zwischen den Versionen des NTFS-Clients (0x28 vs. 0x2C Bytes) — eine der Stellen, an denen Parser zu unterschiedlichen Ergebnissen kommen können.
Die 38 Operationen, gruppiert
Die Namen unten sind die aus enum NTFS_LOG_OPERATION von ntfs3, die auch der Parser verwendet.
| Code | Operation | Gruppe | Forensischer Wert |
|---|---|---|---|
0x00 | Noop | — | Undo-Seite vieler reiner Redo-Einträge |
0x01 | CompensationLogRecord | Transaktion | Wird beim Zurückrollen geschrieben |
0x02 | InitializeFileRecordSegment | FILE-Record | Hoch: vollständiges Abbild des FILE-Records eines neuen oder wiederverwendeten Eintrags |
0x03 | DeallocateFileRecordSegment | FILE-Record | Hoch: Eintrag freigegeben (Löschung) |
0x04 | WriteEndOfFileRecordSegment | FILE-Record | Gering |
0x05 | CreateAttribute | Attribut | Hoch: neues Attribut, z. B. $FILE_NAME oder residentes $DATA |
0x06 | DeleteAttribute | Attribut | Hoch: Attribut entfernt, z. B. altes $FILE_NAME bei Umbenennung |
0x07 | UpdateResidentValue | Attribut | Hoch: Zeiten in $STANDARD_INFORMATION, kleine residente Werte |
0x08 | UpdateNonresidentValue | Attribut | Mittel: Indexpuffer, Schreibvorgänge in $UsnJrnl, andere Streams |
0x09 | UpdateMappingPairs | Attribut | Mittel: Data Runs (Datei wächst) |
0x0A | DeleteDirtyClusters | Attribut | Gering |
0x0B | SetNewAttributeSizes | Attribut | Mittel: Größenänderungen |
0x0C / 0x0D | Add / DeleteIndexEntryRoot | Index | Hoch: Verzeichniseinträge in $INDEX_ROOT |
0x0E / 0x0F | Add / DeleteIndexEntryAllocation | Index | Hoch: Verzeichniseinträge in $INDEX_ALLOCATION |
0x10 | WriteEndOfIndexBuffer | Index | Gering |
0x11 / 0x12 | SetIndexEntryVcnRoot / Allocation | Index | Gering (B-Baum-Mechanik) |
0x13 / 0x14 | UpdateFileNameRoot / Allocation | Index | Mittel: duplizierte $FILE_NAME-Informationen in $I30 |
0x15 / 0x16 | Set / ClearBitsInNonresidentBitMap | Bitmap | Allein gering: Belegung von Clustern oder MFT-Einträgen |
0x17 | HotFix | — | Selten |
0x18 | EndTopLevelAction | Transaktion | Struktur |
0x19–0x1B | Prepare / Commit / ForgetTransaction | Transaktion | Transaktionsgrenzen |
0x1C | OpenNonresidentAttribute | Tabelle | Nötig, um Ziele aufzulösen |
0x1D–0x20 | Dumps von OpenAttributeTable / AttributeNames / DirtyPageTable / TransactionTable | Checkpoint | Nötig, um Ziele aufzulösen |
0x21 / 0x22 | UpdateRecordDataRoot / Allocation | Index | Mittel: Indizes außer $I30 ($ObjId, $Quota, $Secure) |
0x23 / 0x24 | UpdateRelativeDataInIndex / 2 | Index | Gering |
0x25 | ZeroEndOfFileRecord | FILE-Record | Gering |
Undo ist meist die Umkehrung von Redo: AddIndexEntryRoot wird durch DeleteIndexEntryRoot rückgängig gemacht, CreateAttribute durch DeleteAttribute, SetBits… durch ClearBits… und UpdateResidentValue durch ein weiteres UpdateResidentValue mit den alten Bytes. Operationen, deren Rücknahme nichts erfordert, etwa das Initialisieren eines frischen FILE-Records, haben als Undo oft Noop.
Wie typische Aktionen aussehen
Das sind die Muster, nach denen ein Parser sucht. Echte Abfolgen enthalten mehr Einträge (Bitmaps, Größen, Sicherheit, USN-Schreibvorgänge), und die genaue Reihenfolge kann je nach Windows-Version variieren.
Datei anlegen
InitializeFileRecordSegmentauf dem neuen Eintrag. Redo = vollständiges Abbild des FILE-Records: Header (Sequenznummer, Flags),$STANDARD_INFORMATION,$FILE_NAME(Name, Referenz auf den übergeordneten Ordner, vier Zeiten), eventuell ein kleines$DATA.AddIndexEntryRootoderAddIndexEntryAllocationim$I30-Index des übergeordneten Verzeichnisses. Der Indexeintrag enthält eine Kopie von$FILE_NAME.
Löschen
DeleteIndexEntryRoot/DeleteIndexEntryAllocationentfernt den Namen aus dem übergeordneten Ordner (der entfernte Eintrag steht in den Undo-Bytes, manchmal auch im Redo).DeallocateFileRecordSegmentauf dem Eintrag, dazuClearBitsInNonresidentBitMapauf der MFT-Bitmap.
Umbenennen oder Verschieben
- dfir.ru dokumentiert eine Umbenennung als
DeleteIndexEntryRoot,DeleteAttribute(altes$FILE_NAME),CreateAttribute(neues$FILE_NAME),AddIndexEntryRoot(How the $LogFile works?). Eine Verschiebung sieht genauso aus, nur mit einem anderen übergeordneten Ordner im hinzugefügten Eintrag. Bei großen Verzeichnissen erscheinen statt der…Root-Varianten die…Allocation-Varianten.
Zeitstempeländerung
UpdateResidentValueauf das Attribut$STANDARD_INFORMATION(das erste Attribut eines FILE-Records, bei Offset0x38in NTFS-3.1-Records). Redo enthält die neuen Bytes, Undo die alten. Die Aktualisierung kann partiell sein — dfir.ru weist darauf hin, dass sie beim M-Zeitstempel beginnen kann —, daher muss ein Parser den Attribut-Offset nutzen, um zu wissen, welche der vier Zeiten er liest. Darauf baut Timestomping mit dem NTFS-$LogFile erkennen auf.
Inhalt kleiner Dateien
- Ein residentes
$DATA-Attribut, das mitCreateAttributeangelegt wird oder im Abbild des FILE-Records enthalten ist, kann die ersten Bytes einer kleinen Datei tragen. Spätere Bearbeitungen sind eine andere Geschichte: Laut Dokumentation von LogFileParser wird unter modernem Windows der Inhalt von Änderungen an residenten Daten im Allgemeinen nicht gespeichert, nur die Tatsache, dass eine Änderung stattfand. Siehe Spuren gelöschter Dateien im $LogFile finden.
Einträge zu Aktionen gruppieren
Zwei Ansätze:
- Nach Transaktion. Transaktions-ID und die Kette der vorherigen LSNs nutzen, begrenzt durch
ForgetTransaction- oder Commit-Einträge. Am genauesten, aber über Checkpoints hinweg schwerer richtig umzusetzen. - Nach MFT-Eintrag und Nähe. Einträge nehmen, die denselben Eintrag innerhalb eines LSN-Fensters betreffen. Einfacher und robust gegenüber beschädigten Seiten, aber zwei voneinander unabhängige Aktionen an derselben Datei kurz hintereinander können verschmelzen.
Der Parser im Browser nutzt derzeit den zweiten Ansatz (ein Fenster von ±256 Einträgen pro MFT-Eintrag) und zeigt jeden Roheintrag, damit Sie die Gruppierung prüfen können. Gruppierung nach Transaktionen steht auf seiner Roadmap; zur Bestätigung sollten LogFileParser und NTFS Log Tracker herangezogen werden.
Die Bytes selbst lesen
Wenn es auf ein Ereignis ankommt, öffnen Sie den Roheintrag: Prüfen Sie Redo-/Undo-Opcodes, Zielattribut (zum Beispiel #322:$STANDARD_INFORMATION oder $MFT:$DATA), Eintrags-Offset, Attribut-Offset und den Hexdump. Ein 32 Byte langes Undo bei Attribut-Offset 0 von $STANDARD_INFORMATION enthält vier FILETIME-Werte im Little-Endian-Format: Erstellung, Änderung, MFT-Änderung, Zugriff.
Häufige Fragen
Was sind Redo- und Undo-Operationen im $LogFile?
Jeder NTFS-Logeintrag beschreibt eine Metadatenänderung zweimal: Redo-Operation und Redo-Bytes sagen, wie die Änderung erneut angewendet wird, Undo-Operation und Undo-Bytes, wie sie rückgängig gemacht wird. Bei der Wiederherstellung wird Redo für abgeschlossene Transaktionen erneut abgespielt und Undo für nicht abgeschlossene angewendet.
Welche $LogFile-Operationen zeigen, dass eine Datei angelegt oder gelöscht wurde?
Beim Anlegen sieht man meist InitializeFileRecordSegment auf dem neuen FILE-Record plus AddIndexEntryRoot oder AddIndexEntryAllocation im übergeordneten Verzeichnis. Beim Löschen DeleteIndexEntryRoot oder DeleteIndexEntryAllocation, gefolgt von DeallocateFileRecordSegment.
Weiterführende Literatur
- Linux-Kernel,
fs/ntfs3/fslog.c— Strukturdefinitionen und eine vollständige Replay-Implementierung. - Joakim Schicht, Readme von LogFileParser — welche Operationen es dekodiert und was sie enthalten.
- Maxim Suhanov, How the $LogFile works?