javierdejesusda/logflip-closed
GitHub: javierdejesusda/logflip-closed
一款通过反向重放 NTFS $LogFile 撤销链来检测文件时间戳篡改并进行工具家族归属分析的数字取证工具。
Stars: 0 | Forks: 0
# logflip-closed





`logflip` 通过反向遍历 NTFS `$LogFile` 的撤销链,重建记录在被篡改之前*原本携带*的时间戳,随后在得出任何篡改结论之前,通过三个独立的取证通道对该重建结果进行交叉验证。其核心特征在于克制:该 pipeline 的设计使其只能返回 `clean`、`provisional` 或 `anomaly`,且只有当已签名的指纹数据库、真实的 engagement key 以及所有的验证门控达成一致时,才会最终达到 `confirmed` 状态。它的设计初衷是在沉默中犯错,也绝不制造虚假的指控。
命令行工具名为 `logflip`;代码库与 Python 包名为 `logflip-closed`。
## 目录
- [为什么开发它](#why-it-exists)
- [工作原理](#how-it-works)
- [安装说明](#install)
- [快速开始](#quick-start)
- [裁定模型](#the-verdict-model)
- [命令参考](#command-reference)
- [证据完整性](#evidence-integrity)
- [诚实的范围与局限性](#honest-scope-and-limitations)
- [现有技术与创新性](#prior-art-and-novelty)
- [项目状态与质量](#project-status-and-quality)
- [开发说明](#development)
- [文档](#documentation)
- [许可证](#license)
## 为什么开发它
Timestomping(伪造文件的 MACE 时间戳以隐藏其真实的创建或修改时间)是反取证 playbook 中最古老的手段之一,也是事后最难被证伪的手段之一。调查人员如今读取到的 `$STANDARD_INFORMATION` 时间戳,仅仅是攻击者最后一次写入的内容。原始值已从该属性本身消失。
但它们并未从日志中消失。NTFS 将许多元数据操作的*前态*(before image)作为撤销数据记录在 `$LogFile` 中,正是为了让卷能够回滚被中断的事务。`logflip` 将这种恢复机制转化为一种取证手段:它反向重放撤销链以重建篡改前的属性状态,然后探讨该重建结果、`$STANDARD_INFORMATION` 与 `$FILE_NAME` 之间的关系,以及一个已签名的工具指纹数据库是否都指向同一结论。
目前没有任何公开工具能够执行这种反向 `$LogFile` 重放来恢复篡改前状态并进行工具家族归属分析。这种“反向重放+归属分析”的设计正是其创新之处(参见 [现有技术与创新性](#prior-art-and-novelty))。
## 工作原理
`logflip` 将篡改裁定视为必须通过独立证据源之间的相互印证来*获取*的结果,而非基于任何单一信号进行断言。
```
NTFS image / volume
|
v
$MFT + $LogFile + $UsnJrnl parsers (USA fixup, multi-page RCRD reassembly)
|
v
+-----------------------------+-----------------------------+
| reverse-replay inversion | SI-vs-FN delta screen | fingerprint DB attribution |
| (backward LSN walk to | (necessary, not sufficient) | (signed, HMAC-verified) |
| recover pre-tamper SI) | | |
+-----------------------------+-----------------------------+----------------------------+
|
v
corroboration gates -> signed evidence leaf (HKDF + RFC 8785 + HMAC) -> HTML report
```
三个特性使得其输出值得信赖:
- **不完整反转防护。** 循环缓冲区滚动、跨客户端日志链、序列非单调性以及越界的撤销操作,都会被解析为 `INCONCLUSIVE` 而非猜测结果。该工具拒绝在无法可靠重建的情况下进行重建。
- **独立交叉验证。** 一个 `confirmed` 的裁定要求不同的失败模式之间达成一致,因此单一嘈杂的通道绝不能单方面促成定论。
- **结构性杜绝错误确认。** 内置的指纹家族仅包含元数据:采集分析未发现可靠的、特定于工具的 `$LogFile` 字节模式,因此每个 `pattern_hex` 都是空的。一个空模式匹配器防护在结构上使得虚假的字节匹配变得不可能。这是对“目前尚无经过验证的签名”的诚实编码,而不是等待一个签名出现的占位符。
## 安装说明
要求 Python 3.11 或更高版本。
```
python -m pip install -e ".[dev]"
```
这将安装运行时依赖(`pydantic`、`cryptography`、`pynacl`、`blake3`、`rfc8785`)以及开发工具链(`pytest`、`mypy`、`ruff`),并暴露 `logflip` 命令。分析实时卷(`--volume \\.\C:`)需要管理员权限;而分析原始镜像文件则不需要。
## 快速开始
调查一条你已经怀疑的单条记录:
```
python -m logflip detect --image disk.img --mft-record 5 --output leaf.json --report report.html
```
被篡改的记录将以退出码 `2` 退出,并打印 `verdict: provisional`(或者在使用签名数据库和真实 key 的情况下打印 `verdict: confirmed`);正常的记录将以退出码 `0` 退出,并打印 `verdict: clean`。
当你不知道是哪条记录被触碰时,让工具为你寻找候选记录:
```
python -m logflip scan --image disk.img --leaf-dir leaves/ --report-dir reports/
```
```
mft_record verdict tool_family
5 provisional -
12 clean -
31 skipped DetectionError
candidates: 3 findings: 1 skipped: 1
```
当返回至少一个发现时,`scan` 会以退出码 `2` 退出。仅针对非正常(non-clean)的记录写入 leaf 和报告文件。任何已签名的 leaf 随后都可以使用 `python -m logflip verify-leaf` 进行离线重新验证。
## 裁定模型
| 裁定 | 含义 |
| --- | --- |
| `clean` | 反向重放未在目标记录上发现任何字节不一致。 |
| `provisional` | 出现了不一致以及 SI 与 FN 之间的差异,但未确认任何工具家族。这是在使用存根数据库或演示 key 时的最高级别。 |
| `confirmed` | 在与签名数据库及非演示 key 配对的情况下,通过了所有的交叉验证门控。按照设计,存根数据库和演示 key 永远无法得出此裁定。 |
| `anomaly` | 仅有单一来源信号(在没有 `$LogFile` 覆盖的情况下出现了 SI 与 FN 的差异)。通过 `scan --include-mft-deltas` 作为调查线索呈现,绝不作为证明。 |
| 退出码 | 含义 |
| --- | --- |
| `0` | 正常:无篡改证据(或者对于 `scan` 而言,所有候选记录均正常)。 |
| `2` | 发现:至少有一个 provisional 或 confirmed 的裁定。单独的 anomaly 永远不会触发此退出码。 |
| `1` | 错误:输入损坏、I/O 失败或故障安全停止。 |
杜绝错误确认的保证适用于数百个候选记录:如果没有真实的签名 `--db` 和真实的 `--key-file`,`confirmed` 是无法达到的。
## 命令参考
| 命令 | 用途 |
| --- | --- |
| [`detect`](#detect) | 在一条已知的 `$MFT` 记录上运行完整 pipeline。 |
| [`scan`](#scan) | 枚举候选记录并在每条记录上运行 pipeline。 |
| [`verify-leaf`](#verify-leaf) | 离线重新验证已签名的证据 leaf。 |
| [`verify-db`](#verify-db) | 验证已签名指纹数据库的 HMAC 完整性。 |
| [`build-db`](#build-db) | 根据采集清单构建并签名指纹数据库。 |
| [`keygen`](#keygen) | 生成密封的 32 字节 engagement key。 |
| [`ingest-captures`](#ingest-captures) | 将操作员的采集包转换为 `build-db` 清单。 |
所有的 key 都通过 `--key-file` 从原始的 32 字节文件中读取,绝不能作为普通参数传递。
### detect
```
python -m logflip detect (--image IMAGE | --volume VOLUME) --mft-record N
[--db DB_JSON] [--key-file KEY_FILE] [--variant VARIANT_JSON]
[--usnjrnl-record N] [--output LEAF_JSON] [--report REPORT_HTML]
```
`--image` 和 `--volume` 是互斥的,且必须指定其中一个。
| 标志 | 描述 |
| --- | --- |
| `--image IMAGE` | 原始 NTFS 镜像文件的路径。 |
| `--volume VOLUME` | 实时 NTFS 卷路径(例如 `\\.\C:`)。需要管理员权限。 |
| `--mft-record`, `-m` | 要调查的目标 `$MFT` 记录号(必填)。 |
| `--db DB_JSON` | 已签名的指纹数据库。未提供时将使用存根数据库,且裁定至多维持在 provisional。 |
| `--key-file`, `-k` | 原始的 32 字节签名 key 文件。未提供时将使用合成的演示 key(无法产生 `confirmed` 结果)。 |
| `--variant VARIANT_JSON` | 选择多变体数据库中的一种变体(参见 [变体选择](#variant-selection))。 |
| `--usnjrnl-record N` | `$UsnJrnl` 中 `$J` 数据流的 `$MFT` 记录。未提供时将通过 `$Extend` 自动发现。 |
| `--output`, `-o` | 写入已签名 leaf JSON 的路径。 |
| `--report` | 写入 HTML 报告的路径。 |
当你已经知道要检查哪条记录时使用 `detect`;使用 `scan` 让工具自动寻找候选记录。
### scan
枚举解析出的 `$LogFile` 中引用的每一条不同的 `$MFT` 记录,并在每条记录上运行完整的独立 pipeline。
```
python -m logflip scan (--image IMG | --volume VOL)
[--db DB_JSON] [--key-file KEY_FILE] [--variant VARIANT_JSON]
[--leaf-dir DIR] [--report-dir DIR]
[--include-mft-deltas] [--mft-range A:B]
```
`$UsnJrnl` 通过 `$Extend` 自动发现;`scan` 中没有 `--usnjrnl-record` 选项。
| 标志 | 描述 |
| --- | --- |
| `--leaf-dir DIR` | 为每个发现写入 `leaf_.json` 的目录(如果不存在则创建;仅限非正常记录)。 |
| `--report-dir DIR` | 为每个发现写入 `report_.html` 的目录(如果不存在则创建;仅限非正常记录)。 |
| `--include-mft-deltas` | 可选的第二遍扫描,遍历每个已分配的 `$MFT` 记录,并在 `anomaly` 层级报告没有 `$LogFile` 覆盖的 SI 与 FN 差异。 |
| `--mft-range A:B` | 将差异扫描限制在半开的 MFT 范围内(`0 <= A <= B`)。若没有 `--include-mft-deltas` 则无效;输入格式错误将以退出码 `1` 退出。 |
损坏或不可读的记录将作为 `skipped` 行进行报告,绝不会中断扫描。
**关于 anomaly 扫描。** 在真实的完整 `$MFT` 上进行差异扫描时,故意设计为会产生较多噪音:许多合法文件带有 SI 与 FN 的差异,因为 `$FILE_NAME` 仅在创建、重命名和更改父目录时更新,而不是在内容或属性编辑时更新。anomaly 是需要独立证据印证的单一来源线索;它们永远不会触发退出码 `2`,也从不输出工件。
启用两遍扫描的示例(包含两条被篡改记录和一个无日志 anomaly 槽位的合成测试镜像):
```
python -m logflip scan --image disk.img --include-mft-deltas
```
```
mft_record verdict tool_family
5 provisional -
7 provisional -
9 clean -
12 anomaly -
candidates: 4 findings: 2 anomalies: 1 skipped: 0
anomalies are single-source ($STANDARD_INFORMATION vs $FILE_NAME) and require corroboration; they are not confirmed findings.
```
由于两个 provisional 发现,退出码为 `2`;record-12 的 anomaly 并不影响退出码。
### 变体选择
`detect` 和 `scan` 都接受 `--variant VARIANT_JSON`,它是一个 JSON 对象字符串,用于在多变体已签名数据库中选择一个变体。内置的 `win10_22h2` 数据库包含两个以 Windows 内部版本号和簇大小为键的变体,因此每当 `--db` 指向它时,就需要使用 `--variant` 参数:
```
python -m logflip detect --image disk.img --mft-record 5 \
--db win10_22h2.json --key-file key.bin \
--variant '{"windows_build":"win10-22h2","cluster_size_bytes":4096}'
```
内置的两个变体键分别是
`{"windows_build":"win10-22h2","cluster_size_bytes":4096}` 和
`{"windows_build":"win10-22h2","cluster_size_bytes":65536}`。该标志仅对于扁平(单一变体)数据库是可选的。
### verify-leaf
利用恢复出的 key 材质重新验证已签名的 leaf:它会根据提供的 key 重新计算 leaf 的 HMAC,并将其与存储的 MAC 进行恒定时间比较。
```
python -m logflip verify-leaf --key-file KEY_FILE LEAF_JSON
```
通过则退出码为 `0`,失败或错误则为 `1`。
### verify-db
基于其 RFC 8785 规范格式(除 `hmac` 外的所有字段)验证已签名指纹数据库的 HMAC。与变体无关:它可以在扁平数据库和多变体数据库上工作,而无需 `--variant` 参数。
```
python -m logflip verify-db --key-file KEY_FILE DB_JSON
```
通过则退出码为 `0`,失败(打印 `db_integrity_failure`)或错误则为 `1`。
### build-db
从采集清单 JSON 文件构建并签名指纹数据库工件。
```
python -m logflip build-db --key-file KEY_FILE --output OUTPUT_JSON MANIFEST_JSON
```
### keygen
生成 32 字节的加密安全随机 engagement key,并以 `0600` 权限将其作为原始字节写入文件。key 字节绝不会被打印出来,并且除非提供 `--force` 参数,否则该命令拒绝覆盖现有文件。
```
python -m logflip keygen --output KEY_FILE [--force]
```
生成的文件即作为 `build-db`、`detect`、`scan`、`verify-leaf` 和 `verify-db` 的 `--key-file` 参数。生成后请立即按照 [docs/custody_sop.md](docs/custody_sop.md) 的说明进行密封。
###-captures
读取一个或多个由操作员提供的采集包(详见 [docs/capture_spec.md](docs/capture_spec.md)),验证 SHA-256 完整性,校验清单字段,并写入 `build-db` 清单。
```
python -m logflip ingest-captures --output OUTPUT_JSON \
[--dirty-shutdown-fp-rates PATH] CAPTURE_DIR [CAPTURE_DIR ...]
```
当未提供 `--dirty-shutdown-fp-rates` 时,将写入一个带有零值的占位块;提供一个包含测量比率的 JSON 文件即可将它们嵌入其中。这是利用经过认证的采集数据构建内置数据库的路径。对于格式错误的清单、非 UTF-8 编码的输入以及类型错误的字段,系统会以清晰的错误提示予以拒绝,绝不会抛出未捕获的 traceback。
## 证据完整性
`logflip` 输出的每一个工件都在加密层面与 engagement key 绑定:
- **签名。** Leaf 和数据库的 HMAC key 通过域分离的盐值和信息常量在 HKDF-SHA256 下派生,因此 leaf key 和数据库 key 永远不会发生碰撞。
- **规范化。** 签名是基于 RFC 8785 (JCS) 规范格式计算的,因此无论字段顺序如何,被签名的都是字节稳定的序列化结果。
- **验证。** 所有的 MAC 比较均采用恒定时间算法。已签名数据库中的单个字节反转都会导致 `verify-db` 报告 `db_integrity_failure`;被篡改的 leaf 将无法通过 `verify-leaf` 验证。
- **Key 处理。** Key 仅存在于通过 `--key-file` 传递的原始 32 字节文件中,绝不会出现在命令行中。`keygen` 以 `0600` 权限写入它们,且从不打印这些字节。
Engagement key 的保管、托管与销毁流程已在 [docs/custody_sop.md](docs/custody_sop.md) 中详细记录。
## 诚实的范围与局限性
本节记录了内置代码在真实的 Windows NTFS 采集数据中,能够与无法得出何种结论的真实边界。这是为了满足 Daubert 可靠性要求而撰写的方法论透明度声明。
### 解析真实的 LFS 记录;归属分析是缺口所在
反向重放通道能够同时解析由测试套件生成的合成 LFS 固定数据,以及真实的 Windows `$LogFile` 日志。前向解析器 (`logflip/logfile/forward.py`) 从重启区域动态读取 LFS 记录头长度(在捕获的 Win10 22H2 和 Win8.1 构建版本中为 0x30,即 48 字节头部,而非固定值),并在遍历记录前将每个 RCRD 页面的数据区拼接起来以重组跨页日志记录,因此跨越页面边界的记录能够被恢复,而非被丢弃 (`_iter_real_raw_records`)。真实的 LFS 解析路径已通过 `tests/test_real_lfs.py` 在提交的真实采集固定数据上进行了验证。
残留的局限性在于归属分析,而非解析。解析器能从真实的日志中恢复重做/撤销记录,但第一轮和第二轮采集并未得出稳定、非稀疏且特定于工具的 `$LogFile` 字节模式。由于内置的每个家族因此都带有空的 `pattern_hex`,匹配器永远无法将字节模式绑定到真实的采集数据上,因此在真实输入下的 pipeline 极限就是 `provisional`:反向重放和 SI 与 FN 的差异可以揭示发现,但基于字节模式的工具家族归属分析并不可用,因此永远不会达到 `evil_confirmed`。这是正确且杜绝错误确认的行为。
### SetMACE(卸载原始写入):设计上不可归属
SetMACE 在卸载卷后执行原始扇区写入,完全绕过了 NTFS 日志。它针对目标记录不会产生任何 `$LogFile` 事务,也不会增加新的 `$UsnJrnl` reason flag,并且会使用相同的值覆盖 `$STANDARD_INFORMATION` (SI) 和 `$FILE_NAME` (FN) 的时间戳字段,蓄意抹除了 pipeline 用作独立信号的 SI 与 FN 差异。因此,通过任何三个 pipeline 通道都无法对 SetMACE 修改过的记录进行归属分析:在真实的镜像中,除非观察到带外上下文,否则被 SetMACE 篡改的文件与合法的旧文件毫无分别。
### SI 与 FN 差异:具有高灵敏度但存在良性误报的筛查
对于许多篡改工具(那些只更新 SI 的工具)而言,非零的 SI 与 FN 差异是必要条件,但不足以进行归属分析。许多合法文件带有非零差异,因为 `$FILE_NAME` 仅在创建、重命名和更改父目录时更新。`scan` 的 anomaly 层级将这些情况作为调查线索而非发现呈现,并且当不存在印证的日志证据时,该通道会被门控限制为 `INCONCLUSIVE`。它永远不会单独产生 `evil_confirmed`。
### 字节模式归属分析:目前尚无经过验证的模式
在 Windows 10 22H2 上进行的第一轮和第二轮实验室采集,没有发现稳定、非稀疏、且能可靠对应任何被测工具家族的字节模式。不同运行之间唯一共享的注入字节是伪造的时间戳值 (2009-01-01T00:00:00Z),但这也会匹配任何合法的日期为 2009 年的文件,因此是一个不可靠的判别依据。
内置的数据库 (`logflip/fingerprint/data/win10_22h2.manifest.json`) 记录了每个工具家族的*结构性*特征(写入方式、日志行为、SI 与 FN 的关系、依赖于簇大小的行为),但不包含任何字节模式:每个家族的 `pattern_hex` 为空,置信度为 0.0。按结构设定,每个家族的 `fp_rate` 为 0.0(结构零):空模式匹配器防护排除了任何字节匹配。数据库级别的 `dirty_shutdown_fp_rates` 块是经过测量的(在 42 个真实的 Windows 镜像上为 0.000,这是一种受控语料库的方法论演示,而非真实的现实世界总体比率;参见 [docs/false_positive_rates.md](docs/false_positive_rates.md))。
### 覆盖窗口
`$LogFile` 和 `$UsnJrnl` 滚动覆盖的盲区(在活跃卷上大约为 4 到 12 小时)是主要的覆盖限制:早于实时日志窗口的事件将被判定为 `INCONCLUSIVE`。
## 现有技术与创新性
NTFS Log Tracker 是最接近的现有技术工具,它在检测信号层达到了大约 50% 的保真度:它解析 `$LogFile` 的前向记录并提取一些时间戳证据,但它没有执行重建篡改前属性状态所需的反向 LSN 遍历。因此,
目前没有任何公开工具能够对 NTFS $LogFile 进行反向重放,以重建篡改前状态并识别篡改工具家族。
这种组合,即反向遍历撤销记录以恢复原始 SI 时间戳,随后针对已签名的指纹数据库进行篡改工具归属分析,正是 `logflip` 的创新之处。
## 项目状态与质量
`logflip-closed` 当前版本为 `v0.1.0`。核心检测引擎、签名证据 pipeline 及相关文档均已完整并经过验证。
| 信号 | 状态 |
| --- | --- |
| 测试套件 | 809 个通过,18 个跳过(被跳过的测试需要 `ntfsprogs`/`ntfs-3g` 或真实的 NTFS 卷) |
| 类型检查 | 整个包通过了 `mypy --strict` 检查 |
| 代码检查 | `ruff` 检查无问题 |
| Python | 3.11+ |
上述基于诚实的姿态是一项特性,而非缺陷:该工具的设计初衷是绝不输出错误的确认,这一特性已通过测试套件得到验证,而不仅仅是口头声明。
## 开发说明
```
python -m pip install -e ".[dev]"
python -m pytest # full suite
python -m mypy logflip --strict # type gate
python -m ruff check logflip tests
```
标记为 `lab` 的测试需要真实的 NTFS 环境(具有管理员权限的实时 Windows 卷,或 Linux 下的 `ntfsprogs`/`ntfs-3g` 工具链),在缺少这些环境时会自动被跳过。
## 文档
| 文档 | 内容 |
| --- | --- |
| [SETUP.md](SETUP.md) | 双环境(Windows 采集 / SIFT 分析)执行交接。 |
| [docs/capture_spec.md](docs/capture_spec.md) | `ingest-captures` 使用的采集包格式。 |
| [docs/custody_sop.md](docs/custody_sop.md) | Engagement key 的生成、密封、托管与销毁。 |
| [docs/false_positive_rates.md](docs/false_positive_rates.md) | 受控语料库的误报方法论及测量结果。 |
## 许可证
本代码库目前未声明任何许可证。在添加 `LICENSE` 文件之前,作者保留所有权利,且该代码仅供评估使用。在任何公开发布之前必须选择一种许可证。
标签:NTFS, Python, 反取证检测, 安全规则引擎, 数字取证, 文件系统, 无后门, 自动化脚本, 逆向工具