NTFS 结构与 MFT 解析

Windows 默认文件系统,真实案件里出现频率最高的就是它。最突出的设计特征是把每个文件或目录的全部元数据集中存放在一张固定大小的表里——$MFT(Master File Table),每条记录 1024 字节。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:NTFS、MFT、Fixup、Update Sequence Array、$FILE_NAME、$STANDARD_INFORMATION、fls、istat 难度:进阶 前置知识:磁盘分区结构、十六进制分析基础、Sleuth Kit 基本使用 相关文章:文件系统取证总览、只读挂载与安全接入


一、概述

Windows 默认文件系统,真实案件里出现频率最高的就是它。最突出的设计特征是把每个文件或目录的全部元数据集中存放在一张固定大小的表里——$MFT(Master File Table),每条记录 1024 字节。

破案要的字段基本都在这张表里。文件名、四个时间戳、大小、位置、父目录引用、权限,全在一个固定偏移处的定长记录里,不用像 ext 系那样跨多个分散结构拼装。$MFT 自身的起点由 Boot sector 偏移 0x30 的 MFT Cluster Number 指定,字节偏移 = 簇号 × 簇大小。

同一份设计也是攻击面:篡改 $MFT 就能让操作系统"看不见"文件,而文件内容依然在磁盘上。这就是 NTFS 取证的核心张力——你得先能读懂这张表,才谈得上判断它有没有在骗你。

判读 MFT 落在分析环节的元数据判读阶段,也就是文件系统取证总览判定完"这是 NTFS"之后的第一步。上游是卷层判定与接入;下游三路:元数据层面的时间戳异常走向反取证手法与时间戳篡改检测,属性层之外的隐藏数据走向NTFS 数据流与隐藏数据,从现状追到过程走向Steady-State 与 LogFile 还原。

fls / istat / icat 的通用用法在只读挂载与安全接入讲过,这里聚焦的是每个输出字段意味着什么、哪个字段能定案。差异其实很小:同一个 istat 输出里,"Changed 晚于 Modified 3 秒"这句话是一句废话还是一条线索,取决于你懂不懂 2.6 与 $STANDARD_INFORMATION 的更新规则。

落到具体证据上:

  • $MFT 的位置与大小 —— Boot sector 偏移 0x30 的簇号 × 簇大小。第五章案例:簇号 0x40000、簇大小 4096、起始扇区 526336。该值随簇大小变化,不要硬编码
  • $MFT 记录 0–11 的固定身份 —— 0 是 $MFT 自身,1 是 $MFTMirr(主表前 4 条记录的备份,常被漏检),2 是 $LogFile,3 是 $Volume,4 是 $AttrDef,5 是根目录 .(文件名就是单个点),6 是 $Bitmap,9 是 $Secure,11 是 $Extend
  • FILE / BAAD / INDX 三种魔数 —— FILE 是正常基记录;BAAD = Fixup 校验失败后被标记,是人为篡改的强信号;INDX 是索引缓冲区,不是 MFT 记录,容易误判
  • $STANDARD_INFORMATION(0x10) —— 4 个时间戳 + 文件属性标志 + 硬链接数。取证价值 ★★★★★
  • $FILE_NAME(0x30) —— 文件名 + 父目录 MFT 引用,路径还原的核心。一条记录里可能有两个(Win32 长名 + DOS 8.3 短名),短名的存在与否是 NtfsDisable8dot3NameCreation 的弱指标
  • $DATA(0x80) —— 驻留(<700 字节内嵌)与非驻留(独立簇链 + runs)。删文件后 MFT 里的属性描述仍在,只是数据簇被标记空闲
  • $ATTRIBUTE_LIST(0x20) —— 已知反取证手法用它把 $DATA 挪到别处。只读第一条记录而不解析属性列表,会漏掉真实数据
  • MFT 引用的 48 位结构 —— 低 48 位是记录号,高 16 位是序列号。第五章案例里 fls -p 输出的是 11443-128-3,那个 128 就是序列号。只写"条目 11443"无法区分原文件与三个月后复用该条目的新文件
  • Fixup(Update Sequence Array) —— USA offset 通常 0x30,USA count = 记录长/扇区大小 + 1。读取时必须先还原,否则属性结构可能被邻接数据污染,读出错误的文件大小与偏移

一张 MFT 记录能回答的是这些:记录里有哪些字段、每个字段的取证用途、$FILE_NAME 怎么向上追溯成完整路径、Fixup 为什么必须在解析最前面处理,以及怎样用一个 8 字节 MFT 引用把结论表述得可复核。

