反取证手法与时间戳篡改检测

时间戳在取证报告里承担的角色很重——「该文件于某时创建」「该用户在某时访问过目标文档」。但它的可修改性在所有工件里排第一:改一个文件的时间戳不需要管理员权限、不触发安全审计、不留下新的文件内容,只改动 MFT 里的几个数字。

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

定位:本篇讨论的不是「如何隐藏痕迹」,而是痕迹被隐藏后如何被识别出来。核心结论只有一句: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 上做时间戳篡改的痕迹检测,投入产出比极低——可表达的空间太小,反而容易看起来「正常」。


三、操作步骤

3.1

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