Query-farm/vgi-mft

GitHub: Query-farm/vgi-mft

将 NTFS $MFT 解析为 DuckDB SQL 可查询的取证时间线,支持大规模主机的快速分诊与反取证检测。

Stars: 0 | Forks: 0

# vgi-mft 将 NTFS **`$MFT`** (Master File Table) 解析为取证文件系统 时间线 —— 直接在 DuckDB SQL 中完成,支持机群规模,且 **零出口流量**。 `vgi-mft` 是一个 [VGI](https://query.farm) worker(作为 Windows-DFIR 工具集的雏形,与 `vgi-evtx` 并列)。它将收集到的 `$MFT` 数据块或多个 `$MFT` 转储文件,转换为每个 FILE 记录对应的一条宽行数据:包含从 父引用链重建的路径,`$STANDARD_INFORMATION` (SI) 和 `$FILE_NAME` (FN) 两套 **MACB** 时间戳四元组 —— 因此,能够暴露 反取证篡改的 **timestomp**(时间戳篡改)不匹配问题,现在只需一个简单的 `WHERE` 子句即可识别 —— 同时包含逻辑/物理大小、 已分配/已删除以及文件/目录标志、**备用数据流** (ADS),以及 内联的 **常驻** 文件内容。 其核心价值在于支持机群规模、SQL 内的快速分诊:为成千上万个收集的 `$MFT` 生成时间线,并通过单次查询将结果与已知的恶意路径、哈希、CVE、YARA/Sigma 规则以及 泄露的机密模式进行 JOIN 操作 —— 而单机版的 CLI 每次只能生成一个 CSV,还需要手工拼凑整合。该 worker 不开启任何 socket,不读取 任何 secret provider,也不发起任何外部调用:它仅仅解析你已经 收集到的字节 —— 非常适合气隙隔离 / 保持证据监管链的场景。 ## 快速开始 ``` INSTALL vgi FROM community; LOAD vgi; ATTACH 'mft' AS mft (TYPE vgi, LOCATION '/path/to/mft-worker'); SET search_path = 'mft.main'; ``` ## SQL 接口 ``` -- 1. Read a collected $MFT into a full forensic timeline (one row per FILE record). SELECT t.entry, t.full_path, t.is_dir, t.is_deleted, t.si_created, t.si_modified, t.fn_created, t.fn_modified, t.logical_size, t.ads_name FROM read_mft('/cases/IR-2026-04/host01/$MFT') AS t ORDER BY t.si_modified DESC; -- 2. Timestomp hunt across an estate: SI created EARLIER than FN created, -- or a zero-microsecond SI tell — both classic anti-forensic signatures. SELECT host, full_path, si_created, fn_created FROM read_mft('s3://collections/*/$MFT', host := 'estate') AS t -- glob: one file per host WHERE t.is_allocated AND ( t.si_created < t.fn_created -- SI predates FN: impossible naturally OR date_part('microsecond', t.si_modified) = 0 ) -- automated-tool sub-second tell ORDER BY host; -- Or just use the convenience flag, which folds the heuristic into a boolean: SELECT full_path, si_created, fn_created FROM read_mft('/cases/host01/$MFT') WHERE is_timestomp_suspect; -- 3. List deleted records still resident in the table (recoverable artifacts). SELECT entry, sequence, full_path, logical_size, fn_modified FROM read_mft('/cases/host01/$MFT') WHERE is_deleted AND NOT is_dir; -- 4. Surface every alternate data stream (a classic malware hiding spot), -- then join hidden paths to a known-bad IOC table — fleet-wide, one query. SELECT t.host, t.full_path, t.ads_name, t.logical_size FROM read_mft('/cases/host01/$MFT', mode := 'streams') AS t WHERE t.ads_name IS NOT NULL; -- non-default $DATA streams only -- 5. Carve resident file content (small files live inside the MFT record itself). SELECT entry, full_path, resident_data FROM read_mft('/cases/host01/$MFT') WHERE resident_data IS NOT NULL AND full_path LIKE '%\Temp\%'; ``` ### `read_mft(glob_or_blob, host := NULL, mode := 'files')` 核心表函数。第一个参数是 **VARCHAR 路径或 glob 通配符** (`'/cases/*/$MFT'`) 或者是 `$MFT` 字节的 **`BLOB`**。`host :=` 用于限定 收集范围(默认为源文件名);`mode :=` 可以是 `'files'`(每个 记录一行,默认值)、`'streams'`(每个 `$DATA` 流一行 —— 主数据流 + 每个 ADS),或者是 `'allocated'`(仅限现存的文件)。已删除但常驻的记录默认 **会被包含** —— 如果只需现存文件,可以使用 `WHERE is_allocated` 进行过滤。 为每个 FILE 记录返回一条宽行数据:包含 `entry`、`sequence`、`parent_entry`、 `full_path`、`file_name`、`is_dir`、`is_allocated`、`is_deleted`,SI 四元组 (`si_created` / `si_modified` / `si_accessed` / `si_mft_modified`),FN 四元组 (`fn_*`)、`logical_size`、`physical_size`、`hard_link_count`、`dos_attributes`、 `ads_name`、`resident_data`、`is_timestomp_suspect` 以及 `diagnostics`。 ### 标量函数 (基于 `(blob, entry)` 对) ``` -- Pass the $MFT bytes as a BLOB (e.g. from DuckDB's read_blob table function). WITH m AS (SELECT content AS b FROM read_blob('/cases/host01/$MFT')) SELECT mft_record((SELECT b FROM m), 5), -- full lossless STRUCT decode of a record full_path((SELECT b FROM m), 5), -- path reconstruction for one entry record_header((SELECT b FROM m), 0).*, -- cheap FILE-header probe (LSN, flags, sizes) well_formed((SELECT b FROM m), 0).*; -- validate; never panics on corrupt input SELECT mft_version(); -- the running worker version -- The timestomp heuristic as a pure scalar, scoring multiple reasons: SELECT timestomp( {'created': si_created, 'modified': si_modified, 'accessed': si_accessed, 'mft_modified': si_mft_modified}, {'created': fn_created, 'modified': fn_modified, 'accessed': fn_accessed, 'mft_modified': fn_mft_modified}).* FROM read_mft('/cases/host01/$MFT'); ``` ### 属性 / 流展开 (关系输入 / 表输出) DuckDB 表函数不能接受相关列参数,因此 基于 `mft_dump` 风格的深度视图被实现为 **表输入输出** 函数:传入一个包含 `blob` BLOB 列和 `entry` UBIGINT 列的关系。 ``` -- Every attribute of one record: SELECT * FROM attributes((FROM ( SELECT content AS blob, 5::UBIGINT AS entry FROM read_blob('/cases/host01/$MFT')))); -- Every $DATA stream (primary + each ADS) of one record: SELECT * FROM streams((FROM ( SELECT content AS blob, 5::UBIGINT AS entry FROM read_blob('/cases/host01/$MFT')))); ``` ## 工作原理 `vgi-mft` 基于宽松许可证授权的 [`mft` crate](https://crates.io/crates/mft)(作者为 omerbenamram —— 他也是 `vgi-evtx` 背后的 `evtx` crate 的作者),该 crate 负责逐条记录的字节切片、修复应用 以及头部验证。`vgi-mft` 则负责 **路径重建 解析器**(处理 `\$Orphan` / `\$Cycle`,受深度和循环边界限制)、 规范化时间线 schema、**外置化的字节偏移扫描状态** (一个支持 serde 序列化的游标 + 跨 DuckDB 批次和 HTTP 重水合传递的父解析器索引)、**SI 与 FN 的 timestomp 启发式检测**,以及 SQL 接口。 **不可信输入的约束机制。** 从受损或发生故障的 主机收集的 `$MFT` 可能已损坏或带有恶意,因此每一条记录的解码都在独立的 catch 块中进行 —— 格式错误的记录会生成带有 `diagnostics` 标记的行,扫描过程绝不会因此中止;此外,通过属性测试确保解析器在遇到任意或 截断的字节时 **绝不崩溃**(内存分配受限:如果某条记录声称拥有 4 GB 的常驻 `$DATA`,系统不会为其分配任何内存)。 ## 非目标 (v1) 不支持读取实时磁盘 / 卷镜像(该 worker 解析的是已收集的 `$MFT` 数据块, 而非已挂载的 NTFS 卷),因此非常驻 `$DATA` 运行列表仅报告其大小, 而不会进一步跟踪到簇;不支持 `$Secure`/SID 到所有者的解析;不支持 目录索引 B 树重建。NTFS 日志相关的同类功能(`$UsnJrnl`、 `$LogFile`)已列入路线图,将作为独立的 worker 发布。 ## 开发 ``` cargo build --release # build the worker binary cargo test --workspace # unit + golden + zero-panic fuzz tests cargo run -p mft-worker --example gen_fixture # regenerate data/sample.mft # 跨所有 transport 的端到端 SQL(需要 haybarn-unittest 二进制文件): HAYBARN_UNITTEST=/path/to/haybarn-unittest TRANSPORT=subprocess ci/run-integration.sh ``` 有关传输矩阵,请参阅 [ci/README.md](ci/README.md);有关架构,请参阅 [CLAUDE.md](CLAUDE.md)。 ## 许可证 MIT —— 详见 [LICENSE](LICENSE)。版权所有 2026 Query Farm LLC —— https://query.farm
标签:DuckDB, NTFS, SQL, 可视化界面, 多线程, 子域名变形, 数字取证, 文件系统, 系统审计, 自动化脚本, 通知系统