一、概述
NTFS 上有一个绝大多数用户不知道、却对取证极其重要的元数据文件:$UsnJrnl。它是 NTFS 的变更日志,默默记录该卷上每一次文件创建、删除、重命名、写入、加解密操作——并且在文件被删除、目录被清空之后,记录依然保留。
这解决了取证中最难的一个问题。文件删掉就没了,MFT 条目被标记为未分配,目录项不复存在,常规手段无从下手。但 USN 日志在另一个数据流中独立记录了「某个 MFT 条目引用在某个时间被创建、被重命名成什么名字、被删除」。把 MFT 条目引用与 USN 记录结合,即使文件实体早已消失,仍能重建出完整的文件操作时间线。
还有一个直接的价值:重命名产生两条记录(旧名 RENAME_OLD_NAME 与新名 RENAME_NEW_NAME,共享同一个 FileReferenceNumber),这让文件名伪装可以被彻底揭露——攻击者把 payload.exe 改成 setup.exe,两条记录都在;按同一引用号排序即得完整改名链,无论文件当前叫什么、甚至已经删除。
日志本身不产出结论,它的价值在于把别的工件串起来。上游依赖 NTFS 结构的理解:FileReferenceNumber 的低 48 位就是 MFT 条目号,这是 USN 与 MFT 之间的唯一桥梁;没有这个对应,USN 只是一堆孤立记录,基础见NTFS 结构与 MFT 解析。
动手之前要先确认三件事:日志是否启用(fsutil usn queryjournal,必须现场确认)、当前解析的是哪个卷(每个 NTFS 卷有独立的日志,系统盘、U 盘、数据盘互不相同)、以及覆盖边界在哪(用 USN 序号判断,不能假设头部是起点)。三者都不是可选项,少了上游那个桥梁,后面全是空转。下游两处同样明确:改名链是文件名伪装的揭露手段;USN 时间线与执行证据合并后,能区分「程序启动过」与「文件被改过」的先后关系。
落盘形态如下:
| 形态 |
位置 |
关键结构 |
| USN 数据流 |
$Extend\$UsnJrnl:$J |
实际数据流;日志本体在 NTFS 的扩展元数据区 |
| 控制流 |
$Extend\$UsnJrnl:$Max、$Min |
分别保存最大分配大小与最小保留大小 |
| 日志标识 |
$UsnJrnl 控制流 |
首个与下一个 USN、最低与最大有效 USN |
| 记录结构 |
USN_RECORD_V2 |
小端序;含记录长度、FileReferenceNumber、ParentFileReferenceNumber、文件名、Reason 位标志 |
| 重命名对 |
两条记录共享 FileReferenceNumber,ParentFileReferenceNumber 不同 |
一条 RENAME_OLD_NAME、一条 RENAME_NEW_NAME |
| 与 MFT 的桥 |
FileReferenceNumber 低 48 位 = MFT 条目号 |
USN → MFT 关联分析的唯一依据 |
| 状态查询 |
fsutil usn queryjournal <盘符>(活系统) |
输出日志标识、首/下一个 USN、最低/最大有效 USN、最大尺寸、分配增量 |
三个判定要点:每个卷独立日志、覆盖后头部不是起点、fsutil 显示日志不存在或已被清除时,任何解析工具的「无输出」都不能当作「无文件操作」的证据。
据此能回答的是:某个 MFT 条目经历过哪些文件操作、每条操作的时间、它的完整改名链(无论当前叫什么、是否已删除)、以及某次删除或创建发生在什么时刻。
回答不了的部分有七条,前三条最常被写漏:
- 只看元数据变更,不记读操作。 文件被读、被打开通常不产生记录(只读访问一般不写日志)。它证明「做过什么操作」,不证明「看过什么内容」。
- 「无输出」不等于「无文件操作」。 日志是环形覆盖的,装满后从头覆写;被
fsutil usn deletejournal 清除过、被组策略禁用、被镜像工具排除的卷都可能没有日志。这个判断必须以 fsutil 的实际输出为准。
- 不能假设头部是起点。 日志达到
$Max 后新记录从最早的位置开始覆盖,因此日志中总会同时存在「较旧但可能仍完整的时间窗」与「刚写入的最新记录」两段,必须结合 USN 序号判断覆盖边界。
- 当前的文件名与状态。 USN 记录的是操作历史,「这个文件现在还在不在」要查 MFT 与目录。
- 操作的主体。 记录里没有用户名,要落到人需结合事件日志与用户归属。
- 数据内容。 它记元数据变更,不记内容写入了什么。
deletejournal 的不可逆性。 清除日志是不可逆操作,一旦做出就无法恢复,涉及该操作时必须先记录日志的现状与 USN 序号区间。
往下读之前先确认你已经掌握:NTFS 的 MFT 结构与条目号概念(这是 USN 与 MFT 关联的前提)、小端序与位运算(USN_RECORD_V2 的 Reason 位标志必须逐位判读)、NTFS 的备用数据流与 $Extend 机制($UsnJrnl:$J 的定位依赖它)、以及 FILETIME 时间基准。同时清楚不可逆操作的纪律:deletejournal 会永久销毁日志,执行前必须完成固定与记录。
二、核心原理
2.1 位置与存储模型
USN 日志位于 NTFS 的扩展元数据流中:
$Extend\$UsnJrnl:$J
$UsnJrnl:$J 就是实际数据流,$Max 与 $Min 是控制流,分别保存最大分配大小与最小保留大小。
每个 NTFS 卷有独立的日志——系统盘、U 盘、数据盘的日志互不相同,分析时必须明确当前解析的是哪个卷。
覆盖规则:当日志达到 $Max 指定的最大尺寸后,新记录从最早的位置开始覆盖。因此日志中总会同时存在「较旧但可能仍完整的时间窗」与「刚刚写入的最新记录」两段,解析时必须结合 USN 序号判断覆盖边界,不能假设头部是起点。
2.2 启用状态必须现场确认
Vista 及之后的 Windows 客户端通常默认启用 USN Journal,但启用状态、最大尺寸、是否被 fsutil usn deletejournal 清除过,都因系统配置、镜像工具、组策略与攻击行为而异。永远以实际检材为准,在活体系统上直接查询:
fsutil usn queryjournal C:
输出包含日志标识、首个与下一个 USN、最低与最大有效 USN、最大尺寸与分配增量。如果该命令显示日志不存在或已被清除,任何解析工具的「无输出」都不能当作「无文件操作」的证据。
2.3 USN_RECORD_V2 记录结构
每条记录的字段布局如下(小端序):
| 偏移 |
长度 |
字段 |
说明 |
| 0 |
4 |
RecordLength |
本记录总长度,用于遍历到下一条 |
| 4 |
2 |
MajorVersion |
主版本号,V2 为 2;不同系统可能见到 3 或 4 |
| 6 |
2 |
MinorVersion |
次版本号 |
| 8 |
8 |
FileReferenceNumber |
64 位文件 ID(低 48 位为 MFT 条目号,高 16 位为序列号) |
| 16 |
8 |
ParentFileReferenceNumber |
父目录的 64 位文件 ID |
| 24 |
8 |
Usn |
更新序列号,卷内单调递增 |
| 32 |
8 |
TimeStamp |
FILETIME(1601 起 100 纳秒) |
| 40 |
4 |
Reason |
变更原因位标志组合 |
| 44 |
4 |
SourceInfo |
来源信息(数据管理、复制等系统侧来源) |
| 48 |
4 |
SecurityId |
安全标识,与登录会话关联 |
| 52 |
4 |
FileAttributes |
文件属性 |
| 56 |
2 |
FileNameLength |
文件名长度(字节) |
| 58 |
2 |
FileNameOffset |
文件名在记录中的偏移 |
| 60 |
可变 |
FileName |
UTF-16LE 文件名 |
MajorVersion 为 2 时结构即上表;版本 3 与 4 在前部增加了额外的 64 位字段(如 n/a 的安全 ID 扩展),字段偏移随之改变。
解析器必须按 MajorVersion 分支处理。用 V2 布局硬解 V3/V4,会得到完全错位的时间与文件名。
2.4 Reason 位标志(实测值)
Reason 是位组合,同一条记录可以同时含多个标志。创建文件通常同时含 FILE_CREATE 与 CLOSE:
| 位值 |
名称 |
含义 |
0x00000001 |
DATA_OVERWRITE |
默认数据流被覆写 |
0x00000002 |
DATA_EXTEND |
默认数据流被追加 |
0x00000004 |
DATA_TRUNCATION |
默认数据流被截断 |
0x00000010 |
NAMED_DATA_OVERWRITE |
命名数据流(ADS)被覆写 |
0x00000020 |
NAMED_DATA_EXTEND |
命名数据流被追加 |
0x00000040 |
NAMED_DATA_TRUNCATION |
命名数据流被截断 |
0x00000100 |
FILE_CREATE |
文件/目录首次创建 |
0x00000200 |
FILE_DELETE |
文件/目录被删除 |
0x00000400 |
EA_CHANGE |
扩展属性变更 |
0x00000800 |
SECURITY_CHANGE |
访问权限变更 |
0x00001000 |
RENAME_OLD_NAME |
重命名,记录中为旧名 |
0x00002000 |
RENAME_NEW_NAME |
重命名,记录中为新名 |
0x00004000 |
INDEXABLE_CHANGE |
索引状态变更 |
0x00008000 |
BASIC_INFO_CHANGE |
属性或时间戳变更 |
0x00010000 |
HARD_LINK_CHANGE |
硬链接增删 |
0x00020000 |
COMPRESSION_CHANGE |
压缩状态变更 |
0x00040000 |
ENCRYPTION_CHANGE |
加密状态变更(BitLocker/EFS 挂载可见) |
0x00080000 |
OBJECT_ID_CHANGE |
对象 ID 变更 |
0x00100000 |
REPARSE_POINT_CHANGE |
重解析点变更 |
0x00200000 |
STREAM_CHANGE |
命名流增删改名 |
0x00400000 |
TRANSACTED_CHANGE |
TxF 事务内的修改,非事务修改不会带此位 |
0x00800000 |
INTEGRITY_CHANGE |
完整性流变更(Windows 8 起的文件级校验元数据) |
0x80000000 |
CLOSE |
文件/目录被关闭 |
其中最具取证价值的是三个组合:
FILE_CREATE + CLOSE:文件被创建。
RENAME_OLD_NAME + RENAME_NEW_NAME:同一 FileReferenceNumber 的两条记录构成一次重命名的完整前后状态。
FILE_DELETE + CLOSE:文件被删除。这是「已删除文件仍可证明存在」的关键。
2.5 重命名产生两条记录
重命名或移动会生成两条 USN 记录,共享同一个 FileReferenceNumber,但 ParentFileReferenceNumber 不同:一条记录旧父目录与旧名(RENAME_OLD_NAME),一条记录新父目录与新名(RENAME_NEW_NAME)。
这个特性使得文件名伪装可以被彻底揭露。
攻击者把 payload.exe 改名为 setup.exe,日志中会同时留下两条记录。把同一 FileReferenceNumber 的记录按时间排序,即可得到完整的改名链,无论文件当前叫什么、甚至已经删除。
2.6 与 MFT 条目引用结合
FileReferenceNumber 的低 48 位就是 MFT 条目号。这意味着 USN 日志可以作为MFT 条目号的索引,对 MFT 做关联分析:
- 从 USN 日志取出所有相关的 64 位文件 ID。
- 在 MFT 中定位对应条目,读取其当前(可能已删除)的时间戳与属性。
- 两者时间线合并,形成「该文件经历过的全部操作」。
这一组合是 MFT 恢复(carving)能否成功的关键前提。只知道文件名而不知道条目号,就无法验证 MFT 中恢复的记录是否真的是那个文件。
三、操作步骤
3.1 提取 $UsnJrnl
$UsnJrnl 是NTFS 元数据文件(位于 $Extend 目录),不会出现在普通目录遍历结果里,必须先用 fls -m 列出元数据文件、拿到它的元数据地址,再用 icat 按地址导出:
mmls /work/DigiForensics.dd
fsstat -o 2048 /work/DigiForensics.dd
关键:-m 列出元数据文件,$UsnJrnl 只在这里出现
fls -o 2048 -m /work/DigiForensics.dd | grep -i 'UsnJrnl'
输出形如:r/r $UsnJrnl 33554432 ... $Extend
即后续 icat 使用的地址,形如 --
用上一步实际读到的地址导出(不要照抄示例中的数字)
ADDR=$(fls -o 2048 -m /work/DigiForensics.dd | grep -i '$UsnJrnl$' | awk '{print $2}' | head -1)
istat -o 2048 /work/DigiForensics.dd "$ADDR" # 确认是 $UsnJrnl 再导出
icat -o 2048 /work/DigiForensics.dd "$ADDR" > /evidence/usn/UsnJrnl.bin
| 注意点 |
说明 |
必须 -m |
普通 fls 不列元数据文件,会漏掉 $UsnJrnl |
| 地址随镜像而变 |
<inod>-<addr>-<seq> 形式由卷与 MFT 状态决定,必须现场读取,不可硬编码 |
不要用 5-128-3 这类照抄的地址 |
那是某一份检材的元数据地址,换镜像即无效 |
| 输出大小 |
TSK 按数据run list 读取,输出是已分配的实际数据,通常远小于 $Max 声明值;不要按 $Max 预留磁盘 |
$J vs $Max/$Min |
日志记录在 $J 流;$Max/$Min 是控制流。icat 取默认流即日志内容,$Max 用于确认日志容量 |
支持数据流语法的工具可用 $UsnJrnl:$J 显式指定日志流、$Max/$Min 指定控制流。不同工具支持程度不同,以所用工具的实际文档为准;使用前务必用 istat 确认目标确实是 $UsnJrnl。
这一步不做,后面读到的可能只是个空壳。
3.2 确认日志状态
在活体系统上先查状态,这一步不能跳:
fsutil usn queryjournal C:
关键看三项:是否启用、最大尺寸、以及「最大有效 USN」与「下一个 USN」的差值——差值大说明有大量新记录尚未或正在写入。同时记录 Usn Journal ID,它是判断日志是否被删除后重建的重要依据。
3.3 解析并导出
用专门的 USN 解析器导出为结构化格式(CSV/TSV/JSON),字段应至少包含文件名、父目录文件 ID、文件名 ID、时间戳、原因位、属性与 USN 序号。
解析后立刻做三件事。
- 检查
MajorVersion 分布,确认检材是 V2 还是 V3/V4。
- 按 USN 序号排序,识别覆盖边界(序号出现回退处即为被覆写的位置)。
- 统计 Reason 位组合分布,了解该卷上最常见的操作类型。
3.4 与 MFT 关联构建时间线
核心是把 USN 记录与 MFT 条目对齐:
1) 从 MFT 中找到目标文件的条目号(假设为 892)
istat -o 2048 /work/DigiForensics.dd 892
2) 在 USN 导出结果中筛选该条目号的所有记录
grep -iE '(^|[^0-9])892($|[^0-9])' /evidence/usn/usn_journal.csv
| sort -t, -k9 # 按 USN 序号列排序(列序视导出格式调整)