摘要

NTFS并非单一真相源。文件创建、修改、访问时间戳同时存在于MFT的$STANDARD_INFORMATION$FILE_NAME属性中,且更新规则迥异。USN Journal以变更日志形式记录了每一次文件操作的精确时刻与原因码,$LogFile则以事务日志形式保存了更深层的元数据操作序列。攻击者常利用Timestomping技术篡改$SI时间戳以掩盖入侵时间线,但$FN时间戳、USN Record中的USN_REASON_DATA_OVERWRITE事件以及$LogFile中的重做/撤销记录往往被忽略。这三者在微秒级精度上的不一致,恰构成最坚固的证据链。

NTFS文件系统时间戳架构

$STANDARD_INFORMATION与$FILE_NAME

NTFS的核心元素是主文件表(MFT),它存储系统中每个文件的条目。MFT中的每个条目包含许多存储文件描述元数据的属性。其中最为关键的两个属性——$STANDARD_INFORMATION$SI)和$FILE_NAME$FN)——各自独立存储着文件的MAC(B)时间戳:M(修改时间)、A(访问时间)、C(MFT变更时间)、B(创建时间)。

$SI属性中的时间戳大致与文件内容的交互相关——当你编辑一个文档、更改其权限或写入数据时,$SI时间戳会被更新。$FN属性中的时间戳则大致与文件位置和名称的交互相关——当你重命名文件、移动文件到另一个目录时,$FN时间戳会被更新。

这种设计上的分歧制造了一个天然的取证双轨系统:同一个文件,在不同的元数据属性中可能存储着不同的时间信息。关键点在于,攻击者通常只修改$STANDARD_INFORMATION中的时间,而忽略$FILE_NAME属性中的副本。

严格地说,修改$SI时间戳是一种极为常见的Timestomping方式,因为可以通过用户层面上的API进行操作——包括Cobalt Strike、Timestomp.exe和Metasploit在内的主流攻击工具都内置了此项功能。然而,修改$FN则需要调用内核API或滥用$FN时间戳的生成方式。已知的$FN篡改手段仅有两个:其一,在未引入Patch Guard的旧操作系统上,使用NtSetInformationFileNtQueryInformationFile直接写入;其二,在任何操作系统上先篡改$SI,然后移动或重命名文件,Windows会自动将修改后的$SI复制到$FN中。

值得注意的是,在取证分析实践中广泛流传着两个误区:一是认为$FILE_NAME中的时间戳永远不会被篡改,二是认为自动化工具生成的时间戳纳秒部分必定为全零。事实上,高级攻击者完全可以通过上述方法同步修改$FN时间戳,部分经过定制化的工具(如Metasploit的timestomp模块)也支持带纳秒精度的时间戳生成。这要求取证分析师必须超越简单的$SI/$FN比对,深入到USN Journal和$LogFile层面进行交叉验证。

在实际攻击事件中,Timestomping的使用已相当普遍。APT28在入侵后的第一时间即篡改了后门文件的时间戳,APT29(Cozy Bear)在SolarWinds事件中大规模使用时间戳篡改以将其恶意DLL混入合法更新包,Lazarus Group更是直接复制calc.exe的时间戳注入到其恶意投放的文件中——这正是Cobalt Strike内置timestomp模块的标准操作方式。

MFT Record的物理结构

要深入理解上述两类时间戳的差异,必须了解MFT记录的物理结构。

每个MFT记录恰好为1024字节(1 KiB)。前42字节包含FileRecordHeader,紧接着是一系列属性的顺序排列。属性序列以类型为0xFFFFFFFF的结束标记终止。

FileRecordHeader的关键字段包括:

字段
类型
偏移
描述
signature
[u8; 4]
0x00
总是 b”FILE”
usa_offset
u16
0x04
更新序列数组偏移
lsn
u64
0x08
$LogFile序列号,用于日志关联
sequence_number
u16
0x10
每次记录复用递增
attrs_offset
u16
0x14
第一个属性的偏移(通常0x30)
flags
u16
0x16
标志位:0x01=已分配, 0x02=目录, 0x04=扩展记录, 0x08=特殊索引
base_file_record
u64
0x20
对扩展记录而言指向基本记录

其中lsn字段尤为关键:它记录了$LogFile中最后一次修改此MFT记录的日志序列号,是连接MFT与$LogFile两大取证数据源的核心桥梁。

每个属性以一个AttributeHeaderCommon开头,标识属性类型(如0x10 = $STANDARD_INFORMATION,0x30 = $FILE_NAME,0x80 = $DATA),并指定该属性是常驻还是非常驻。常驻属性的数据直接存储在MFT记录中(1024字节以内),非常驻属性则通过Run List指向外部簇。

时间戳

NTFS时间戳使用64位FILETIME格式存储,精度为100纳秒,起始点为1601年1月1日。一个自然的文件时间戳通常具有极为精确的纳秒分量——例如131683876627452045(0x01D40F28B2C3A0D),该值折合为2018年4月16日21:27:42.7452045。小数点后7位的精度是正常文件操作的自然产物。

