Das meiste Python auf einer Forensik-Workstation ist Kleber. Du erwirbst Artefakte mit nativen Tools, parst sie mit Bibliotheken, die jemand anderes geschrieben hat, und verwendest Python, um die Ergebnisse zu Berichten und Timelines zusammenzunähen. $MFT ist einer der Parser, bei denen die Versuchung, einen eigenen zu schreiben, am größten ist, weil das Format klein ist und die Bibliotheken auf PyPI raue Kanten haben. Widersteh der Versuchung. Es gibt bessere Optionen.
Das ist der praktische Python-MFT-Parsing-Beitrag: welche Bibliothek wann, mit dem Code, den ich tatsächlich verwende.
Was du liest
Die NTFS-Master File Table ist eine Folge von Datensätzen fester Größe von 1.024 Bytes. Um sie aus Python zu parsen, musst du:
- Die
$MFT-Datei öffnen (oder sie aus einem Disk-Image lesen). - In 1.024-Byte-Schritten hindurchgehen.
- Das Fixup-Array auf jeden Datensatz anwenden (der Mechanismus zur Erkennung zerrissener Schreibvorgänge; siehe den Beitrag zur Datensatzanatomie).
- Den Attributstrom innerhalb jedes Datensatzes durchlaufen.
Die unten genannten Bibliotheken erledigen alle vier Schritte. Auf struct.unpack zurückzufallen ist nur lohnenswert, wenn eine Bibliothek ein Feld nicht exponiert, das du brauchst.
Option 1: analyzeMFT (reines Python, einfach zu deployen)
analyzeMFT ist der klassische reine Python-Parser, ursprünglich von David Kovar, immer noch gepflegt. CLI-zuerst, importierbar. Langsam, aber zuverlässig auf den Datensätzen, die er versteht.
# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer
analyzer = MFTAnalyzer(mft_file="path/to/$MFT", output_file="out.csv")
analyzer.analyze()
Die CSV, die er produziert, hat eine Zeile pro Datensatz mit Zeitstempeln aus sowohl $STANDARD_INFORMATION als auch $FILE_NAME. Gut genug für tabellengesteuerte Triage, wenn die MFT klein ist.
Verwende es, wenn:
- Die
$MFTklein ist (einige hundert MB oder weniger). - Du in einer luftgespaltenen Python-only-Umgebung arbeitest.
- Du eine einfache CSV willst, ohne native Abhängigkeiten anzufassen.
Lass es weg, wenn:
- Die Eingaben mehrere GB groß sind. analyzeMFT ist Single-Threaded reines Python. Eine 4-GB-MFT kann 20+ Minuten dauern, die der Rust-Parser in 30 Sekunden erledigt.
- Du Logik schreiben willst, die Datensätze programmatisch durchläuft. Das Objektmodell ist auf CSV-Ausgabe ausgerichtet, nicht auf Analyse.
Option 2: libmft (typisiertes Objektmodell)
Wenn du Datensätze als Python-Objekte abfragen willst, exponiert libmft ein typisiertes Modell nah an der On-Disk-Struktur.
# pip install libmft
from libmft.api import MFT
with open("path/to/$MFT", "rb") as f:
mft = MFT(f)
for entry in mft:
if not entry.is_deleted():
continue
name = entry.get_full_path()
si = entry.get_attributes(0x10)[0] # $STANDARD_INFORMATION
print(name, si.created, si.modified)
libmft löst Eltern-Referenzen auf, sodass du jeden Eintrag nach seinem vollständigen Pfad fragen kannst, ohne die Traversierung selbst zu schreiben. Es behandelt auch $ATTRIBUTE_LIST-Erweiterungsdatensätze transparent, die die CSV-Schicht von analyzeMFT vor dir versteckt.
Verwende es, wenn:
- Du Logik schreiben willst, die Datensätze durchläuft, nach Attribut filtert und eine eigene Form ausgibt.
- Du Zugriff auf das typisierte Objektmodell (Sicherheitsdeskriptoren, Reparse Points, Runlisten) brauchst, statt flacher CSV.
Lass es weg, wenn:
- Performance zählt. libmft ist schneller als analyzeMFT, aber immer noch reines Python; rechne mit 5 bis 10 Minuten bei einer 4-GB-MFT.
Option 3: Auf einen Rust-Parser shellen
Wenn die MFT groß ist oder du über viele Platten batchst, ist die schnellste praktische Option, auf omerbenamram/mft_dump zu shellen und seine JSON-Lines-Ausgabe zu lesen.
import json
import subprocess
# omerbenamram/mft — `cargo install mft` oder eine Release-Binärdatei herunterladen
proc = subprocess.Popen(
["mft_dump", "-o", "json", "path/to/$MFT"],
stdout=subprocess.PIPE, text=True,
)
for line in proc.stdout:
record = json.loads(line)
if record["header"]["flags"] & 0x1 == 0: # IN_USE clear → gelöscht
print(record["entry"], record["file_name"]["name"])
mft_dump gibt einen Datensatz pro Zeile aus, der sauber in Python streamt, ohne die vollständige Ausgabe in den Speicher zu laden. Im Vergleich zu analyzeMFT bei derselben Eingabe ist der Rust-Parser typischerweise 10- bis 50-mal schneller und verbraucht ein Zehntel des Speichers.
Verwende es, wenn:
- Produktions-Pipelines.
- Große Eingaben.
- Überall, wo die Parsing-Zeit zählt.
Der einzige Haken: Du hängst davon ab, dass die Binärdatei installiert ist. Pin eine Version, liefere sie mit deinem Tooling aus und dokumentiere die Installation in deinem Runbook.
$MFT direkt aus einem Disk-Image lesen
Wenn du ein rohes .dd- oder .E01-Image statt einer extrahierten $MFT-Datei hast, verwende pytsk3 (Python-Bindings für The Sleuth Kit), um zu $MFT auf dem Volume zu springen und seine Bytes zu streamen:
import pytsk3
img = pytsk3.Img_Info("disk.dd")
fs = pytsk3.FS_Info(img, offset=0) # NTFS-Partitionsoffset verwenden
mft_file = fs.open_meta(inode=0) # $MFT ist immer Inode 0
size = mft_file.info.meta.size
data = mft_file.read_random(0, size)
# data enthält jetzt $MFT; gib es an libmft oder schreibe es auf die Platte
Das ist der sauberste Ansatz, wenn das Volume auf Partitionsebene verschlüsselt ist, aber über einen Entschlüsseler gemountet wird, der dir ein rohes Image gibt. Es ist auch das richtige Tool, wenn das Image VSS-Snapshots enthält und du $MFT aus jedem extrahieren willst. Kombiniere mit libvshadow für die Snapshot-Enumeration.
Ein kurzes Skript, das ich aufhebe
Etwa das Skript, zu dem ich zuerst greife, wenn ich mir eine unbekannte MFT anschaue. Es findet gelöschte Datensätze mit residenten Daten und dumpt ihren Inhalt.
import json
import subprocess
proc = subprocess.Popen(
["mft_dump", "-o", "json", "path/to/$MFT"],
stdout=subprocess.PIPE, text=True,
)
for line in proc.stdout:
rec = json.loads(line)
if rec["header"]["flags"] & 0x1:
continue # in use
for attr in rec.get("attributes", []):
if attr["header"]["type_code"] != 0x80:
continue # nicht $DATA
if not attr["header"]["is_resident"]:
continue # Daten leben anderswo
# Resident, gelöscht, hat $DATA inline. Der interessante Fall.
data = bytes.fromhex(attr["data"]["resident_data"])
print(f"rec={rec['entry']} seq={rec['header']['sequence']} "
f"name={rec.get('file_name', {}).get('name')} "
f"bytes={len(data)}")
# In eine nach Datensatznummer benannte Datei zur Überprüfung schreiben.
with open(f"deleted_resident_{rec['entry']}.bin", "wb") as f:
f.write(data)
Dieses einzelne Skript hat über Untersuchungen hinweg genug gelöschte Skripte, Konfigs und Einzeiler-Dropper aufgedeckt, um sich vielfach zu rechtfertigen. Residente Daten sitzen in MFT-Datensätzen, an die niemand denkt zu schauen. Siehe residente Daten dafür, was passt.
Häufige Fallstricke
- Das Fixup-Array vergessen. Das Lesen roher 1.024-Byte-Brocken ohne Anwendung der USA gibt dir Müll an Offsets 510 und 1022 jedes Datensatzes. Die obigen Bibliotheken erledigen das für dich. Rolle deinen eigenen Parser nur, wenn du den Fixup-Mechanismus im Beitrag zur Datensatzanatomie verstehst.
- Die Datensatznummer als Identität behandeln. Datensatznummern werden wiederverwendet. Die 64-Bit-Dateireferenz (Datensatznummer plus Sequenznummer) ist der Identifikator, der nicht kollidiert. Wenn dein Skript nur nach Datensatznummer gruppiert, wird es stillschweigend gelöschte Vorgänger mit ihren wiederverwendenden Nachfolgern vermischen.
- Die zwei Zeitstempelsets verwechseln. Jeder Datensatz trägt Zeitstempel in
$STANDARD_INFORMATION(häufig aktualisiert) und$FILE_NAME(meist stabil). Für die Timestomping-Erkennung brauchst du beide. Siehe die vier MFT-Zeitstempel. - Erweiterungsdatensätze nicht behandeln. Eine Datei, deren Attribute einen Datensatz überlaufen, hat ein
$ATTRIBUTE_LIST(0x20), das auf Erweiterungsdatensätze zeigt. Viele naive Skripte geben den Basisdatensatz aus und übersehen Attribute, die in den Erweiterungen leben. libmft kümmert sich darum; wenn du deinen eigenen Walk rollst, vergiss es nicht.
Wann Python ganz überspringen
Für eine einmalige interaktive Analyse ohne jede Installation lege die $MFT auf den Browser-Parser auf dieser Seite. Er führt dieselbe omerbenamram/mft-Crate aus, kompiliert zu WebAssembly, filtert und sucht clientseitig und exportiert CSV. Kein Python erforderlich.
Weiterführende Literatur
omerbenamram/mft. Der Rust-Parser, dessen JSON-Ausgabe das obige Skript konsumiert.pytsk3. Python-Bindings für The Sleuth Kit; der sauberste Weg,$MFTdirekt aus einem Disk-Image zu lesen.- David Cowen, Daily Blog und Sunday Funday. Jahre an Praktiker-Python-Snippets, die auf realen MFTs funktionieren.