DEV Community

[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 a void* 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 for usize (matching the windows-sys 0.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.
  • GetCurrentProcess and GetCurrentProcessId: 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(); - Use mem::zeroed() to create and initialize a PROCESS_MEMORY_COUNTERS structure so that all of its fields are zero. PROCESS_MEMORY_COUNTERS is a predefined Windows structure used to store process memory statistics.
  • let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32; - Use mem::size_of to compute the size of the PROCESS_MEMORY_COUNTERS structure in bytes. Convert that size to u32, 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 the PROCESS_MEMORY_COUNTERS structure, 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 K32GetProcessMemoryInfo to fill mem_counters with 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 of MEMORY_BASIC_INFORMATION in bytes and store it in MEMINFO_SIZE. Reason: The VirtualQueryEx function 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 to min_addr. base_addr represents 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: Use mem::zeroed() to create and initialize a MEMORY_BASIC_INFORMATION structure, setting all fields to zero. Reason: This structure will store the result of VirtualQueryEx, 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.