而当攻击者使用系统工具(如PowerShell的$(Get-Item file).CreationTime=$(Get-Date "01/01/2018"))进行时间戳篡改时,NTFS时间戳的纳秒分量会被置为零——因为大多数用户界面和脚本工具只支持秒级精度。131683876620000000虽然也是2018年4月16日21:27:42,但其纳秒部分全零——在整数表示中显得格外突出,标志着这是一个伪造的时间戳。

然而,不可过于依赖这一指标。正如前文所述,经过定制的高级攻击工具可以生成带有真实纳秒精度的时间戳。纳秒全零检测仅能捕获使用标准系统工具的低水平攻击者——对于使用Metasploit高级timestomp选项或自研工具的APT组织,纳秒检测本身不足以排除时间戳篡改的可能性。

USN Journal

变更日志的机制

如果说MFT是文件系统的“身份证”,那么USN Journal就是文件系统的“记录仪”。Update Sequence Number Journal是NTFS的一项核心功能,为卷上发生的每一次文件系统变更提供持久化日志。它记录高层次的操作——文件创建、删除、重命名、数据变更,无论操作何时发生、由谁触发、结果如何。

UsnJrnl是一个元数据文件,位于每个NTFS卷根目录下的\$Extend\$UsnJrnl。它包含两个备用数据流:$J(包含实际变更记录)和$Max(包含$J的配置信息)。UsnJrnl日志为取证提供了一个强大且相对完整的文件系统活动时间线,可以揭示已删除的文件、追踪文件移动轨迹,并建立详细的用户活动时间线。

USN Record三种版本

USN Journal经历了三个主要版本的演进。v2版本随着Windows Vista的发布而引入,使用64位文件引用号(File Reference Number),记录基本信息:文件引用号、父文件引用、单调递增的USN编号、时间戳和原因标志(Reason Flags)。v3版本在Windows 8中引入,将文件引用号扩展为128位(FILE_ID_128),开始包含更细致的操作细节,尤其是在重命名和备用数据流(ADS)变更方面。v4版本在Windows 10中引入,v4记录不包含文件名——Windows保证文件的最后一个v4记录会紧跟一个v3记录,后者至少包含USN_REASON_CLOSE并附带文件名。

三个版本的对比可从以下维度理解:

特性
v2(XP/Vista)
v3(Windows 8)
v4(Windows 10+)
文件引用大小
64位
128位
128位
包含文件名
包含时间戳
扩展跟踪
默认启用
Vista+
Windows 8+
Range Tracking启用时

v4记录不包含文件名和时间戳这一特性,在实际取证中意味着必须配合v3记录进行路径重构——仅使用v4记录的分析将丢失文件的名称和精确时间信息。

USN_REASON编码

USN Journal中的每条记录都携带一个Reason字段,这是一个位掩码,精确标识了触发此次记录的文件系统操作类型。不同操作对应不同的Reason标志组合:

  • 文件/目录创建 → USN_REASON_FILE_CREATE(0x00000100)
  • 文件/目录删除 → USN_REASON_FILE_DELETE(0x00000200)
  • 重命名(旧名称) → USN_REASON_RENAME_OLD_NAME(0x00001000)
  • 重命名(新名称) → USN_REASON_RENAME_NEW_NAME(0x00002000)
  • 数据覆盖 → USN_REASON_DATA_OVERWRITE(0x00000002)
  • 数据扩展 → USN_REASON_DATA_EXTEND(0x00000004)
  • 数据截断 → USN_REASON_DATA_TRUNCATION(0x00000008)
  • 文件关闭 → USN_REASON_CLOSE(0x80000000)

对于时间戳取证而言,USN_REASON_DATA_OVERWRITE(0x00000002)是最关键的原因码之一——它精确标记了文件数据被覆盖的时刻。当攻击者使用Timestomping工具修改文件元数据后,如果后续对文件有任何数据写入操作,该操作将在USN Journal中被记录为DATA_OVERWRITE事件,其时间戳即为真实的写入时刻——攻击者随后对MFT中$SI时间戳的修改无法追溯性地删除或篡改这条USN记录。类似地,USN_REASON_BASIC_INFO_CHANGE(0x00008000)直接对应文件属性的修改——包括时间戳篡改本身——因此它本身就是Timestomping操作的直接证据。

USN Journal物理结构

从系统编程角度理解USN Record的结构,对编写自定义解析器或审计工具的取证工程师至关重要。以exhume_ntfs开源工具中Rust语言的统一UsnRecord结构体为例:

// exhume_ntfs/src/usnjrn.rs — USN Journal 统一记录结构

