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",
] }

Enter fullscreen mode Exit fullscreen mode

  • 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;

Enter fullscreen mode Exit fullscreen mode

  • 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,
}

Enter fullscreen mode Exit fullscreen mode

  • 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,
}

Enter fullscreen mode Exit fullscreen mode


Get the current process handle and process ID (these must go inside an unsafe block):

let this_proc = GetCurrentProcess();
let this_pid = GetCurrentProcessId();

Enter fullscreen mode Exit fullscreen mode


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);

Enter fullscreen mode Exit fullscreen mode

  1. 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.
  2. 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.
  3. 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,
};

Enter fullscreen mode Exit fullscreen mode


Define the starting and ending addresses for the scan:

let min_addr: PVOID = 0 as PVOID;

Enter fullscreen mode Exit fullscreen mode


Set a typical upper bound for a 64-bit user-mode address space:

let max_addr: PVOID = 0x00007FFF_FFFF_FFFF as PVOID;

Enter fullscreen mode Exit fullscreen mode


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);

Enter fullscreen mode Exit fullscreen mode


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();

Enter fullscreen mode Exit fullscreen mode

  1. 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.
  2. 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.
  3. 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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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",
] }

Enter fullscreen mode Exit fullscreen mode

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 the value you need
}

Enter fullscreen mode Exit fullscreen mode

  • let pid = some_process_id;: Get the current process ID
  • OpenProcess(pid);: Open this process

Linux provides simple APIs: process_vm_readv() and process_vm_writev(), which correspond to ReadProcessMemory() and WriteProcessMemory() on Windows.