← Torna al blog

Parsare $MFT da Python senza perderci il weekend

· 5 min di lettura

La maggior parte del Python su una workstation forense è colla. Acquisisci artefatti con strumenti nativi, li parsi con librerie scritte da qualcun altro, e usi Python per cucire i risultati in report e timeline. $MFT è uno dei parser dove la tentazione di farsene uno proprio è massima perché il formato è piccolo e le librerie su PyPI hanno spigolature. Resisti alla tentazione. Ci sono opzioni migliori.

Questo è il post pratico di parsing MFT in Python: quale libreria quando, con il codice che uso davvero.

Cosa stai leggendo

La Master File Table di NTFS è una sequenza di record di dimensione fissa da 1.024 byte. Per parsarla da Python devi:

  1. Aprire il file $MFT (o leggerlo da un'immagine disco).
  2. Avanzare 1.024 byte alla volta.
  3. Applicare l'array di fixup a ogni record (il meccanismo di rilevamento di scritture lacerate; vedi il post sull'anatomia del record).
  4. Percorrere il flusso degli attributi dentro ogni record.

Le librerie sotto gestiscono tutti e quattro i passaggi. Ricadere su struct.unpack conviene solo quando una libreria non espone un campo che ti serve.

Opzione 1: analyzeMFT (Python puro, facile da distribuire)

analyzeMFT è il classico parser puro Python, originariamente di David Kovar, ancora mantenuto. Prima CLI, importabile. Lento, ma affidabile sui record che capisce.

# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer

analyzer = MFTAnalyzer(mft_file="path/to/$MFT", output_file="out.csv")
analyzer.analyze()

Il CSV che produce ha una riga per record con timestamp sia da $STANDARD_INFORMATION che da $FILE_NAME. Abbastanza buono per triage guidato da foglio di calcolo quando la MFT è piccola.

Usalo quando:

  • La $MFT è piccola (qualche centinaio di MB o meno).
  • Stai lavorando in un ambiente air-gapped solo Python.
  • Vuoi un CSV semplice senza toccare dipendenze native.

Salta quando:

  • Gli input sono multi-GB. analyzeMFT è Python puro single-thread. Una MFT da 4 GB può prendere 20+ minuti che il parser Rust fa in 30 secondi.
  • Vuoi scrivere logica che percorra i record programmaticamente. Il modello a oggetti è orientato all'emissione CSV, non all'analisi.

Opzione 2: libmft (modello a oggetti tipato)

Se vuoi interrogare i record come oggetti Python, libmft espone un modello tipato vicino alla struttura 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 risolve i riferimenti al padre così puoi chiedere a ogni voce il suo percorso completo senza scrivere tu stesso il traversamento. Gestisce anche in modo trasparente i record di estensione $ATTRIBUTE_LIST, che il layer CSV di analyzeMFT ti nasconde.

Usalo quando:

  • Vuoi scrivere logica che percorra i record, filtri per attributo ed emetta una forma personalizzata.
  • Hai bisogno di accedere al modello a oggetti tipato (descrittori di sicurezza, reparse point, runlist) piuttosto che a un CSV piatto.

Salta quando:

  • La performance conta. libmft è più veloce di analyzeMFT ma è ancora puro Python; aspettati 5-10 minuti su una MFT da 4 GB.

Opzione 3: richiamare un parser Rust

Quando la MFT è grande o stai elaborando in batch molti dischi, l'opzione pratica più veloce è richiamare omerbenamram/mft_dump e leggere il suo output JSON Lines.

import json
import subprocess

# omerbenamram/mft — `cargo install mft` o scarica un binario 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 azzerato → eliminato
        print(record["entry"], record["file_name"]["name"])

mft_dump emette un record per riga, che fluisce pulito in Python senza caricare l'intero output in memoria. Confrontato con analyzeMFT sullo stesso input, il parser Rust è tipicamente 10-50× più veloce e usa un decimo della memoria.

Usalo quando:

  • Pipeline di produzione.
  • Input grandi.
  • Ovunque conti il tempo di parsing.