pub struct UsnRecord {
    // ===== 核心字段(所有版本共有) =====
    pub record_len: u32,           // 记录总长度(8字节对齐)
    pub major_version: u16,        // 主版本号 (2, 3, 或 4)
    pub minor_version: u16,        // 次版本号 (通常为 0)
    pub file_ref: u128,            // 128位文件引用(v2仅使用低64位)
    pub parent_ref: u128,          // 128位父目录引用
    pub file_mft_record_number: u64,  // 48位MFT条目索引
    pub parent_mft_record_number: u64, // 48位父MFT条目索引
    pub usn: i64,                  // 更新序列号(日志偏移)
    pub timestamp: u64,            // FILETIME 时间戳(v4中为0)
    pub reason: u32,               // USN_REASON_* 位掩码
    pub source_info: u32,          // 源信息标志
    pub security_id: u32,          // 安全描述符ID(v4中为0)
    pub file_attrs: u32,           // 文件属性标志(v4中为0)

    // ===== 版本特有字段 =====
    pub name: Option<String>,      // v2/v3: UTF-16 文件名(不含路径)
    pub remaining_extents: Option<u32>,  // v4: 后续扩展数量
    pub extents: Option<Vec<UsnExtent>>, // v4: 文件内修改的字节范围

    // ===== 取证增强字段(由高层处理逻辑填充) =====
    pub parent_path: Option<String>,  // 父目录完整路径
    pub full_path: Option<String>,    // 文件完整路径(含文件名)
    pub reused_records: Option<Vec<ReusedElement>>, // MFT条目复用检测
}

该结构体的字段设计反映了三个关键取证点:

  1. reason字段的位掩码可直接用于筛选特定类型的变更(如仅关注BASIC_INFO_CHANGE以检测Timestomping)
  2. timestamp字段为FILETIME格式,精度100纳秒,可直接与MFT时间戳进行比较
  3. extents字段仅在v4中存在,记录了文件被修改的具体字节偏移范围——对于确定攻击者具体修改了文件的哪些部分具有独特价值

USN_RECORD_V4深度解析

结构定义与成员含义

在Windows 10及以上版本中,USN_RECORD_V4是Range Tracking启用的核心结构。根据Microsoft官方文档,其C结构定义如下:

// USN_RECORD_V4 结构体 (ntifs.h)
typedef struct {
    USN_RECORD_COMMON_HEADER Header;   // 通用记录头
    FILE_ID_128 FileReferenceNumber;   // 128位文件引用号
    FILE_ID_128 ParentFileReferenceNumber; // 128位父目录引用
    USN Usn;                           // USN编号
    ULONG Reason;                      // 原因标志
    ULONG SourceInfo;                  // 源信息
    ULONG RemainingExtents;            // 后续扩展数量
    USHORT NumberOfExtents;            // 当前记录中的扩展数
    USHORT ExtentSize;                 // 扩展大小
    USN_RECORD_EXTENT Extents[1];      // 扩展数组(可变长度)
} USN_RECORD_V4;

// 通用头部
typedef struct {
    DWORD RecordLength;    // 记录总字节数(8字节对齐)
    WORD MajorVersion;     // 主版本(4)
    WORD MinorVersion;     // 次版本(0)
} USN_RECORD_COMMON_HEADER;

v4特性与取证策略

v4记录的核心设计转变在于关注“文件被修改的具体字节范围”而非“文件的名称和时间戳”。这种设计使其特别适合检测针对文件部分区域的精准篡改——例如攻击者仅修改了PE文件的特定字节以绕过哈希黑名单,而文件其余部分保持不变。

实际取证中的一个关键注意事项:v4记录不包含文件名和时间戳。这意味着仅依赖v4记录的分析会丢失被修改文件的名称和精确操作时刻。正确的取证方法是:将v4记录与对应的v3记录配对分析——Windows保证,当Range Tracking产生v4记录后,文件关闭时会追加一条v3记录(包含USN_REASON_CLOSE),该v3记录包含完整的文件名和时间戳。

USN_RECORD_EXTENT的取证

USN_RECORD_EXTENT结构的引入是v4版本最核心的取证价值所在:

typedef struct {
    LONGLONG Offset;  // 相对于文件开头的字节偏移
    LONGLONG Length;  // 修改的长度(字节数)
} USN_RECORD_EXTENT;

每个扩展精确记录了一个字节范围的修改。当攻击者使用Timestomping修改文件元数据时,如果同时修改了文件的任何数据字节(例如在植入后门时对文件末尾追加了恶意代码段),这些修改将被v4记录捕获为特定偏移范围内的DATA_OVERWRITE事件。取证分析师可以通过汇总同一文件的所有v4记录,精确还原攻击者在文件中修改的具体位置和规模——这是传统MFT分析完全无法获得的信息。

$LogFile事务日志

日志记录的物理特征与操作码

$LogFile是NTFS文件系统的事务日志,其主要功能是记录文件系统元数据的变更操作,以确保在系统意外中断时能够快速恢复文件系统的一致性。它通过预写式日志机制跟踪文件系统结构的变化,为系统提供故障恢复能力。