最后一条是最实用的产出习惯。条目被删除后可能被新文件复用,所以取证引用必须写成"记录号-序列号-世代",否则报告里的坐标区分不了两个不同的文件。第五章案例通篇用 11443-128-3 这种形式引用,不是格式洁癖——它让复核者能原样重跑你的查询。

有几件事不归这一层管:命名流(ADS)、$I30、slack 与未分配空间在NTFS 数据流与隐藏数据;从现状追到元数据变化过程靠 $LogFile 的 LSN 与日志回放,见Steady-State 与 LogFile 还原;这个时间戳可不可信——MFT 记录只告诉你它写了什么,不告诉你它有没有被改过,检出能力在反取证手法与时间戳篡改检测;文件雕刻则是簇链断了的情况,走另一条路。

往下读之前需要三样前置:分区与卷层概念(fls 的 -o 2048 是什么)、十六进制与位运算(MFT 记录头那一串偏移长度表要自己核),以及 Sleuth Kit 命令行的基本使用(这里不复述 fls / istat / icat 的通用语法)。

建议动手做一次手工核对:按第三章步骤 1 算出 MFT 起始扇区,dd 出一个记录,用 xxd 验证魔数是 FILE、USA offset 是 0x30、USA count 是 3。第五章案例第 1 步就是这个动作。亲手验证过之后,2.2 那张两段结构表你就再也不会把"属性序列的起点"和"USA 数组的位置"搞混——这两者是 MFT 记录里最容易读错的两块。

二、核心原理

2.1 $MFT 的定位

项目 值
主表位置 Boot sector 的 MFT Cluster Number 字段(偏移 0x30)指定
记录大小 1024 字节(一个簇;现代实现未启用 4096 字节 MFT 记录)
记录 0 $MFT 自身
记录 1 $MFTMirr(主表前 4 条记录的备份)
记录 2 $LogFile(元数据日志,见 04)
记录 3 $Volume(卷名、NTFS 版本号、dirty flag)
记录 4 $AttrDef(属性定义表)
记录 5 .(根目录,文件名就是单个点)
记录 6 $Bitmap(簇分配位图)
记录 7 $Boot(引导记录,作为文件存在)
记录 8 $BadClus(坏簇映射)
记录 9 $Secure(安全描述符与 ACL)
记录 10 $UpCase(Unicode 大写映射表)
记录 11 $Extend(扩展目录,含 $UsnJrnl / $ObjId / $Quota / $Reparse)
记录 12–15 保留
fsstat -o 2048 /work/DigiForensics.dd | grep -iE 'cluster|mft'
# Cluster Size: 4096              MFT Cluster: 0x40000    MFT Mirror Cluster: 0x40006
# Boot Sector Cluster: 0x8000     Volume Serial Number: 9a3f21c4    Volume Name: OSDisk

换算:MFT 字节偏移 = 簇号 × 簇大小。例中 MFT 起点 = 0x40000 × 4096,相对卷起始(加分区起始扇区才是相对磁盘的偏移)。该值随簇大小变化,不要硬编码。

2.2 MFT 记录的两段结构

每条 1024 字节记录分成头部 + 属性序列两段:

偏移 长度 字段 偏移 长度 字段
0x00 4 魔数 FILE 0x14 2 首个属性偏移
0x04 2 USA offset(通常 0x30) 0x16 2 标志(0x01 在用 / 0x02 目录)
0x06 2 USA count(= 记录长/扇区大小 + 1) 0x18 4 已用字节数
0x08 8 $LogFile 日志序列号 LSN 0x1C 4 已分配字节数(8 字节对齐)
0x10 2 序列号 Sequence Number 0x20 8 基础文件引用
0x12 2 硬链接计数 0x28 2 下一属性 ID
USA 数组 USA offset USA count × 2 属性序列 首个属性偏移 → 0x2FF 见 2.3 / 2.6

记录类型由魔数区分:

魔数 类型 含义
FILE 基记录 文件/目录的完整属性都在这里
BAAD 损坏记录 Fixup 校验失败后被标记(人为篡改的强信号)
0 填充 空闲 该 MFT 条目从未使用或已释放
INDX 索引记录 不是 MFT 记录,是索引缓冲区(见 03)

2.3 属性列表(Attribute List)

每条 MFT 记录内含若干属性(attribute),最常见四个:

属性类型 ID 作用 取证价值
$STANDARD_INFORMATION 0x10 4 个时间戳、文件属性标志、硬链接数 ★★★★★ 时间线核心
$FILE_NAME 0x30 文件名 + 父目录 MFT 引用 ★★★★★ 路径还原核心
$DATA 0x80 文件内容(或目录的 $I30 索引) ★★★★★ 内容提取
$ATTRIBUTE_LIST 0x20 记录"本条记录装不下,属性在别处" ★★★ 属性可拆分藏匿

