vmemory/
lib.rs

1//! Read and write the virtual memory of other processes on Windows, Linux, and macOS. This attempts to write memory regardless of memory region protections such as being read-only
2//! 
3//! Examples can be found at https://crates.io/crates/vmemory
4#![allow(unused_imports)]
5#![allow(dead_code)]
6#[cfg(target_family = "windows")]
7mod memory_windows;
8
9#[cfg(target_vendor = "apple")]
10mod memory_darwin;
11
12#[cfg(target_vendor = "unknown")]
13mod memory_linux;
14
15#[cfg(target_vendor = "unknown")]
16use nix::sys::ptrace;
17#[cfg(any(target_vendor = "unknown", target_os = "macos"))]
18use nix::libc::{SIGCONT, SIGKILL, SIGTRAP, WIFSTOPPED, WSTOPSIG, c_char, fork, kill, pid_t, waitpid};
19#[cfg(any(target_vendor = "unknown", target_os = "macos"))]
20use nix::unistd::close;
21
22#[cfg(target_family = "windows")]
23use winapi::um::handleapi::CloseHandle;
24#[cfg(target_family = "windows")]
25use winapi::um::handleapi::INVALID_HANDLE_VALUE;
26#[cfg(target_family = "windows")]
27use winapi::um::processthreadsapi::TerminateProcess;
28#[cfg(target_family = "windows")]
29use winapi::um::winnt::HANDLE;
30
31use std::mem::MaybeUninit;
32
33#[cfg(target_os = "macos")]
34use nix::libc::{POSIX_SPAWN_START_SUSPENDED, posix_spawn, posix_spawnattr_init, posix_spawnattr_setflags, posix_spawnattr_t};
35
36use std::ffi::CString;
37
38//
39// A handle (or mach port on macOS) to designate access to the task/process
40// A base address indicating the first region of memory in the process
41// A PID to refer to the identifier of the task
42// A thread to refer to the first thread in the process (Windows)
43//
44#[cfg(any(target_os = "macos", target_family = "windows"))]
45pub struct ProcessMemory {
46    base_address: usize,
47    handle: usize,
48    pid: u32,
49    thread: usize,
50    #[cfg(target_os = "macos")]
51    base_size: usize
52}
53
54//
55// macOS and Windows share a ProcessMemory definition because they both have similar concepts: (ports/handles),
56// macOS can ignore the thread field which is generally used for application start-up
57// ptrace on Linux/BSD simply needs a process ID
58//
59#[cfg(target_vendor = "unknown")]
60pub struct ProcessMemory {
61    base_address: usize,
62    pid: u32
63}
64
65//
66// Safely close the handle to the process
67//
68#[cfg(target_family = "windows")]
69fn close_valid_handle(value: HANDLE) -> bool {
70    if is_valid_handle!(value) {
71        unsafe { CloseHandle(value) };
72        return true;
73    }
74    return false;
75}
76
77//
78// Separate a vector of strings into one string, delimited by space characters
79//
80#[cfg(target_family = "windows")]
81fn delimit(text: &Vec<String>) -> String {
82    let mut result: String = String::new();
83    for s in text {
84        result.push_str(s.as_str());
85        result.push(' ');
86    }
87    result.pop();
88    result
89}
90
91
92//
93// Referenced from <https://github.com/dfinity/ic/blob/c58c75a687621530b2635b22630e9562424fa3b3/rs/canister_sandbox/common/src/process.rs>
94// Using the Apache 2 License <http://www.apache.org/licenses/LICENSE-2.0>
95// Null-terminate an array of likely null-terminated strings
96//
97#[cfg(target_os = "macos")]
98fn make_null_terminated_string_array(strings: &mut Vec<std::ffi::CString>) -> Vec<*mut c_char> {
99    let mut result = Vec::<*mut c_char>::new();
100    for s in strings {
101        result.push(s.as_ptr() as *mut c_char);
102    }
103    result.push(std::ptr::null::<c_char>() as *mut c_char);
104    result
105}
106
107//
108// Referenced from <https://github.com/dfinity/ic/blob/c58c75a687621530b2635b22630e9562424fa3b3/rs/canister_sandbox/common/src/process.rs>
109// Using the Apache 2 License <http://www.apache.org/licenses/LICENSE-2.0>
110// Parse the vector of argv strings into C strings and return it
111//
112#[cfg(target_os = "macos")]
113fn collect_argv(argv: &[String]) -> Vec<std::ffi::CString> {
114    argv.iter()
115        .map(|s| std::ffi::CString::new(s.as_bytes()).unwrap())
116        .collect::<Vec<std::ffi::CString>>()
117}
118
119//
120// Referenced from <https://github.com/dfinity/ic/blob/c58c75a687621530b2635b22630e9562424fa3b3/rs/canister_sandbox/common/src/process.rs>
121// Using the Apache 2 License <http://www.apache.org/licenses/LICENSE-2.0>
122// Collect the environment variables and return a vector of FFI-compatible C strings
123//
124#[cfg(target_os = "macos")]
125fn collect_env() -> Vec<std::ffi::CString> {
126    use std::os::unix::ffi::OsStrExt;
127    std::env::vars_os().map(|(key, value) | {
128        std::ffi::CString::new(
129            [
130                key.as_os_str().as_bytes(),
131                &[b'='],
132                value.as_os_str().as_bytes(),
133            ]
134            .concat()
135        )
136        .unwrap()
137    })
138    .collect::<Vec<std::ffi::CString>>()
139}
140
141//
142// Get the base address for the the first section in the process via procfs
143// Address will be the text value before the first '-' character, convert it to an integer
144//
145#[cfg(target_vendor = "unknown")]
146fn get_main_module(pid: u32) -> usize {
147    use std::fs::File;
148    use std::io::BufRead;
149    let proc = format!("/proc/{process_id}/maps", process_id=pid);
150    let file = File::open(proc).unwrap();
151    let reader = std::io::BufReader::new(file);
152
153    for line in reader.lines() {
154        for token in line.unwrap().split("-") {
155            return usize::from_str_radix(token, 16).unwrap();
156        }
157    }
158    0
159}
160
161//
162// Extension of creating a child process, by enabling ptrace(2)
163// Parsing the arguments, and then replacing the module with execv(2)
164//
165#[cfg(target_vendor = "unknown")]
166fn create_reference_process(file_name: &str, arguments: &Vec<String>) {
167    ptrace::traceme().unwrap();
168    let cfile = CString::new(file_name).unwrap();
169    let mut cfile_args: Vec<CString> = vec![cfile];
170
171    for argument in arguments {
172        cfile_args.push(CString::new(argument.as_str()).unwrap());
173    }
174
175    nix::unistd::execv(&cfile_args[0], &cfile_args).unwrap();
176}
177
178impl ProcessMemory {
179
180    /// Attach via ptrace(2) to the process specified by the PID for Linux
181    /// 
182    /// On Windows this opens a handle to the process
183    /// 
184    /// On macOS this gets the mach task port for the process
185    #[cfg(target_family = "windows")]
186    pub fn attach_process(pid: u32) -> Option<ProcessMemory> {
187        use winapi::um::{processthreadsapi::OpenProcess, winnt::PROCESS_ALL_ACCESS};
188        use winapi::um::handleapi::INVALID_HANDLE_VALUE;
189        use winapi::um::winnt::HANDLE;
190        use crate::memory_windows::get_base_address;
191
192        //
193        // Get access to the process via a handle,
194        // Return a None value if the handle could not be opened
195        //
196        let process = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false as _, pid) };
197
198        if !is_valid_handle!(process) {
199            return None
200        }
201
202        //
203        // Get the base address of the first module loaded in the process
204        // Return the ProcessMemory structure which will allow arbitrary read/write operations when successfully unwrapped
205        //
206        let base = get_base_address(process, None, pid).unwrap();
207
208        Some(
209            ProcessMemory{
210                base_address: base,
211                handle: process as _,
212                pid: pid,
213                thread: 0
214            }
215        )
216    }
217
218    //
219    // Retrieve a port for the process, for reading/writing memory
220    //
221    #[cfg(target_vendor = "apple")]
222    pub fn attach_process(pid: u32) -> Option<ProcessMemory> {
223        let task = memory_darwin::get_task_for_pid(pid as _);
224
225        if task == 0 {
226            return None
227        }
228
229        let base = memory_darwin::get_base_address(task, 1).unwrap();
230
231        Some(
232            ProcessMemory{
233                base_address: base.0,
234                handle: task as _,
235                pid: pid as _,
236                thread: 0,
237                base_size: base.1,
238            }
239        )
240    }
241
242
243    //
244    // Ptrace the process and enumerate procfs
245    //
246    #[cfg(target_vendor = "unknown")]
247    pub fn attach_process(pid: u32) ->  Option<ProcessMemory> {
248        let nix_pid = nix::unistd::Pid::from_raw(pid as _);
249
250        match ptrace::attach(nix_pid) {
251            Ok(_) => (),
252            Err(_) => return None
253        }
254
255        //
256        // Get the contents of /proc/{pid}/maps, returning the base address of first loaded mapping
257        //
258        let base = get_main_module(pid);
259
260        if base == 0 {
261            return None
262        }
263
264        Some(
265            ProcessMemory{
266                base_address: base,
267                pid: pid
268            }
269        )
270    }
271
272    /// This spawns a process suspended and has to be manually resumed via public self.resume() 
273    /// 
274    /// On Linux, this creates a new process via fork() which maps to clone(2) depending on libc
275    /// ptrace the fork and replace the current image with a new one in create_reference_process
276    /// 
277    /// On Windows, this calls CreateProcess with the flag CREATE_SUSPENDED
278    /// 
279    /// On macOS, this calls posix_spawn(2) with the flag POSIX_SPAWN_START_SUSPENDED
280    /// 
281    /// Accepts a file path, as well as arguments to the new process
282    #[cfg(target_vendor = "unknown")]
283    pub fn new_process(file_name: &str, arguments: &Vec<String>) -> Option<ProcessMemory> {
284        let pid: pid_t = unsafe { fork() };
285
286        match pid {
287            0 => create_reference_process(file_name, &arguments),
288            -1 => return None,
289            _ => ()
290        }
291
292        //
293        // Wait for the ptrace(2) and execv(2) operations to be successful
294        //
295        unsafe {
296            let mut status: i32 = 0;
297            waitpid(pid, &mut status, 0);
298            if WIFSTOPPED(status) && WSTOPSIG(status) != SIGTRAP {
299                panic!("waitpid failed");
300            }
301        }
302
303        //
304        // Get the base address of the first loaded mapping via procfs
305        //
306        let base = get_main_module(pid as _);
307        
308        Some(
309            ProcessMemory {
310                base_address: base,
311                pid: pid as _
312            }
313        )
314    }
315
316    //
317    // Spawn a process suspended using posix_spawn(2)
318    // Later resumed via public self.resume()
319    //
320    #[cfg(target_vendor = "apple")]
321    pub fn new_process(file_name: &str, arguments: &Vec<String>) -> Option<ProcessMemory> {
322        let mut pid: pid_t = 0;
323        unsafe {
324            //
325            // Allocate, initialize a POSIX spawn attribute structure with the flags POSIX_SPAWN_START_SUSPENDED which will create a process in a suspended state
326            //
327            let mut posix_attr = MaybeUninit::<posix_spawnattr_t>::uninit();
328            posix_spawnattr_init(posix_attr.as_mut_ptr());
329            let mut posix_attr = posix_attr.assume_init();
330            posix_spawnattr_setflags(&mut posix_attr, POSIX_SPAWN_START_SUSPENDED as _);
331
332            //
333            // Collect the environment variables and launch arguments and pass them to posix_spawn(2)
334            //
335            let mut envp = collect_env();
336            let envpp = make_null_terminated_string_array(&mut envp);
337
338            let mut argvp = collect_argv(arguments.as_slice());
339            let argvpp = make_null_terminated_string_array(&mut argvp);
340
341            let cfile_name = CString::new(file_name).unwrap();
342            //
343            // Spawn a new process and receive the process ID into pid
344            //
345            if posix_spawn(
346                &mut pid,
347                cfile_name.as_ptr(),
348                0 as _, 
349                &posix_attr, 
350                argvpp.as_ptr(), 
351                envpp.as_ptr()
352            ) != 0 {
353                return None
354            }
355        }
356
357        //
358        // Retrieve the task port for the process for memory manipulation and other task operations
359        //
360        let task = memory_darwin::get_task_for_pid(pid as _);
361
362        if task == 0 {
363            panic!("Failed to get task port for process");
364        }
365
366        //
367        // Retrieve the base address via mach_vm_region
368        //
369        let base = memory_darwin::get_base_address(task, 1).unwrap();
370
371        Some(
372            ProcessMemory{
373                base_address: base.0,
374                handle: task as _,
375                pid: pid as _,
376                thread: 0,
377                base_size: base.1
378            }
379        )
380    }
381
382    //
383    // Create a new process for Windows, in a suspended state
384    //
385    #[cfg(target_family = "windows")]
386    pub fn new_process(file_path: &str, args: &Vec<String>) -> Option<ProcessMemory> {
387        use winapi::um::{processthreadsapi::{CreateProcessA, PROCESS_INFORMATION, STARTUPINFOA}, winbase::CREATE_SUSPENDED};
388
389        use crate::memory_windows::get_base_address;
390
391        let proc_string = CString::new(file_path).unwrap();
392        let argv_text = delimit(&args);
393        let argv = CString::new(argv_text.as_str()).unwrap();
394
395        //
396        // Retrieve process creation information into these structures (only proc_info is used here, in this case)
397        //
398        let mut start_up: STARTUPINFOA = unsafe { std::mem::MaybeUninit::<STARTUPINFOA>::zeroed().assume_init() };
399        let mut proc_info: PROCESS_INFORMATION = unsafe { std::mem::MaybeUninit::<PROCESS_INFORMATION>::zeroed().assume_init() };
400
401        //
402        // Create the process and check if the operation failed
403        //
404        let result = unsafe {
405            CreateProcessA(
406                proc_string.as_ptr(),
407                argv.as_ptr() as _,
408                0 as _,
409                0 as _,
410                0,
411                CREATE_SUSPENDED,
412                0 as *mut _,
413                0 as *mut _,
414                &mut start_up as *mut _,
415                &mut proc_info as *mut _
416            )
417        };
418
419        if result == 0 {
420            return None;
421        }
422
423        //
424        // Get the base address via the remote process's PEB (Process Environment Block)
425        // If this was an attachment, Module32First from Toolhelp would be used
426        //
427        let base = get_base_address(proc_info.hProcess, Some(proc_info.hThread), proc_info.dwProcessId).unwrap();
428        Some(
429            ProcessMemory{
430                base_address: base,
431                handle: proc_info.hProcess as _,
432                pid: proc_info.dwProcessId,
433                thread: proc_info.hThread as _
434            }
435        )
436    }
437
438    /// Write the buffer (vector, identifier: data) at the address in the process
439    /// 
440    /// If the offset bool is set to true, then **only** an offset is given to this function, 
441    /// relative to the first mapping/module in the process. 
442    /// 
443    /// Example, the first module is loaded at 0x00400000
444    /// 
445    /// offset is set to true, and _address = 5
446    /// 
447    /// Memory would be written at 0x00400005
448    /// 
449    /// If offset is false, it takes an immediate - direct address.
450    pub fn write_memory(&self, _address: usize, data: &Vec<u8>, offset: bool) {
451        let mut address: usize = _address;
452        if offset {
453            address = self.base_address + address;
454        }
455
456        #[cfg(target_family = "windows")] {
457            memory_windows::write_memory(self.handle as _, address, &data).unwrap()
458        }
459
460        #[cfg(target_os = "macos")] {
461            memory_darwin::write_memory(self.handle as _, address, &data).unwrap()
462        }
463
464        #[cfg(target_vendor = "unknown")] {
465            memory_linux::write_memory(self.pid, address, &data).unwrap()
466        }
467    }
468
469    /// Read memory from the process and return a vector.
470    /// If the offset bool is set to true, then **only** an offset is given to this function, 
471    /// relative to the first mapping/module in the process. 
472    /// 
473    /// Example, the first module is loaded at 0x00400000
474    /// 
475    /// offset is set to true, 
476    /// 
477    /// and _address = 5
478    /// 
479    /// Memory would be read from 0x00400005
480    /// 
481    /// If offset is false, it takes an immediate - direct address. 
482    /// 
483    /// For example, _address = 0x00400005
484    pub fn read_memory(&self, _address: usize, size: usize, offset: bool) -> Vec<u8>  {
485        let mut address: usize = _address;
486        if offset {
487            address = self.base_address + address;
488        }
489
490        #[cfg(target_vendor = "unknown")] {
491            memory_linux::read_memory(self.pid, address, size).unwrap()
492        }
493
494        #[cfg(target_family = "windows")] {
495            memory_windows::read_memory(self.handle as _, address, size).unwrap()
496        }
497        
498        #[cfg(target_os = "macos")] {
499            memory_darwin::read_memory(self.handle as _, address, size).unwrap()
500        }
501    }
502
503    /// Resume the process by resuming the first thread (Windows)
504    /// or sending a continue signal (Unix)
505    pub fn resume(&self) {
506        #[cfg(target_family = "unix")]
507        unsafe { kill(self.pid as _, SIGCONT);}
508        #[cfg(target_family = "windows")]
509        unsafe { winapi::um::processthreadsapi::ResumeThread(self.thread as _) };
510    }
511
512    /// Retrieve the first mapping/module loaded into memory for the process
513    pub fn base(&self) -> usize {
514        self.base_address
515    }
516
517    /// Kill the process by sending a forceful SIGKILL or via TerminateProcess
518    pub fn kill(&self) {
519        #[cfg(target_family = "unix")]
520        unsafe { kill(self.pid as _, SIGKILL);}
521        #[cfg(target_family = "windows")] unsafe {
522            TerminateProcess(self.handle as _, 0);
523        }
524    }
525
526    /// Get the process ID
527    pub fn pid(&self) -> u32 {
528        self.pid
529    }
530
531    /// Retrieve the size of the first section/module
532    #[cfg(target_os = "macos")]
533    pub fn base_size(&self) -> usize {
534        self.base_size
535    }
536
537    /// Retrieve a raw handle/task port, or the PID for Linux as ptrace(2) does not use file descriptors
538    pub fn raw_descriptor(&self) -> usize {
539        #[cfg(any(target_os = "macos", target_family = "windows"))] {
540            self.handle
541        }
542        #[cfg(target_vendor = "unknown")] {
543            self.pid as _
544        }
545    }
546
547    /// This will close the handles/task ports or detach from a ptrace(2) session, making the ProcessMemory object effectively useless with the exception of retaining data.
548    pub fn early_close(&mut self) {
549        #[cfg(target_family = "windows")]
550        close_valid_handle(self.handle as _);
551        #[cfg(target_os = "macos")]
552        unsafe { mach::mach_port::mach_port_deallocate(mach::traps::mach_task_self(), self.handle as _) };
553        #[cfg(target_vendor = "unknown")]
554        ptrace::detach(nix_pid, None).unwrap(); 
555    }
556}
557
558impl Drop for ProcessMemory {
559    //
560    // Close the thread (if present) and process handles
561    //
562    #[cfg(target_family = "windows")]
563    fn drop(&mut self) {
564        close_valid_handle(self.handle as _);
565        close_valid_handle(self.thread as _);
566    }
567
568    //
569    // Deallocate the task port retrieved from task_for_pid(2)
570    //
571    #[cfg(target_os = "macos")]
572    fn drop(&mut self) {
573        unsafe {
574            mach::mach_port::mach_port_deallocate(mach::traps::mach_task_self(), self.handle as _);
575        }
576    }
577
578    //
579    // Detach from the ptrace session
580    //
581    #[cfg(target_vendor = "unknown")]
582    fn drop(&mut self) {
583        let nix_pid = nix::unistd::Pid::from_raw(self.pid as _);
584        ptrace::detach(nix_pid, None).unwrap();   
585    }
586}