摘要
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的旧操作系统上,使用NtSetInformationFile和NtQueryInformationFile直接写入;其二,在任何操作系统上先篡改$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的关键字段包括:
其中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并附带文件名。
三个版本的对比可从以下维度理解:
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条目复用检测
} 该结构体的字段设计反映了三个关键取证点:
reason字段的位掩码可直接用于筛选特定类型的变更(如仅关注BASIC_INFO_CHANGE以检测Timestomping)timestamp字段为FILETIME格式,精度100纳秒,可直接与MFT时间戳进行比较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的设计基于“写前日志”原理——在对元数据进行实际修改之前,NTFS首先在$LogFile中写入一条记录,详细说明将要执行的操作以及如何撤销。当系统崩溃发生时,NTFS在挂载卷时使用这些记录将文件系统恢复到一致状态。
这一机制的关键取证含义在于:$LogFile提供了一条永久性的、不可逆的修改记录,这与MFT中可以被多次覆盖的时间戳字段形成鲜明对比。
时间戳篡改的日志签名
当攻击者使用Timestomping修改文件的$SI时间戳时,$LogFile中会留下一个可识别的操作序列:
OpenFileRecord— 打开目标文件的MFT记录SetBasicInfo— 修改$STANDARD_INFORMATION属性中的时间戳字段UpdateStandardInfo— 确认MFT记录中的$SI属性已被更新CloseFileRecord— 关闭MFT记录
如果攻击者同时通过重命名或移动文件来同步修改$FN时间戳(利用Windows自动复制$SI到$FN的机制),$LogFile中还会包含:
DeleteIndexEntry— 从父目录索引中移除旧文件名UpdateFileName— 更新$FILE_NAME属性中的时间戳AddIndexEntry— 向父目录索引中添加新文件名
这一整套操作序列在$LogFile中形成了一条前后连贯、时间对齐的证据链——无论攻击者在MFT中如何修改时间戳,$LogFile中各操作的相对时序和间隔都揭示了真实的修改过程。
四源交叉验证
四套时间戳体系并行验证
NTFS文件系统中,一个文件的创建、修改和访问历史实际上被四套独立的时间戳体系所记录:
交叉验证逻辑:如果一个文件在这四套时间戳之间显示不一致(例如$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_NAME和USN_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便从一个可以被操纵的谎言机器,转变为一条不可磨灭的数字痕迹链。