Files
ghost/ghost-core/src/memory.rs

813 lines
26 KiB
Rust

//! Memory region enumeration and analysis.
//!
//! This module provides cross-platform memory introspection capabilities,
//! allowing analysis of process memory layouts, protection flags, and content.
use serde::{Deserialize, Serialize};
use std::fmt;
/// PE header constants
#[cfg(windows)]
pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; // "MZ"
#[cfg(windows)]
pub const IMAGE_NT_SIGNATURE: u32 = 0x00004550; // "PE\0\0"
/// DOS header structure (first 64 bytes of a PE file)
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageDosHeader {
pub e_magic: u16, // Magic number ("MZ")
pub e_cblp: u16, // Bytes on last page
pub e_cp: u16, // Pages in file
pub e_crlc: u16, // Relocations
pub e_cparhdr: u16, // Size of header in paragraphs
pub e_minalloc: u16, // Minimum extra paragraphs
pub e_maxalloc: u16, // Maximum extra paragraphs
pub e_ss: u16, // Initial SS value
pub e_sp: u16, // Initial SP value
pub e_csum: u16, // Checksum
pub e_ip: u16, // Initial IP value
pub e_cs: u16, // Initial CS value
pub e_lfarlc: u16, // File address of relocation table
pub e_ovno: u16, // Overlay number
pub e_res: [u16; 4], // Reserved
pub e_oemid: u16, // OEM identifier
pub e_oeminfo: u16, // OEM information
pub e_res2: [u16; 10], // Reserved
pub e_lfanew: i32, // File address of new exe header
}
/// PE file header structure
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageFileHeader {
pub machine: u16,
pub number_of_sections: u16,
pub time_date_stamp: u32,
pub pointer_to_symbol_table: u32,
pub number_of_symbols: u32,
pub size_of_optional_header: u16,
pub characteristics: u16,
}
/// PE optional header structure (64-bit)
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ImageOptionalHeader64 {
pub magic: u16,
pub major_linker_version: u8,
pub minor_linker_version: u8,
pub size_of_code: u32,
pub size_of_initialized_data: u32,
pub size_of_uninitialized_data: u32,
pub address_of_entry_point: u32,
pub base_of_code: u32,
pub image_base: u64,
pub section_alignment: u32,
pub file_alignment: u32,
pub major_operating_system_version: u16,
pub minor_operating_system_version: u16,
pub major_image_version: u16,
pub minor_image_version: u16,
pub major_subsystem_version: u16,
pub minor_subsystem_version: u16,
pub win32_version_value: u32,
pub size_of_image: u32,
pub size_of_headers: u32,
pub check_sum: u32,
pub subsystem: u16,
pub dll_characteristics: u16,
pub size_of_stack_reserve: u64,
pub size_of_stack_commit: u64,
pub size_of_heap_reserve: u64,
pub size_of_heap_commit: u64,
pub loader_flags: u32,
pub number_of_rva_and_sizes: u32,
}
/// PE header validation result
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PEHeaderValidation {
Valid,
InvalidDosSignature,
InvalidNtSignature,
InvalidHeaderOffset,
MismatchedImageBase,
SuspiciousEntryPoint,
CorruptedHeader,
NotPE,
}
impl fmt::Display for PEHeaderValidation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Valid => write!(f, "Valid PE header"),
Self::InvalidDosSignature => write!(f, "Invalid DOS signature"),
Self::InvalidNtSignature => write!(f, "Invalid NT signature"),
Self::InvalidHeaderOffset => write!(f, "Invalid header offset"),
Self::MismatchedImageBase => write!(f, "Image base mismatch"),
Self::SuspiciousEntryPoint => write!(f, "Suspicious entry point"),
Self::CorruptedHeader => write!(f, "Corrupted PE header"),
Self::NotPE => write!(f, "Not a PE file"),
}
}
}
/// Memory protection flags for a memory region.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MemoryProtection {
NoAccess,
ReadOnly,
ReadWrite,
ReadExecute,
ReadWriteExecute,
Execute,
WriteCopy,
Unknown,
}
impl fmt::Display for MemoryProtection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::NoAccess => "---",
Self::ReadOnly => "R--",
Self::ReadWrite => "RW-",
Self::ReadExecute => "R-X",
Self::ReadWriteExecute => "RWX",
Self::Execute => "--X",
Self::WriteCopy => "WC-",
Self::Unknown => "???",
};
write!(f, "{}", s)
}
}
/// Information about a memory region within a process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRegion {
/// Base address of the memory region.
pub base_address: usize,
/// Size of the region in bytes.
pub size: usize,
/// Memory protection flags.
pub protection: MemoryProtection,
/// Type of memory region (IMAGE, MAPPED, PRIVATE, etc.).
pub region_type: String,
}
impl fmt::Display for MemoryRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:#016x} - {:#016x} {} {}",
self.base_address,
self.base_address + self.size,
self.protection,
self.region_type
)
}
}
/// Validates a PE header in process memory
#[cfg(windows)]
pub fn validate_pe_header(pid: u32, base_address: usize) -> anyhow::Result<PEHeaderValidation> {
use std::mem;
// Read DOS header
let dos_header_size = mem::size_of::<ImageDosHeader>();
let dos_header_bytes = read_process_memory(pid, base_address, dos_header_size)?;
if dos_header_bytes.len() < dos_header_size {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let dos_header = unsafe {
std::ptr::read(dos_header_bytes.as_ptr() as *const ImageDosHeader)
};
// Validate DOS signature
if dos_header.e_magic != IMAGE_DOS_SIGNATURE {
return Ok(PEHeaderValidation::InvalidDosSignature);
}
// Validate e_lfanew offset (should be reasonable)
if dos_header.e_lfanew < 0 || dos_header.e_lfanew > 0x1000 {
return Ok(PEHeaderValidation::InvalidHeaderOffset);
}
// Read NT headers
let nt_header_address = base_address.wrapping_add(dos_header.e_lfanew as usize);
// Read NT signature (4 bytes)
let nt_sig_bytes = read_process_memory(pid, nt_header_address, 4)?;
if nt_sig_bytes.len() < 4 {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let nt_signature = u32::from_le_bytes([
nt_sig_bytes[0],
nt_sig_bytes[1],
nt_sig_bytes[2],
nt_sig_bytes[3],
]);
if nt_signature != IMAGE_NT_SIGNATURE {
return Ok(PEHeaderValidation::InvalidNtSignature);
}
// Read file header
let file_header_address = nt_header_address + 4;
let file_header_size = mem::size_of::<ImageFileHeader>();
let file_header_bytes = read_process_memory(pid, file_header_address, file_header_size)?;
if file_header_bytes.len() < file_header_size {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let file_header = unsafe {
std::ptr::read(file_header_bytes.as_ptr() as *const ImageFileHeader)
};
// Read optional header (64-bit)
let optional_header_address = file_header_address + file_header_size;
let optional_header_size = mem::size_of::<ImageOptionalHeader64>();
let optional_header_bytes = read_process_memory(pid, optional_header_address, optional_header_size)?;
if optional_header_bytes.len() < optional_header_size {
return Ok(PEHeaderValidation::CorruptedHeader);
}
let optional_header = unsafe {
std::ptr::read(optional_header_bytes.as_ptr() as *const ImageOptionalHeader64)
};
// Validate image base matches memory address
if optional_header.image_base != base_address as u64 {
return Ok(PEHeaderValidation::MismatchedImageBase);
}
// Validate entry point (should be within the image)
let entry_point_rva = optional_header.address_of_entry_point;
if entry_point_rva == 0 || entry_point_rva >= optional_header.size_of_image {
return Ok(PEHeaderValidation::SuspiciousEntryPoint);
}
// Additional validation: check if sections count is reasonable
if file_header.number_of_sections > 96 {
return Ok(PEHeaderValidation::CorruptedHeader);
}
Ok(PEHeaderValidation::Valid)
}
/// Stub for non-Windows platforms
#[cfg(not(windows))]
pub fn validate_pe_header(_pid: u32, _base_address: usize) -> anyhow::Result<PEHeaderValidation> {
Ok(PEHeaderValidation::NotPE)
}
/// Gets PE header information from process memory
#[cfg(windows)]
pub fn read_pe_header_info(pid: u32, base_address: usize) -> anyhow::Result<Option<PEHeaderInfo>> {
use std::mem;
let dos_header_size = mem::size_of::<ImageDosHeader>();
let dos_header_bytes = read_process_memory(pid, base_address, dos_header_size)?;
if dos_header_bytes.len() < dos_header_size {
return Ok(None);
}
let dos_header = unsafe {
std::ptr::read(dos_header_bytes.as_ptr() as *const ImageDosHeader)
};
if dos_header.e_magic != IMAGE_DOS_SIGNATURE {
return Ok(None);
}
if dos_header.e_lfanew < 0 || dos_header.e_lfanew > 0x1000 {
return Ok(None);
}
let nt_header_address = base_address.wrapping_add(dos_header.e_lfanew as usize);
// Read NT signature
let nt_sig_bytes = read_process_memory(pid, nt_header_address, 4)?;
if nt_sig_bytes.len() < 4 {
return Ok(None);
}
let nt_signature = u32::from_le_bytes([
nt_sig_bytes[0],
nt_sig_bytes[1],
nt_sig_bytes[2],
nt_sig_bytes[3],
]);
if nt_signature != IMAGE_NT_SIGNATURE {
return Ok(None);
}
// Read file header
let file_header_address = nt_header_address + 4;
let file_header_size = mem::size_of::<ImageFileHeader>();
let file_header_bytes = read_process_memory(pid, file_header_address, file_header_size)?;
if file_header_bytes.len() < file_header_size {
return Ok(None);
}
let file_header = unsafe {
std::ptr::read(file_header_bytes.as_ptr() as *const ImageFileHeader)
};
// Read optional header
let optional_header_address = file_header_address + file_header_size;
let optional_header_size = mem::size_of::<ImageOptionalHeader64>();
let optional_header_bytes = read_process_memory(pid, optional_header_address, optional_header_size)?;
if optional_header_bytes.len() < optional_header_size {
return Ok(None);
}
let optional_header = unsafe {
std::ptr::read(optional_header_bytes.as_ptr() as *const ImageOptionalHeader64)
};
Ok(Some(PEHeaderInfo {
dos_signature: dos_header.e_magic,
nt_signature,
machine: file_header.machine,
number_of_sections: file_header.number_of_sections,
image_base: optional_header.image_base,
entry_point_rva: optional_header.address_of_entry_point,
size_of_image: optional_header.size_of_image,
size_of_headers: optional_header.size_of_headers,
}))
}
/// PE header information extracted from process memory
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PEHeaderInfo {
pub dos_signature: u16,
pub nt_signature: u32,
pub machine: u16,
pub number_of_sections: u16,
pub image_base: u64,
pub entry_point_rva: u32,
pub size_of_image: u32,
pub size_of_headers: u32,
}
#[cfg(not(windows))]
pub fn read_pe_header_info(_pid: u32, _base_address: usize) -> anyhow::Result<Option<PEHeaderInfo>> {
Ok(None)
}
#[cfg(windows)]
mod platform {
use super::{MemoryProtection, MemoryRegion};
use anyhow::{Context, Result};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Memory::{
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE,
PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
};
fn parse_protection(protect: u32) -> MemoryProtection {
match protect & 0xFF {
p if p == PAGE_NOACCESS.0 => MemoryProtection::NoAccess,
p if p == PAGE_READONLY.0 => MemoryProtection::ReadOnly,
p if p == PAGE_READWRITE.0 => MemoryProtection::ReadWrite,
p if p == PAGE_EXECUTE.0 => MemoryProtection::Execute,
p if p == PAGE_EXECUTE_READ.0 => MemoryProtection::ReadExecute,
p if p == PAGE_EXECUTE_READWRITE.0 => MemoryProtection::ReadWriteExecute,
p if p == PAGE_WRITECOPY.0 || p == PAGE_EXECUTE_WRITECOPY.0 => {
MemoryProtection::WriteCopy
}
_ => MemoryProtection::Unknown,
}
}
pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> {
let mut regions = Vec::new();
if pid == 0 || pid == 4 {
return Ok(regions);
}
unsafe {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)
.context("Failed to open process")?;
let mut address: usize = 0;
let mut mbi = MEMORY_BASIC_INFORMATION::default();
loop {
let result = VirtualQueryEx(
handle,
Some(address as *const _),
&mut mbi,
std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
);
if result == 0 {
break;
}
if mbi.State == MEM_COMMIT {
let region_type = if mbi.Type == MEM_IMAGE {
"IMAGE"
} else if mbi.Type == MEM_MAPPED {
"MAPPED"
} else if mbi.Type == MEM_PRIVATE {
"PRIVATE"
} else {
"UNKNOWN"
}
.to_string();
regions.push(MemoryRegion {
base_address: mbi.BaseAddress as usize,
size: mbi.RegionSize,
protection: parse_protection(mbi.Protect.0),
region_type,
});
}
address = (mbi.BaseAddress as usize)
.checked_add(mbi.RegionSize)
.unwrap_or(usize::MAX);
if address == usize::MAX {
break;
}
}
let _ = CloseHandle(handle);
}
Ok(regions)
}
/// Reads memory from a process at the specified address.
///
/// # Safety
///
/// This function reads arbitrary process memory. The caller must ensure
/// the address and size are valid for the target process.
pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result<Vec<u8>> {
if pid == 0 || pid == 4 {
return Err(anyhow::anyhow!("Cannot read system process memory"));
}
unsafe {
let handle = OpenProcess(PROCESS_VM_READ, false, pid)
.context("Failed to open process for memory read")?;
let mut buffer = vec![0u8; size];
let mut bytes_read = 0usize;
let success = ReadProcessMemory(
handle,
address as *const _,
buffer.as_mut_ptr() as *mut _,
size,
Some(&mut bytes_read),
);
let _ = CloseHandle(handle);
if success.is_ok() && bytes_read > 0 {
buffer.truncate(bytes_read);
Ok(buffer)
} else {
Err(anyhow::anyhow!(
"Failed to read process memory at {:#x}",
address
))
}
}
}
}
#[cfg(target_os = "linux")]
mod platform {
use super::{MemoryProtection, MemoryRegion};
use anyhow::{Context, Result};
use std::fs;
pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> {
let maps_path = format!("/proc/{}/maps", pid);
let content = fs::read_to_string(&maps_path)
.context(format!("Failed to read {}", maps_path))?;
let mut regions = Vec::new();
for line in content.lines() {
if let Some(region) = parse_maps_line(line) {
regions.push(region);
}
}
Ok(regions)
}
fn parse_maps_line(line: &str) -> Option<MemoryRegion> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
return None;
}
let addr_range = parts[0];
let perms = parts.get(1)?;
let pathname = parts.get(5..).map(|p| p.join(" ")).unwrap_or_default();
let (start, end) = {
let mut split = addr_range.split('-');
let start = usize::from_str_radix(split.next()?, 16).ok()?;
let end = usize::from_str_radix(split.next()?, 16).ok()?;
(start, end)
};
let protection = parse_linux_perms(perms);
let region_type = determine_region_type(&pathname);
Some(MemoryRegion {
base_address: start,
size: end.saturating_sub(start),
protection,
region_type,
})
}
fn parse_linux_perms(perms: &str) -> MemoryProtection {
let r = perms.contains('r');
let w = perms.contains('w');
let x = perms.contains('x');
match (r, w, x) {
(false, false, false) => MemoryProtection::NoAccess,
(true, false, false) => MemoryProtection::ReadOnly,
(true, true, false) => MemoryProtection::ReadWrite,
(true, false, true) => MemoryProtection::ReadExecute,
(true, true, true) => MemoryProtection::ReadWriteExecute,
(false, false, true) => MemoryProtection::Execute,
_ => MemoryProtection::Unknown,
}
}
fn determine_region_type(pathname: &str) -> String {
if pathname.is_empty() || pathname == "[anon]" {
"PRIVATE".to_string()
} else if pathname.starts_with('[') {
match pathname {
"[heap]" => "HEAP".to_string(),
"[stack]" => "STACK".to_string(),
"[vdso]" | "[vvar]" | "[vsyscall]" => "SYSTEM".to_string(),
_ => "SPECIAL".to_string(),
}
} else if pathname.ends_with(".so") || pathname.contains(".so.") {
"IMAGE".to_string()
} else {
"MAPPED".to_string()
}
}
pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result<Vec<u8>> {
let mem_path = format!("/proc/{}/mem", pid);
let mut file = fs::File::open(&mem_path)
.context(format!("Failed to open {}", mem_path))?;
use std::io::{Read, Seek, SeekFrom};
file.seek(SeekFrom::Start(address as u64))
.context("Failed to seek to memory address")?;
let mut buffer = vec![0u8; size];
let bytes_read = file.read(&mut buffer).context("Failed to read memory")?;
buffer.truncate(bytes_read);
Ok(buffer)
}
}
#[cfg(target_os = "macos")]
mod platform {
use super::{MemoryProtection, MemoryRegion};
use anyhow::{Context, Result};
pub fn enumerate_memory_regions(pid: u32) -> Result<Vec<MemoryRegion>> {
use libc::{
mach_port_t, mach_vm_address_t, mach_vm_size_t, natural_t, vm_region_basic_info_64,
VM_REGION_BASIC_INFO_64,
};
use std::mem;
extern "C" {
fn task_for_pid(
target_tport: mach_port_t,
pid: i32,
task: *mut mach_port_t,
) -> i32;
fn mach_task_self() -> mach_port_t;
fn mach_vm_region(
target_task: mach_port_t,
address: *mut mach_vm_address_t,
size: *mut mach_vm_size_t,
flavor: i32,
info: *mut i32,
info_count: *mut u32,
object_name: *mut mach_port_t,
) -> i32;
}
let mut regions = Vec::new();
unsafe {
let mut task: mach_port_t = 0;
let kr = task_for_pid(mach_task_self(), pid as i32, &mut task);
if kr != 0 {
// KERN_SUCCESS = 0
return Err(anyhow::anyhow!(
"task_for_pid failed with error code {}. Requires root or taskgated entitlement.",
kr
));
}
let mut address: mach_vm_address_t = 0;
loop {
let mut size: mach_vm_size_t = 0;
let mut info: vm_region_basic_info_64 = mem::zeroed();
let mut info_count = (mem::size_of::<vm_region_basic_info_64>()
/ mem::size_of::<natural_t>()) as u32;
let mut object_name: mach_port_t = 0;
let kr = mach_vm_region(
task,
&mut address,
&mut size,
VM_REGION_BASIC_INFO_64,
&mut info as *mut _ as *mut i32,
&mut info_count,
&mut object_name,
);
if kr != 0 {
// End of address space or error
break;
}
let protection = parse_mach_protection(info.protection);
let region_type = determine_mach_region_type(&info);
regions.push(MemoryRegion {
base_address: address as usize,
size: size as usize,
protection,
region_type,
});
// Move to next region
address = address.saturating_add(size);
if address == 0 {
break;
}
}
}
Ok(regions)
}
fn parse_mach_protection(prot: i32) -> MemoryProtection {
// VM_PROT_READ = 1, VM_PROT_WRITE = 2, VM_PROT_EXECUTE = 4
let r = (prot & 1) != 0;
let w = (prot & 2) != 0;
let x = (prot & 4) != 0;
match (r, w, x) {
(false, false, false) => MemoryProtection::NoAccess,
(true, false, false) => MemoryProtection::ReadOnly,
(true, true, false) => MemoryProtection::ReadWrite,
(true, false, true) => MemoryProtection::ReadExecute,
(true, true, true) => MemoryProtection::ReadWriteExecute,
(false, false, true) => MemoryProtection::Execute,
_ => MemoryProtection::Unknown,
}
}
fn determine_mach_region_type(info: &libc::vm_region_basic_info_64) -> String {
// Determine region type based on characteristics
if info.shared != 0 {
"SHARED".to_string()
} else if info.reserved != 0 {
"RESERVED".to_string()
} else {
"PRIVATE".to_string()
}
}
pub fn read_process_memory(pid: u32, address: usize, size: usize) -> Result<Vec<u8>> {
use libc::mach_port_t;
extern "C" {
fn task_for_pid(
target_tport: mach_port_t,
pid: i32,
task: *mut mach_port_t,
) -> i32;
fn mach_task_self() -> mach_port_t;
fn mach_vm_read_overwrite(
target_task: mach_port_t,
address: u64,
size: u64,
data: u64,
out_size: *mut u64,
) -> i32;
}
unsafe {
let mut task: mach_port_t = 0;
let kr = task_for_pid(mach_task_self(), pid as i32, &mut task);
if kr != 0 {
return Err(anyhow::anyhow!(
"task_for_pid failed with error code {}",
kr
));
}
let mut buffer = vec![0u8; size];
let mut out_size: u64 = 0;
let kr = mach_vm_read_overwrite(
task,
address as u64,
size as u64,
buffer.as_mut_ptr() as u64,
&mut out_size,
);
if kr != 0 {
return Err(anyhow::anyhow!(
"mach_vm_read_overwrite failed with error code {}",
kr
));
}
buffer.truncate(out_size as usize);
Ok(buffer)
}
}
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
mod platform {
use super::MemoryRegion;
use anyhow::Result;
pub fn enumerate_memory_regions(_pid: u32) -> Result<Vec<MemoryRegion>> {
Err(anyhow::anyhow!(
"Memory enumeration not supported on this platform"
))
}
pub fn read_process_memory(_pid: u32, _address: usize, _size: usize) -> Result<Vec<u8>> {
Err(anyhow::anyhow!(
"Memory reading not supported on this platform"
))
}
}
/// Enumerates all memory regions for a process.
///
/// # Platform Support
///
/// - **Windows**: Uses VirtualQueryEx to enumerate regions.
/// - **Linux**: Parses /proc/[pid]/maps.
/// - **macOS**: Not yet implemented.
pub fn enumerate_memory_regions(pid: u32) -> anyhow::Result<Vec<MemoryRegion>> {
platform::enumerate_memory_regions(pid)
}
/// Reads raw memory content from a process.
///
/// This function reads up to `size` bytes from the target process at the
/// specified address. Requires appropriate privileges.
///
/// # Platform Support
///
/// - **Windows**: Uses ReadProcessMemory API.
/// - **Linux**: Reads from /proc/[pid]/mem.
/// - **macOS**: Not yet implemented.
pub fn read_process_memory(pid: u32, address: usize, size: usize) -> anyhow::Result<Vec<u8>> {
platform::read_process_memory(pid, address, size)
}