属性通用头结构:

0x00 4  属性类型(0x10/0x30/0x80...)      0x0A 2  名称偏移
0x04 4  属性长度(8 字节对齐)              0x0C 2  标志(0x0001压缩/0x4000加密/0x8000稀疏)
0x08 1  非驻留标志(0=驻留 1=非驻留)       0x0E 2  属性 ID
0x09 1  名称长度(0 = 匿名流)              0x10+ 值长度(驻留) / VCN+数据偏移(非驻留)

驻留与非驻留是重要分界:驻留 = 小数据(<700 字节)直接内嵌在 MFT 记录里;非驻留 = 存在独立簇链上,MFT 只存起始 VCN 与映射信息。

删掉文件后 MFT 里的属性描述仍在,只是数据簇被标记为空闲——这是文件雕刻之外的另一条恢复路径基础。** $ATTRIBUTE_LIST 则让属性可分散在多条记录中**:攻击者用它把 $DATA 挪到别处是已知反取证手法,只读第一条记录而不解析属性列表会漏掉真实数据。

2.4 MFT 引用:48 位的"记录号+序列号"结构

$FILE_NAME 属性里有一个 8 字节的父目录引用。MFT 引用共 64 位,但只有低 48 位有效:

位 宽度 含义
0–47 6 字节 MFT 记录号(低位在前)
48–63 2 字节 序列号(Sequence Number,同一条目被重用后的代数)

为什么序列号重要:MFT 条目被删除后可能被新文件复用——此时记录号相同但序列号递增。取证引用若只写"条目 11443",分不出"原来的文件"和"三个月后占用同一条目的新文件"。

⚠️ TSK 元数据地址不是这个结构:Sleuth Kit 输出的 11443-128-3 是 MFT 条目号 - 属性类型码 - 属性 ID,其中 128 = 0x80 = $DATA 的属性类型码,3 是该属性在记录内的实例 ID。中间那个 128 与 MFT 序列号毫无关系——把它读成"条目被复用 128 次"是常见误判。真正的序列号要看 istat 输出里的 Sequence: 行。

多层引用怎么写:一个文件可能有多个 $DATA(主数据流 + 多个 ADS),这时必须补上属性 ID 才能唯一定位;只有一个同名类型属性时写 条目-类型 即可。

TSK 会为跨 MFT 记录的属性重新分配唯一 ID,所以报告里直接引用 istat 打印的那串地址最稳妥。

2.5 $FILE_NAME 还原完整路径

$FILE_NAME 属性(0x30)结构:

偏移  长度  字段
0x00  8   父目录 MFT 引用(48 位有效,见 2.4)
0x08  24  创建 / 修改 / MFT 变更 三个 FILETIME
0x20  8   访问时间 FILETIME
0x28  4   分配大小 Allocated Size      0x2C 4  真实大小 Real Size
0x30  4   标志(1=目录 2=压缩 4=隐藏 8=系统 16=临时)
0x34  4   重解析点值                   0x38 1  名称长度(字符数)
0x39  1   名称类型(1=POSIX 2=Win32 3=DOS 4=Win32&DOS)    0x3A n  UTF-16LE 文件名

一条 MFT 记录里可能同时存在两个 $FILE_NAME:一个 Win32 长名,一个 DOS 8.3 短名(TXT~1.TXT)。短名存在与否是 8.3 名字生成功能开关(NtfsDisable8dot3NameCreation)留下的痕迹——是"该卷是否被刻意配置过"的弱指标,但不能单独定性。

路径还原算法:取当前记录的 $FILE_NAME 父目录引用 → 跳到父记录取它的 $FILE_NAME → 直到条目 5(根目录 .)→ 反向拼接。TSK 的 fls -p 已内部完成;自己解析时这就是全部工作。

2.6 Fixup(Update Sequence Array)——NTFS 的最后一道校验

问题:MFT 记录跨多个扇区。若某扇区是坏道或写入时中断,该扇区里文件的其他数据就会破坏此记录的结构。Fixup 机制为此设计:

  1. 写入记录时,把每扇区最后 2 字节的原始值备份到记录尾部的 Update Sequence Array(USA);
  2. 写入后,这些最后 2 字节被改写为 USA 数组的索引值(0、1、2、3…);
  3. 读取时:读出 USA → 校验各扇区末 2 字节是否等于预期索引 → 用 USA 备份值还原 → 结构仍不合法则标记为 BAAD。
