搜索全站

输入至少 2 个字符,查找相关内容。

↑↓ 选择 Enter 打开Esc 关闭

Steady State 与 $LogFile 还原

NTFS 的 MFT 解析回答的是现状:某个条目现在有哪些属性、哪些流、什么时间戳。"这个条目原本是什么,是谁在什么时候改掉的"是另一个问题。现状写在 $MFT 的当前字节里,日志型文件系统还会另写一份"改动过程"——NTFS 的这份记录就是 $LogFile(MFT 记录 2)与 $UsnJrnl。

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

一、概述

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 什么程序访问过该文件 需卷未加密、程序曾运行

$UsnJrnl 在多数实战中比 $LogFile 更实用:它结构简单、字段清晰(文件名、父目录引用、原因码、时间戳),且能直接回答"这个文件什么时候被创建/修改/删除/重命名"。

两者结合是元数据时间线的标准打法。

常见原因码(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

① 提取 $LogFile(MFT 记录 2)

icat -o 2048 /work/DigiForensics.dd 2-0-0 > /evidence/LogFile.bin ls -l /evidence/LogFile.bin

② 查卷的脏标志(不同 fsstat 版本字段名略有差异)

fsstat -o 2048 /work/DigiForensics.dd | grep -iE 'dirty|state|journal|logfil

安全验证 当前请求需要先完成一次滑块验证。