$LogFile位于每个NTFS卷的根目录,是二进制格式,需借助专业工具解析。与UsnJrnl记录高层次操作不同,$LogFile记录的是底层的元数据变更,一个单一的UsnJrnl事件可能在$LogFile中对应数十条甚至上百条低层次记录。

常见的$LogFile操作模式及其取证含义:

操作
$LogFile操作码
$UsnJrnl对应事件
文件/目录创建
InitializeFileRecordSegment + AddIndexEntryAllocation
FileCreate
文件/目录删除
DeleteIndexEntryAllocation + DeallocateFileRecordSegment
FileDelete
文件/目录重命名
DeleteIndexEntryAllocation + AddIndexEntryAllocation
RenameOldName → RenameNewName
ADS创建
CreateAttribute (名称以”:ADS”结尾)
StreamChange
文件数据修改
(操作码本身不足以确定修改类型)
DataOverwrite / DataExtend / DataTruncation

事务原子性与证据完整性

$LogFile的设计基于“写前日志”原理——在对元数据进行实际修改之前,NTFS首先在$LogFile中写入一条记录,详细说明将要执行的操作以及如何撤销。当系统崩溃发生时,NTFS在挂载卷时使用这些记录将文件系统恢复到一致状态。

这一机制的关键取证含义在于:$LogFile提供了一条永久性的、不可逆的修改记录,这与MFT中可以被多次覆盖的时间戳字段形成鲜明对比。

时间戳篡改的日志签名

当攻击者使用Timestomping修改文件的$SI时间戳时,$LogFile中会留下一个可识别的操作序列:

  1. OpenFileRecord — 打开目标文件的MFT记录
  2. SetBasicInfo — 修改$STANDARD_INFORMATION属性中的时间戳字段
  3. UpdateStandardInfo — 确认MFT记录中的$SI属性已被更新
  4. CloseFileRecord — 关闭MFT记录

如果攻击者同时通过重命名或移动文件来同步修改$FN时间戳(利用Windows自动复制$SI$FN的机制),$LogFile中还会包含:

  1. DeleteIndexEntry — 从父目录索引中移除旧文件名
  2. UpdateFileName — 更新$FILE_NAME属性中的时间戳
  3. AddIndexEntry — 向父目录索引中添加新文件名

这一整套操作序列在$LogFile中形成了一条前后连贯、时间对齐的证据链——无论攻击者在MFT中如何修改时间戳,$LogFile中各操作的相对时序和间隔都揭示了真实的修改过程。

四源交叉验证

四套时间戳体系并行验证

NTFS文件系统中,一个文件的创建、修改和访问历史实际上被四套独立的时间戳体系所记录:

数据源
时间戳类型
精度
可被篡改程度
存储位置
$SI
MACB
100纳秒
用户态API即可修改
MFT $STANDARD_INFORMATION属性
$FN
MACB
100纳秒
需要内核API或文件重命名
MFT $FILE_NAME属性
USN Journal
操作时间
取决于记录
需内核权限且操作会留下新记录
UsnJrnl:$J
$LogFile
事务时间
取决于记录
极高难度,需直接写磁盘
卷根目录$LogFile

交叉验证逻辑:如果一个文件在这四套时间戳之间显示不一致(例如$SI显示2018年的创建时间,但USN Journal记录显示该文件于2025年才首次出现FILE_CREATE事件,且$LogFile显示同期的InitializeFileRecordSegment操作),那么$SI的时间戳必然是伪造的。

实际取证流程

假设取证分析在进行入侵事件响应时,发现了一个可疑的可执行文件svchost_backdoor.exe,其$SI创建时间为2020年3月15日——这一日期远早于已知的攻击事件时间线(2025年8月)。合理的分析流程如下:

第一步:$SI / $FN 比对

使用任意MFT解析工具提取文件的两组时间戳。如果$SI$FN存在显著差异(例如$SI显示2020年但$FN显示2025年),立即标记为可疑。

第二步:USN Journal 回溯

使用MFTECmd等工具解析$UsnJrnl:$J,搜索该文件的MFT引用号。

下载地址:

https://github.com/EricZimmerman/MFTECmd

重点关注:

  • 是否存在USN_REASON_FILE_CREATE(0x00000100)记录——该记录的时间戳即为文件被首次创建的精确时刻
  • 是否存在USN_REASON_BASIC_INFO_CHANGE(0x00008000)记录——该记录的时间戳通常对应Timestomping操作发生的时刻

如果USN Journal显示文件首次出现在系统中的时间是2025年8月17日,而MFT $SI却声称创建于2020年3月,矛盾成立。

第三步:$LogFile 深度验证

使用NTFS Log Tracker解析$LogFile,搜索与该文件MFT记录关联的事务日志条目。重点关注SetBasicInfo操作记录——该操作修改了$SI属性中的时间戳字段。此操作发生的时间即为Timestomping的真实时刻。

