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-systo call Windows APIs such asGetCurrentProcess,K32GetProcessMemoryInfo, andVirtualQueryEx. 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 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,
}
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
-
let mut mem_counters: PROCESS_MEMORY_COUNTERS = mem::zeroed();- Use
mem::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.
- Use
-
let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;- Use
mem::size_ofto compute the size of thePROCESS_MEMORY_COUNTERSstructure in bytes. - Convert that size to
u32, which is then used as a parameter in the API call below.
- Use
-
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.
-
Parameter explanation:
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
-
let MEMINFO_SIZE = mem::size_of::<MEMORY_BASIC_INFORMATION>();-
Purpose: Compute the size of
MEMORY_BASIC_INFORMATIONin bytes and store it inMEMINFO_SIZE. -
Reason: The
VirtualQueryExfunction requires the size of this structure buffer so it can write the query result correctly.
-
Purpose: Compute the size of
-
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.
-
Purpose: Initialize the starting address for the virtual-memory scan by setting the first scan address to
-
let mut mem_info: MEMORY_BASIC_INFORMATION = mem::zeroed();-
Purpose: Use
mem::zeroed()to create and initialize aMEMORY_BASIC_INFORMATIONstructure, 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. -
Purpose: Use
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.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.