L'essentiel du Python sur une station forensique est de la colle. Vous acquérez des artefacts avec des outils natifs, vous les parsez avec des bibliothèques écrites par quelqu'un d'autre, et vous utilisez Python pour assembler les résultats en rapports et timelines. $MFT est l'un des parsers où la tentation de faire le sien est la plus forte parce que le format est petit et que les bibliothèques sur PyPI ont des aspérités. Résistez à la tentation. Il y a de meilleures options.
Voici le billet pratique de parsing MFT en Python : quelle bibliothèque quand, avec le code que j'utilise vraiment.
Ce que vous lisez
La Master File Table NTFS est une séquence d'enregistrements de taille fixe de 1 024 octets. Pour la parser depuis Python, vous devez :
- Ouvrir le fichier
$MFT(ou le lire depuis une image disque). - Avancer par tranches de 1 024 octets.
- Appliquer le tableau de fixup à chaque enregistrement (le mécanisme de détection d'écritures déchirées ; voir le billet sur l'anatomie d'un enregistrement).
- Parcourir le flux d'attributs à l'intérieur de chaque enregistrement.
Les bibliothèques ci-dessous gèrent les quatre étapes. Se rabattre sur struct.unpack ne vaut le coup que quand une bibliothèque n'expose pas un champ dont vous avez besoin.
Option 1 : analyzeMFT (pur Python, facile à déployer)
analyzeMFT est le parser pur Python classique, à l'origine de David Kovar, encore maintenu. CLI d'abord, importable. Lent, mais fiable sur les enregistrements qu'il comprend.
# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer
analyzer = MFTAnalyzer(mft_file="path/to/$MFT", output_file="out.csv")
analyzer.analyze()
Le CSV qu'il produit a une ligne par enregistrement avec les horodatages à la fois de $STANDARD_INFORMATION et de $FILE_NAME. Suffisant pour un triage piloté par tableur quand la MFT est petite.
Utilisez-le quand :
- La
$MFTest petite (quelques centaines de Mo ou moins). - Vous travaillez dans un environnement air-gappé uniquement Python.
- Vous voulez un CSV simple sans toucher aux dépendances natives.
Sautez-le quand :
- Les entrées sont multi-Go. analyzeMFT est du pur Python mono-thread. Une MFT de 4 Go peut prendre 20+ minutes que le parser Rust fait en 30 secondes.
- Vous voulez écrire de la logique qui parcourt les enregistrements programmatiquement. Le modèle d'objets est orienté émission CSV, pas analyse.
Option 2 : libmft (modèle d'objets typé)
Si vous voulez interroger les enregistrements comme objets Python, libmft expose un modèle typé proche de la structure on-disk.
# 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 résout les références parent pour que vous puissiez demander à chaque entrée son chemin complet sans écrire vous-même le parcours. Il gère aussi de manière transparente les enregistrements d'extension $ATTRIBUTE_LIST, que la couche CSV d'analyzeMFT vous cache.
Utilisez-le quand :
- Vous voulez écrire de la logique qui parcourt les enregistrements, filtre par attribut et émet une forme personnalisée.
- Vous avez besoin d'accéder au modèle d'objets typé (descripteurs de sécurité, reparse points, runlists) plutôt qu'à un CSV plat.
Sautez-le quand :
- La performance compte. libmft est plus rapide qu'analyzeMFT mais reste du pur Python ; comptez 5 à 10 minutes sur une MFT de 4 Go.
Option 3 : shell out vers un parser Rust
Quand la MFT est grosse ou que vous traitez en batch beaucoup de disques, l'option pratique la plus rapide est de shell out vers omerbenamram/mft_dump et lire sa sortie JSON Lines.
import json
import subprocess
# omerbenamram/mft — `cargo install mft` ou téléchargez un binaire de release
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 effacé → supprimé
print(record["entry"], record["file_name"]["name"])
mft_dump émet un enregistrement par ligne, ce qui streame proprement dans Python sans charger la sortie complète en mémoire. Comparé à analyzeMFT sur la même entrée, le parser Rust est typiquement 10 à 50× plus rapide et utilise un dixième de la mémoire.
Utilisez-le quand :
- Pipelines de production.
- Grandes entrées.
- Là où le temps de parsing compte.
Le seul hic : vous dépendez de l'installation du binaire. Épinglez une version, livrez-le avec votre outillage et documentez l'installation dans votre runbook.
Lire $MFT directement depuis une image disque
Si vous avez une image brute .dd ou .E01 plutôt qu'un fichier $MFT extrait, utilisez pytsk3 (bindings Python pour The Sleuth Kit) pour vous positionner sur $MFT dans le volume et streamer ses octets :
import pytsk3
img = pytsk3.Img_Info("disk.dd")
fs = pytsk3.FS_Info(img, offset=0) # utilisez l'offset de la partition NTFS
mft_file = fs.open_meta(inode=0) # $MFT est toujours l'inode 0
size = mft_file.info.meta.size
data = mft_file.read_random(0, size)
# data contient maintenant $MFT ; passez-le à libmft ou écrivez-le sur disque
C'est l'approche la plus propre quand le volume est chiffré au niveau de la partition mais monté via un déchiffreur qui vous donne une image brute. C'est aussi le bon outil quand l'image contient des snapshots VSS et que vous voulez extraire $MFT de chacun. Combinez avec libvshadow pour l'énumération des snapshots.
Un petit script que je garde sous la main
Grosso modo le script que je prends en premier quand je regarde une MFT inconnue. Il trouve les enregistrements supprimés avec des données résidentes et dump leur contenu.
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 # en cours d'utilisation
for attr in rec.get("attributes", []):
if attr["header"]["type_code"] != 0x80:
continue # pas $DATA
if not attr["header"]["is_resident"]:
continue # les données vivent ailleurs
# Résident, supprimé, $DATA inline. Le cas intéressant.
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)}")
# Écrire dans un fichier nommé par numéro d'enregistrement pour revue.
with open(f"deleted_resident_{rec['entry']}.bin", "wb") as f:
f.write(data)
Ce seul script a fait remonter, à travers les enquêtes, assez de scripts supprimés, de configs et de droppers en une ligne pour s'auto-justifier maintes fois. Les données résidentes sont dans des enregistrements MFT que personne ne pense à vérifier. Voir données résidentes pour ce qui rentre.
Pièges courants
- Oublier le tableau de fixup. Lire des morceaux bruts de 1 024 octets sans appliquer l'USA vous donne des ordures aux offsets 510 et 1022 de chaque enregistrement. Les bibliothèques ci-dessus le font pour vous. Ne faites votre propre parser que si vous comprenez le mécanisme de fixup dans le billet sur l'anatomie d'un enregistrement.
- Traiter le numéro d'enregistrement comme une identité. Les numéros d'enregistrement sont réutilisés. La référence fichier 64 bits (numéro d'enregistrement plus numéro de séquence) est l'identifiant qui n'entre pas en collision. Si votre script regroupe seulement par numéro d'enregistrement, il va silencieusement confondre des prédécesseurs supprimés avec leurs successeurs réutilisateurs.
- Confondre les deux jeux d'horodatages. Chaque enregistrement porte des horodatages dans
$STANDARD_INFORMATION(mis à jour fréquemment) et$FILE_NAME(largement stable). Pour la détection de timestomping, vous avez besoin des deux. Voir les quatre horodatages MFT. - Ne pas gérer les enregistrements d'extension. Un fichier dont les attributs débordent un enregistrement a un
$ATTRIBUTE_LIST(0x20) qui pointe vers des enregistrements d'extension. Beaucoup de scripts naïfs émettent l'enregistrement de base et ratent les attributs qui vivent dans les extensions. libmft s'en occupe ; si vous faites votre propre parcours, ne l'oubliez pas.
Quand sauter Python tout court
Pour une analyse interactive ponctuelle sans aucune installation, déposez la $MFT sur le parser navigateur de ce site. Il fait tourner le même crate omerbenamram/mft compilé en WebAssembly, filtre et cherche côté client, et exporte du CSV. Pas de Python requis.
Lectures complémentaires
omerbenamram/mft. Le parser Rust dont la sortie JSON est consommée par le script ci-dessus.pytsk3. Bindings Python pour The Sleuth Kit ; la manière la plus propre de lire$MFTdirectement depuis une image disque.- David Cowen, Daily Blog et Sunday Funday. Des années de snippets Python de praticiens qui marchent sur des MFTs du monde réel.