trailofbits/mquire
GitHub: trailofbits/mquire
零依赖的 Linux 内存取证工具,利用内核内嵌的 BTF 和 kallsyms 实现无需外部调试符号的类型感知内存分析。
Stars: 46 | Forks: 2
# mquire
mquire 取自 `memory`(内存)与 `inquire`(查询)的组合,是一款受 [osquery](https://github.com/osquery/osquery) 启发的内存查询工具。
## 核心优势:无需外部调试符号
**mquire 可以在不需要外部调试符号的情况下分析 Linux 内核内存快照。**
分析所需的一切信息都已嵌入在内存转储本身之中。这意味着你可以分析:
- 从未见过的未知或自定义内核
- 任何 Linux 发行版,无需事先准备
- 外部调试符号不可用或丢失的内存快照
### 系统要求
**内核版本要求:**
- **BTF 支持**:4.18 或更新版本的内核,且启用了 BTF(大多数现代发行版默认启用)
- **Kallsyms 支持**:6.4 或更新版本的内核(由于 `scripts/kallsyms.c` 格式的变更)
## 工作原理
mquire 通过读取嵌入在现代 Linux 内核中的两类信息来分析内核内存:
1. **来自 BTF 的类型信息**([BPF Type Format](https://www.kernel.org/doc/html/next/bpf/btf.html))——描述内核数据类型的结构和布局。BTF 数据使用 [btfparse crate](https://crates.io/crates/btfparse) 进行解析。
2. **来自 Kallsyms 的符号信息**——提供内核符号的内存位置(与 `/proc/kallsyms` 使用的数据相同)
通过结合类型信息和符号位置,mquire 可以查找并读取复杂的内核数据结构,例如:
- 进程内存映射(使用 maple tree 结构)
- 缓存的文件数据(使用 XArray 结构)
- 内核日志消息
这使得可以直接从内核的文件缓存中提取文件成为可能,即使它们已从磁盘上删除。
### 兼容性说明
Kallsyms 扫描器依赖于内核源码中 `scripts/kallsyms.c` 的数据格式。如果未来的内核版本更改了此格式,扫描器的启发式规则可能需要更新。
## 功能特性
### 表
mquire 提供 SQL 表来查询系统的不同方面或工具本身的状态:
#### 系统信息
- **os_version** - 内核版本和架构
- **system_info** - 主机名和域名
- **boot_time** - 系统启动时间
- **kallsyms** - 内核符号地址(数据同 `/proc/kallsyms`)
- **dmesg** - 内核环形缓冲区消息(数据同 `dmesg` 命令)
#### 进程信息
- **tasks** - 正在运行的进程及其命令行和二进制路径
- **task_open_files** - 每个进程打开的文件(需要 `task` 约束 - 参见下方示例)
- **memory_mappings** - 每个进程映射的内存区域(需要 `task` 约束)
#### 内核模块
- **kernel_modules** - 已加载的内核模块及其元数据(名称、状态、版本、参数、污染标志)
#### 网络信息
- **network_connections** - 活动的网络连接(TCP sockets)
- **network_interfaces** - 网络接口及其 IP 地址和 MAC 地址
#### 文件系统
- **syslog_file** - 从内核文件缓存中读取的系统日志(即使日志文件被删除或不可用,只要它们被缓存在内存中即可工作)
#### 调试
- **log_messages** - 内部 mquire 日志,显示分析进度、警告和错误
## 命令
mquire 提供三个主要命令:
- **`mquire shell`** - 启动交互式 SQL shell 以查询内存快照
- **`mquire query`** - 执行单个 SQL 查询并输出结果(支持 JSON 或表格格式)
- **`mquire command`** - 对内存快照执行自定义命令(例如 `.task_tree`、`.system_version`、`.dump`)
## 点命令
mquire 提供以点(`.`)为前缀的特殊命令,以便与 SQL 查询区分开。
### 内置命令
这些命令可在交互式 shell 和 `mquire query` 中使用:
- **`.tables`** - 列出所有可用的表
- **`.schema`** - 显示所有表的结构
- **`.schema `** - 显示特定表的结构
- **`.commands`** - 列出所有可用的自定义命令
- **`.exit`** - 退出交互式 shell(仅限 shell)
### 自定义命令
这些命令可在交互式 shell 和 `mquire command` 中使用:
使用 `--help` 查看任何命令的可用选项和用法信息。例如:`.task_tree --help`
#### `.system_version`
显示操作系统版本信息。
这是一个便捷命令,等同于 `SELECT * FROM os_version`,但具有格式化的输出。
#### `.task_tree`
显示正在运行的进程和线程的层级树,类似于 Linux 上的 `pstree` 命令。
**选项:**
- `--show-threads` - 除进程外还包括线程。启用时,为每个条目显示 TGID 和 TID。
- `--use-real-parent` - 使用 `real_parent` 字段而不是 `parent` 来构建树结构。`real_parent` 字段显示重定父级之前的原始父进程(有助于跟踪进程创建链,即使在父进程退出后)。
**注意:**
- 显示线程时格式为 `[TGID TID]`,隐藏线程时为 `[TGID]`。TGID(Thread Group ID)即通常所说的 PID。对于主线程(TGID == TID),这两个值将相同。
#### `.carve`
将一块虚拟内存区域提取到磁盘。此命令使用给定的页表从特定的虚拟地址范围提取原始内存内容,适用于提取进程内存、堆内容或其他内存区域。
**参数:**
- `ROOT_PAGE_TABLE` - 根页表的物理地址(可选 0x 前缀的十六进制字符串)。这决定了用于转换的地址空间。
- `VIRTUAL_ADDRESS` - 开始提取的虚拟地址(可选 0x 前缀的十六进制字符串)。
- `SIZE` - 要提取的字节数。
- `DESTINATION_PATH` - 写入提取内存的输出文件路径。
**注意:**
- 命令在写入前会显示已映射与未映射区域的摘要。
- 未映射区域在输出文件中填充为零。
#### `.dump`
从内核文件缓存中提取文件,直接从内存中恢复文件。此命令遍历所有任务及其打开的文件描述符,从页缓存中提取文件内容。
**参数:**
- `OUTPUT` - 提取文件的输出目录。文件按 TGID 组织(例如 `tgid_1234/path/to/file`)。
**注意:**
- 目前适用于通过文件描述符打开的文件(来自进程文件描述符表)。
- 尚不支持从内存映射文件中提取数据。
- 空文件(页缓存中无数据)会被跳过。
- 读取错误的区域在输出中填充为零。
## 使用场景
mquire 专为以下场景设计:
- **取证分析** - 分析来自受损系统的内存快照,以了解正在运行的程序和访问的文件
- **应急响应** - 快速查询内存转储以查找恶意活动的证据
- **安全研究** - 从内存快照研究内核内部结构和进程行为
- **恶意软件分析** - 检查正在运行的进程及其文件操作而不被发现
- **自定义工具** - 使用 **mquire** 库 crate 构建你自己的分析工具,该库为内核内存分析提供了可复用的 API
## 构建与安装
### CI 预构建包
预构建包可作为 CI 运行的产物下载。你可以从 [Actions tab](https://github.com/trailofbits/mquire/actions) 选择一个成功的工作流运行并下载产物来获取它们。提供以下包格式:
- **linux-deb-package** - Debian/Ubuntu `.deb` 包
- **linux-rpm-package** - Fedora/RHEL/CentOS `.rpm` 包
- **linux-tgz-package** - 通用 Linux `.tar.gz` 归档
### 从源码构建
mquire 使用 Rust 编写。构建方法:
```
# Clone 仓库
git clone https://github.com/trailofbits/mquire
cd mquire
# Build 项目
cargo build --release
# Binary 将位于 target/release/
# - mquire: 具有 shell、query 和 command 模式的统一工具
```
## 获取内存快照
1. 编译 [LiME](https://github.com/504ensicsLabs/LiME) 项目
2. 通过加载构建的内核模块获取原始内存快照:`insmod ./lime-x.x.x-xx-generic.ko 'path=/path/to/memory/dump.bin format=padded'`
## 快速入门
一旦你有了内存快照,就可以使用 SQL 查询和自定义命令与之交互。mquire 提供了三种与快照交互的方式:
### 交互式 shell
启动交互式 SQL shell:
```
mquire shell /path/to/memory.raw
```
这将打开一个提示符,你可以在其中交互式地运行 SQL 查询和命令:
```
mquire> .tables # List all available tables
mquire> .schema tasks # Show schema for a specific table
mquire> SELECT * FROM tasks; # Run SQL queries
mquire> .task_tree --show-threads # Run custom commands
mquire> .exit # Exit the shell
```
### 一次性 SQL 查询
从命令行执行单个 SQL 查询或内置命令:
```
# 输出为 JSON 格式 (默认)
mquire query /path/to/memory.raw "SELECT * FROM os_version"
# 输出为 table 格式
mquire query /path/to/memory.raw "SELECT * FROM tasks" --format table
# Built-in 命令也可用
mquire query /path/to/memory.raw ".tables"
mquire query /path/to/memory.raw ".schema tasks"
```
### 执行自定义命令
运行自定义命令进行专门分析:
```
# 列出所有可用命令 (默认行为)
mquire command /path/to/memory.raw
# 显示 system version
mquire command /path/to/memory.raw ".system_version"
# 显示 process 树
mquire command /path/to/memory.raw ".task_tree"
# 显示带 threads 的 process 树
mquire command /path/to/memory.raw ".task_tree --show-threads"
# 获取命令帮助
mquire command /path/to/memory.raw ".task_tree --help"
```
## 自动启动 SQL 文件
mquire 在启动 shell 或执行查询时,会自动从 `~/.config/trailofbits/mquire/autostart/` 加载并执行 SQL 文件。这对于以下情况很有用:
- 创建可复用的 SQL 视图
- 设置自定义表
- 定义常用查询
**特性:**
- SQL 文件按字母顺序执行
- 文件必须具有 `.sql` 扩展名
- 错误会显示但不会阻止执行
- 同时适用于 `mquire shell` 和 `mquire query` 命令
### 创建用于进程网络连接的可复用视图
创建文件 `~/.config/trailofbits/mquire/autostart/001_process_network_connections.sql`:
```
CREATE VIEW IF NOT EXISTS process_network_connections AS
WITH
network_connections_mat AS MATERIALIZED (
SELECT * FROM network_connections
),
task_open_files_mat AS MATERIALIZED (
SELECT * FROM task_open_files
),
-- Deduplicate tasks by virtual_address since the default query returns
-- results from multiple enumeration sources
tasks_mat AS MATERIALIZED (
SELECT DISTINCT virtual_address, pid, tgid, comm, binary_path
FROM tasks
WHERE type = 'thread_group_leader'
)
SELECT
t.pid,
t.comm,
t.binary_path,
nc.protocol,
nc.local_address,
nc.local_port,
nc.remote_address,
nc.remote_port,
nc.state,
nc.type as ip_type,
nc.inode
FROM network_connections_mat nc
JOIN task_open_files_mat tof ON nc.inode = tof.inode
JOIN tasks_mat t ON tof.task = t.virtual_address
ORDER BY t.pid, nc.local_port;
```
然后查询该视图:
```
SELECT * FROM process_network_connections WHERE comm = 'sshd';
```
### 比较 task 枚举方法以进行 Rootkit 检测
Rootkit 通常通过将进程从内核的任务列表中取消链接来隐藏进程,同时保持它们运行。mquire 支持多种任务枚举策略,可以进行比较以检测此类隐藏进程。
创建文件 `~/.config/trailofbits/mquire/autostart/002_hidden_process_detection.sql`:
```
CREATE VIEW IF NOT EXISTS hidden_processes AS
WITH
tasks_from_task_list AS MATERIALIZED (
SELECT virtual_address, pid, comm
FROM tasks
WHERE source = 'task_list'
),
tasks_from_pid_ns AS MATERIALIZED (
SELECT virtual_address, pid, comm
FROM tasks
WHERE source = 'pid_ns'
)
SELECT
COALESCE(tl.pid, pn.pid) AS pid,
COALESCE(tl.comm, pn.comm) AS comm,
COALESCE(tl.virtual_address, pn.virtual_address) AS virtual_address,
CASE
WHEN tl.virtual_address IS NULL THEN 'hidden_from_task_list'
WHEN pn.virtual_address IS NULL THEN 'hidden_from_pid_ns'
ELSE 'visible'
END AS visibility
FROM tasks_from_task_list tl
FULL OUTER JOIN tasks_from_pid_ns pn
ON tl.virtual_address = pn.virtual_address
WHERE tl.virtual_address IS NULL OR pn.virtual_address IS NULL;
```
然后查询可能隐藏的进程:
```
SELECT * FROM hidden_processes;
```
## 查询优化
**mquire 查询需要利用嵌入的类型信息和调试符号,通过解引用指针从虚拟内存重建内核数据结构。这种处理可能开销很大,因此请使用查询优化技术来显著提高性能。**
### 使用 `AS MATERIALIZED` 进行物化
当表在 JOIN 中使用或被多次访问时,使用 `AS MATERIALIZED` 提示来缓存表结果。
**何时物化:**
- 生成成本高昂的表(例如,`tasks` 需要遍历进程结构的链表,每个进程解引用多个指针)
- 在 JOIN 中使用的表(在查询执行期间被多次访问)
- 在同一查询中被多次引用的表
**示例:**
```
-- Find network connections for a specific process using materialization
WITH
target_tasks AS MATERIALIZED (
SELECT * FROM tasks WHERE comm = 'sshd' AND type = 'thread_group_leader'
),
network_connections_mat AS MATERIALIZED (
SELECT * FROM network_connections
)
SELECT
t.tgid,
t.comm,
nc.local_address,
nc.local_port,
nc.remote_address,
nc.remote_port,
nc.state,
nc.protocol
FROM target_tasks t
JOIN task_open_files tof ON tof.task = t.virtual_address
JOIN network_connections_mat nc ON nc.inode = tof.inode;
```
**注意:** `task_open_files` 和 `memory_mappings` 表使用 `task` 列作为生成器输入。当与 `tasks` 表连接时,SQLite 会通过嵌套循环连接自动传递约束,这使得直接 JOIN 非常高效。
**性能影响:** 物化可以显著加快包含 JOIN 的查询(通常快 2-5 倍)
**示例基准测试结果:**
*测试在 Ubuntu 24.04 快照(内核 6.8.0-63)上进行,351 个进程,50 个连接,2142 个打开的文件。性能会因快照大小、内核版本和硬件而异。*
| Method | Real Time | User Time | Speedup |
|--------|-----------|-----------|---------|
| WITHOUT materialization | 12.067s | 16.373s | baseline |
| WITH materialization | 3.171s | 8.786s | **3.8x faster** |
### JOIN 顺序优化
**从最小的表开始并向较大的表进行 JOIN**,以尽量减少查询管道早期处理的行数。
**典型表大小:**
- `network_connections`:最小 - 仅包含有网络活动的进程
- `tasks`:中等 - 所有进程
- `task_open_files`:最大 - 所有打开的文件描述符
**最佳顺序:**
从过滤后的 tasks 表开始并向较大的表连接:
```
FROM target_tasks t -- filtered tasks
JOIN task_open_files tof ON tof.task = t.virtual_address -- open files
JOIN network_connections_mat nc ON nc.inode = tof.inode -- matching connections
```
### 了解查询执行
使用 `EXPLAIN QUERY PLAN` 查看 SQLite 如何执行你的查询:
```
EXPLAIN QUERY PLAN
SELECT ...
FROM target_tasks t
JOIN task_open_files tof ON tof.task = t.virtual_address
JOIN network_connections_mat nc ON nc.inode = tof.inode;
```
关注:
- **BLOOM FILTER**:SQLite 针对大型 JOIN 的优化
- **AUTOMATIC COVERING INDEX**:为查找创建的临时索引
- **SCAN**:全表扫描(对于驱动表来说是预期的)
- **SEARCH**:基于索引的查找(高效)
### 最佳实践
1. **始终物化在 JOIN 中使用的昂贵表**
2. **从最小的表开始作为驱动表**
3. **在脚本中使用多行 SQL** 以提高可读性
4. **使用 `EXPLAIN QUERY PLAN` 检查复杂查询的查询计划**
5. **在生产环境中避免 `SELECT *`** - 仅指定所需的列
### 文件提取
将文件从内存提取到磁盘:
```
mquire command /path/to/memory.raw ".dump /output/directory"
```
### 示例查询
所有查询均使用标准 SQL 语法。
#### 系统版本
```
$ mquire shell ubuntu2404_6.14.0-37-generic.lime
mquire> SELECT * FROM os_version;
arch:"x86_64" kernel_version:"6.14.0-37-generic" system_version:"#37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2"
```
#### 系统信息
```
$ mquire shell ubuntu2404_6.14.0-37-generic.lime
mquire> SELECT * FROM system_info;
domain:"(none)" hostname:"ubuntu2404"
```
#### 内核模块
```
$ mquire shell ubuntu2404_6.14.0-37-generic.lime
mquire> SELECT name, state, src_version, parameters FROM kernel_modules LIMIT 5;
name:"snd_seq_dummy" state:"live" src_version:"7A40E0FD47A0746D1C9CD85" parameters:"ump (perm: 0o444), duplex (perm: 0o444), ports (perm: 0o444)"
name:"snd_hrtimer" state:"live" src_version:"81EE6D58896E2C2E63E252D" parameters:"