第四步:纳秒精度检验

检查可疑时间戳的纳秒分量。如果FILETIME值的低32位显示纳秒部分全为零,这是使用系统工具进行时间戳篡改的强烈信号——尽管不适用于高级攻击者。

第五步:结论

综合以上四个数据源的发现,形成完整的证据链,明确指出时间戳何时被篡改、通过何种手段进行以及篡改前的真实文件操作时间线。

取证分析的局限性

需要指出,即使是四源交叉验证也并非万能的。以下情况可能导致误判:

  • $LogFile的高周转率$LogFile的大小通常限制在64MB以内。在文件系统繁忙的服务器上,旧的事务日志可能在几分钟内就被新记录覆盖。攻击事件数月后才进行的取证分析可能无法从$LogFile中获取相关记录。
  • USN Journal的大小限制$UsnJrnl:$J的大小由系统决定(通常为32MB至128MB)。在繁忙系统上,USN记录可能仅保留数小时至数天的历史。
  • MFT条目复用:当文件被删除后,其MFT条目可能被新文件复用。如果sequence_number不匹配,USN记录中引用的MFT条目可能指向一个完全不同的文件——exhume_ntfs等工具通过追踪MFT条目复用事件(检测sequence_number变化)来减少此类误判。

实战取证工具链

MFTECmd

MFTECmd是Eric Zimmerman开发的命令行取证工具,用于批量解析$MFT$J文件。对于$J分析,该工具还需要MFT文件进行交叉链接,因为UsnJrnl包含文件引用号但不直接包含文件名和路径。

使用方法:

# 解析 MFT
MFTECmd.exe -f "C:\ForensicEvidence\$MFT" --csv "C:\Output"

# 解析 USN Journal ($J) — 需要同时提供 $MFT 进行交叉链接
MFTECmd.exe -f "C:\ForensicEvidence\$J" -m "C:\ForensicEvidence\$MFT" --csv "C:\Output"

# 解析 $LogFile
MFTECmd.exe -f "C:\ForensicEvidence\$LogFile" --csv "C:\Output"

解析库与Rust实现

exhume_ntfs是一个由forensicxlab开发的Rust语言开源NTFS取证解析库,提供了完整的MFT、USN Journal和$LogFile解析功能。以下是其MFT解析的核心流程:

// exhume_ntfs/src/mft.rs

impl MftParser {
    /// 从原始字节解析单个MFT记录
    pub fn parse_record(&self, raw_bytes: &[u8]) -> Result<MftRecord> {
        // 步骤1: 读取FileRecordHeader(固定42字节)
let header = FileRecordHeader::parse(&raw_bytes[..42])?;

        // 步骤2: 验证签名
if &header.signature != b"FILE" {
return Err(Error::InvalidSignature);
        }

        // 步骤3: 应用更新序列数组修复
let fixed_bytes = self.apply_usa_fixup(raw_bytes, &header)?;

        // 步骤4: 从attrs_offset开始解析属性链
let mut offset = header.attrs_offset as usize;
let mut attributes = Vec::new();

        loop {
let attr_header = AttributeHeaderCommon::parse(&fixed_bytes[offset..])?;

            // 遇到结束标记 (0xFFFFFFFF)
if attr_header.attr_type == 0xFFFFFFFF {
break;
            }

            // 根据属性类型选择解析器
let attr = match attr_header.attr_type {
                0x10 => self.parse_standard_information(&fixed_bytes[offset..])?,
                0x30 => self.parse_file_name(&fixed_bytes[offset..])?,
                0x80 => self.parse_data(&fixed_bytes[offset..])?,
                _ => self.parse_generic_attribute(&fixed_bytes[offset..])?,
            };

            offset += attr_header.total_size as usize;
            attributes.push(attr);
        }

        Ok(MftRecord { header, attributes })
    }
}

Timestomping 的技术全貌

三种 Timestomping 手段

用户态API直接修改

攻击者使用最基础的Timestomping手段——通过用户态API直接修改$SI时间戳。Cobalt Strike内置的timestomp模块、Metasploit的timestomp后渗透模块以及独立工具Timestomp.exe均使用此方法:

# 方法1: PowerShell — 仅修改 $SI,精度秒级
(Get-Item "backdoor.dll").CreationTime = (Get-Date "2020-03-15")
(Get-Item "backdoor.dll").LastWriteTime = (Get-Date "2020-03-15")
(Get-Item "backdoor.dll").LastAccessTime = (Get-Date "2020-03-15")

# 方法2: fsutil — 内核级 API,同样仅修改 $SI
fsutil behavior set disablelastaccess 0

这两种方法仅修改$SI时间戳,且纳秒分量通常被置零。取证人员可通过$SI/$FN比对和纳秒全零检测轻易发现。

利用文件重命名同步篡改$FN

攻击者首先使用Timestomping修改文件的$SI属性,然后移动或重命名该文件。Windows会自动将修改后的$SI时间戳复制到$FN属性中,从而使两组时间戳保持一致。

