定位:本篇讨论的不是「如何隐藏痕迹」,而是痕迹被隐藏后如何被识别出来。核心结论只有一句:MFT 内部的一致性检查最多只能把文件判为「存疑」,要坐实篡改必须引入 MFT 之外的独立时钟。
与本板块其他文章的关系:时间戳换算 讲的是「怎么正确读时间戳」,本文讲的是「读出来之后怎么发现它在骗你」。前者是格式与换算,后者是对抗与验证——两者都必须掌握,缺一不可。
读者前提:理解 MFT 记录结构与 $STANDARD_INFORMATION / $FILE_NAME 两个属性(见 NTFS 结构与 MFT 解析),并已掌握 USN 日志与文件操作时间线。
关键词:timestomp · 时间戳篡改 · $STANDARD_INFORMATION · $FILE_NAME · MFT Modified · 反取证 · USN 交叉验证 · 亚秒精度 · 阴性发现
一、概述
时间戳在取证报告里承担的角色很重——「该文件于某时创建」「该用户在某时访问过目标文档」。但它的可修改性在所有工件里排第一:改一个文件的时间戳不需要管理员权限、不触发安全审计、不留下新的文件内容,只改动 MFT 里的几个数字。
这份脆弱性有个不对称。破坏真实性很容易,任意用户态程序调 SetFileTime 即可,通常不需要特权;恢复真实性很难,被改掉的数值没有备份,原值不保留在介质上。于是能高置信度地证明「这个时间戳是假的」,但几乎不可能高置信度地证明「这个时间戳是真的」——除非引入一个篡改者没碰过的独立时钟。
检测能力的来源是 NTFS 的双属性结构。每个 MFT 记录里有八个 FILETIME 值,分属 $STANDARD_INFORMATION($SI,0x10)与 $FILE_NAME($FN,0x30)两个属性,语义一一对应:Created 偏移 +0x00 / +0x08,Modified 偏移 +0x08 / +0x10,MFT Modified 偏移 +0x10 / +0x18,Accessed 偏移 +0x18 / +0x20。
都是 64 位 FILETIME,纪元 1601-01-01 UTC,单位 100 纳秒。
$SI 的第四个值最容易被搞错。媒体上常把它叫「MFT Modified」「MFT Changed」或「Change Time」,准确语义是「这条 MFT 记录本身最后一次被更新」——改名、改权限、改时间戳、写入新数据流都会推进它,它不是文件的第四个业务时间。任何文件被读取时它都会推进,所以 $SI 的 MFT Modified 晚于 $SI 其他三个值是正常状态而不是异常。判据只能是差值是否异常大(示例阈值用 30 天),而不是「是否存在差距」。
两套属性的更新时机不对称。$SI 由用户态 API 直接控制,CreateFile/WriteFile/ReadFile/SetFileTime/重命名/属性修改都会更新它。$FN 只在名字变化时更新,触发条件只有首次命名、重命名、跨目录移动三个。$SI 一直在动、$FN 长期不动,这构成了整个检测体系的基石。
SetFileTime 的文档化能力是三个字段:$SI 的 creation、last access、last write。$SI 的 MFT Modified 不在其列,由内核在记录写入时自动设为当前时刻,这就是它常被称为「漏出来的真实时间」的原因。$FN 四值对普通用户态程序没有直接写接口,改它需要绕过内核。
典型 timestomp 之后 $SI 三值是伪造的 2019-05-20 02:02:02.0000000,$SI 的 MFT Modified 是真实的 2024-03-16 09:13:58.6042217,而 $FN 四值保留着 2024-03-15 18:58:13.4471902 这个原样。
$SI 与 $FN 的 Created 差了将近五年,这已经在 MFT 内部构成强信号,但仍然是信号而不是铁证。
$SI/$FN 差异就是篡改的「宽度」,纯 MFT 内部可得,不需要任何外部证据。它的上限也很明确:懂行的操作者会做「双重改名」——先 rename a.txt → a.tmp 强制 $FN 跟着走,再 SetFileTime 回拨,再 rename a.tmp → a.txt 让 $FN 携带伪造值。
完成后两套时间完全一致,$FN 这条旁证失效。但这一步挡不住别的东西:两次重命名都会写 USN 日志且时间是内核打的,两次重命名都要过 $LogFile 留下重命名事务,有卷影副本的话旧快照里存着改之前的 $FN 值。所以 $SI/$FN 一致恰恰是「有人处理过」的信号,交叉验证才是兜底。
亚秒精度提供一条高价值痕迹。FILETIME 的分辨率是 100 纳秒,内核时钟不会产生整齐的亚秒零,真实活动时间戳长这样:2024-03-15 18:58:13.4471902。而大量 timestomp 工具在写值前把时间截断到整秒,结果是 2019-05-20 02:02:02.0000000。
判据是:同一批次文件里若一批文件的 $SI 四个时间亚秒部分全为 0,而同卷其他文件不是这样,这是高价值信号——真实活动的亚秒部分是噪声,整秒截断会把噪声完全抹平,而一个文件系统上不会有大批文件恰好共享同一个亚秒模式。
这条判据的边界同样明确。填了随机亚秒的工具直接绕过它,所以漏检不等于不存在;老旧安装程序与部分复制工具会产出整齐的时间,会有假阳性;对 FAT/exFAT 完全不适用,因为 DOS 时间格式结构上就没有亚秒部分,判据必然全量假阳性。$SI 四值中三个相同这条判据也必须以统计形式使用——单文件三个时间相同在正常场景下相当常见,价值在于一个目录下 80% 的文件集中出现、亚秒全为 0、时间戳集中在某一分钟。
检测脚本实现 7 条判据,按证据强度分两级,硬矛盾(H1 SI 创建晚于 SI 修改、H2 SI 四值全为 0、H3 FAT 目录项时间早于 1980-01-01)是正常文件系统活动不可能产生的,弱信号(W1 SI/FN 创建时间分歧超 365 天、W2 亚秒全为 0、W3 SI 四值中三个相同、W4 MFT 变更时间滞后超 30 天)需要旁证。
输出分 TAMPERED/SUSPECT/CONSISTENT 三级。阈值必须从数据里来而不是从经验来:先在已知干净样本上统计分布,取「正常数据里的极值 + 余量」,或用分位数。GAP_D = 365.0 与 AGE_D = 30.0 是示例值不是推荐值。
上述 7 条判据仅适用于 NTFS。ext4 的对应结构是 inode 的 i_ctime/i_mtime/i_atime 加目录项里冗余存的一份 i_ctime,可用 debugfs、istat 读,不能用 stat;把 NTFS 判据原样搬过去会全部落空,而「什么都没检出」与「该卷干净」在结论里长得一模一样。
APFS 与 HFS+ 同样不适用。FAT 的天花板是 DOS 时间只有 16 位字段、起点 1980-01-01、粒度 2 秒,结构上无法表达 1980 年之前的日期,字段会回绕看起来像正常时间。
检测的关键一步是引入独立时钟。USN 日志是最容易取到的一个,时间戳由内核在写入时打上,篡改者改不了。判读它靠三个 Reason 值:FILE_CREATE 是 0x00000100;成对的 RENAME_OLD_NAME(0x00001000)+ RENAME_NEW_NAME(0x00002000)连续出现两次且最终名字与初始相同,是双重改名的直接指纹;
BASIC_INFO_CHANGE 是 0x00008000,数值要记准,0x00004000 是 INDEXABLE_CHANGE,这两个经常被工具文档写反。BASIC_INFO_CHANGE 记录的是元数据被写这个动作而非内容被改,两类事件在时间线上要画成两个不同的事件点。
CLOSE(0x80000000)是伴随标志,统计实质操作时必须排除。
USN 日志可能被清空或被滚转覆盖($Max 默认约 64 MB),这时要找第二个独立时钟。Sysmon Event ID 2 价值最高,它含 CreationUtcTime(新值)与 PreviousCreationUtcTime(原值),在篡改发生的那一刻就记下了改前值,缺点是默认不启用不能假设它存在。
Prefetch 是内核写入的执行时间,文件路径哈希可与 MFT 交叉。Amcache / ShimCache 记录程序路径与最后运行时间。事件日志的 Security 104/1102 记录日志被清。$LogFile 含元数据事务流水。卷影副本的旧快照里存的是篡改前的 $FN 原值,对同一 MFT 记录号分别解析主卷与快照,两套 $FN 值 diff 出来的差异即被改写的时间。
分层定级把线索转成结论,依据是证据组合而不是判据数量。命中 5 条弱信号但没有外部时钟,定级仍是 L2,因为存在「所有弱信号都源于同一次合法批量操作」的可能。L1 无异常对应「未发现时间戳异常特征」;L2 仅命中弱信号,对应「时间戳存在异常特征,与已知篡改手法的形态相符,需进一步核实」;
L3 弱信号加外部时钟矛盾(USN / Prefetch / 快照任一),对应「时间戳与独立记录的时间存在冲突,判定时间戳已被改写」;L4 抓到改写动作的直接记录(Sysmon EID 2 / USN BASIC_INFO_CHANGE 时刻吻合 / 快照 diff 出原值),对应「已确认该文件时间戳被改写,改写动作发生于 T,原始时间戳为 V」。
L3 与 L4 的区不要在于 L4 拿到了动作或原值,L3 只有矛盾。「独立时钟」这四个字要出现在报告里,它是 L2 和 L3 的分界线。
$SI 的 MFT Modified 是上界不是点估计。它记的是「这条 MFT 记录最后一次被更新」,修改权限、添加数据流、被同步软件 touch 都会推进它,所以只能推出「该文件的 MFT 记录在 T 时刻被更新过」或「不早于这个时刻仍然存在或刚被操作过」,推不出「文件创建于 T」。要拿创建时刻得用 $FN Created、USN FILE_CREATE 记录或 Prefetch 首次执行时间。
反取证不是单一技术,是一个按「动的是什么」分层的谱系。元数据层动的是 MFT / inode 时间字段,包括时间戳回拨与时间戳清零,这层深度覆盖;同层的「改文件名 / 扩展名」动的是目录项,只部分覆盖。数据层动的是数据簇,覆写文件内容或空闲空间只说明检测思路;同层的加壳、隐写动的是文件内部结构。
痕迹层动的是日志文件本身,清除事件日志、Prefetch、USN 属于交叉验证范围;同层的禁用时间戳更新动的是系统配置。密钥层动的是全部内容,加密卷、EFS、BitLocker 归入加密板块。排查清单按这五层逐层走,共十项,从事件日志、审计策略、USN 日志、Prefetch、卷影副本,到文件时间戳、空闲空间、回收站、系统配置、MFT 记录复用。
「清日志」类操作最好检,因为它们自己会留下新日志——Security 1102 记录审计日志被清、Security 104 记录某个日志文件被清。
时间戳一致性检测查不了删除型反取证。一个文件被删除后,$MFT 记录还在、$FN 属性还在、目录项也还在(只是被标记为已删除),时间戳字段一个都不会少,而被删文件的四个时间之间的关系本来就是自洽的。删除线要单独看:$MFT 记录的状态位、$FILE_NAME 中的 FILE_NAME_FLAG_DELETED(父目录索引里的删除标记)、$I30 目录索引的 slack、未分配簇的内容残留、回收站 $RECYCLE.BIN。
阴性表述也分开写:「时间戳字段未发现改写特征」与「已删除文件共 N 个,其时间戳字段未见异常」覆盖的是完全不同的范围。
阴性结论不能用绝对表述。「本机未发现反取证痕迹」这句话在技术上不可能成立,因为检材只是某个时间点的快照,痕迹可能在镜像制作之前就已被抹掉,而抹除动作本身留下的痕迹也随之一并消失。合格的写法是范围 + 检查 + 未发现三段式,四个要素是检查了什么(7 条判据)、检查了多少(记录号区间)、结论的强度(未发现命中硬矛盾)、没查什么(未覆盖范围)。
「USN 日志为空」的正确表述是「未获取到 USN 日志数据」,不是「USN 日志中无篡改记录」——日志为空的五种原因里只有「确实没有相关记录」是正常解释。
分析过程本身会改变时间戳,因此有三条纪律。所有挂载一律 -o ro 加 noatime;篡改检测必须在任何提取操作之前跑,用原始 MFT 一次成型,不要反复重跑;检测脚本的输出与 MFT 导出文件一起归档,作为「检测时的原始输入」便于第三方复核。
二、核心原理
2.1 NTFS 的八个时间戳
每个 MFT 记录里有八个 FILETIME 值,分属两个属性、语义完全对应:
| 语义 |
$STANDARD_INFORMATION($SI,0x10) |
$FILE_NAME($FN,0x30) |
| Created 创建 |
偏移 +0x00 |
偏移 +0x08 |
| Modified 内容修改 |
偏移 +0x08 |
偏移 +0x10 |
| MFT Modified 记录变更 |
偏移 +0x10 |
偏移 +0x18 |
| Accessed 访问 |
偏移 +0x18 |
偏移 +0x20 |
都是 64 位 FILETIME,纪元 1601-01-01 UTC,单位 100 纳秒。
⚠️ 第四个值的名字最容易被搞错。 媒体上常把它叫「MFT Modified」「MFT Changed」或「Change Time」。它的准确语义是「这条 MFT 记录本身最后一次被更新是什么时候」——任何属性变化(改名、改权限、改时间戳、写入新数据流)都会推进它,它不是文件的第四个业务时间。
业内流传的「NTFS 记录创建 / 修改 / 访问 / 变更四个时间」这个说法里的「变更」,指的就是它。这一点在 2.3 会直接决定检测的成败。
2.2 Windows 自身如何更新这八个值
要设计判据,先必须知道正常情况下这些值怎么变。关键事实:
$STANDARD_INFORMATION 由用户态 API 直接控制。
CreateFile / WriteFile / ReadFile / SetFileTime / 重命名 / 属性修改,都会更新 $SI。资源管理器显示的属性对话框、绝大多数取证工具显示的时间,都是这一组。
$FILE_NAME 只在「名字变了」时更新。
具体触发条件只有三个:首次命名、重命名、跨目录移动(本质也是重命名)。文件改名之后,如果名字不再变,$FN 就长期静止。
这条「$SI 一直在动、$FN 长期不动」的不对称,是整个检测体系的基石。
MFT Modified 通常由内核维护,普通 API 改不到。
SetFileTime 的文档化能力是三个字段:creation、last access、last write。$SI 的第四个值(MFT Modified)不在其列,Windows 会在记录被写入时自动把它设为当前时刻。这就是为什么它常被称为「漏出来的真实时间」。
Windows Vista 及以后默认关闭 last access 更新。
注册表项 HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\NtfsDisableLastAccessUpdate 默认为 1,意味着读取文件不再更新访问时间。
这带来两个后果:现代系统上 $SI 的 Accessed 值往往比 Modified 还旧,看起来「反常」但完全正常;而一些工具会通过修改该注册表项来「抹掉」访问痕迹——这个动作本身在 Sysmon Event ID 13 和该注册表的 LastWriteTime 上留痕。
2.3 SetFileTime 能改什么、不能改什么
这是整个检测逻辑的因果起点。
可以改($SI 三个值):
$SI Created ← SetFileTime(hFile, &creation, ...)
$SI Modified ← SetFileTime(hFile, NULL, &lastwrite, ...)
$SI Accessed ← SetFileTime(hFile, NULL, &lastaccess, ...)
改不了:
$SI MFT Modified ← 不在 SetFileTime 的参数里,由内核在记录写入时自动维护
$FN 四值 ← 用户态没有接口直接写;改它需要绕过内核(改盘 / 双重改名)
于是典型 timestomp 之后,一条 MFT 记录会长这样:
$STANDARD_INFORMATION
Created: 2019-05-20 02:02:02.0000000 ← 伪造
Modified: 2019-05-20 02:02:02.0000000 ← 伪造
MFT Mod: 2024-03-16 09:13:58.6042217 ← 真实:伪造动作发生的时刻
Accessed: 2019-05-20 02:02:02.0000000 ← 伪造
$FILE_NAME
Created: 2024-03-15 18:58:13.4471902 ← 真实
Modified: 2024-03-15 18:58:13.4471902 ← 真实
MFT Mod: 2024-03-15 18:58:13.4471902 ← 真实
Accessed: 2024-03-15 18:58:13.4471902 ← 真实
读这张表的正确方式是:$SI 的 MFT Modified 晚于 $SI 的其他三个值,是「正常状态」而不是「异常」。 每一个文件在被读取时都会推进 MFT Modified。判据应该是「这个差距是否异常大」,而不是「是否存在差距」。
而 $SI 与 $FN 的 Created 差了将近五年,这在单纯的 MFT 内部就已经是一个强信号。
但它仍然是信号,不是铁证。原因见 4.1。
2.4 SI/FN 不对称:检测能力的来源
$FN 的不可写(对普通用户态程序而言)成就了两件事。
第一,$FN 成为 $SI 的旁证。 篡改者改了 $SI,$FN 保持原样,两者的差值就是篡改的「宽度」。这是纯 MFT 内部可得的、不需要任何外部证据的信号。
第二,懂行的操作者会做「双重改名」来消除这个信号:
1. rename a.txt → a.tmp (强制 $FN 跟着 SI 走)
2. SetFileTime (此时 $SI 与 $FN 都被回拨)
3. rename a.tmp → a.txt ($FN 再次更新,携带伪造值)
完成后两套时间完全一致,$FN 这条旁证失效。
但这一步挡不住别的东西:
- 两次重命名都会写 USN 日志,日志里的时间是内核打的,伪造不了;
- 两次重命名都要过
$LogFile,会在事务日志里留下重命名事务;
- 如果有卷影副本,旧快照里存着改之前的 $FN 值,diff 一下就出来。
所以「$SI/$FN 不一致」的检测能力是有上限的,交叉验证才是兜底。
2.5 亚秒精度:一条不起眼的高价值痕迹
FILETIME 的分辨率是 100 纳秒,内核时钟不会产生整齐的亚秒零。真实活动时间戳长这样:
2024-03-15 18:58:13.4471902
2024-03-16 09:13:58.6042217
而大量 timestomp 工具在写值之前会把时间截断到整秒,结果是:
2019-05-20 02:02:02.0000000
2019-05-20 02:02:02.0000000
判据:同一批次文件里,若一批文件的 $SI 四个时间亚秒部分全为 0,而同卷其他文件不是这样,这是一个高价值信号。
为什么说它高价值:真实活动的亚秒部分是「噪声」,而这种噪声在整秒截断下会被完全抹平。一个文件系统上不会有大批文件恰好共享同一个亚秒模式。
它的边界必须说清楚:
- 优质工具会填随机亚秒,直接绕过这条判据 → 漏检不等于不存在;
- 某些批量处理(老旧安装程序、部分复制工具)会产出整齐的时间 → 会有假阳性;
- 对 FAT/exFAT 完全不适用——DOS 时间格式结构上就没有亚秒部分,判据必然全量假阳性。
2.6 其他文件系统的对应关系
timestomp 不是 NTFS 独有的。各文件系统的「可改/不可改」边界不同,判据必须逐一对应:
| 文件系统 |
存放位置 |
篡改面 |
本篇判据是否适用 |
| NTFS |
$SI / $FN 双属性 |
$SI 三值 |
✅ 全部适用 |
| ext4 |
inode + 目录项 |
inode 的 i_ctime / i_mtime / i_atime + 目录项时间 |
部分(见下) |
| APFS |
inode + 目录项 |
多数时间戳可改 |
❌ 不适用本篇判据 |
| FAT/exFAT |
目录项 16 位字段 |
3 个 DOS 时间 |
❌ 精度与表达力都不够 |
| HFS+ |
目录 + 文件 Fork |
多数可改 |
❌ 不适用 |
ext4 的特殊性:ext4 在目录项里冗余存了一份 i_ctime,用来判断「文件是否在目录项缓存有效期内被改过」。如果 inode 的时间被回拨而目录项没改,或反之,ls 与 stat 可能给出不同的结果。
文件系统级别的「inode 与目录项时间不一致」是 ext4 版的 SI/FN 对照,但要通过底层工具(debugfs、istat)而不是 stat 才能看到。
FAT 的天花板:DOS 时间是 16 位字段,起点 1980-01-01、粒度 2 秒,结构上无法表达 1980 年之前的日期。把一个文件的 FAT 时间设成 1970 年或更早,字段会回绕,看起来像 1980 年之后的一个正常时间。
在 FAT 上做时间戳篡改的痕迹检测,投入产出