← 返回博客

用 Python 解析 $MFT 又不至于赔上一个周末

· 阅读 2 分钟

取证工作站上的多数 Python 都是胶水。你用原生工具采集工件,用别人写好的库解析,再用 Python 把结果拼成报告与时间线。$MFT 是几个最容易让人手痒自己造轮子的解析器之一,因为格式不大而 PyPI 上的库又粗糙。请抵制这种冲动。还有更好的选项。

这就是关于 Python 解析 MFT 的实务文章:何时用哪个库,以及我实际用的代码。

你在读什么

NTFS 的 Master File Table 是一连串固定大小 1,024 字节的记录。要在 Python 里解析它,你必须:

  1. 打开 $MFT 文件(或从磁盘镜像中读取)。
  2. 每次步进 1,024 字节。
  3. 对每条记录应用 fixup 数组(torn write 检测机制;见 记录解剖一文)。
  4. 遍历每条记录内部的属性流。

下面这些库都处理了这四步。只有当某个库没有暴露你需要的字段时,回退到 struct.unpack 才值得做。

选项 1:analyzeMFT(纯 Python、易于部署)

analyzeMFT 是经典的纯 Python 解析器,最初由 David Kovar 开发,至今仍在维护。CLI 优先,可作为库导入。慢,但在它能理解的记录上很可靠。

# pip install analyzeMFT
from analyzeMFT.mft_analyzer import MFTAnalyzer

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

它产生的 CSV 每条记录一行,包含来自 $STANDARD_INFORMATION$FILE_NAME 两组的时间戳。在 MFT 较小时用表格驱动地做取证已经够用。

什么时候用它:

  • $MFT 较小(几百 MB 以内)。
  • 你在一个无网络的纯 Python 环境工作。
  • 你想要一个简单的 CSV,且不愿意碰原生依赖。

什么时候跳过它:

  • 输入达到数 GB。analyzeMFT 是单线程纯 Python。一个 4 GB 的 MFT 可能要 20 分钟以上,而 Rust 解析器只要 30 秒。
  • 你想写代码以编程方式走读记录。它的对象模型是面向 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 层会替你藏起来。

什么时候用它:

  • 你想写一段代码走读记录、按属性过滤、并输出自定义形状。
  • 你需要访问类型化对象模型(安全描述符、reparse points、runlists),而不是扁平 CSV。

什么时候跳过它:

  • 性能至关重要。libmft 比 analyzeMFT 快,但仍是纯 Python;4 GB 的 MFT 预计要 5 到 10 分钟。

选项 3:shell out 到 Rust 解析器

当 MFT 较大或者你要批量处理很多盘时,最快的实用选项是 shell out 到 omerbenamram/mft_dump 并读取其 JSON Lines 输出。

import json
import subprocess

# omerbenamram/mft —— `cargo install mft` 或下载一个 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 被清 → 已删除
        print(record["entry"], record["file_name"]["name"])

mft_dump 每行输出一条记录,可以干净地流入 Python,不必把全部输出载入内存。在相同输入上,与 analyzeMFT 比,Rust 解析器通常快 10 到 50 倍,内存使用是其十分之一。

什么时候用它:

  • 生产流水线。
  • 大型输入。
  • 任何解析时间重要的场合。

唯一注意点:你依赖这个二进制存在。固定一个版本,把它和你的工具链一起发布,并在运行手册里记录安装方式。

直接从磁盘镜像读 $MFT

如果你拿到的是原始 .dd.E01 镜像而不是一份已抽取的 $MFT 文件,可以用 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 或写到磁盘

当卷是分区级加密但通过一个能给出原始镜像的解密器挂载时,这是最干净的方式。当镜像里含 VSS 快照、你想从每份快照里抽取 $MFT 时,这也是合适的工具。可以结合 libvshadow 做快照枚举。

我一直随身带的一个小脚本

大致就是我看一份陌生 MFT 时最先伸手用的脚本。它找出带常驻数据的已删除记录,并把其内容转储出来。

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  # 在用
    for attr in rec.get("attributes", []):
        if attr["header"]["type_code"] != 0x80:
            continue  # 不是 $DATA
        if not attr["header"]["is_resident"]:
            continue  # 数据在别处
        # 常驻、已删除、$DATA 内联。有意思的情况。
        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)}")
        # 用记录号命名输出文件,便于复核。
        with open(f"deleted_resident_{rec['entry']}.bin", "wb") as f:
            f.write(data)

就这一个脚本,在不少调查里挖出过足够多的已删除脚本、配置和一行 dropper,足以反复证明自己值得。常驻数据就坐在大家想不到去看的 MFT 记录里。能塞进去的内容见 resident data

常见陷阱

  • 忘了 fixup 数组。 不应用 USA 直接读 1,024 字节块,会让每条记录在偏移 510 与 1022 处变成垃圾。上面这些库会替你处理。只有当你已经在 记录解剖一文 里理解了 fixup 机制,才动手自己写解析器。
  • 把记录号当作身份。 记录号是会被复用的。64 位文件引用(记录号加序列号)才是不会撞的标识。如果你的脚本只按记录号分组,会悄无声息地把已删除的前任与复用它的继任者混在一起。
  • 把两组时间戳搞混。 每条记录都有 $STANDARD_INFORMATION(频繁更新)和 $FILE_NAME(基本稳定)这两组时间戳。检测 timestomping 时两者都要。见 四个 MFT 时间戳
  • 不处理扩展记录。 当一个文件的属性溢出一条记录,它会有 $ATTRIBUTE_LIST0x20)指向扩展记录。许多朴素脚本只输出基础记录,漏掉了住在扩展里的属性。libmft 会替你处理;自己写遍历时不要忘记。

什么时候完全跳过 Python

要做一次性的交互式分析且不打算装任何东西,把 $MFT 拖到本站的 浏览器解析器 上。它运行同样被编译为 WebAssembly 的 omerbenamram/mft crate,在客户端做过滤与搜索,并支持导出 CSV。不需要 Python。

延伸阅读

  • omerbenamram/mft。上面脚本所消费的 JSON 输出的 Rust 解析器。
  • pytsk3。The Sleuth Kit 的 Python 绑定;从磁盘镜像直接读 $MFT 最干净的方式。
  • David Cowen,Daily Blog 与 Sunday Funday。多年针对真实 MFT 的实用 Python 片段。

外部资源