跳到主要内容

NTFS 文件系统时间戳取证分析:Timestomping 检测与四源交叉验证框架

· 阅读需 19 分钟
ICE Lab
Institute of Cyber Environment

摘要

文件系统时间戳是数字取证中重建攻击时间线的核心依据,但其本身也是攻击者篡改的重点目标。NTFS 文件系统同时维护四套独立的时间戳记录机制——MFT 的 $STANDARD_INFORMATION 与 $FILE_NAME 属性、USN Journal 变更日志与 $LogFile 事务日志,各自以不同的精度、更新规则与访问权限运行。本文系统分析 NTFS 时间戳架构的取证价值,分类讨论三种 Timestomping 手段的技术原理与固有痕迹,提出基于四源交叉验证的自动化检测框架,并给出基于 MFTECmd 与 exhume_ntfs 的实战工具链实现。

关键词:NTFS;Timestomping;数字取证;USN Journal;MFT

1. 引言

NTFS 是 Windows 操作系统的核心文件系统,其时间戳存储机制远非单一的时间值数组。MFT(主文件表)中的 $STANDARD_INFORMATION($SI)和 $FILE_NAME($FN)属性各自独立维护文件的 MACB(修改、访问、MFT 变更、创建)时间戳,且更新规则迥异。此外,USN Journal 以变更日志形式记录每次文件操作的精确时刻与原因码,$LogFile 则以事务日志形式保存元数据操作的底层序列。

这种多源冗余设计在文件系统一致性层面或属偶然,但在数字取证领域构成了天然的时间戳交叉验证体系。攻击者常利用 Timestomping 技术篡改 $SI 时间戳以掩盖入侵痕迹,但 $FN、USN Journal 及 $LogFile 中的对应记录往往被忽略——三者之间的微秒级精度不一致,恰构成最坚固的证据链。

2. NTFS 时间戳架构

2.1 $SI 与 $FN 双轨机制

NTFS 以 MFT 为核心索引结构,每个文件对应一条 MFT 记录。每条记录包含多个属性,其中 $STANDARD_INFORMATION(属性类型 0x10)与 $FILE_NAME(属性类型 0x30)各自独立存储文件的 MACB 时间戳。

$SI 属性的时间戳与文件内容的交互相关——文档编辑、权限变更或数据写入均会触发 $SI 更新。$FN 属性的时间戳与文件位置和名称的交互相关——重命名、移动等操作触发 $FN 更新。这种设计分歧制造了天然的双轨取证系统:同一文件在不同元数据属性中可能存储着差异化的时间信息。

关键点在于,攻击者通常只修改 $SI 时间戳,而忽略 $FN 中的副本。修改 $SI 可通过用户态 API 完成——Cobalt Strike、Timestomp.exe 和 Metasploit 均内置此功能。修改 $FN 则需内核 API 或利用 Windows 的文件重命名机制将 $SI 自动复制到 $FN。

2.2 MFT 记录物理结构

每条 MFT 记录固定为 1024 字节(1 KiB)。前 42 字节为 FileRecordHeader,其后为属性序列,以 0xFFFFFFFF 结束标记终止。关键字段如下:

