取证工作站上的多数 Python 都是胶水。你用原生工具采集工件,用别人写好的库解析,再用 Python 把结果拼成报告与时间线。$MFT 是几个最容易让人手痒自己造轮子的解析器之一,因为格式不大而 PyPI 上的库又粗糙。请抵制这种冲动。还有更好的选项。
这就是关于 Python 解析 MFT 的实务文章:何时用哪个库,以及我实际用的代码。
你在读什么
NTFS 的 Master File Table 是一连串固定大小 1,024 字节的记录。要在 Python 里解析它,你必须:
- 打开
$MFT文件(或从磁盘镜像中读取)。 - 每次步进 1,024 字节。
- 对每条记录应用 fixup 数组(torn write 检测机制;见 记录解剖一文)。
- 遍历每条记录内部的属性流。
下面这些库都处理了这四步。只有当某个库没有暴露你需要的字段时,回退到 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_LIST(0x20)指向扩展记录。许多朴素脚本只输出基础记录,漏掉了住在扩展里的属性。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 片段。