SecurityRonin/sqlite-forensic

GitHub: SecurityRonin/sqlite-forensic

一款 Rust 编写的 SQLite 取证库,通过直接解析底层文件格式恢复标准工具无法读取的已删除数据行,并提供分级的异常审计与置信度评分。

Stars: 1 | Forks: 0

[![Docs](https://img.shields.io/badge/docs-securityronin.github.io-blue.svg)](https://securityronin.github.io/sqlite-forensic/) [![Rust edition 2021](https://img.shields.io/badge/rust-edition%202021-orange.svg)](https://doc.rust-lang.org/edition-guide/rust-2021/index.html) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) [![Sponsor](https://img.shields.io/badge/sponsor-h4x0r-ea4aaa?logo=github-sponsors)](https://github.com/sponsors/h4x0r) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](#trust-but-verify) [![security: cargo-deny](https://img.shields.io/badge/security-cargo--deny-success.svg)](deny.toml) # sqlite-forensic **无需信任、无需写入,且绝不重现任何活动行,即可从 SQLite 数据库中提取(Carve)已删除的行。** 每一个浏览器历史记录、每一个聊天应用、每一个移动设备取证遗迹都是一个 SQLite 文件——而取证中最有价值的行通常是那些*已删除*的行。标准的 `sqlite3`/rusqlite 路径无法看到它们:它只读取活动的 b-tree 然后停止。`sqlite-forensic` 直接读取原始文件格式——包括空闲列表页(freelist pages)、页内空闲块(in-page free blocks)、已删除表的页,以及未检查点(uncheckpointed)的 WAL 覆盖层——从而恢复活动查询无法获取的内容,表现为经过严重性分级和置信度评分的观察结果。 这是一个包含 CLI(`sqlite4n6`)的 Rust 库工作区(workspace)。最快的路径是——指向一个数据库,直接从空闲空间中读取已删除的行。它以**只读**方式打开证据,绝不写入该文件或其附属文件: ``` $ sqlite4n6 carve History.db # deleted rows, table view $ sqlite4n6 carve History.db --format jsonl # one JSON object per record $ sqlite4n6 carve History.db --min-confidence medium # drop low-confidence carves $ sqlite4n6 carve History.db --no-fragments # full rows only (fragments shown by default) $ sqlite4n6 audit History.db # graded anomaly findings ``` 当存在 `-wal` 附属文件时,`carve` 会自动检测它,并提取**完整的单次提交 WAL 时间线**——每一个可实体化的状态,都标有其日志序列坐标:磁盘上的基础镜像、WAL 的**每一次提交快照**,以及未检查点的 WAL 帧残留(residue)。在事务历史后期被删除的行,在*较早*提交的页面镜像中仍然是一个活动的 cell,因此快照列会告诉你被删除行最后一次存活时所在的准确提交状态。这是真正的 N-快照时间模型——而不是一个“磁盘上 vs 最新版本”的两点近似。 ``` $ sqlite4n6 carve chat.db # auto-detects chat.db-wal page offset rowid recovery_source conf snapshot values 2 1581 130 commit-snapshot 0.90 commit:(3131615003,3836839008,0) 130 | bob | secret body 130 2 1261 ? commit-snapshot 0.40 commit:(3131615003,3836839008,1) NULL | NULL | ... $ sqlite4n6 carve chat.db --wal /path/to/chat.db-wal # point at an explicit sidecar $ sqlite4n6 carve chat.db --no-wal # on-disk image only, no snapshot column ``` `snapshot` 列包含带有盐值限定(salt-qualified)的 LSN——对于已提交的快照,它是 `commit:(salt1,salt2,commit_frame_index)`;对于原始帧残留,它是 `wal-frame:(salt1,salt2,frame_index)`;对于基础镜像,则是 `on-disk`。在各个视图中完全相同的记录会被折叠到其最早的已提交坐标。`--no-wal` 仅提取磁盘上的镜像(单一视图,无 `snapshot` 列)。证据文件及其附属文件**绝不会**被写入。 在此基础之上还有两种恢复面。如果一个已删除行的负载超过了页面大小(`> usable − 35` 字节),它就会溢出到**溢出页链(overflow-page chain))**上;当每个链页都作为空闲列表的叶子节点存留时(内容得以保留),`carve` 会将这样的行重新组装成完整的记录——这是一种刻意受限的能力,其评级低于页内层级,因为如果某个链页被重新分配为空闲列表的*主干(trunk)*,就会破坏该记录。此外,`carve` **默认会展示一个 Tier-2 片段分区**(使用 `--no-fragments` 可屏蔽它):当某行的完整标识被破坏,但某个具有特征的 cell 连续存活下来时(一个 `TEXT ≥ 4` 字节或一个 `REAL`),该片段就会被挽救,并与高精度的整行层级**严格区分开**——这是单一存活 cell 仍能锚定的部分证据,绝不会混入整行集合中。 或者直接驱动该库——将分析器指向文件字节,即可获得分级的结果以及提取出的已删除记录: ``` use sqlite_core::Database; use sqlite_forensic::{audit, carve_all_deleted_records}; let db = Database::open(std::fs::read("History")?)?; // read-only, owns the bytes // 1. Graded header / freelist / WAL anomalies for anomaly in audit(&db) { println!("[{:?}] {} — {}", anomaly.severity, anomaly.code, anomaly.kind.note()); } // 2. Deleted rows carved from free space — column count inferred per record for rec in carve_all_deleted_records(&db) { println!("recovered rowid {} from page {} (allocated: {})", rec.rowid, rec.page, rec.allocated); } ``` 读取器(`sqlite-core`)回答的是*“这个文件实际包含什么?”*;分析器(`sqlite-forensic`)则对具有取证价值的部分进行评级,并恢复已删除的部分。 ## 你将获得什么 | | sqlite-forensic | rusqlite / `sqlite3` | |---|:-:|:-:| | 读取活动行 | ✅ | ✅ | | 对证据文件只读 | ✅ | ✅ (需谨慎) | | 从空闲列表页恢复已删除的行 | ✅ | — | | 从页内空闲块恢复已删除的行 | ✅ | — | | 恢复已删除表的行 (推断列数) | ✅ | — | | 重新组装负载溢出到溢出页链的已删除行 | ✅ 部分 | — | | 将部分行作为独立的 Tier-2 片段层抢救出来 (存活的特征 cell) | ✅ 默认 | — | | 将未检查点的 WAL 覆盖层作为独立视图读取 | ✅ | 静默应用 | | 提取每一个 WAL 提交快照,并标记 LSN (单次提交时间线) | ✅ | — | | 分级且带有置信度评分的异常发现 | ✅ | — | | 拒绝将活动行作为“已删除”行重新展示 | ✅ | n/a | | `forbid(unsafe)`,对恶意输入无 panic | ✅ | C / FFI | ## 两个 crate 这是一个工作区(`sqlite-forensic`):包含两个遵循“阅读器/分析器”分离设计的库 crate,以及使用它们的 `sqlite4n6` CLI: | Crate | 角色 | 入口点 | |---|---|---| | [`sqlite-core`](core) | 原始、只读、无 panic 的文件格式读取器:解析头部、遍历 b-tree、空闲列表 + 溢出链,以及映射到规范 `forensicnomicon::history` 时间队列的只读 WAL 覆盖层(每次提交都是一个带盐值的 `[H]` 状态)。不产生结果。 | `Database::open`, `Database::open_with_wal`, `freelist_pages`, `read_table`, `carve_free_regions`, `live_rowids`, `wal_timeline`, `WalTimeline::to_temporal_cohort` | | [`sqlite-forensic`](forensic) | 异常审计器 + 已删除记录提取器:将观察结果分级为 `forensicnomicon::report::Finding`,并恢复已删除的行。依赖于 `sqlite-core`。 | `audit`, `audit_findings`, `carve_all_deleted_records`, `carve_deleted_records` | `sqlite-forensic` 接受内存中的 `Database`(由 `&[u8]` 构建)——它与介质无关,不依赖于任何镜像格式或容器层。发现结果会汇入共享的 `forensicnomicon::report` 模型,因此 SQLite 数据库的异常可以与分类报告中的分区/容器/文件系统层整齐划一地聚合在一起。 ## 异常代码 `audit()` 会发出稳定的、带有方案前缀的代码(这是一个公开的契约——绝不会改变拼写)。每一个都是一种**观察**(“与...一致”),并按严重性分级;检验员需自行得出结论。 | 代码 | 严重性 | 观察内容 | |---|:-:|---| | `SQLITE-DELETED-RECORD-RECOVERED` | Medium | 从未分配空间恢复出的记录状 cell——与尚未被覆盖的已删除行一致。带有 page / offset / rowid 来源。 | | `SQLITE-FREELIST-NONEMPTY` | Low | 数据库包含空闲页——与之前的删除操作一致(`DELETE` 而没有执行 `VACUUM`);这些页可能保留了可恢复的行。 | | `SQLITE-WAL-UNCHECKPOINTED` | Medium | `-wal` 附属文件包含主文件未反映的已提交页版本——仅凭主文件会少报真实状态。 | | `SQLITE-PAGECOUNT-MISMATCH` | High | 头部中的页数与文件长度暗示的页数不一致——与截断、提取或带外修改一致。 | | `SQLITE-RESERVED-SPACE-NONZERO` | Low | 头部为每页保留了字节——这并非标准做法;与页面级扩展(如加密 (SQLCipher/SEE) 或校验和 VFS)一致。 | `AnomalyKind` enum 被标记为 `#[non_exhaustive]`:可以在不构成破坏性更改的情况下添加新代码,因此下游的 `match` 分支必须包含一个 `_` 分支。 ## 信任但验证 在证据数据库中,一个*过度*报告的提取器比无用更糟糕——它会凭空制造出从未被删除的行。因此,这个提取器的设计目标是在召回率(recall)之上优先保证精确度(precision),这是通过结构性约束而非单纯检查来实现的: - **只读、无 panic、`forbid(unsafe)`** —— `Database::open` 拥有一个 `Vec` 并且永远不会写回到工件中;整个工作区在编译时禁用 `unsafe`,并通过边界检查的辅助函数读取每个长度/偏移量,因此格式错误的、受攻击者控制的数据库无法触发原始指针路径或引发 panic。 - **以独立的第三方真值(ground truth)为基准进行衡量。** 针对每个数据库的召回率和精确度是对照 **SQLite Forensic Corpus** (Nemetz, Schmitt & Freiling, DFRWS-EU 2018, CC0) 计算得出的,该语料库的作者提供了逐行的已删除记录答案标准——因此真值集是他们的,而不是我们的。测试套件(`forensic/tests/nemetz_metrics.rs`)会生成一个可复现的混淆矩阵;完整的表格位于 [`docs/recovery-comparison.md`](docs/recovery-comparison.md) 中。 - **结构性的高精确度——绝不会重读活动行。** 我们的提取器仅提取页面上活动 cell 范围的*补集*,然后丢弃任何 rowid 当前处于活动状态的提取记录。在整个 Nemetz 召回语料库中,它产生了 **0 次活动行重读**(已对照答案标准的活动行进行了验证),只有一小部分低置信度的**幽灵**类(推断提取器在一连串零字节上匹配到的全空/NULL 记录)。参考预言机(oracle)在无删除数据库上表现出的两种过度报告的失败模式——重读活动 cell,以及重现活动行的陈旧字节副本——我们的提取器都不存在。 - **通过 freeblock 重构实现强大的页内召回率——如实报告。** 在最干净的类别(`0C`:就地删除的记录,`secure_delete=0`,没有覆盖,因此**每一个**被删除行的字节都存活了下来)中,该提取器恢复了 84 行跨工具评分行中的 **70 行**(召回率 **0.833**),领先于 `fqlite` 的 0.798。SQLite 会用 freeblock 指针覆盖被释放 cell 的前四个字节(payload-length + rowid varints, `header_len`, leading serial);`reconstruct_freeblock_records` 会利用存活下来的 serial-type 尾部,加上从同一页面上的活动 cell 推导出的 schema 模板来重构每条记录,而被破坏的 rowid 则标记为未知。它的精确度高于 `fqlite`,且**重读活动行次数为 0**。 - **溢出页链:部分恢复,且边界明确。** 如果一个已删除行的负载溢出到了被释放的溢出链上,**只有当每个链页都作为空闲列表的叶子节点存留时**,它才会被重新组装成完整的行;如果一个链页被重新分配为空闲列表的*主干*,就会破坏该记录,随后该记录将被拒绝进入整行层,仅作为一个 Tier-2 片段展现。在 Nemetz 的 `0E` 类别中,这重组了唯一一个字节级完美恢复的溢出链(已对照答案标准验证了 `assert_eq!`,底层召回率 **1.000**),**端到端 `0E` 召回率为 0.333**——这是一种刻意受限的能力,评级低于页内层,绝不被宣称为完整的溢出恢复。 - **辅助检查仍保持其原有的标记。** undark/fqlite 的差异对比([`docs/validation.md`](docs/validation.md))是**工具间一致性检查**(预言机之间彼此存在分歧——这只代表一致,不代表正确),而 DC3 `sqlite_dissect` 语料库是一个**无误报(no-false-positive)回归集**(它的 `expected_rows` 是活动内容,而不是已删除的集合),绝不是用于验证召回率的预言机。 被提取的记录依然是**带有置信度分级的观察结果**(“与已删除行一致”),而不是最终判决。诚实的总结是:经过独立真值验证的严格精确度准则,以及一个已记录在案的页内召回率差距——并不是对完美召回率的声明也不是对正确性的证明。 **诚实的差距(在追踪中,未隐藏):** 这个仓库目前**没有 CI 工作流**,也**没有代码行覆盖率门槛**,而且该提取器**尚未经过模糊测试**——这三项都已列入计划,以使其达到整个工具链中其余部分所遵循的 Paranoid-Gatekeeper 标准。安全 lint(`unsafe_code = forbid`,`unwrap_used`/`expect_used = deny`)和 `cargo-deny` 供应链关卡*目前*已处于强制执行状态。 ## 文档 - [`docs/validation.md`](docs/validation.md) —— Doer-Checker 差异对比:提取器是如何与 undark 和 fqlite 进行对账的,页面级分歧诊断,构建方案。 - [`docs/recovery-comparison.md`](docs/recovery-comparison.md) —— 对照独立的 Nemetz 真值测量出的单数据库召回率/精确度混淆矩阵,并将 undark/fqlite 一致性和 DC3 无误报回归集作为辅助检查。 - [`docs/corpus-catalog.md`](docs/corpus-catalog.md) —— 每一个测试夹具(fixture)及其逐字生成器命令和 MD5。 - [`tests/data/README.md`](tests/data/README.md) —— 已提交的合成夹具,同位存放。 ## RapidTriage 生态系统 sqlite-forensic 是 [RapidTriage](https://github.com/SecurityRonin/rapidtriage) DFIR 工具包中的 SQLite 文件格式解析器: | Crate | 遗迹家族 | |---|---| | [sqlite-forensic](https://github.com/SecurityRonin/sqlite-forensic) | SQLite 数据库 (b-tree, 空闲列表, WAL, 已删除记录提取) | | [browser-forensic](https://github.com/SecurityRonin/browser-forensic) | Chrome / Firefox / Safari | | [winevt-forensic](https://github.com/SecurityRonin/winevt-forensic) | Windows 事件日志 (EVTX) | | [srum-forensic](https://github.com/SecurityRonin/srum-forensic) | Windows SRUM / ESE | | [memory-forensic](https://github.com/SecurityRonin/memory-forensic) | 进程内存,页表 | | [forensicnomicon](https://github.com/SecurityRonin/forensicnomicon) | 遗迹目录,格式常量,报告模型 | [隐私政策](https://securityronin.github.io/sqlite-forensic/privacy/) · [服务条款](https://securityronin.github.io/sqlite-forensic/terms/) · © 2026 Security Ronin Ltd
标签:Rust, SQLite, 可视化界面, 数字取证, 数据库, 数据恢复, 网络流量审计, 自动化脚本, 通知系统