尽管$SI$FN之间不再存在不一致,但这种手段仍然在USN Journal中留下了可检测的痕迹:USN_REASON_BASIC_INFO_CHANGE记录对应Timestomping时刻,随后紧跟的USN_REASON_RENAME_OLD_NAMEUSN_REASON_RENAME_NEW_NAME记录对应文件重命名时刻。攻击者对这两类事件的时间戳无法进行追溯性修改。

内核级API直接写入$FN

在未引入Patch Guard的旧版Windows系统上,攻击者可以使用SetMace命令调用API——NtSetInformationFile和NtQueryInformationFile,直接修改文件的$FN属性中的时间戳。此外,通过直接访问磁盘并写入原始NTFS结构,攻击者可以修改MFT中两个属性的时间戳而不触发任何文件系统级事件——这是一种极其隐蔽的操作,即使USN Journal和$LogFile也可能无法捕获。但需要强调的是,关闭$LogFile本身需要管理员权限,且直接写磁盘的操作极其危险——一个字节的错误就可能损坏整个文件系统。大多数攻击者选择接受USN Journal中的痕迹,而非冒险进行更隐蔽的操作。

为什么大多数攻击者做不到完全无痕

从上述三种手段可以看出,绝大多数Timestomping操作都不同程度地留下了痕迹。根本原因在于:NTFS的文件系统日志机制与时间戳存储是耦合设计的——修改元数据属性的行为本身会被日志记录,日志中的操作时间戳无法通过用户态手段进行追溯性修改。

真正无痕的Timestomping需要满足以下全部条件:

  • 在内核级别通过直接磁盘写入同时修改$SI$FN
  • 同时修改或删除USN Journal中相关的BASIC_INFO_CHANGE记录
  • 同时修改或删除$LogFile中相关的SetBasicInfo事务记录
  • 确保所有修改的纳秒精度一致

在实际攻击中,同时完成上述所有操作极其困难——USN Journal和$LogFile使用不同的二进制格式和存储机制,且对日志的修改本身又会产生新的日志记录。这使得完全无痕的Timestomping在技术上是理论可行的,但在实际操作中几乎无法实现。

$LogFile漏洞

CVE-2025-49689

2025年,安全研究员Sergey Tarasov在NTFS的$LogFile日志重放机制中发现了一个已存在超过二十年的本地提权漏洞——CVE-2025-49689。该漏洞自Windows XP SP1时代就已存在,影响范围覆盖至今的Windows 11 22H2。

漏洞的根源在于NTFS挂载卷时调用的一条函数链——ntfs!NtfsMountVolume在系统启动时解析$LogFile中的事务记录,通过日志重放恢复文件系统一致性。在这一过程中,一个整型溢出触发了逻辑不一致性链,最终赋予了攻击者强大的利用原语,包括任意覆写和安全描述符篡改。

漏洞取证

CVE-2025-49689的核心教训在于:$LogFile不仅是取证数据的来源,其本身也是攻击面的一部分。

攻击者可以利用$LogFile的日志重放机制向文件系统注入恶意记录,制造虚假的操作时间线,或者利用任意覆写原语直接擦除$LogFile中的关键事务记录。

这一发现对取证分析提出了新的挑战:在利用$LogFile作为“不可篡改的证据源”时,分析人员必须同时评估$LogFile本身是否可能已被漏洞利用或恶意篡改。常规的取证假设——“$LogFile记录总是真实可信的”——需要在漏洞利用场景下重新审视。

自动化检测

自动化交叉验证脚本

手动执行上述所有比对步骤对取证分析师而言过于耗时。以下是一个自动化的四源交叉验证Python脚本:

#!/usr/bin/env python3
"""
自动化四源 Timestomping 检测引擎
基于 MFT ($SI/$FN)、USN Journal ($J) 和 $LogFile 的交叉验证
依赖: MFTECmd 已预先生成的 CSV 输出文件
"""

import csv
import struct
from datetime import datetime, timedelta
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional, List, Dict, Tuple

# ===== FILETIME 工具函数 =====
WINDOWS_TICK = 10000000
SEC_TO_UNIX_EPOCH = 11644473600

def filetime_to_datetime(ft: int) -> datetime:
"""将 64 位 FILETIME 转换为 datetime 对象"""
if ft == 0:
return datetime(1601, 1, 1)
return datetime.utcfromtimestamp((ft - SEC_TO_UNIX_EPOCH * WINDOWS_TICK) / WINDOWS_TICK)

def check_nanosecond_zero(ft: int) -> bool:
"""检查 FILETIME 的纳秒部分是否全为零 (Timestomping 信号)"""
# FILETIME 的低 32 位包含纳秒精度
return (ft % 10000000) == 0

