A comprehensive framework for Virtual Machine Introspection (VMI) implemented in Rust, providing safe abstractions for analyzing and manipulating virtual machine state from the outside.
- Introduction
- Features
- Quick Start
- Installation
- Examples
- Core Concepts
- Architecture
- ISR
- Current Limitations
- See Also
- License
VMI is a powerful technique for analyzing and manipulating virtual machines from the outside. It is used in a variety of security applications, including malware analysis, intrusion detection, and digital forensics.
However, VMI is complex and error-prone, requiring low-level interactions with the virtual machine. This framework aims to simplify VMI by providing a high-level, type-safe API for common operations, such as memory access, CPU register manipulation, and OS-specific introspection.
The framework is designed to be modular and extensible, supporting multiple CPU architectures, hypervisors, and operating systems. It includes built-in support for AMD64 architecture, Xen hypervisor, and Windows and Linux operating systems.
VMI involves interacting with a virtual machine at a very low level, often requiring direct manipulation of memory and registers. A common challenge is the semantic gap between these low-level operations (e.g., reading memory) and the higher-level understanding of the guest OS needed for meaningful analysis (e.g., enumerating processes and analyzing their modules).
This framework addresses this gap through a layered architecture,
from raw hypervisor interactions, through OS-specific abstractions
like WindowsOs
and LinuxOs
, to integration with the ISR
library, providing version-agnostic access to OS internals.
Let's be honest, VMI doesn't get the love it deserves. While incredibly useful, it's not as widely supported as it should be. Xen is currently the champion of VMI support among major hypervisors. Other hypervisors, like VMware, Hyper-V, and VirtualBox, haven't quite jumped on the VMI bandwagon yet.
There have been attempts to bring VMI to other platforms, such as the KVM-VMI project. Unfortunately, these efforts haven't been merged into the mainline and the project hasn't been updated in a while.
This project aims to shine a spotlight on VMI and encourage wider adoption. While currently focused on Xen, the framework is designed to be hypervisor-agnostic. We're optimistically waiting for the day when other hypervisors join the VMI party!
This project is still in its early stages and under active development. Expect breaking changes and rough edges. Feedback, bug reports, and contributions are welcome!
-
Configurable caching mechanisms for physical page lookups and Virtual-to-Physical address translations to improve performance.
-
ISR library for version-agnostic OS introspection.
-
Sophisticated error handling, including robust page-fault handling.
-
Modular architecture allowing for seamless integration of new hypervisor drivers, CPU architectures, and OS support.
-
Batteries included:
- Built-in OS support with
WindowsOs
andLinuxOs
. - Powerful utilities like
BreakpointManager
,PageTableMonitor
, andInjectorHandler
.
- Built-in OS support with
Add the following to your Cargo.toml
:
[dependencies]
vmi = "0.1"
Basic usage example:
use isr::{cache::JsonCodec, IsrCache};
use vmi::{
arch::amd64::Amd64,
driver::xen::VmiXenDriver,
os::windows::WindowsOs,
VcpuId, VmiCore, VmiSession,
};
use xen::XenDomainId;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup VMI.
let driver = VmiXenDriver::<Amd64>::new(XenDomainId(1))?;
let core = VmiCore::new(driver)?;
// Try to find the kernel information.
// This is necessary in order to load the profile.
let kernel_info = {
let _pause_guard = core.pause_guard()?;
let registers = core.registers(VcpuId(0))?;
WindowsOs::find_kernel(&core, ®isters)?.expect("kernel information")
};
// Load the profile.
// The profile contains offsets to kernel functions and data structures.
let isr = IsrCache::<JsonCodec>::new("cache")?;
let entry = isr.entry_from_codeview(kernel_info.codeview)?;
let profile = entry.profile()?;
// Create the VMI session.
let os = WindowsOs::<VmiXenDriver<Amd64>>::new(&profile)?;
let session = VmiSession::new(&core, &os);
// Get the list of processes and print them.
let _pause_guard = session.pause_guard()?;
let registers = session.registers(VcpuId(0))?;
let processes = session.os().processes(®isters)?;
println!("Processes: {processes:#?}");
Ok(())
}
But first, you need to install the prerequisites.
The framework has been tested on Ubuntu 22.04 and Xen 4.19. Note that Xen 4.19 is the minimum version required to use the framework, and it is the current version (at the time of writing).
Unfortunately, Xen 4.19 is not available in the official Ubuntu repositories, so it must be built from source.
This guide assumes you have a fresh Ubuntu 22.04 installation.
Sorry, the guide is still under construction. Please check back later.
The framework includes several examples demonstrating various VMI capabilities, from basic operations to more complex scenarios.
-
Demonstrates fundamental VMI operations like retrieving the Interrupt Descriptor Table (IDT) for each virtual CPU.
-
Shows how to retrieve and display a list of running processes in the guest VM.
-
Illustrates the usage of the
BreakpointManager
andPageTableMonitor
to set and manage breakpoints on Windows systems. -
A simple example of code injection using a recipe to display a message box in the guest.
-
Demonstrates injecting code that writes data to a file in the guest.
-
windows-recipe-writefile-advanced.rs
A more complex example showing how to write to a file in chunks and handle potential errors during injection.
The framework uses distinct types to represent different kinds of memory addresses within the guest:
These types provide type safety and support arithmetic operations, comparisons and formatting.
Example:
use vmi::{
arch::amd64::{Amd64, Cr3},
Architecture as _, Gfn, Pa, Va,
};
let gfn = Gfn(0x1aa);
let pa = Amd64::pa_from_gfn(gfn);
assert_eq!(pa, Pa(0x1aa000));
let pa = Pa(0x1aa000);
let gfn = Amd64::gfn_from_pa(pa);
assert_eq!(gfn, Gfn(0x1aa));
let cr3 = Cr3(0x1aa000);
let va = Va(0xfffff804590c8980);
let pa = Amd64::translate_address(vmi, va, cr3.into())?;
Additionally, two key structures manage address translation:
-
AddressContext
: Combines a virtual address (Va
) and a translation root (Pa
, typically theCR3
register) to provide a complete context for virtual-to-physical address translation.This structure is used as input for address translation and memory access functions.
Example:
let cr3 = Cr3(0x1aa000); let va = Va(0xfffff804590c8980); let address_context = AddressContext::new(va, cr3);
-
AccessContext
: Defines the context for memory operations, encapsulating the target address and theTranslationMechanism
. This allows for both direct physical access and paging-based translation.Example:
// Direct physical memory access: let access_context = AccessContext::direct(Pa(0x1fc7980)); assert!(matches!( access_context.mechanism, TranslationMechanism::Direct )); // Paging-based translation: let cr3 = Cr3(0x1aa000); let va = Va(0xfffff804590c8980); let access_context = AccessContext::paging(va, cr3); assert!(matches!( access_context.mechanism, TranslationMechanism::Paging { root: Some(Pa(0x1aa000)) } ));
The framework is designed to be modular and extensible, supporting multiple CPU architectures, hypervisors, and operating systems.
The core components of the framework are:
-
Architecture
: A trait abstracting CPU architecture-specific logic, such as register definitions and address translation.Currently, the framework includes an
Amd64
implementation. -
VmiDriver
: A trait defining the interface for interacting with the hypervisor. This allows the framework to support multiple hypervisors.Currently, the framework includes a
VmiXenDriver
for Xen. -
VmiCore
: Provides raw VMI operations, interacting directly with theVmiDriver
and leveraging theArchitecture
. It handles memory access, address translation, and register manipulation, but has no inherent OS awareness.Importantly,
VmiCore
does not store register state, requiring it to be explicitly provided for operations that depend on it. -
VmiOs
: A trait defining OS-specific introspection operations. Implementations of this trait, such asWindowsOs
andLinuxOs
, provide higher-level functions for interacting with the guest OS, bridging the semantic gap between raw memory access and meaningful OS analysis. -
VmiSession
: Combines aVmiCore
with aVmiOs
implementation to provide OS-aware operations. This enables high-level introspection tasks, but - likeVmiCore
-VmiSession
does not store register state. -
VmiContext
: Represents a point-in-time state of the virtual CPU during event handling. UnlikeVmiCore
(andVmiSession
),VmiContext
does hold the register state at the time of the event, simplifying event handler logic.It provides access to both
VmiCore
andVmiOs
functionality within a specificVmiEvent
. -
VmiError
: Represents errors that can occur during VMI operations, including translation faults (PageFault
).
Each of these structures can be implicitly dereferenced down the hierarchy.
This means that VmiContext
implements Deref
to VmiSession
,
which in turn implements Deref
to VmiCore
.
This design enables convenient access to lower-level functionality:
-
Access
VmiCore
methods directly from aVmiSession
orVmiContext
without explicit dereferencing. -
Pass a
&VmiContext
to functions expecting a&VmiSession
or&VmiCore
.
Both VmiSession
and VmiContext
provide access to OS-specific
functionality through the os()
method. This method returns a structure
implementing the VmiOs
trait methods, as well as any additional
OS-specific operations.
As pointed out above, VmiCore
and VmiSession
do not store register
state. This means that functions requiring register information (e.g.,
for address translation or OS-specific operations) must be explicitly
provided with the register state.
VmiContext
, on the other hand, does hold the register state at
the time of the event. This difference has important implications for
how you interact with these components:
-
With
VmiCore
andVmiSession
, you must explicitly provide the translation root (e.g.,CR3
) when performing memory operations:let va = Va(0xfffff804590c8980); // let vmi: &VmiSession = ...; let registers = vmi.registers(VcpuId(0))?; let value = vmi.read_u64((va, registers.cr3.into()))?; // Explicitly pass the translation root (CR3)
-
With
VmiContext
, register state is managed internally:// let vmi: &VmiContext = ...; let value = vmi.read_u64(va)?; // No need to pass the translation root
This extends to OS-specific operations as well.
VmiSession
requires explicit register state:
// let vmi: &VmiSession = ...;
let registers = vmi.registers(VcpuId(0))?;
let process = vmi.os().current_process(®isters)?;
let process_id = vmi.os().process_id(®isters, process)?;
VmiContext
simplifies this by providing register state implicitly:
// let vmi: &VmiContext = ...;
let process = vmi.os().current_process()?;
let process_id = vmi.os().process_id(process)?;
The event system allows responding to guest activities:
-
VmiEvent
: Represents various guest events (memory access, interrupts, register changes). Carries event-specific data and register state at the time of the event. -
VmiHandler
: A trait for implementing event handlers. Thehandle_event
method defines how your application responds to specific guest events. -
VmiEventResponse
: Controls guest execution after an event. Options include continuing, single-stepping and modifying registers.
Several utility components are provided to simplify common VMI tasks:
-
PageTableMonitor
: Tracks page table modifications, generatingPageIn
/PageOut
events. -
BreakpointManager
: Manages software breakpoints, handlingPageIn
/PageOut
events. -
InjectorHandler
: Provides a high-level interface for code injection, handling thread hijacking and argument marshalling. -
Interceptor
: Low-level breakpoint management. UseBreakpointManager
instead whenever possible.
Consult the
isr
crate documentation for more information and examples.
The framework leverages Intermediate Symbol Representation (ISR) for version-agnostic OS introspection. It avoids the need for hardcoding offsets and makes the code adaptable to different OS versions.
-
IsrCache
: Manages symbol files (PDB for Windows, DWARF for Linux). Automatically downloads and caches PDBs based on CodeView information (Windows) or kernel version banner (Linux). -
symbols!
macro: Defines symbols for lookup.Example:
use isr::macros::symbols; symbols! { pub struct Symbols { NtCreateFile: u64, PsActiveProcessHead: u64, } }
-
offsets!
macro: Defines structure offsets.Example:
use isr::macros::{offsets, Field}; offsets! { pub struct Offsets { struct _EPROCESS { UniqueProcessId: Field, ActiveProcessLinks: Field, } } }
-
Architecture Support: Currently only AMD64 is supported. No x86 (32-bit) support, including 32-bit paging or code injection into 32-bit processes. 5-level paging is also not supported.
-
Hypervisor Support: Only Xen is supported through
VmiXenDriver
. -
Operating System Support:
- Windows: Good support for Windows 7 and later.
- Linux: Limited functionality. Many features are still under development.
- No other operating systems are currently supported.
If you're new to VMI or looking for more information, check out these amazing projects and resources:
- libvmi: A popular VMI library written in C.
- hvmi: Hypervisor Memory Introspection from Bitdefender.
- drakvuf: Dynamic malware analysis system using VMI.
- KVM-VMI: A project to bring VMI to the KVM hypervisor.
This project is licensed under the MIT license.