字段类型偏移描述
signature[u8; 4]0x00总是 b"FILE"
usa_offsetu160x04更新序列数组偏移
lsnu640x08$LogFile 序列号,用于日志关联
sequence_numberu160x10每次记录复用递增
attrs_offsetu160x14首个属性偏移(通常 0x30
flagsu160x160x01=已分配, 0x02=目录
base_file_recordu640x20扩展记录指向基本记录

其中 lsn 字段是连接 MFT 与 $LogFile 两大取证数据源的核心桥梁,记录了最后一次修改此 MFT 记录的日志序列号。属性头部标识类型(0x10 = $SI,0x30 = $FN,0x80 = $DATA)及常驻/非常驻状态。

2.3 时间戳精度与纳秒分析

NTFS 时间戳以 64 位 FILETIME 格式存储,精度 100 纳秒,起始点 1601 年 1 月 1 日。正常文件操作产生的时间戳具有精确的纳秒分量——例如 131683876627452045 对应 2018-04-16 21:27:42.7452045。当攻击者使用系统工具(如 PowerShell 的 Get-Item)修改时间戳时,纳秒分量通常被置零——131683876620000000 虽然对应同一时刻,但纳秒部分全零在整数表示中极为突出。

值得强调的是,纳秒全零检测仅能捕获使用标准系统工具的低水平攻击者。经过定制的高级攻击工具(如 Metasploit 的 timestomp 模块)可生成带真实纳秒精度的时间戳,此时纳秒检测本身不足以排除篡改可能。

3. 文件系统日志机制

3.1 USN Journal 变更日志

USN Journal(\$Extend\$UsnJrnl)是 NTFS 为每次文件系统变更提供的持久化日志,包含两个备用数据流:$J(实际变更记录)和 $Max(配置信息)。其核心价值在于记录文件创建、删除、重命名与数据变更的高层次操作,不受操作者权限或意图影响。

USN Record 经历三个版本演进:

版本引入版本文件引用大小包含文件名包含时间戳核心特性
v2Vista64 位基础记录
v3Windows 8128 位扩展文件引用
v4Windows 10128 位Range Tracking

v4 记录的核心设计转变在于关注文件被修改的具体字节范围而非文件名与时间戳。其 USN_RECORD_EXTENT 结构(Offset / Length)精确记录修改的字节偏移范围。Windows 保证每次 Range Tracking 产生 v4 记录后,文件关闭时会追加一条含 USN_REASON_CLOSE 的 v3 记录以补全文件名和时间戳。

USN_REASON 位掩码的关键取证含义:

  • USN_REASON_DATA_OVERWRITE0x00000002):精确标记文件数据被覆盖的时刻。攻击者修改 MFT $SI 时间戳后,后续任何数据写入操作均被记录,且此记录无法被追溯性删除或篡改。
  • USN_REASON_BASIC_INFO_CHANGE0x00008000):直接对应文件属性修改,包括时间戳篡改本身——该记录就是 Timestomping 操作的最直接证据。
  • USN_REASON_FILE_CREATE0x00000100):文件首次创建的精确时刻,可用于与 MFT 声称的创建时间进行比对。

3.2 $LogFile 事务日志

$LogFile 是 NTFS 的事务日志,以预写式日志(WAL)机制记录文件系统元数据的每一次变更操作。与 USN Journal 记录高层次操作不同,$LogFile 记录的是底层元数据变更——一个 USN 事件可能在 $LogFile 中对应数十条低层次记录。

常见操作码与取证含义:

操作$LogFile 操作USN Journal 对应事件
文件创建InitializeFileRecordSegment + AddIndexEntryAllocationFileCreate
文件删除DeleteIndexEntryAllocation + DeallocateFileRecordSegmentFileDelete
重命名DeleteIndexEntryAllocation + AddIndexEntryAllocationRenameOldNameRenameNewName
时间戳修改SetBasicInfo + UpdateStandardInfoBasicInfoChange

其关键取证价值在于:当攻击者修改 $SI 时间戳时,$LogFile 中会留下 OpenFileRecord → SetBasicInfo → UpdateStandardInfo → CloseFileRecord 的操作序列。若攻击者通过文件重命名同步修改 $FN,还会包含 DeleteIndexEntry → UpdateFileName → AddIndexEntry。无论 MFT 中如何修改,$LogFile 中各操作的相对时序和间隔均揭示真实过程。

4. Timestomping 技术分类分析

4.1 用户态 API 修改($SI 层)

攻击者利用用户态 API 直接修改 $SI 时间戳,是最基础也最广泛使用的 Timestomping 手段:

(Get-Item "backdoor.dll").CreationTime = (Get-Date "2020-03-15")
(Get-Item "backdoor.dll").LastWriteTime = (Get-Date "2020-03-15")

痕迹:仅修改 $SI,$FN 不随动,两者时间戳产生偏差;纳秒分量被置零;USN Journal 中产生 BASIC_INFO_CHANGE 记录。

4.2 文件重命名同步修改($FN 层)

攻击者先通过用户态 API 修改 $SI,随后移动或重命名文件。Windows 自动将修改后的 $SI 时间戳复制到 $FN 属性,使两组时间戳保持一致。

痕迹:尽管 $SI/$FN 不再矛盾,但 USN Journal 中同时留下 BASIC_INFO_CHANGE(Timestomping 时刻)和 RENAME_OLD_NAME / RENAME_NEW_NAME(重命名时刻)的记录序列。攻击者无法追溯性修改这两类事件的时间戳。

4.3 内核级 API 直接写入($SI + $FN 层)

在未引入 Patch Guard 的旧版 Windows 上,攻击者可通过 NtSetInformationFileNtQueryInformationFile 直接修改 $FN 时间戳。通过直接磁盘写入,还可同时修改两条 MFT 属性而不触发文件系统级事件。

痕迹:$SI 与 $FN 一致,USN Journal 仍会记录 BASIC_INFO_CHANGE。直接磁盘写入虽可绕过部分日志,但对 $LogFile 的篡改本身会产生新的日志记录——完全无痕在技术上极其困难。

4.4 局限性分析

