[Advanced Rust] 1.8. Memory Pt.6 - Scanning Address Space Through the Operating System
1.8.1. Scan the Address Space Through the Operating System (Example)
Operating systems provide interfaces that let programs make requests - system calls. On Windows, KERNEL32.DLL provides functions for inspecting and manipulating the memory of running processes. This example is performed on Windows.
Why use Windows as the example? The function names are easy to understand. No knowledge of the POSIX API is required.
1.8.2. Dependencies
This example uses the windows-sys crate, which provides low-level bindings to the Windows API. Add the following dependency to Cargo.toml:
[dependencies]
windows-sys = { version = "0.59.0" , features = [
"Win32_Foundation",
"Win32_System_Memory",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
] }
Here we use windows-sys to call Windows APIs such as GetCurrentProcess, K32GetProcessMemoryInfo, and VirtualQueryEx. Those modules are feature-gated, so the features above must be enabled.
1.8.3. Main Program
Then bring the required items into scope in main.rs:
use std :: ffi :: c_void ;
use std :: mem ;
use windows_sys :: Win32 :: System :: Memory ::{ VirtualQueryEx , MEMORY_BASIC_INFORMATION };
use windows_sys :: Win32 :: System :: ProcessStatus ::{ PROCESS_MEMORY_COUNTERS , K32GetProcessMemoryInfo };
use windows_sys :: Win32 :: System :: Threading ::{ GetCurrentProcess , GetCurrentProcessId };
/// Windows `PVOID` / `SIZE_T` as used by the Win32 APIs.
/// In `windows-sys` 0.59+, these are expressed as raw Rust types rather than
/// named aliases under `Win32::Foundation`.
type PVOID = * mut c_void ;
type SIZE_T = usize ;
PVOID: Represents avoid*pointer, used to describe an opaque memory address. Here it is a local alias for*mut c_void.SIZE_T: Corresponds to an unsigned integer type used to represent the size of a memory region. Here it is a local alias forusize(matching thewindows-sys0.59+ signatures).MEMORY_BASIC_INFORMATION: A built-in system structure used to describe the basic information of a memory region.PROCESS_MEMORY_COUNTERS: A structure used to record a process’s memory usage.K32GetProcessMemoryInfo: This function retrieves memory information for the current process.GetCurrentProcessandGetCurrentProcessId: Retrieve the current process handle and process ID, respectively.
For convenient Debug output, we wrap the PROCESS_MEMORY_COUNTERS returned by the Windows API in a custom ProcessInfo structure:
#[derive(Debug)]
struct ProcessInfo {
cb : u32 ,
page_fault_count : u32 ,
peak_working_set_size : usize ,
working_set_size : usize ,
quota_peak_paged_pool_usage : usize ,
quota_paged_pool_usage : usize ,
quota_peak_non_paged_pool_usage : usize ,
quota_non_paged_pool_usage : usize ,
pagefile_usage : usize ,
peak_pagefile_usage : usize ,
}
cb(u32): Description: The size of the structure in bytes. Purpose: Identifies the size of the structure for compatibility.page_fault_count(u32): Description: The total number of page faults since the process started. Purpose: A page fault is the handling process triggered when a memory access misses physical memory. It includes soft faults (data obtained from the file cache) and hard faults (data loaded from disk).peak_working_set_size(usize): Description: The peak size of the working set used by the process, meaning the memory currently resident in physical memory. Purpose: Used to monitor the process’s peak memory usage.working_set_size(usize): Description: The current size of the process’s working set. Purpose: Shows how much physical memory the process is currently using.quota_peak_paged_pool_usage(usize): Description: The peak size of the process’s paged-pool quota usage. Purpose: The paged pool is kernel-mode memory that can be paged out to disk.quota_paged_pool_usage(usize): Description: The current size of the process’s paged-pool quota usage. Purpose: Used to monitor the amount of pageable kernel memory currently in use.quota_peak_non_paged_pool_usage(usize): Description: The peak size of the process’s non-paged-pool quota usage. Purpose: The non-paged pool is kernel-mode memory that remains permanently resident in physical memory.quota_non_paged_pool_usage(usize): Description: The current size of the process’s non-paged-pool quota usage. Purpose: Used to monitor the amount of non-pageable kernel memory currently in use.pagefile_usage(usize): Description: The current amount of space used by the process in the page file. Purpose: Indicates how much of the process’s data has been paged to disk.peak_pagefile_usage(usize): Description: The peak amount of page-file space used by the process. Purpose: Used to monitor the process’s page-file high-water mark.
MEMORY_BASIC_INFORMATION from windows-sys does not implement Debug, so we also wrap its fields for printing:
#[derive(Debug)]
struct MemoryBasicInfo {
base_address : * mut c_void ,
allocation_base : * mut c_void ,
allocation_protect : u32 ,
region_size : usize ,
state : u32 ,
protect : u32 ,
type_ : u32 ,
}
Get the current process handle and process ID
These must go inside an unsafe block:
let this_proc = GetCurrentProcess ();
let this_pid = GetCurrentProcessId ();
Retrieve process memory information
let mut mem_counters : PROCESS_MEMORY_COUNTERS = mem :: zeroed ();
let mem_counters_size = mem :: size_of :: < PROCESS_MEMORY_COUNTERS > () as u32 ;
K32GetProcessMemoryInfo ( this_proc , & mut mem_counters , mem_counters_size );
let mut mem_counters: PROCESS_MEMORY_COUNTERS = mem::zeroed();- Usemem::zeroed()to create and initialize aPROCESS_MEMORY_COUNTERSstructure so that all of its fields are zero.PROCESS_MEMORY_COUNTERSis a predefined Windows structure used to store process memory statistics.let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;- Usemem::size_ofto compute the size of thePROCESS_MEMORY_COUNTERSstructure in bytes. Convert that size tou32, which is then used as a parameter in the API call below.K32GetProcessMemoryInfo(this_proc, &mut mem_counters, mem_counters_size);- Parameter explanation:this_proc: The current process handle, indicating which process’s memory data you want to query.&mut mem_counters: A mutable reference to thePROCESS_MEMORY_COUNTERSstructure, used to receive the memory statistics returned by the API.mem_counters_size: The size of the structure, ensuring the API can correctly read and populate the structure data.- Purpose: Call
K32GetProcessMemoryInfoto fillmem_counterswith the current process’s memory state, such as the page-fault count, working-set size, and page-file usage.
Wrap the memory information in our custom ProcessInfo
let proc_info = ProcessInfo {
cb : mem_counters .cb ,
page_fault_count : mem_counters .PageFaultCount ,
peak_working_set_size : mem_counters .PeakWorkingSetSize ,
working_set_size : mem_counters .WorkingSetSize ,
quota_peak_paged_pool_usage : mem_counters .QuotaPeakPagedPoolUsage ,
quota_paged_pool_usage : mem_counters .QuotaPagedPoolUsage ,
quota_peak_non_paged_pool_usage : mem_counters .QuotaPeakNonPagedPoolUsage ,
quota_non_paged_pool_usage : mem_counters .QuotaNonPagedPoolUsage ,
pagefile_usage : mem_counters .PagefileUsage ,
peak_pagefile_usage : mem_counters .PeakPagefileUsage ,
};
Define the starting and ending addresses for the scan
let min_addr : PVOID = 0 as PVOID ;
// Set a typical upper bound for a 64-bit user-mode address space
let max_addr : PVOID = 0x00007FFF_FFFF_FFFF as PVOID ;
Print the process information and the address range
println! ( "{:p} @ {:p}" , this_pid as * const (), this_proc as * const ());
println! ( "{:?}" , proc_info );
println! ( "min: {:p}, max: {:p}" , min_addr , max_addr );
Initialize the parameters required by VirtualQueryEx
let MEMINFO_SIZE = mem :: size_of :: < MEMORY_BASIC_INFORMATION > ();
let mut base_addr : PVOID = min_addr ;
let mut mem_info : MEMORY_BASIC_INFORMATION = mem :: zeroed ();
let MEMINFO_SIZE = mem::size_of::<MEMORY_BASIC_INFORMATION>();- Purpose: Compute the size ofMEMORY_BASIC_INFORMATIONin bytes and store it inMEMINFO_SIZE. Reason: TheVirtualQueryExfunction requires the size of this structure buffer so it can write the query result correctly.let mut base_addr: PVOID = min_addr;- Purpose: Initialize the starting address for the virtual-memory scan by setting the first scan address tomin_addr.base_addrrepresents the starting address of the current query and will be incremented in the loop below to traverse the entire virtual address space.let mut mem_info: MEMORY_BASIC_INFORMATION = mem::zeroed();- Purpose: Usemem::zeroed()to create and initialize aMEMORY_BASIC_INFORMATIONstructure, setting all fields to zero. Reason: This structure will store the result ofVirtualQueryEx, namely the detailed information of the memory region corresponding to the current query address.
Scan the entire address space by calling VirtualQueryEx in a loop
loop {
let rc : SIZE_T = VirtualQueryEx ( this_proc , base_addr , & mut mem_info , MEMINFO_SIZE as SIZE_T );
if rc == 0 {
break ;
}
// `MEMORY_BASIC_INFORMATION` from `windows-sys` does not implement `Debug`,
// so wrap the fields we care about for printing.
let printable = MemoryBasicInfo {
base_address : mem_info .BaseAddress ,
allocation_base : mem_info .AllocationBase ,
allocation_protect : mem_info .AllocationProtect ,
region_size : mem_info .RegionSize ,
state : mem_info .State ,
protect : mem_info .Protect ,
type_ : mem_info .Type ,
};
println! ( "{:#?}" , printable );
// Add the size of the current region to get the next query address
base_addr = (( base_addr as usize ) + mem_info .RegionSize ) as PVOID ;
if ( base_addr as usize ) >= ( max_addr as usize ) {
break ;
}
}
1.8.4. Full Code
main.rs:
use std :: ffi :: c_void ;
use std :: mem ;
use windows_sys :: Win32 :: System :: Memory ::{ VirtualQueryEx , MEMORY_BASIC_INFORMATION };
use windows_sys :: Win32 :: System :: ProcessStatus ::{ PROCESS_MEMORY_COUNTERS , K32GetProcessMemoryInfo };
use windows_sys :: Win32 :: System :: Threading ::{ GetCurrentProcess , GetCurrentProcessId };
/// Windows `PVOID` / `SIZE_T` as used by the Win32 APIs.
/// In `windows-sys` 0.59+, these are expressed as raw Rust types rather than
/// named aliases under `Win32::Foundation`.
type PVOID = * mut c_void ;
type SIZE_T = usize ;
/// To allow Debug-formatted output, we wrap PROCESS_MEMORY_COUNTERS ourselves.
#[derive(Debug)]
struct ProcessInfo {
cb : u32 ,
page_fault_count : u32 ,
peak_working_set_size : usize ,
working_set_size : usize ,
quota_peak_paged_pool_usage : usize ,
quota_paged_pool_usage : usize ,
quota_peak_non_paged_pool_usage : usize ,
quota_non_paged_pool_usage : usize ,
pagefile_usage : usize ,
peak_pagefile_usage : usize ,
}
/// `MEMORY_BASIC_INFORMATION` from `windows-sys` does not implement `Debug`.
#[derive(Debug)]
struct MemoryBasicInfo {
base_address : * mut c_void ,
allocation_base : * mut c_void ,
allocation_protect : u32 ,
region_size : usize ,
state : u32 ,
protect : u32 ,
type_ : u32 ,
}
fn main () {
unsafe {
// Get the current process handle and process ID
let this_proc = GetCurrentProcess ();
let this_pid = GetCurrentProcessId ();
// Get process memory information
let mut mem_counters : PROCESS_MEMORY_COUNTERS = mem :: zeroed ();
let mem_counters_size = mem :: size_of :: < PROCESS_MEMORY_COUNTERS > () as u32 ;
K32GetProcessMemoryInfo ( this_proc , & mut mem_counters , mem_counters_size );
// Wrap the memory information in our custom ProcessInfo
let proc_info = ProcessInfo {
cb : mem_counters .cb ,
page_fault_count : mem_counters .PageFaultCount ,
peak_working_set_size : mem_counters .PeakWorkingSetSize ,
working_set_size : mem_counters .WorkingSetSize ,
quota_peak_paged_pool_usage : mem_counters .QuotaPeakPagedPoolUsage ,
quota_paged_pool_usage : mem_counters .QuotaPagedPoolUsage ,
quota_peak_non_paged_pool_usage : mem_counters .QuotaPeakNonPagedPoolUsage ,
quota_non_paged_pool_usage : mem_counters .QuotaNonPagedPoolUsage ,
pagefile_usage : mem_counters .PagefileUsage ,
peak_pagefile_usage : mem_counters .PeakPagefileUsage ,
};
// Define the start and end addresses of the scan
let min_addr : PVOID = 0 as PVOID ;
// Set a typical upper bound for a 64-bit user-mode address space
let max_addr : PVOID = 0x00007FFF_FFFF_FFFF as PVOID ;
// Print
println! ( "{:p} @ {:p}" , this_pid as * const (), this_proc as * const ());
println! ( "{:?}" , proc_info );
println! ( "min: {:p}, max: {:p}" , min_addr , max_addr );
// Initialize the parameters required by VirtualQueryEx
let MEMINFO_SIZE = mem :: size_of :: < MEMORY_BASIC_INFORMATION > ();
let mut base_addr : PVOID = min_addr ;
let mut mem_info : MEMORY_BASIC_INFORMATION = mem :: zeroed ();
// Loop over VirtualQueryEx to scan the entire address space
loop {
let rc : SIZE_T = VirtualQueryEx ( this_proc , base_addr , & mut mem_info , MEMINFO_SIZE as SIZE_T );
if rc == 0 {
break ;
}
let printable = MemoryBasicInfo {
base_address : mem_info .BaseAddress ,
allocation_base : mem_info .AllocationBase ,
allocation_protect : mem_info .AllocationProtect ,
region_size : mem_info .RegionSize ,
state : mem_info .State ,
protect : mem_info .Protect ,
type_ : mem_info .Type ,
};
println! ( "{:#?}" , printable );
// Add the size of the current region to get the next query address
base_addr = (( base_addr as usize ) + mem_info .RegionSize ) as PVOID ;
if ( base_addr as usize ) >= ( max_addr as usize ) {
break ;
}
}
}
}
Cargo.toml:
[package]
name = "RustStudy"
version = "0.1.0"
edition = "2021"
[dependencies]
windows-sys = { version = "0.59.0" , features = [
"Win32_Foundation",
"Win32_System_Memory",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
] }
1.8.5. Steps for Reading and Writing Process Memory
The logic for reading and writing process memory is fairly simple. Pseudocode:
let pid = some_process_id ;
OpenProcess ( pid );
loop over the address space {
call VirtualQueryEx () to reach the next memory block
use ReadProcessMemory () to access the memory block
search for a specific pattern
call WriteProcessMemory () with t
Comments
No comments yet. Start the discussion.