La mayor parte del Python en una workstation forense es pegamento. Adquieres artefactos con herramientas nativas, los parseas con bibliotecas que escribió otro, y usas Python para coser los resultados en informes y líneas temporales. $MFT es uno de esos parsers en los que la tentación de hacerse el propio es alta porque el formato es pequeño y las bibliotecas en PyPI tienen aristas. Resiste la tentación. Hay mejores opciones.
Este es el post práctico de parseo MFT en Python: qué biblioteca cuándo, con el código que realmente uso.
Qué estás leyendo
La Master File Table de NTFS es una secuencia de registros de tamaño fijo de 1.024 bytes. Para parsearla desde Python tienes que:
- Abrir el archivo
$MFT(o leerlo de una imagen de disco). - Avanzar por él de 1.024 en 1.024 bytes.
- Aplicar el array de fixup a cada registro (el mecanismo de detección de escrituras desgarradas; mira el post de la anatomía del registro).
- Recorrer el flujo de atributos dentro de cada registro.
Las bibliotecas siguientes manejan los cuatro pasos. Recurrir a struct.unpack solo merece la pena cuando una biblioteca no expone un campo que necesitas.
Opción 1: analyzeMFT (Python puro, fácil de desplegar)
analyzeMFT es el clásico parser puro de Python, originalmente de David Kovar, aún mantenido. Primero CLI, importable. Lento, pero fiable sobre los registros que entiende.
# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer
analyzer = MFTAnalyzer(mft_file="path/to/$MFT", output_file="out.csv")
analyzer.analyze()
El CSV que produce tiene una fila por registro con marcas de tiempo tanto de $STANDARD_INFORMATION como de $FILE_NAME. Suficiente para triage en hoja de cálculo cuando la MFT es pequeña.
Úsalo cuando:
- La
$MFTes pequeña (algunos cientos de MB o menos). - Trabajas en un entorno air-gapped solo con Python.
- Quieres un CSV simple sin tocar dependencias nativas.
Sáltatelo cuando:
- Las entradas son de varios GB. analyzeMFT es Python puro mono-hilo. Una MFT de 4 GB puede tardar 20+ minutos que el parser de Rust hace en 30 segundos.
- Quieres escribir lógica que recorra registros programáticamente. El modelo de objetos está orientado a la emisión de CSV, no al análisis.
Opción 2: libmft (modelo de objetos tipado)
Si quieres consultar registros como objetos de Python, libmft expone un modelo tipado cercano a la estructura 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 resuelve las referencias padre para que puedas pedir a cada entrada su ruta completa sin escribir tú la travesía. También maneja los registros de extensión $ATTRIBUTE_LIST de forma transparente, lo cual la capa CSV de analyzeMFT te oculta.
Úsalo cuando:
- Quieres escribir lógica que recorra registros, filtre por atributo y emita una forma personalizada.
- Necesitas acceso al modelo de objetos tipado (descriptores de seguridad, reparse points, runlists) en vez de CSV plano.
Sáltatelo cuando:
- El rendimiento importa. libmft es más rápido que analyzeMFT pero sigue siendo Python puro; espera de 5 a 10 minutos en una MFT de 4 GB.
Opción 3: invocar un parser de Rust
Cuando la MFT es grande o estás procesando muchas unidades, la opción práctica más rápida es invocar omerbenamram/mft_dump y leer su salida JSON Lines.
import json
import subprocess
# omerbenamram/mft — `cargo install mft` o descarga 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 limpio → eliminado
print(record["entry"], record["file_name"]["name"])
mft_dump emite un registro por línea, lo que streama limpiamente hacia Python sin cargar toda la salida en memoria. Comparado con analyzeMFT sobre la misma entrada, el parser de Rust es típicamente de 10 a 50 veces más rápido y usa una décima parte de la memoria.
Úsalo cuando:
- Pipelines de producción.
- Entradas grandes.
- Donde el tiempo de parseo importa.
El único pero: dependes de que el binario esté instalado. Fija una versión, distribúyela junto a tu tooling y documenta la instalación en tu runbook.
Leer $MFT directamente de una imagen de disco
Si tienes una imagen cruda .dd o .E01 en vez de un archivo $MFT extraído, usa pytsk3 (bindings de Python para The Sleuth Kit) para saltar a $MFT en el volumen y stream sus bytes:
import pytsk3
img = pytsk3.Img_Info("disk.dd")
fs = pytsk3.FS_Info(img, offset=0) # usa el offset de la partición NTFS
mft_file = fs.open_meta(inode=0) # $MFT siempre es el inodo 0
size = mft_file.info.meta.size
data = mft_file.read_random(0, size)
# data ahora contiene $MFT; pásalo a libmft o escríbelo a disco
Es el enfoque más limpio cuando el volumen está cifrado a nivel de partición pero montado vía un descifrador que te da una imagen cruda. También es la herramienta correcta cuando la imagen contiene instantáneas VSS y quieres extraer $MFT de cada una. Combínalo con libvshadow para la enumeración de snapshots.
Un script corto que tengo guardado
Aproximadamente el script al que recurro primero cuando miro una MFT desconocida. Encuentra registros eliminados con datos residentes y vuelca su contenido.
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 uso
for attr in rec.get("attributes", []):
if attr["header"]["type_code"] != 0x80:
continue # no es $DATA
if not attr["header"]["is_resident"]:
continue # los datos viven en otro sitio
# Residente, eliminado, con $DATA inline. El caso interesante.
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)}")
# Escribir a un archivo con nombre por número de registro para revisión.
with open(f"deleted_resident_{rec['entry']}.bin", "wb") as f:
f.write(data)
Ese único script ha sacado a la luz, a lo largo de las investigaciones, suficientes scripts eliminados, configs y droppers de una línea como para justificarse muchas veces. Los datos residentes viven en registros MFT que la gente nunca se acuerda de comprobar. Mira datos residentes para lo que cabe.
Trampas comunes
- Olvidar el array de fixup. Leer trozos crudos de 1.024 bytes sin aplicar el USA te da basura en los offsets 510 y 1022 de cada registro. Las bibliotecas de arriba lo hacen por ti. Hazte tu propio parser solo si entiendes el mecanismo de fixup en el post de la anatomía del registro.
- Tratar el número de registro como identidad. Los números de registro se reutilizan. La referencia de archivo de 64 bits (número de registro más número de secuencia) es el identificador que no colisiona. Si tu script agrupa solo por número de registro, va a confundir silenciosamente predecesores eliminados con sus sucesores reutilizadores.
- Confundir los dos conjuntos de marcas de tiempo. Cada registro lleva marcas en
$STANDARD_INFORMATION(actualizadas con frecuencia) y$FILE_NAME(en su mayoría estables). Para detección de timestomping necesitas ambas. Mira las cuatro marcas de tiempo de la MFT. - No manejar los registros de extensión. Un archivo cuyos atributos desbordan un registro tiene un
$ATTRIBUTE_LIST(0x20) que apunta a registros de extensión. Muchos scripts ingenuos emiten el registro base y pierden atributos que viven en las extensiones. libmft maneja esto; si haces tu propio recorrido, no lo olvides.
Cuándo saltarse Python por completo
Para análisis interactivo puntual sin instalar nada, suelta la $MFT en el parser en navegador de este sitio. Corre el mismo crate omerbenamram/mft compilado a WebAssembly, filtra y busca del lado del cliente y exporta CSV. Sin Python.
Lecturas adicionales
omerbenamram/mft. El parser de Rust cuya salida JSON consume el script de arriba.pytsk3. Bindings de Python para The Sleuth Kit; la forma más limpia de leer$MFTdirectamente de una imagen de disco.- David Cowen, Daily Blog y Sunday Funday. Años de snippets de Python de profesionales que funcionan sobre MFTs reales.