# ===== 数据结构定义 =====
@dataclass
class MftTimestampRecord:
"""MFT 时间戳记录 — 从 MFTECmd 输出的 MFT CSV 中提取"""
    file_name: str
    full_path: str
    mft_entry: int
    si_created: datetime      # $SI 创建时间
    si_modified: datetime     # $SI 修改时间
    si_accessed: datetime     # $SI 访问时间
    si_mft_changed: datetime  # $SI MFT 变更时间
    fn_created: datetime      # $FN 创建时间
    fn_modified: datetime     # $FN 修改时间
    fn_accessed: datetime     # $FN 访问时间
    fn_mft_changed: datetime  # $FN MFT 变更时间
    si_created_raw: int       # 原始 FILETIME 值 (用于纳秒检测)
    fn_created_raw: int

@dataclass
class UsnRecord:
"""USN Journal 记录"""
    timestamp: datetime
    reason: str
    reason_code: int
    mft_entry: int
    file_name: str

@dataclass
class LogFileRecord:
"""$LogFile 事务记录"""
    timestamp: datetime
    operation: str
    mft_entry: int
    attribute_type: Optional[str]
    lsn: int

@dataclass
class TimestompingAlert:
"""Timestomping 检测告警"""
    file_path: str
    severity: str           # LOW / MEDIUM / HIGH / CRITICAL
    alert_type: str         # 告警类型
    evidence: List[str]     # 证据列表
    si_time: Optional[datetime]
    fn_time: Optional[datetime]
    usn_time: Optional[datetime]
    logfile_time: Optional[datetime]


class TimestompingDetector:
    def __init__(self):
        self.mft_records: Dict[int, MftTimestampRecord] = {}
        self.usn_records: Dict[int, List[UsnRecord]] = defaultdict(list)
        self.logfile_records: Dict[int, List[LogFileRecord]] = defaultdict(list)

    def load_mft_csv(self, csv_path: str):
"""加载 MFTECmd 的 MFT CSV 输出"""
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
for row in reader:
                entry = int(row.get('EntryNumber', 0))
                si_c = int(row.get('SICreated', 0))
                fn_c = int(row.get('FNCreated', 0))

                record = MftTimestampRecord(
                    file_name=row.get('FileName', ''),
                    full_path=row.get('FullPath', ''),
                    mft_entry=entry,
                    si_created=filetime_to_datetime(si_c),
                    si_modified=filetime_to_datetime(int(row.get('SILastModified', 0))),
                    si_accessed=filetime_to_datetime(int(row.get('SILastAccess', 0))),
                    si_mft_changed=filetime_to_datetime(int(row.get('SIMFTChanged', 0))),
                    fn_created=filetime_to_datetime(fn_c),
                    fn_modified=filetime_to_datetime(int(row.get('FNLastModified', 0))),
                    fn_accessed=filetime_to_datetime(int(row.get('FNLastAccess', 0))),
                    fn_mft_changed=filetime_to_datetime(int(row.get('FNMFTChanged', 0))),
                    si_created_raw=si_c,
                    fn_created_raw=fn_c,
                )
                self.mft_records[entry] = record

    def load_usn_csv(self, csv_path: str):
"""加载 MFTECmd 的 USN Journal CSV 输出"""
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
for row in reader:
                entry = int(row.get('FileMFTEntryNumber', 0))
                ts = filetime_to_datetime(int(row.get('TimeStamp', 0)))
                reason = row.get('Reason', '')
                reason_code = int(row.get('ReasonCode', 0), 16) if row.get('ReasonCode', '0').startswith('0x') else int(row.get('ReasonCode', 0))

                self.usn_records[entry].append(UsnRecord(
                    timestamp=ts,
                    reason=reason,
                    reason_code=reason_code,
                    mft_entry=entry,
                    file_name=row.get('Name', '')
                ))

    def load_logfile_csv(self, csv_path: str):
"""加载 $LogFile 解析输出"""
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
for row in reader:
                entry = int(row.get('MftEntry', 0))
                self.logfile_records[entry].append(LogFileRecord(
                    timestamp=datetime.fromisoformat(row.get('Timestamp', '1601-01-01T00:00:00')),
                    operation=row.get('Operation', ''),
                    mft_entry=entry,
                    attribute_type=row.get('AttributeType', None),
                    lsn=int(row.get('LSN', 0))
                ))

    def detect_all(self) -> List[TimestompingAlert]:
"""运行所有检测规则"""
        alerts = []
for entry, mft in self.mft_records.items():
            alerts.extend(self._check_si_fn_discrepancy(mft))
            alerts.extend(self._check_nanosecond_zero(mft))
            alerts.extend(self._check_usn_mft_conflict(mft))
return alerts

    def _check_si_fn_discrepancy(self, mft: MftTimestampRecord) -> List[TimestompingAlert]:
"""检测 $SI 与 $FN 时间戳不一致"""
        alerts = []
        discrepancy_threshold = timedelta(seconds=1)