L'unico inghippo: dipendi dal binario installato. Fissa una versione, distribuiscila accanto al tuo tooling e documenta l'installazione nel tuo runbook.

Leggere $MFT direttamente da un'immagine disco

Se hai un'immagine grezza .dd o .E01 invece di un file $MFT estratto, usa pytsk3 (binding Python per The Sleuth Kit) per posizionarti su $MFT nel volume e streammare i suoi byte:

import pytsk3

img = pytsk3.Img_Info("disk.dd")
fs = pytsk3.FS_Info(img, offset=0)  # usa l'offset della partizione NTFS
mft_file = fs.open_meta(inode=0)    # $MFT è sempre l'inode 0
size = mft_file.info.meta.size
data = mft_file.read_random(0, size)
# data ora contiene $MFT; passalo a libmft o scrivilo su disco

È l'approccio più pulito quando il volume è cifrato a livello di partizione ma montato tramite un decifratore che ti dà un'immagine grezza. È anche lo strumento giusto quando l'immagine contiene snapshot VSS e vuoi estrarre $MFT da ciascuno. Combina con libvshadow per l'enumerazione degli snapshot.

Un breve script che tengo a portata

Più o meno lo script a cui ricorro per primo guardando una MFT sconosciuta. Trova i record eliminati con dati residenti e ne scarica il contenuto.

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 uso
    for attr in rec.get("attributes", []):
        if attr["header"]["type_code"] != 0x80:
            continue  # non $DATA
        if not attr["header"]["is_resident"]:
            continue  # i dati vivono altrove
        # Residente, eliminato, ha $DATA inline. Il caso interessante.
        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)}")
        # Scrivi su un file nominato per numero di record per revisione.
        with open(f"deleted_resident_{rec['entry']}.bin", "wb") as f:
            f.write(data)

Quel singolo script ha fatto emergere, nelle indagini, abbastanza script eliminati, config e dropper in una riga da giustificarsi molte volte. I dati residenti stanno in record MFT a cui nessuno pensa di guardare. Vedi dati residenti per cosa ci sta.

Trappole comuni

  • Dimenticare l'array di fixup. Leggere blocchi grezzi da 1.024 byte senza applicare l'USA ti dà spazzatura agli offset 510 e 1022 di ogni record. Le librerie sopra lo fanno per te. Crea il tuo parser solo se capisci il meccanismo di fixup nel post sull'anatomia del record.
  • Trattare il numero di record come identità. I numeri di record vengono riusati. Il riferimento file a 64 bit (numero di record più numero di sequenza) è l'identificatore che non collide. Se il tuo script raggruppa solo per numero di record, confonderà silenziosamente predecessori eliminati con i loro successori riusatori.
  • Confondere i due set di timestamp. Ogni record porta timestamp in $STANDARD_INFORMATION (aggiornati di frequente) e $FILE_NAME (per lo più stabili). Per il rilevamento del timestomping ti servono entrambi. Vedi i quattro timestamp MFT.
  • Non gestire i record di estensione. Un file i cui attributi traboccano da un record ha un $ATTRIBUTE_LIST (0x20) che punta a record di estensione. Molti script ingenui emettono il record base e perdono gli attributi che vivono nelle estensioni. libmft gestisce questo; se fai il tuo percorso, non dimenticarlo.

Quando saltare Python del tutto

Per un'analisi interattiva una tantum senza alcuna installazione, trascina la $MFT sul parser nel browser di questo sito. Esegue la stessa crate omerbenamram/mft compilata in WebAssembly, filtra e cerca lato client ed esporta CSV. Niente Python richiesto.

Letture aggiuntive

  • omerbenamram/mft. Il parser Rust il cui output JSON è consumato dallo script sopra.
  • pytsk3. Binding Python per The Sleuth Kit; il modo più pulito di leggere $MFT direttamente da un'immagine disco.
  • David Cowen, Daily Blog e Sunday Funday. Anni di snippet Python di praticanti che funzionano su MFT del mondo reale.

Risorse esterne