真正无痕的 Timestomping 需同时满足:内核级直接磁盘写入修改 $SI 和 $FN;修改或删除 USN Journal 中关联的 BASIC_INFO_CHANGE 记录;修改或删除 $LogFile 中关联的事务记录;确保所有修改的纳秒精度一致。USN Journal 与 $LogFile 使用不同的二进制格式和存储机制,且对日志的修改本身又会产生新的记录——这使得完全无痕的 Timestomping 在理论可行但实际操作中几乎无法实现。

4.5 $LogFile 漏洞(CVE-2025-49689)

2025 年,安全研究员 Sergey Tarasov 披露了 $LogFile 日志重放机制中的本地提权漏洞 CVE-2025-49689,影响自 Windows XP SP1 以来所有版本。漏洞根源在于 ntfs!NtfsMountVolume 解析 $LogFile 事务记录时的整型溢出,可导致任意覆写与安全描述符篡改。

这一发现对取证分析提出了新的挑战:攻击者可利用 $LogFile 日志重放机制向文件系统注入恶意记录,制造虚假操作时间线,或直接擦除关键事务记录。常规的取证假设——"$LogFile 记录总是真实可信"——需要在漏洞利用场景下重新审视。

5. 四源交叉验证框架

5.1 验证逻辑

给定以下四套独立时间戳记录体系:

数据源时间戳类型精度可篡改程度存储位置
$SIMACB100 纳秒用户态 APIMFT $STANDARD_INFORMATION
$FNMACB100 纳秒需内核 API 或重命名MFT $FILE_NAME
USN Journal操作时间取决于记录需内核权限,操作留痕\$UsnJrnl:\$J
$LogFile事务时间取决于记录极高难度,需直接写磁盘\$LogFile

交叉验证的核心逻辑:若同一文件的四套时间戳之间存在不一致(如 $SI 显示 2018 年的创建时间,但 USN Journal 显示该文件 2025 年才首次出现 FILE_CREATE 事件),则 $SI 时间戳必然是伪造的。

5.2 验证流程

以可疑可执行文件为例:

  1. $SI / $FN 比对:使用 MFT 解析工具提取两组时间戳。差异超过阈值则标记可疑。
  2. USN Journal 回溯:搜索文件的 MFT 引用号,关注 FILE_CREATE(首现时刻)和 BASIC_INFO_CHANGE(Timestomping 时刻)记录。
  3. $LogFile 深度验证:搜索关联事务日志条目,SetBasicInfo 操作的时间即为 Timestomping 真实时刻。
  4. 纳秒精度检验:检查 FILETIME 值的纳秒分量是否为全零——低级 Timestomping 的信号。

5.3 自动化检测引擎

以下 Python 脚本基于 MFTECmd 的 CSV 输出实现四源交叉验证:

#!/usr/bin/env python3
"""
四源 Timestomping 检测引擎 — 基于 MFTECmd CSV 输出的交叉验证
"""

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

WINDOWS_TICK = 10000000
SEC_TO_UNIX_EPOCH = 11644473600

def filetime_to_datetime(ft: int) -> 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:
return (ft % 10000000) == 0

@dataclass
class MftTimestampRecord:
file_name: str; full_path: str; mft_entry: int
si_created: datetime; si_modified: datetime
si_accessed: datetime; si_mft_changed: datetime
fn_created: datetime; fn_modified: datetime
fn_accessed: datetime; fn_mft_changed: datetime
si_created_raw: int; fn_created_raw: int

@dataclass
class UsnRecord:
timestamp: datetime; reason: str
reason_code: int; mft_entry: int; file_name: str

@dataclass
class TimestompingAlert:
file_path: str; severity: str
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)

def load_mft_csv(self, csv_path: str):
with open(csv_path, 'r', encoding='utf-8') as f:
for row in csv.DictReader(f):
entry = int(row.get('EntryNumber', 0))
si_c = int(row.get('SICreated', 0))
fn_c = int(row.get('FNCreated', 0))
self.mft_records[entry] = 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)

def load_usn_csv(self, csv_path: str):
with open(csv_path, 'r', encoding='utf-8') as f:
for row in csv.DictReader(f):
entry = int(row.get('FileMFTEntryNumber', 0))
ts = filetime_to_datetime(int(row.get('TimeStamp', 0)))
reason_code_str = row.get('ReasonCode', '0')
if reason_code_str.startswith('0x'):
reason_code = int(reason_code_str, 16)
else:
reason_code = int(reason_code_str)
self.usn_records[entry].append(UsnRecord(
timestamp=ts, reason=row.get('Reason', ''),
reason_code=reason_code, mft_entry=entry,
file_name=row.get('Name', '')))

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) -> List[TimestompingAlert]:
alerts = []
threshold = timedelta(seconds=1)
if abs(mft.si_created - mft.fn_created) > 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}"],
si_time=mft.si_created, fn_time=mft.fn_created,
usn_time=None, logfile_time=None))
return alerts