if abs(mft.si_created - mft.fn_created) > discrepancy_threshold:
            alerts.append(TimestompingAlert(
                file_path=mft.full_path,
                severity="HIGH",
                alert_type="SI_FN_CREATION_MISMATCH",
                evidence=[
                    f"$SI 创建时间: {mft.si_created}",
                    f"$FN 创建时间: {mft.fn_created}",
                    f"时间差: {abs(mft.si_created - mft.fn_created)}"
                ],
                si_time=mft.si_created, fn_time=mft.fn_created,
                usn_time=None, logfile_time=None
            ))
return alerts

    def _check_nanosecond_zero(self, mft: MftTimestampRecord) -> List[TimestompingAlert]:
"""检测时间戳纳秒部分全为零 (系统工具 Timestomping 的信号)"""
        alerts = []
if mft.si_created_raw > 0 and check_nanosecond_zero(mft.si_created_raw):
            alerts.append(TimestompingAlert(
                file_path=mft.full_path,
                severity="MEDIUM",
                alert_type="NANOSECOND_ZERO",
                evidence=[
                    f"$SI 创建时间纳秒全零: {mft.si_created}",
"信号: 可能使用系统工具 (PowerShell/fsutil) 进行 Timestomping"
                ],
                si_time=mft.si_created, fn_time=None,
                usn_time=None, logfile_time=None
            ))
return alerts

    def _check_usn_mft_conflict(self, mft: MftTimestampRecord) -> List[TimestompingAlert]:
"""检测 USN Journal 与 MFT 时间戳冲突"""
        alerts = []
        usn_list = self.usn_records.get(mft.mft_entry, [])

for usn in usn_list:
if usn.reason_code & 0x00000100:  # USN_REASON_FILE_CREATE
if usn.timestamp > mft.si_created:
                    alerts.append(TimestompingAlert(
                        file_path=mft.full_path,
                        severity="CRITICAL",
                        alert_type="USN_CREATE_AFTER_SI_CREATED",
                        evidence=[
                            f"USN FILE_CREATE: {usn.timestamp}",
                            f"$SI 创建时间: {mft.si_created}",
"矛盾: 文件在 MFT 中声称的创建时间早于 USN Journal 记录的实际创建时间!"
                        ],
                        si_time=mft.si_created, fn_time=mft.fn_created,
                        usn_time=usn.timestamp, logfile_time=None
                    ))

if usn.reason_code & 0x00008000:  # USN_REASON_BASIC_INFO_CHANGE
                alerts.append(TimestompingAlert(
                    file_path=mft.full_path,
                    severity="HIGH",
                    alert_type="BASIC_INFO_CHANGE_DETECTED",
                    evidence=[
                        f"USN BASIC_INFO_CHANGE: {usn.timestamp}",
"该记录直接对应 Timestomping 操作",
"USN Journal 中的此时间戳无法通过用户态手段篡改"
                    ],
                    si_time=None, fn_time=None,
                    usn_time=usn.timestamp, logfile_time=None
                ))
return alerts


# ===== 使用示例 =====
if __name__ == "__main__":
    detector = TimestompingDetector()

# 加载由 MFTECmd 预先生成的 CSV 文件
    detector.load_mft_csv("./forensic_output/MFT.csv")
    detector.load_usn_csv("./forensic_output/USNJrnl.csv")

# 检测
    alerts = detector.detect_all()

# 分级输出
print("=" * 70)
print("NTFS Timestomping 检测报告")
print("=" * 70)

    severity_count = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
for alert in sorted(alerts, key=lambda a: ["CRITICAL","HIGH","MEDIUM","LOW"].index(a.severity)):
        severity_count[alert.severity] += 1
print(f"\n[{alert.severity}] {alert.alert_type}")
print(f"    文件: {alert.file_path}")
for evidence in alert.evidence:
print(f"    - {evidence}")

print(f"\n总计: {len(alerts)} 条告警 "
          f"(CRITICAL: {severity_count['CRITICAL']}, "
          f"HIGH: {severity_count['HIGH']}, "
          f"MEDIUM: {severity_count['MEDIUM']})")

说明:该Python脚本实现了自动化四源交叉验证引擎。通过加载MFTECmd预先生成的CSV文件,检测器运行三种核心规则:$SI/$FN不一致检测(关注创建时间差异超1秒的情况)、纳秒全零检测(识别使用系统工具的初级Timestomping)、以及USN Journal与MFT冲突检测(当USN Journal记录的FILE_CREATE时间晚于MFT $SI声称的创建时间时触发最高级别告警)。该脚本可与MFTECmd输出的CSV文件无缝集成,适用于大规模批量取证场景。

结语

攻击者可以伪造文件的时间,但无法伪造文件在文件系统中的存在历史。当取证的同时掌握MFT的$SI/$FN差异比对、USN Journal的操作序列回溯和$LogFile的事务日志深挖这三项技能,NTFS便从一个可以被操纵的谎言机器,转变为一条不可磨灭的数字痕迹链。