← Voltar ao blog

Analisar $MFT em Python sem perder o fim-de-semana

· 6 min de leitura

Boa parte do Python numa estação forense é cola. Adquire artefactos com ferramentas nativas, analisa-os com bibliotecas que outros escreveram, e usa Python para coser os resultados em relatórios e timelines. O $MFT é um dos parsers em que a tentação de fazer o seu próprio é maior porque o formato é pequeno e as bibliotecas no PyPI têm arestas. Resista à tentação. Há melhores opções.

Este é o post prático de parsing de MFT em Python: que biblioteca quando, com o código que de facto uso.

O que está a ler

A Master File Table do NTFS é uma sequência de registos de tamanho fixo de 1.024 bytes. Para a analisar a partir de Python tem de:

  1. Abrir o ficheiro $MFT (ou lê-lo a partir de uma imagem de disco).
  2. Avançar 1.024 bytes de cada vez.
  3. Aplicar o fixup array a cada registo (o mecanismo de detecção de torn write; veja o post sobre anatomia do registo).
  4. Percorrer o fluxo de atributos dentro de cada registo.

As bibliotecas abaixo tratam dos quatro passos. Recorrer ao struct.unpack só vale a pena quando uma biblioteca não expõe um campo que precisa.

Opção 1: analyzeMFT (Python puro, fácil de implantar)

O analyzeMFT é o clássico parser em Python puro, originalmente de David Kovar, ainda mantido. CLI first, importável. Lento, mas fiável nos registos que percebe.

# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer

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

O CSV que produz tem uma linha por registo com carimbos temporais tanto do $STANDARD_INFORMATION como do $FILE_NAME. Suficiente para triagem em folha de cálculo quando o MFT é pequeno.

Use-o quando:

  • O $MFT é pequeno (algumas centenas de MB ou menos).
  • Está num ambiente air-gapped só com Python.
  • Quer um CSV simples sem tocar em dependências nativas.

Pule-o quando:

  • Os inputs têm muitos GB. O analyzeMFT é Python puro single-thread. Um MFT de 4 GB pode demorar 20+ minutos que o parser Rust faz em 30 segundos.
  • Quer escrever lógica que percorra os registos programaticamente. O modelo de objectos está orientado para emissão de CSV, não para análise.

Opção 2: libmft (modelo de objectos tipado)

Se quiser consultar registos como objectos Python, o libmft expõe um modelo tipado próximo da estrutura em disco.

# 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)

O libmft resolve as referências de pai, por isso pode pedir a cada entrada o caminho completo sem escrever a travessia. Também trata transparentemente os registos de extensão $ATTRIBUTE_LIST, coisa que a camada CSV do analyzeMFT esconde de si.

Use-o quando:

  • Quer escrever lógica que percorra registos, filtre por atributo, e emita uma forma personalizada.
  • Precisa de acesso ao modelo de objectos tipado (descritores de segurança, reparse points, runlists) em vez de CSV plano.

Pule-o quando:

  • O desempenho importa. O libmft é mais rápido que o analyzeMFT mas continua a ser Python puro; espere 5 a 10 minutos num MFT de 4 GB.

Opção 3: shell-out para um parser Rust

Quando o MFT é grande ou está a processar muitos discos em lote, a opção prática mais rápida é fazer shell-out para o omerbenamram/mft_dump e ler a sua saída JSON Lines.

import json
import subprocess

# omerbenamram/mft — `cargo install mft` ou descarregue um binário 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 limpo → eliminado
        print(record["entry"], record["file_name"]["name"])

O mft_dump emite um registo por linha, que faz stream limpo para Python sem carregar a saída completa para memória. Comparado com o analyzeMFT no mesmo input, o parser Rust é tipicamente 10 a 50x mais rápido e usa um décimo da memória.

Use-o quando:

  • Pipelines de produção.
  • Inputs grandes.
  • Em qualquer sítio onde o tempo de parsing importa.