def _check_nanosecond_zero(self, mft) -> List[TimestompingAlert]:
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=["$SI 纳秒全零: 可能使用 PowerShell/fsutil"],
si_time=mft.si_created, fn_time=None,
usn_time=None, logfile_time=None))
return alerts

def _check_usn_mft_conflict(self, mft) -> List[TimestompingAlert]:
alerts = []
for usn in self.usn_records.get(mft.mft_entry, []):
if usn.reason_code & 0x00000100:
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 记录的实际创建时间"],
si_time=mft.si_created, fn_time=mft.fn_created,
usn_time=usn.timestamp, logfile_time=None))
if usn.reason_code & 0x00008000:
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 操作"],
si_time=None, fn_time=None,
usn_time=usn.timestamp, logfile_time=None))
return alerts

if __name__ == "__main__":
detector = TimestompingDetector()
detector.load_mft_csv("./forensic_output/MFT.csv")
detector.load_usn_csv("./forensic_output/USNJrnl.csv")
alerts = detector.detect_all()
for alert in sorted(alerts, key=lambda a:
["CRITICAL","HIGH","MEDIUM","LOW"].index(a.severity)):
print(f"[{alert.severity}] {alert.alert_type}")
print(f" 文件: {alert.file_path}")
for ev in alert.evidence:
print(f" - {ev}")

该引擎实现三条核心规则:$SI/$FN 创建时间差异超过 1 秒触发 SI_FN_CREATION_MISMATCH(HIGH);$SI 纳秒全零触发 NANOSECOND_ZERO(MEDIUM);USN Journal 的 FILE_CREATE 时间晚于 $SI 创建时间触发 USN_CREATE_AFTER_SI_CREATED(CRITICAL),BASIC_INFO_CHANGE 记录直接触发 BASIC_INFO_CHANGE_DETECTED(HIGH)。

5.4 实战工具链

推荐工具链整合如下:

  • MFTECmd(Eric Zimmerman):批量解析 $MFT$J 文件。使用方法:MFTECmd.exe -f "\$J" -m "\$MFT" --csv "C:\Output"
  • exhume_ntfs(forensicxlab):Rust 语言开源 NTFS 取证解析库,提供完整的 MFT、USN Journal 和 $LogFile 解析功能,支持 MFT 条目复用检测
  • NTFS Log Tracker:解析 $LogFile 事务日志,搜索与特定 MFT 记录关联的 SetBasicInfo 操作

6. 取证局限性

四源交叉验证虽提供了时间戳篡改检测的体系化方法,但仍存在以下局限:

  • $LogFile 高周转率:$LogFile 通常限制为 64MB,在繁忙服务器上旧记录可能在数分钟内被覆盖。
  • USN Journal 大小限制$UsnJrnl:\$J 通常为 32MB–128MB,在繁忙系统上仅保留数小时至数天的历史。
  • MFT 条目复用:文件删除后 MFT 条目可能被新文件复用,sequence_number 不匹配时 USN 记录可能指向错误文件。exhume_ntfs 等工具通过追踪复用事件来缓解此问题。
  • CVE-2025-49689 的挑战:攻击者可利用 $LogFile 漏洞向文件系统注入虚假记录或擦除真实记录,破坏 $LogFile 作为可信证据源的前提。

结论

NTFS 的四套时间戳记录机制构成了天然的时间线交叉验证体系。$SI 与 $FN 的双轨存储使简单的时间戳篡改留下可检测的差异;USN Journal 以不可追溯性修改的方式记录每一次属性变更与数据写入操作;$LogFile 以事务日志形式保存元数据操作的底层序列。三者结合的交叉验证框架能够有效检测从 PowerShell 脚本到内核级工具的不同层次 Timestomping 行为。CVE-2025-49689 的发现提醒取证分析人员,$LogFile 本身也是攻击面的一部分,需要纳入可信度评估范围。

参考文献

[1] Eric Zimmerman. "MFTECmd: MFT and USN Journal Parser." https://github.com/EricZimmerman/MFTECmd

[2] forensicxlab. "exhume_ntfs: NTFS Forensic Parsing Library." Rust implementation.

[3] Sergey Tarasov. "CVE-2025-49689: NTFS $LogFile Replay Privilege Escalation."

[4] Microsoft. "NTFS Technical Reference." MSDN Documentation.

[5] Harlan Carvey. "Windows Forensic Analysis Toolkit." 4th Edition.