关键词:Steady State、$LogFile、元数据日志、LSN、日志回放、MFT 还原、时间戳追踪、LogMiner
难度:高级
前置知识:NTFS MFT 结构、元数据基础概念、日志型文件系统通则
相关文章:NTFS 结构与 MFT 解析、NTFS 数据流与隐藏数据
一、概述
NTFS 的 MFT 解析回答的是现状:某个条目现在有哪些属性、哪些流、什么时间戳。"这个条目原本是什么,是谁在什么时候改掉的"是另一个问题。现状写在 $MFT 的当前字节里,日志型文件系统还会另写一份"改动过程"——NTFS 的这份记录就是 $LogFile(MFT 记录 2)与 $UsnJrnl。
不读日志的代价很具体:一个被删又重建的同名文件,$MFT 里只留下"未使用"标记和新条目的时间戳。你无法区分"这文件刚被创建"和"这文件被删掉之后又用同样的名字创建了一个"——后者意味着原文件被覆盖,两者的法律与取证意义完全不同。
这一层工作在分析环节的元数据纵深阶段,紧跟在 NTFS 结构与 MFT 解析 之后、结论汇总之前。三个数据源的分工必须先分清:
| 数据源 |
记录什么 |
覆盖窗口 |
价值 |
$MFT 当前内容 |
现在的元数据 |
无窗口概念 |
基准,但不能回答历史 |
$LogFile |
元数据改动的 REDOP/UNDO 序列,带 LSN |
环形缓冲,容量决定 |
崩溃恢复依据 + 历史元数据 |
$UsnJrnl |
文件操作流水(原因码 + 文件引用号 + 文件名) |
环形缓冲,通常几天 |
重命名/移动的时间与顺序 |
三者不是替代关系。第五章案例里 季度汇报.docx 这个已不存在的名字,靠 $UsnJrnl 与 $LogFile 两个独立源互相印证才敢写入结论——只有一个源支撑时,结论强度要降级。
上游是 $MFT 解析结果(读日志要靠 MFT 记录的 LSN 字段定位);下游是时间线合并与主体归属。元数据日志能证明"文件被改名",但证明不了"是谁改的",那一跳要跳到 Windows 执行痕迹。
落到具体证据上:
$LogFile(MFT 记录 2) —— 元数据日志本体。$DATA 是原位的、:$J 是非原位的,icat 提取时要选对地址
- LSN(LogFile Sequence Number) —— 每条日志记录与每个 MFT 条目携带的序列号。比对"日志里的 LSN"与"MFT 条目的 LSN"是判断某条目是否被日志覆盖过的关键动作
- REDOP / UNDO 记录 —— 重做与撤销对。一个逻辑操作通常对应多条记录,不能按"一条 = 一次操作"计数
- 卷 dirty 位 —— 关机是否干净。非 Steady State 意味着日志里有未提交事务,2.x 的还原结论不能直接用
$UsnJrnl 与 $UsnJrnl%3A$J —— 变更日志本体与它的非原位流。环形缓冲结构,满了覆盖旧记录
- USN 原因码 —— 如
0x80000023(文件被关闭)、0x00000102(文件名变更)。原因码是判断操作语义的第一手依据
- MFT 条目的四项时间戳 ——
$STANDARD_INFORMATION 的 Created / Modified / Changed / Accessed。"改元数据只动 Changed、不动 Modified"是重命名的典型特征
- MFT 条目的
Last Access Update Sequence Number —— 系统当前 LSN 位置。与日志最大 LSN 比较可直接判断条目是否已被新事务覆盖
DigiForensics.dd 中记录 2 的 $DATA 提取结果 —— 日志二进制本体。体积明显小于配置上限,说明近期记录还在
DigiForensics.dd 中记录 132 的提取结果 —— $UsnJrnl 提取结果。strings 能捞出文件名,但时间是变长结构,位置需按版本验证
走完这一层能回答的是这些:这个卷是否处于 Steady State、某个 MFT 条目是否仍在日志覆盖范围内、某个已消失的旧文件名在什么时间窗口内出现过、以及某项元数据变化能否被两个独立日志源互相印证。
回答不了的几件事必须提前说清:
- 文件内容 ——
$LogFile 记录的是元数据(文件名、大小、簇链描述),不记录数据流内容。想恢复内容走 slack 与未分配空间路线
- "谁"改的 —— 日志里没有用户身份,只有文件引用号与原因码。主体归属要靠 UsnJrnl 之外的执行痕迹(登录会话、进程创建)
- 精确到秒的操作时刻 —— USN 记录是变长结构,时间字段偏移需按版本验证。未做结构化解析时只能给时间窗口,不能给时刻
- 超出缓冲窗口的旧操作 —— 两个日志都是环形缓冲。窗口外的事实不是"没发生",是"查不到"
- 数据流层面的藏匿 —— 命名流与隐写属于 NTFS 数据流与隐藏数据 的范围,日志管不到
往下读之前必须已读 NTFS 结构与 MFT 解析——要理解 LSN 在 MFT 记录头里的位置、$STANDARD_INFORMATION 的字段布局、记录号与文件引用号的换算,这三样是这里所有操作的输入,日志结构解析绕不开它们。
另外需要一点二进制结构阅读的耐心。这里刻意包含一条"无法结构化解析时怎么办"的路径(2.6),因为真实委托里工具版本经常不匹配。知道退化路径是什么,比记住某个工具的语法更有用。
LSN 体系与 REDOP/UNDO 的结构推导在核心原理,提取与判读步骤在操作步骤。这一层的产出直接决定你能写多强的结论,所以常见陷阱那一章不是可选的——4.1 组的每一条都会让"已删除"或"已重命名"这类判断从成立变成不成立。
原因码的完整表、记录结构与多记录合并成一次操作的技术,见UsnJrnl 与文件操作时间线。完整还原流程见案例示例,提取命令见命令速查。
二、核心原理
2.1 Steady State:文件系统什么时候"静止"
定义:一个日志型文件系统处于 Steady State 时,意味着:
- 日志中已完成的记录所描述的元数据改动,已经全部落到磁盘上的真实元数据结构中;
- 因此,磁盘上的元数据是当前有效的、最新的一致状态;
- 日志中可能仍有未完成的记录(预写入但未提交),但这些不影响"已提交改动已落盘"这个性质。
时间轴 →
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 日志记录写入 │ 元数据落盘 │ 日志记录标记 │ 日志空间复用 │
│ (REDOP/UNDO) │ (真实 MFT) │ 为已完成 │ (环形覆盖) │
└──────────────┴──────────────┴──────────────┴──────────────┘
│ │ │
崩溃可能点① 崩溃可能点② 崩溃可能点③
日志有、盘无 盘改了一半 日志被覆盖丢失
Steady State 的取证意义:在 Steady State 镜像上看到的元数据 = 有效状态,不需要回放日志就能得到正确目录树(正常关机检材的常见情形);非 Steady State 检材(异常断电)必须先评估日志,因为可能存在"日志里有、盘上没有"或"盘改了一半"的中间态。
判断顺序永远是先状态、后内容。
如何判断是否处于 Steady State:
icat -o 2048 /work/DigiForensics.dd 2-0-0 > /evidence/LogFile.bin # 头部字段解释需结合版本验证
ls -l /evidence/LogFile.bin
file /evidence/LogFile.bin
| 观察 |
推断 |
| 系统正常关机(有干净关机事件/标记) |
大概率 Steady State |
| 文件系统标记为 dirty / needs recovery |
非 Steady State,必须评估日志 |
$LogFile 体积远小于配置上限 |
环形缓冲未填满,近期日志尚在 |
$LogFile 体积接近上限且很老 |
近期日志可能已被覆盖 |
关键提醒:$LogFile 是固定大小的环形缓冲区(典型 4–64 MB,取决于卷大小与版本)。空间写满后最早的记录被覆盖。所以"日志里没有"永远不等于"没发生过",只说明"那条记录已被覆盖"。
2.2 $LogFile 记录什么、不记录什么
| 类别 |
是否记录 |
说明 |
| 文件创建/删除/重命名 |
✅ |
对应 MFT 条目的标志位与 $FILE_NAME 改动 |
| 大小/时间戳变化 |
✅ |
多数涉及 MFT 记录的元数据更新 |
文件内容($DATA 数据块) |
❌ |
日志不记录数据内容 |
| 目录项的完整重建 |
部分 |
复杂目录操作可能触发整块元数据重写 |
| 簇分配/释放 |
部分 |
取决于操作是否触发 $Bitmap 更新入日志 |
结论:$LogFile 能回答"元数据怎么变的",不能回答"文件里是什么"。把两者混淆会导致报告出现无法支撑的结论。
2.3 通用日志机制:REDOP / UNDO / 事务
几乎所有日志型文件系统(NTFS、ext3/4、XFS、JFS)都采用同一套抽象:
| 记录类型 |
全称 |
作用 |
| REDOP |
Redo Operation(重做) |
记录"要做什么":把某处元数据设为某值。回放它 = 重做这个操作 |
| UNDO |
Undo Information(撤销) |
记录"改之前是什么":把某处元数据原值保存下来。用于回滚未完成事务 |
| Checkpoint |
检查点 |
标记"到此为止的日志都已成为稳定状态",并记录当时的 LSN |
LSN(Log Sequence Number,日志序列号) 贯穿整个机制:每条日志记录都带 LSN;MFT 记录头也存了 $LogFile 的 LSN(该 MFT 条目最后一次由日志更新时的 LSN)。比较两者即可判断"这个 MFT 条目的当前状态,是否反映了日志里的最新状态":
MFT记录.LSN >= 日志中该操作的LSN → 磁盘状态已包含该日志描述的改动
MFT记录.LSN < 日志中该操作的LSN → 磁盘可能仍是改之前的旧状态
实操说明:MFT 记录头偏移 0x08、长度 8 字节存放该 LSN(结构见 02 篇 2.2),跨版本稳定。但 LSN 字段在 $LogFile 记录内部的字节位置随版本变化,需用实际解析工具确认,不要凭本文硬编码偏移。
2.4 LogMiner 类思路:不逐条解析,先做差分
直接逐字节解析 $LogFile 难度高、版本差异大。LogMiner 类工具的核心思路是"黑盒差分",绕开结构解析——让两个 $LogFile 前缀分别驱动一次挂载,比较两次挂载后元数据的差异:
原始 $LogFile(长)
├── 取前 N 字节 → 补丁A → 挂载 → 导出目录树 → 快照A
├── 取前 M 字节 → 补丁B → 挂载 → 导出目录树 → 快照B (M > N)
└── 差分:快照A ⊕ 快照B = 区间 (N, M] 的日志导致的元数据变化
| 优势 |
局限 |
| 完全不需要理解内部结构 |
需要能写盘(在副本上,且驱动需支持日志补丁) |
| 版本差异不敏感 |
差分只说"变了什么",不说何时变、谁变的 |
| 可精确定位"哪段日志有效应" |
差分结果仍需人工归因 |
这类方法属于"辅助生成假设"的技术,产出的时间与主体信息需另找证据($UsnJrnl、事件日志、Prefetch)佐证。
换来的是黑盒代价:写盘。
2.5 元数据变化追踪:从"现状"到"过程"
即使不深入解析 $LogFile,元数据变化追踪本身也是高价值维度:
| 数据源 |
回答的问题 |
局限 |
$MFT 四个时间戳 |
文件何时创建/修改/访问/元数据变更 |
无历史,只有当前值 |
$FILE_NAME 时间戳 |
改名发生的时间 |
改名后原名不再可读 |
$UsnJrnl(USN 日志) |
何时做了文件级操作(带操作类型与原因) |
环形缓冲,有上限;不含内容 |
$LogFile |
元数据如何被一步步修改 |
环形覆盖;不含内容;需工具解析 |
| 事件日志 |
谁登录、执行了什么程序 |
依赖审计策略是否开启 |
| Prefetch / Amcache |
什么程序访问过该文件 |
需卷未加密、程序曾运行 |
两者结合是元数据时间线的标准打法。
常见原因码(Reason Codes)(用于判断操作性质):
| 原因 |
含义 |
0x00000001 |
数据被覆盖/写入 |
0x00000002 |
数据被截断/扩展 |
0x00001000 |
旧文件名(rename 的旧名) |
0x00002000 |
新文件名(rename 的新名) |
0x00002001 |
创建 |
0x00002002 |
删除 |
0x80000000 |
Close(文件关闭) |
成对出现的 0x1000 + 0x2000(旧名 + 新名)就是一次重命名的完整指纹——这是 NTFS 环境下认定"文件被改名过"最可靠的证据。
2.6 无法解析日志时,能做什么
必须诚实面对的现实:$LogFile 的完整解析是 NTFS 取证的深水区。
本篇不给字节偏移,但给三条不依赖深度解析的可用路径:
| 路径 |
方法 |
能得到什么 |
| A. 状态判定 |
判断是否 Steady State(关机状态 / dirty 标志) |
确定"要不要关心日志" |
| B. 内容直读 |
用支持 NTFS 日志的驱动读取日志(见 3.2) |
日志记录的文本/字符串部分(文件名常以 UTF-16 出现) |
| C. 交叉补全 |
用 $UsnJrnl 补时间线,用事件日志补主体 |
元数据变化的时间与主体,不依赖 $LogFile 深度解析 |
路径 B 的现实价值常被低估:日志中记录的文件名/路径以 UTF-16LE 字符串形式落盘,直接关键字搜索往往能捞出"已经不在目录树里的文件名":
# 从 $LogFile 中提取 UTF-16 字符串(文件名常以此形式存在)
strings -e l -n 6 /evidence/LogFile.bin | sort -u > /evidence/logfile-names.txt
head -50 /evidence/logfile-names.txt
# 与当前 $MFT 树做差集 → 日志中出现但目录树里没有的名字
comm -23 /evidence/logfile-names.txt <(cut -d' ' -f7- /evidence/fls-all.txt | sort -u) | head -30
定性要求:在日志里搜到一个名字,只说明"这个名字曾在元数据日志中出现过"。它可能是重命名的旧名、可能是已正常删除的文件、也可能是误报(随机字节恰好解码成可读字符)。必须与其他证据交叉验证后才能作为结论。
三、操作步骤
3.1 第 1 步:判断是否 Steady State