0x00  FILE
0x04  USA Offset = 0x30
0x06  USA Count = 3        (= 1024/512 扇区 + 1 个数组项)
记录中部:每 512 字节末 2 字节被写成 0x0000 / 0x0001 / 0x0002
0x30  00 00  01 00  XX XX  YY YY   ← USA 数组:索引值 + 各扇区原始末 2 字节

取证意义(三点,都很重要):

视角 含义
为什么解析器必须先处理 Fixup 不还原则属性结构可能被邻接数据污染,读出错误的文件大小与偏移
BAAD 魔数 = 强信号 正常磁盘故障极少产生 BAAD;大量 BAAD 记录指向人为篡改或定向破坏
Fixup 差异是残留痕迹 若某记录的 USA 索引值与扇区位置不匹配(不连续/乱序),说明有人手工改写过扇区末 2 字节

多数成熟解析器(TSK 等)自动完成 Fixup 处理,你不会看到原始 USA。但当解析结果"某个文件的 $DATA 长度完全不合理"时,手工 dump 原始 MFT 记录、验证 USA 一致性是必要的取证动作:

# MFT 起始扇区 = 分区起始 + MFT簇号 × 簇大小 / 512 = 2048 + 0x40000*4096/512
dd if=/work/DigiForensics.dd bs=512 skip=526336 count=2 status=none | xxd -l 64
# 期望:4649 4C45("FILE")… 3000 0300(USA offset=0x30, count=3)

三、操作步骤

3.1 第 1 步:定位 MFT 并提取

# ① 从超级块拿到 MFT 簇号与簇大小(不假定簇大小,务必实测)
fsstat -o 2048 /work/DigiForensics.dd | grep -iE 'cluster|mft|serial'

# ② 按簇号换算字节偏移,提取前若干条 MFT 记录(记录 0 = $MFT,记录 5 = 根目录)
dd if=/work/DigiForensics.dd bs=4096 skip=$((0x40000)) count=8 of=/tmp/mft-head.bin status=none

# ③ 确认魔数:xxd -l 4 /tmp/mft-head.bin  → 46 49 4C 45 = "FILE"

3.2 第 2 步:导出全量元数据

fls -o 2048 -r -p /work/DigiForensics.dd > /evidence/fls-all.txt
grep -i deleted /evidence/fls-all.txt                        # 已删除项
fls -o 2048 -r -p /work/DigiForensics.dd | grep -E '\$[A-Za-z]'   # 系统元数据文件

3.3 第 3 步:逐文件读元数据(istat 核心用法)

istat 是本篇的主力工具。关键在于读懂它的每一段输出:

istat -o 2048 /work/DigiForensics.dd 11443-128-3

输出结构与解读(-o 为起始扇区,inode 用 fls 输出的 MFT-序列-属性 形式):

Class: 1
Type: Regular        ← 1=文件 2=目录 3=特殊(设备/FIFO等)
Size: 24576
MFT Cluster: 262144  ← 所在 MFT 记录号
Created: 2024-03-11 09:22:41     ← $STANDARD_INFORMATION
Modified: 2024-06-02 14:07:19
Accessed: 2024-06-02 14:33:55
Changed: 2024-06-02 14:07:22     ← MFT 记录自身的修改时间(FSN)

Attributes:
0x10 $STANDARD_INFORMATION  72 bytes
0x30 $FILE_NAME             104 bytes  Name: report.docx
0x80 $DATA                   24576 bytes  Resident: NO   Runs: (fc,1) (fd,1) ...
字段 定性价值 使用建议
Type 确认是文件还是目录 1=Regular 2=Directory;异常类型需解释
Created / Modified / Accessed / Changed 时间线四要素 Changed 与 Modified 不一致 = 元数据被改过
File Name 属性名 真实文件名(含短名) 与 fls 显示名不符时以 $FILE_NAME 为准
$DATA Resident 小文件内嵌在 MFT Resident: YES 意味着内容直接在 MFT 里
Runs(簇链) 数据物理位置 稀疏/压缩文件会出现异常跳变

重要判读:Changed 时间(FSN,"File Sequence Number" 变更时间)只在 MFT 记录本身被改写时更新——重命名、改权限、写入 ADS 都会更新它,但不会在普通内容修改时更新。Modified 变而 Changed 不变 = 内容被改;Changed 变而 Modified 不变 = 元数据被改。 这对判定"文件被重命名过/权限被篡改过"极有用。

3.4 第 4 步:提取文件内容并验证