Master File Table 中的每个条目都是 1024 字节,布局方式始终一致:固定头部、一小段修复结构,随后是一段类型化的属性流。本文是对这一布局的近距离观察 —— 当你在十六进制编辑器中阅读原始 $MFT 字节,或者自己写解析器时所需要的细节。
FILE 签名
每条记录的前四个字节是 ASCII 字符串 FILE(46 49 4C 45)。解析器把它当作扫描原始卷以寻找孤立记录的锚点:任何位于 1024 字节边界、以 FILE 开头的位置,都是一条 MFT 记录 —— 即便表本身的头部已不可读。
如果一条记录的内容是 chkdsk 无法修复的,它会改为承载 BAAD(42 41 41 44)签名。NTFS 会把这类记录保留在原位而不擦除 —— 这样可以保留该槽的序列号 —— 但其余字节不应被信任。
记录头部
签名之后的 56 字节描述了这是哪种记录,以及如何安全地读取它:
- 更新序列数组的偏移(2 字节)与 数组以 16 位字为单位的大小(2 字节)。两者共同描述 fixup 数据;详见下文。
$LogFile序列号(8 字节)。回指事务日志,用于崩溃恢复。- 序列号(2 字节)。每次该记录槽被重新使用时递增。
- 硬链接计数(2 字节)。每个
$FILE_NAME属性对应一条。 - 第一个属性的偏移(2 字节)。
- 标志位(2 字节)。bit 0 是
IN_USE;bit 1 表示目录。 - 已用大小(4 字节)与 已分配大小(4 字节)。在标准卷上,已分配大小始终为 1024。
- 基础文件记录引用(8 字节)。在 扩展记录 上为非零 —— 当一个文件的属性超出单条记录时,会使用额外的槽。
- 下一个属性 ID(2 字节)。用于给新属性分配唯一 ID。
- 记录号(4 字节,NTFS 3.1+)。自引用,便于对解析结果做正确性校验。
序列号是让已删除文件分析变得可信的关键。仅凭记录号是会被重用的;由记录号加序列号构成的 64 位 file reference 才不会。如果某个引用与对应记录号处的当前序列号不一致,那就是指向上一任占用者的指针 —— 通常是一个已删除的文件。
fixup 数组
NTFS 通过一个名为 更新序列数组(USA),或称 fixup 数组 的小技巧来检测撕裂写入。每条 1024 字节的记录也被划分为两个 512 字节的扇区。NTFS 会:
- 选定一个 16 位的 update sequence number(USN —— 与
$UsnJrnl的 USN 同名但无关)。 - 在写入前,把 每个 512 字节扇区的最后两字节 替换为该 USN。
- 按顺序把每个扇区原本的最后两字节存放到 fixup 数组里,紧接在 USN 自身之后。
读取时,NTFS 会检查每个扇区的最后两字节是否与 USN 相符。如果相符,说明这些扇区是一起被写入的;原始字节会从 fixup 数组中取出并放回原位。如果有任何扇区尾部不匹配,说明写入被撕裂,该记录就值得怀疑。
这意味着,如果一个解析器直接逐字节读取 $MFT 而不应用 fixup,就会在每条记录的偏移 510 和 1022 处得到乱码。任何严肃的 MFT 解析器都会在定位到一条记录后,把 fixup 作为第一步。
属性流
头部(以及 fixup 数组)之后,就是该文件的各项属性。每个属性都有自己的简短头部 —— 类型码、长度、resident/non-resident 标志、可选名称 —— 后面跟着属性数据。属性以 8 字节对齐,首尾相接地排列,并以一个 0xFFFFFFFF 的哨兵作结。
一条最小的记录会携带三个属性:
$STANDARD_INFORMATION(0x10) —— 时间戳与 DOS 标志。$FILE_NAME(0x30) —— 名称、父目录引用,以及第二组时间戳。$DATA(0x80) —— 文件内容,小文件直接内联常驻,否则是一组簇运行。
记录还可以携带更多属性,包括当文件属性超出单个 1024 字节槽时使用的 $ATTRIBUTE_LIST。完整列表见 Master File Table 参考。
这对取证为何重要
稳定的 1024 字节布局加上 FILE 签名,正是让已删除 NTFS 文件能够被「雕刻」恢复(carving)的基础。即便 $MFT 自身已不存在,只要对原始卷扫描 FILE 头,大多数记录都能被恢复 —— 包括已删除条目的名称、时间戳,以及(对于小文件)其数据。fixup 机制意味着,只要 USA 校验通过,这些被雕刻出来的记录就可以被信任。