A única ressalva: depende do binário estar instalado. Fixe uma versão, distribua-a junto com a sua tooling, e documente a instalação no runbook.

Ler o $MFT directamente de uma imagem de disco

Se tiver uma imagem .dd ou .E01 em vez de um ficheiro $MFT extraído, use pytsk3 (bindings Python para o Sleuth Kit) para fazer seek até ao $MFT no volume e fazer stream dos seus bytes:

import pytsk3

img = pytsk3.Img_Info("disk.dd")
fs = pytsk3.FS_Info(img, offset=0)  # use o offset da partição NTFS
mft_file = fs.open_meta(inode=0)    # o $MFT é sempre o inode 0
size = mft_file.info.meta.size
data = mft_file.read_random(0, size)
# data agora contém o $MFT; dê-o ao libmft ou escreva-o em disco

Esta é a abordagem mais limpa quando o volume está encriptado ao nível da partição mas está montado via um decryptor que lhe dá uma imagem bruta. É também a ferramenta certa quando a imagem contém snapshots VSS e quer extrair o $MFT de cada um. Combine com libvshadow para a enumeração dos snapshots.

Um script curto que ando sempre comigo

Aproximadamente o script a que recorro primeiro quando olho para um MFT desconhecido. Encontra registos eliminados com dados residentes e despeja o conteúdo deles.

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  # em uso
    for attr in rec.get("attributes", []):
        if attr["header"]["type_code"] != 0x80:
            continue  # não é $DATA
        if not attr["header"]["is_resident"]:
            continue  # os dados vivem noutro sítio
        # Residente, eliminado, com $DATA inline. O 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)}")
        # Escreve para um ficheiro nomeado pelo número do registo para revisão.
        with open(f"deleted_resident_{rec['entry']}.bin", "wb") as f:
            f.write(data)

Esse único script fez aparecer scripts eliminados, configs, e droppers de uma linha em investigações suficientes para se justificar muitas vezes. Os dados residentes ficam em registos do MFT onde as pessoas nem pensam em olhar. Veja resident data para o que cabe.

Armadilhas comuns

  • Esquecer o fixup array. Ler chunks brutos de 1.024 bytes sem aplicar o USA dá-lhe lixo nos offsets 510 e 1022 de cada registo. As bibliotecas acima fazem isto por si. Só faça o seu próprio parser se compreender o mecanismo de fixup no post sobre anatomia do registo.
  • Tratar o número de registo como identidade. Os números de registo são reutilizados. A referência de ficheiro de 64 bits (número de registo mais número de sequência) é o identificador que não colide. Se o seu script agrupar só por número de registo, vai conflagrar silenciosamente predecessores eliminados com os seus sucessores reutilizantes.
  • Confundir os dois conjuntos de carimbos temporais. Cada registo carrega carimbos em $STANDARD_INFORMATION (actualizado com frequência) e $FILE_NAME (maioritariamente estável). Para detecção de timestomping precisa de ambos. Veja os quatro carimbos temporais do MFT.
  • Não tratar registos de extensão. Um ficheiro cujos atributos transbordam um registo tem um $ATTRIBUTE_LIST (0x20) a apontar para registos de extensão. Muitos scripts ingénuos emitem o registo base e perdem atributos que vivem nas extensões. O libmft trata disto; se fizer a sua própria travessia, não se esqueça.

Quando pular o Python inteiramente

Para análise interactiva pontual sem qualquer instalação, largue o $MFT no parser de browser deste site. Corre a mesma crate omerbenamram/mft compilada para WebAssembly, filtra e procura do lado do cliente, e exporta CSV. Sem Python necessário.

Leituras adicionais

  • omerbenamram/mft. O parser Rust cuja saída JSON o script acima consome.
  • pytsk3. Bindings Python para o Sleuth Kit; a forma mais limpa de ler $MFT directamente de uma imagem de disco.
  • David Cowen, Daily Blog e Sunday Funday. Anos de snippets Python práticos que funcionam em MFTs do mundo real.

Recursos externos