procmod-core 1.0.1

Cross-platform process memory read/write
Documentation
use crate::error::{Error, Result};
use crate::module::Module;
use crate::region::{MemoryRegion, Protection};
use std::ffi::c_void;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::System::Memory::{
    VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_EXECUTE, PAGE_EXECUTE_READ,
    PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY,
};
use windows_sys::Win32::System::ProcessStatus::{
    EnumProcessModulesEx, GetModuleBaseNameW, GetModuleFileNameExW, GetModuleInformation,
    LIST_MODULES_ALL, MODULEINFO,
};
use windows_sys::Win32::System::Threading::{
    OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
};

extern "system" {
    fn ReadProcessMemory(
        process: HANDLE,
        base_address: *const c_void,
        buffer: *mut c_void,
        size: usize,
        bytes_read: *mut usize,
    ) -> i32;

    fn WriteProcessMemory(
        process: HANDLE,
        base_address: *mut c_void,
        buffer: *const c_void,
        size: usize,
        bytes_written: *mut usize,
    ) -> i32;
}

pub struct ProcessHandle {
    handle: HANDLE,
}

impl Drop for ProcessHandle {
    fn drop(&mut self) {
        unsafe {
            CloseHandle(self.handle);
        }
    }
}

pub fn attach(pid: u32) -> Result<ProcessHandle> {
    let access =
        PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION;
    let handle = unsafe { OpenProcess(access, 0, pid) };

    if handle.is_null() {
        let err = std::io::Error::last_os_error();
        let raw = err.raw_os_error().unwrap_or(0);
        if raw == 5 {
            return Err(Error::PermissionDenied { pid });
        }
        if raw == 87 {
            return Err(Error::ProcessNotFound { pid });
        }
        return Err(Error::AttachFailed { pid, source: err });
    }

    Ok(ProcessHandle { handle })
}

pub fn read_bytes(handle: &ProcessHandle, address: usize, buf: &mut [u8]) -> Result<()> {
    if buf.is_empty() {
        return Ok(());
    }

    let mut bytes_read: usize = 0;
    let result = unsafe {
        ReadProcessMemory(
            handle.handle,
            address as *const c_void,
            buf.as_mut_ptr() as *mut c_void,
            buf.len(),
            &mut bytes_read,
        )
    };

    if result == 0 {
        return Err(Error::ReadFailed {
            address,
            source: std::io::Error::last_os_error(),
        });
    }

    if bytes_read != buf.len() {
        return Err(Error::ReadFailed {
            address,
            source: std::io::Error::new(
                std::io::ErrorKind::UnexpectedEof,
                format!(
                    "partial read: expected {} bytes, got {}",
                    buf.len(),
                    bytes_read
                ),
            ),
        });
    }

    Ok(())
}

pub fn write_bytes(handle: &ProcessHandle, address: usize, buf: &[u8]) -> Result<()> {
    if buf.is_empty() {
        return Ok(());
    }

    let mut bytes_written: usize = 0;
    let result = unsafe {
        WriteProcessMemory(
            handle.handle,
            address as *mut c_void,
            buf.as_ptr() as *const c_void,
            buf.len(),
            &mut bytes_written,
        )
    };

    if result == 0 {
        return Err(Error::WriteFailed {
            address,
            source: std::io::Error::last_os_error(),
        });
    }

    if bytes_written != buf.len() {
        return Err(Error::WriteFailed {
            address,
            source: std::io::Error::new(
                std::io::ErrorKind::WriteZero,
                format!(
                    "partial write: expected {} bytes, wrote {}",
                    buf.len(),
                    bytes_written
                ),
            ),
        });
    }

    Ok(())
}

fn page_protection_to_flags(protect: u32) -> Protection {
    match protect {
        PAGE_READONLY => Protection {
            read: true,
            write: false,
            execute: false,
        },
        PAGE_READWRITE | PAGE_WRITECOPY => Protection {
            read: true,
            write: true,
            execute: false,
        },
        PAGE_EXECUTE => Protection {
            read: false,
            write: false,
            execute: true,
        },
        PAGE_EXECUTE_READ => Protection {
            read: true,
            write: false,
            execute: true,
        },
        PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY => Protection {
            read: true,
            write: true,
            execute: true,
        },
        _ => Protection {
            read: false,
            write: false,
            execute: false,
        },
    }
}

pub fn regions(handle: &ProcessHandle, _pid: u32) -> Result<Vec<MemoryRegion>> {
    let mut result = Vec::new();
    let mut address: usize = 0;

    loop {
        let mut info = unsafe { std::mem::zeroed::<MEMORY_BASIC_INFORMATION>() };
        let written = unsafe {
            VirtualQueryEx(
                handle.handle,
                address as *const _,
                &mut info,
                std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
            )
        };

        if written == 0 {
            break;
        }

        if info.State == MEM_COMMIT {
            result.push(MemoryRegion {
                base: info.BaseAddress as usize,
                size: info.RegionSize,
                protection: page_protection_to_flags(info.Protect),
            });
        }

        address = info.BaseAddress as usize + info.RegionSize;
    }

    Ok(result)
}

pub fn modules(handle: &ProcessHandle, _pid: u32) -> Result<Vec<Module>> {
    let mut h_modules: [HANDLE; 1024] = [std::ptr::null_mut(); 1024];
    let mut cb_needed: u32 = 0;

    let result = unsafe {
        EnumProcessModulesEx(
            handle.handle,
            h_modules.as_mut_ptr(),
            std::mem::size_of_val(&h_modules) as u32,
            &mut cb_needed,
            LIST_MODULES_ALL,
        )
    };

    if result == 0 {
        return Err(Error::ModuleEnumFailed {
            source: std::io::Error::last_os_error(),
        });
    }

    let count = cb_needed as usize / std::mem::size_of::<HANDLE>();
    let mut modules = Vec::with_capacity(count);

    for &h_module in &h_modules[..count] {
        let mut name_buf = [0u16; 260];
        let name_len = unsafe {
            GetModuleBaseNameW(
                handle.handle,
                h_module,
                name_buf.as_mut_ptr(),
                name_buf.len() as u32,
            )
        };

        let name = if name_len > 0 {
            String::from_utf16_lossy(&name_buf[..name_len as usize])
        } else {
            String::new()
        };

        let mut mod_info = unsafe { std::mem::zeroed::<MODULEINFO>() };
        let info_result = unsafe {
            GetModuleInformation(
                handle.handle,
                h_module,
                &mut mod_info,
                std::mem::size_of::<MODULEINFO>() as u32,
            )
        };

        let (base, size) = if info_result != 0 {
            (mod_info.lpBaseOfDll as usize, mod_info.SizeOfImage as usize)
        } else {
            (0, 0)
        };

        let mut path_buf = [0u16; 260];
        let path_len = unsafe {
            GetModuleFileNameExW(
                handle.handle,
                h_module,
                path_buf.as_mut_ptr(),
                path_buf.len() as u32,
            )
        };

        let path = if path_len > 0 {
            String::from_utf16_lossy(&path_buf[..path_len as usize])
        } else {
            name.clone()
        };

        modules.push(Module {
            name,
            base,
            size,
            path,
        });
    }

    Ok(modules)
}