← ブログに戻る

Python で $MFT を解析する方法

· 読了 1 分

結論: インストール可能な環境では純 Python の analyzeMFT、型付きオブジェクト モデルが欲しいなら libmft、速度を重視するなら omerbenamram/mft にシェル アウト、というのが基本方針です。純 Python による解析は Rust クレートの約 10〜50 倍遅いですが、1 回限りのスクリプトには十分です。

読むもの

NTFS の Master File Table は 1,024 バイトの固定サイズ レコードの連続です。Python から解析するには、次の手順だけで足ります。

  1. $MFT ファイル(あるいはディスク イメージから読み出したもの)を開きます。
  2. 1,024 バイトずつ進めて辿ります。
  3. 各レコードにフィックスアップ配列を適用します。バイト レベルのレイアウトは レコードの解剖 を参照してください。
  4. 各レコード内の属性ストリームを走査します。

下記のライブラリはこの 4 ステップすべてを処理します。たいていのアナリストは、ライブラリが必要なフィールドを公開していないときにのみ生の struct.unpack にフォールバックします。

選択肢 1: analyzeMFT

analyzeMFT は古典的な純 Python の MFT パーサーで、もともと David Kovar 作、現在もメンテナンスされています。CLI 先行ですが、import 可能です。

# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer

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

生成される CSV はレコードごとに 1 行で、$STANDARD_INFORMATION$FILE_NAME 両方のタイムスタンプを含みます。スプレッドシート ベースのトリアージには十分です。

使うべき時: 小さな $MFT ファイル、その場限りのスクリプト、ネイティブ依存が不可。

制約: 数 GB の入力では遅く(シングルスレッドの純 Python)、オブジェクト モデルはプログラム的な走査よりも CSV 出力向きです。

選択肢 2: libmft(型付きオブジェクト モデル)

レコードを Python オブジェクトとしてクエリしたい場合、libmft はディスク構造に近い型付きモデルを公開します。

# 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 は親参照を解決するため、各エントリにそのフル パスを尋ねれば、自分で走査ロジックを書く必要はありません。$ATTRIBUTE_LIST 拡張レコードも透過的に処理してくれます — analyzeMFT の CSV 層では隠れてしまうものです。

使うべき時: レコードを走査し、属性で絞り込み、独自の形式を出力するロジックを書きたい場合。

選択肢 3: Rust パーサーにシェル アウト

$MFT が大きい(約 1 GB 以上)、または多数のディスクをバッチ処理する場合、現実的に最速なのは Python からネイティブ パーサーにシェル アウトし、その JSON を読むことです。

import json
import subprocess

# omerbenamram/mft — `cargo install mft` またはリリース バイナリをダウンロード
proc = subprocess.run(
    ["mft_dump", "-o", "json", "path/to/$MFT"],
    capture_output=True, check=True,
)
for line in proc.stdout.splitlines():
    record = json.loads(line)
    if record["header"]["flags"] & 0x1 == 0:  # IN_USE がクリア → 削除済み
        print(record["entry"], record["file_name"]["name"])

mft_dump は JSON Lines — 1 レコード 1 行 — を出力するので、出力全体をメモリにロードせず Python にきれいにストリームできます。同じ入力に対して analyzeMFT と比較すると、Rust パーサーは通常 10〜50 倍速く、メモリは 1/10 程度です。

使うべき時: 本番パイプライン、大きな入力、または解析時間が問題になる場面。

ディスク イメージから直接 $MFT を読む

抽出済み $MFT ファイルではなく生の .dd.E01 イメージを持っている場合、pytsk3(The Sleuth Kit の Python バインディング)でボリューム上の $MFT までシークして、そのバイトをストリームできます。

import pytsk3

img = pytsk3.Img_Info("disk.dd")
fs = pytsk3.FS_Info(img, offset=0)  # NTFS パーティションのオフセットを使う
mft_file = fs.open_meta(inode=0)    # $MFT は常に inode 0
size = mft_file.info.meta.size
data = mft_file.read_random(0, size)
# data に $MFT が入った状態。libmft に渡すか、ディスクに書き出す

ボリュームがパーティション レベルで暗号化されているが、生イメージを返す復号器でマウントされている場合に、これが最もきれいなアプローチです。

よくある落とし穴

  • フィックスアップ配列を忘れる。 USA を適用せずに 1,024 バイトの生のチャンクを読むと、各レコードのオフセット 510 と 1022 がゴミになります。上記のどのライブラリもこれを代わりにやってくれます — フィックスアップ機構を理解している場合だけ自前のパーサーを書いてください(レコード解剖の記事 を参照)。
  • レコード番号を ID として扱う。 レコード番号は再利用されます。衝突しない識別子は 64 ビットの ファイル参照(レコード番号 + シーケンス番号)です。
  • 2 組のタイムスタンプを混同する。 すべてのレコードは $STANDARD_INFORMATION(頻繁に更新)と $FILE_NAME(ほぼ安定)の両方にタイムスタンプを持ちます。タイムストンピング検出には両方が必要です — MFT の 4 つのタイムスタンプ を参照してください。

Python を使わない選択肢

何もインストールせず 1 回限りの対話的解析がしたいなら、本サイトの ブラウザ パーサー$MFT をドロップしてください。WebAssembly にコンパイルされた同じ omerbenamram/mft クレートが動き、クライアント側でフィルタと検索を行い、CSV をエクスポートします — Python は不要です。

外部リソース