mquire
mquire, a play on the memory and inquire words, is a memory querying tool inspired by osquery.
Key advantage: No external debug symbols needed
mquire can analyze Linux kernel memory snapshots without requiring external debug symbols.
Everything needed for analysis is already embedded in the memory dump itself. This means you can analyze:
- Unknown or custom kernels you've never seen before
- Any Linux distribution without preparation
- Memory snapshots where external debug symbols are unavailable or lost
Requirements
Kernel version requirements:
- BTF support: Kernel 4.18 or newer with BTF enabled (most modern distributions enable it by default)
- Kallsyms support: Kernel 6.4 or newer (due to changes in
scripts/kallsyms.cformat)
How it works
mquire analyzes kernel memory by reading two types of information that are embedded in modern Linux kernels:
- Type information from BTF (BPF Type Format) - Describes the structure and layout of kernel data types. BTF data is parsed using the btfparse crate.
- Symbol information from Kallsyms - Provides the memory locations of kernel symbols (same data used by
/proc/kallsyms)
By combining type information with symbol locations, mquire can find and read complex kernel data structures like:
- Process memory mappings (using maple tree structures)
- Cached file data (using XArray structures)
- Kernel log messages
This makes it possible to extract files directly from the kernel's file cache, even if they've been deleted from disk.
Compatibility notes
The Kallsyms scanner depends on the data format from scripts/kallsyms.c in the kernel source. If future kernel versions change this format, the scanner heuristics may need updates.
Capabilities
Tables
mquire provides SQL tables to query different aspects of the system or the state of the tool itself:
System information
- os_version - Kernel version and architecture
- system_info - Hostname and domain name
- boot_time - System boot time
- kallsyms - Kernel symbol addresses (same data as
/proc/kallsyms) - dmesg - Kernel ring buffer messages (same data as
dmesgcommand)
Process information
- tasks - Running processes with command lines and binary paths
- task_open_files - Files opened by each process (requires
taskconstraint - see examples below) - memory_mappings - Memory regions mapped by each process (requires
taskconstraint)
Kernel modules
- kernel_modules - Loaded kernel modules with metadata (name, state, version, parameters, taint flags)
Network information
- network_connections - Active network connections (TCP sockets)
- network_interfaces - Network interfaces with IP addresses and MAC addresses
File system
- syslog_file - System logs read from the kernel's file cache (works even if log files are deleted or unavailable, as long as they're cached in memory)
Debugging
- log_messages - Internal mquire logs showing analysis progress, warnings, and errors
Commands
mquire provides three main commands:
mquire shell- Start an interactive SQL shell to query memory snapshotsmquire query- Execute a single SQL query and output results (supports JSON or table format)mquire command- Execute custom commands on memory snapshots (e.g.,.task_tree,.system_version,.dump)
Dot Commands
mquire provides special commands prefixed with a dot (.) to distinguish them from SQL queries.
Built-in Commands
These commands work in the interactive shell and with mquire query:
.tables- List all available tables.schema- Show schema for all tables.schema <table>- Show schema for a specific table.commands- List all available custom commands.exit- Exit the interactive shell (shell only)
Custom Commands
These commands work in the interactive shell and with mquire command:
Use --help with any command to see available options and usage information. For example: .task_tree --help
.system_version
Display the operating system version information.
This is a convenience command equivalent to SELECT * FROM os_version, but with formatted output.
.task_tree
Display a hierarchical tree of running processes and threads, similar to the pstree command on Linux.
Options:
--show-threads- Include threads in addition to processes. When enabled, displays both TGID and TID for each entry.--use-real-parent- Use thereal_parentfield instead ofparentfor building the tree structure. Thereal_parentfield shows the original parent process before any reparenting (useful for tracking process creation chains even after parent processes exit).
Notes:
- The format is
[TGID TID]when showing threads, or[TGID]when threads are hidden. TGID (Thread Group ID) is what's commonly called PID. For main threads (where TGID == TID), both values will be the same.
.carve
Carve a region of virtual memory to disk. This command extracts raw memory content from a specific virtual address range using a given page table, useful for extracting process memory, heap contents, or other memory regions.
Arguments:
ROOT_PAGE_TABLE- The physical address of the root page table (hex string with optional 0x prefix). This determines the address space to use for translation.VIRTUAL_ADDRESS- The virtual address to start carving from (hex string with optional 0x prefix).SIZE- Number of bytes to carve.DESTINATION_PATH- Output file path where the carved memory will be written.
Notes:
- The command shows a summary of mapped vs unmapped regions before writing.
- Unmapped regions are filled with zeros in the output file.
.dump
Extract files from the kernel's file cache to recover files directly from memory. This command iterates through all tasks and their open file descriptors, extracting file contents from the page cache.
Arguments:
OUTPUT- Output directory for extracted files. Files are organized by TGID (e.g.,tgid_1234/path/to/file).
Notes:
- Currently works with files opened through file descriptors (from the process file descriptor table).
- Does not yet support extracting data from memory-mapped files.
- Empty files (no data in page cache) are skipped.
- Regions with read errors are zero-padded in the output.
Use cases
mquire is designed for:
- Forensic analysis - Analyze memory snapshots from compromised systems to understand what was running and what files were accessed
- Incident response - Quickly query memory dumps to find evidence of malicious activity
- Security research - Study kernel internals and process behavior from memory snapshots
- Malware analysis - Examine running processes and their file operations without detection
- Custom tooling - Build your own analysis tools using the mquire library crate, which provides a reusable API for kernel memory analysis
Building and installation
Pre-built packages from CI
Pre-built packages are available as artifacts from CI runs. You can download them from the Actions tab by selecting a successful workflow run and downloading the artifacts. The following package formats are available:
- linux-deb-package - Debian/Ubuntu
.debpackage - linux-rpm-package - Fedora/RHEL/CentOS
.rpmpackage - linux-tgz-package - Generic Linux
.tar.gzarchive
Building from source
mquire is written in Rust. To build it:
# Clone the repository
# Build the project
# The binary will be in target/release/
# - mquire: Unified tool with shell, query, and command modes
Acquiring a memory snapshot
We recommend AVML for acquiring memory snapshots. LiME was previously suggested but is no longer actively maintained.
Using AVML
Important: Do not use
--compresswhen acquiring snapshots for mquire. mquire does not support compressed AVML snapshots. If you have a compressed snapshot, useavml-convertto decompress it first:avml-convert compressed.lime uncompressed.lime
See the AVML documentation for additional options.
Getting started
Once you have a memory snapshot, you can interact with it using SQL queries and custom commands. mquire provides three ways to interact with snapshots:
Interactive shell
Start an interactive SQL shell:
This opens a prompt where you can run both SQL queries and commands interactively:
; # Run SQL queries
One-off SQL queries
Execute a single SQL query or built-in command from the command line:
# Output as JSON (default)
# Output as table format
# Built-in commands work too
Execute custom commands
Run custom commands for specialized analysis:
# List all available commands (default behavior)
# Display system version
# Show process tree
# Show process tree with threads
# Get help for a command
Autostart SQL Files
mquire automatically loads and executes SQL files from ~/.config/trailofbits/mquire/autostart/ when starting the shell or executing queries. This is useful for:
- Creating reusable SQL views
- Setting up custom tables
- Defining frequently-used queries
Features:
- SQL files are executed in alphabetical order
- Files must have a
.sqlextension - Errors are displayed but don't block execution
- Works with both
mquire shellandmquire querycommands
Creating a reusable view for process network connections
Create a file ~/.config/trailofbits/mquire/autostart/001_process_network_connections.sql:
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;
Then query the view:
SELECT * FROM process_network_connections WHERE comm = 'sshd';
Comparing task enumeration methods for rootkit detection
Rootkits often hide processes by unlinking them from the kernel's task list while keeping them running. mquire supports multiple task enumeration strategies that can be compared to detect such hidden processes.
Note: The
task_listsource aggressively follows pointers withintask_struct(e.g.,parent,children,sibling,group_leader) to maximize process discovery. This may occasionally yield invalid entries from corrupted or stale pointers in memory.
Create a file ~/.config/trailofbits/mquire/autostart/002_hidden_process_detection.sql:
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;
Then query for potentially hidden processes:
SELECT * FROM hidden_processes;
Query Optimization
mquire queries require reconstructing kernel data structures from virtual memory by dereferencing pointers using embedded type information and debug symbols. This processing can be expensive, so use query optimization techniques to improve performance dramatically.
Materialization with AS MATERIALIZED
Use the AS MATERIALIZED hint to cache table results when tables are used in JOINs or accessed multiple times.
When to materialize:
- Tables that are expensive to generate (e.g.,
tasksrequires walking linked lists of process structures, dereferencing multiple pointers per process) - Tables used in JOINs (accessed multiple times during query execution)
- Tables referenced multiple times in the same query
Example:
-- 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;
Note: The task_open_files and memory_mappings tables use the task column as a generator input. When joined with the tasks table, SQLite automatically passes the constraint via nested loop joins, making direct JOINs efficient.
Performance impact: Materialization can provide significant speedup for queries with JOINs (typically 2-5x faster)
Example benchmark results:
Test performed on an Ubuntu 24.04 snapshot (kernel 6.8.0-63), 351 processes, 50 connections, 2142 open files. Performance will vary based on snapshot size, kernel version, and hardware.
| Method | Real Time | User Time | Speedup |
|---|---|---|---|
| WITHOUT materialization | 12.067s | 16.373s | baseline |
| WITH materialization | 3.171s | 8.786s | 3.8x faster |
JOIN Order Optimization
Start with the smallest table and JOIN toward larger tables to minimize rows processed early in the query pipeline.
Typical table sizes:
network_connections: smallest - only processes with network activitytasks: medium - all processestask_open_files: largest - all open file descriptors
Optimal order:
Start with the filtered tasks table and join toward larger tables:
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
Understanding Query Execution
Use EXPLAIN QUERY PLAN to see how SQLite executes your query:
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;
Look for:
- BLOOM FILTER: SQLite's optimization for large JOINs
- AUTOMATIC COVERING INDEX: Temporary indexes created for lookups
- SCAN: Full table scan (expected for the driving table)
- SEARCH: Index-based lookup (efficient)
Best Practices
- Always materialize expensive tables used in JOINs
- Start with the smallest table as your driving table
- Use multiline SQL in scripts for readability
- Check query plans with
EXPLAIN QUERY PLANfor complex queries - Avoid
SELECT *in production - specify only needed columns
File extraction
Extract files from memory to disk:
Example queries
All queries use standard SQL syntax.
System version
;
System information
;
Kernel modules
;
Running tasks
;
Connections
Find network connections for a specific process by joining tasks, task_open_files, and network_connections.
;
Task open files
List open files for specific processes by joining tasks with task_open_files:
;
Command-line query examples
JSON output (default)
[
{
}
Table output
Custom command examples
List available commands
Display system version
Display process tree
|
)
)
) \x)\x
)
)
)
)
Note: When multiple task_struct entries exist with the same TID (Thread ID, which can occur due to memory corruption or snapshot timing), duplicate entries are displayed with the continuation symbol ╎ ↳ indented under the primary entry. The format is [TGID] (virtual_address) name when threads are hidden, or [TGID TID] when showing threads (where TGID is the Thread Group ID, commonly known as PID).
Extract files from memory
)
)
)
Troubleshooting
Use the --debug flag to enable verbose debug messages during initialization and analysis:
shellandquerymodes: Debug messages are stored in thelog_messagesSQL table. Query them withSELECT * FROM log_messages;commandmode: Debug messages are printed directly to stderr.
For initialization issues that prevent mquire from successfully loading the snapshot, using command mode with a simple command like .system_version is recommended, as it prints debug output to stderr immediately without needing to query the log_messages table.
Development
This project uses just as a command runner. Run just to see available commands:
| Command | Description |
|---|---|
just check |
Run all checks (cargo check, cargo clippy, cargo fmt, ruff, mypy) |
just test |
Run unit tests |
just format |
Format code (cargo fmt, ruff) |
just integration-test |
Run SQL query integration tests |
just integration-update |
Update expected test output |
just package |
Build release packages |
SQL query integration tests
These tests verify mquire produces correct output for queries against memory snapshots.
just integration-test- Run tests and compare output to expected JSON filesjust integration-update- Update expected JSON files with actual output (use when changing table schemas)
After running integration-update, review the git diff to ensure changes match your expectations before committing.
Adding new tests: Create a .sql file and matching .json file in the appropriate snapshot directory, then run just integration-update to populate the expected output.
Contributing
Contributions are welcome! When contributing, please follow these guidelines:
- Test your changes - Make sure your changes work correctly before submitting
- Keep dependencies minimal - Only add new dependencies when absolutely necessary
- Avoid caching volatile data - Do not cache values that could move or change within kernel objects. Only cache stable references like:
- Kallsyms location
init_taskvirtual address- BTF data
License
This project is licensed under the Apache License 2.0. See the LICENSE file for details.