raminspect/lib.rs
1//! raminspect is a crate that allows for the inspection and manipulation of the memory and code of
2//! a running process on a Linux system. It provides functions for finding and replacing search terms
3//! in a processes' memory, as well as an interface that allows for the injection of arbitrary shellcode
4//! running in the processes' context. All of this requires root privileges, for obvious reasons.
5
6// Starting from v0.3.0, we use libc and alloc instead of std and nix to support
7// architectures like 32-bit RISCV which don't have standard library support. This
8// complicates the code quite a bit but it's a price I'm willing to pay for cross-
9// platform support.
10
11#![no_std]
12extern crate alloc;
13
14use libc::*;
15use core::sync::atomic::Ordering;
16use core::sync::atomic::AtomicUsize;
17
18use alloc::vec;
19use alloc::format;
20use alloc::vec::Vec;
21use alloc::sync::Arc;
22use alloc::vec::IntoIter;
23use alloc::string::String;
24/// Used for cleaner handling of errors from calling libc functions
25
26trait IntoResult: Sized {
27 fn into_result(self, error: RamInspectError) -> Result<Self, RamInspectError>;
28}
29
30macro_rules! impl_into_result_for_num {
31 ($num_ty:ty) => {
32 impl IntoResult for $num_ty {
33 fn into_result(self, error: RamInspectError) -> Result<Self, RamInspectError> {
34 if self < 0 {
35 Err(error)
36 } else {
37 Ok(self)
38 }
39 }
40 }
41 }
42}
43
44impl_into_result_for_num!(i32);
45impl_into_result_for_num!(i64);
46impl_into_result_for_num!(isize);
47
48impl<T> IntoResult for *mut T {
49 fn into_result(self, error: RamInspectError) -> Result<Self, RamInspectError> {
50 if self.is_null() {
51 Err(error)
52 } else {
53 Ok(self)
54 }
55 }
56}
57
58/// A wrapper around a raw file descriptor that closes itself when
59/// dropped. This exists to prevent leaks.
60
61struct FileWrapper {
62 descriptor: i32
63}
64
65impl FileWrapper {
66 fn open(path: &str, mode: i32, on_err: RamInspectError) -> Result<Self, RamInspectError> {
67 // Assert that the provided string is null terminated
68 assert!(path.ends_with('\0'));
69
70 Ok(Self {
71 descriptor: unsafe {
72 // This is safe because we already asserted that the path is null-terminated
73 open(path.as_ptr() as _, mode).into_result(on_err)?
74 }
75 })
76 }
77}
78
79impl Drop for FileWrapper {
80 fn drop(&mut self) {
81 unsafe {
82 close(self.descriptor);
83 }
84 }
85}
86
87/// A packet sent to the backend kernel module through an 'ioctl' call
88/// that requests the current instruction pointer of an application.
89
90#[repr(C)]
91struct InstructionPointerRequest {
92 pid: i32,
93 instruction_pointer: u64,
94}
95
96// ioctl command definitions
97const RESTORE_REGS: c_ulong = 0x40047B03;
98const GET_INST_PTR: c_ulong = 0xC0107B02;
99const WAIT_FOR_FINISH: c_ulong = 0x40047B00;
100const TOGGLE_EXEC_WRITE: c_ulong = 0x40047B01;
101
102/// Finds a list of all processes containing a given search term in their
103/// program name. This makes figuring out the process ID of the process
104/// you want to inspect or inject shellcode into easier.
105
106pub fn find_processes(name_contains: &str) -> Vec<i32> {
107 let mut results = Vec::new();
108 const MAX_LINE_LENGTH: usize = 4096;
109
110 unsafe {
111 // Iterate over all process IDs in the /proc directory
112 let dirp = opendir("/proc\0".as_ptr() as _);
113 if dirp.is_null() { return results; }
114
115 loop {
116 let entry_ptr = readdir(dirp);
117 if entry_ptr.is_null() { break; }
118
119 // Convert the array of C chars representing the directory name to a Rust string slice.
120 let name_bytes: [u8; 256] = core::mem::transmute(core::ptr::read(entry_ptr).d_name);
121 let end_of_str = name_bytes.iter().position(|byte| *byte == 0).unwrap();
122 let name = core::str::from_utf8(&name_bytes[..end_of_str]).unwrap();
123
124 // Make sure it's a PID's proc directory before continuing.
125 let pid = match name.parse::<i32>() {
126 Ok(pid) => pid,
127 Err(_) => continue,
128 };
129
130 let path = format!("/proc/{}/cmdline\0", pid);
131 let fd = open(path.as_ptr() as _, O_RDONLY);
132 if fd < 0 { continue; }
133
134 let mut buf = vec![0; MAX_LINE_LENGTH];
135 if read(fd, buf.as_mut_ptr() as _, buf.len()) < 0 {
136 close(fd);
137 continue;
138 }
139
140 // The first occurence of a null byte in /proc/pid/cmdline delineates the end of
141 // the processes' command invocation name. See the corresponding section on this
142 // page for more information: https://man7.org/linux/man-pages/man5/proc.5.html
143
144 let executable_name = core::str::from_utf8(
145 &buf[..buf.iter().position(|byte| *byte == 0).unwrap_or(buf.len())]
146 ).unwrap();
147
148 if executable_name.contains(name_contains) {
149 results.push(pid);
150 }
151
152 close(fd);
153 }
154 }
155
156 results
157}
158
159/// This is the primary interface used by the crate to search through, read, and modify an
160/// arbitrary processes' memory and code.
161///
162/// Note that when an inspector is created for a process, the process will be paused until
163/// the inspector is dropped in order to ensure that we have exclusive access to the
164/// processes' memory, unless it is manually resumed through a call to
165/// [`RamInspector::resume_process`].
166///
167/// # Example Usage
168///
169/// ```rust
170/// //! This example changes the current text in Firefox's browser search bar from
171/// //! "Old search text" to "New search text". To run this example, open an instance
172/// //! of Firefox and type "Old search text" in the search bar. If all goes well, when
173/// //! you run this example as root, it should be replaced with "New search text",
174/// //! although you may have to click on the search bar again in order for it to
175/// //! render the new text.
176///
177/// fn main() {
178/// use raminspect::RamInspector;
179/// // Iterate over all running Firefox instances
180/// for pid in raminspect::find_processes("/usr/lib/firefox/firefox") {
181/// let mut inspector = match RamInspector::new(pid) {
182/// Ok(inspector) => inspector,
183/// Err(_) => continue,
184/// };
185///
186/// for (proc_addr, memory_region) in inspector.search_for_term(b"Old search text").unwrap() {
187/// if !memory_region.writable() {
188/// continue;
189/// }
190///
191/// unsafe {
192/// // This is safe because modifying the text in the Firefox search bar will not crash
193/// // the browser or negatively impact system stability in any way.
194///
195/// println!("Writing to process virtual address: 0x{:X}", proc_addr);
196/// inspector.queue_write(proc_addr, b"New search text");
197/// }
198/// }
199///
200/// unsafe {
201/// // This is safe since the process is not currently resumed, which would possibly cause a data race.
202/// inspector.flush().unwrap();
203/// }
204/// }
205/// }
206/// ```
207
208pub struct RamInspector {
209 pid: i32,
210 max_iovs: usize,
211 proc_maps_file: *mut FILE,
212 resume_count: Arc<AtomicUsize>,
213 write_requests: Vec<(usize, Vec<u8>)>,
214}
215
216// This is safe because the pointer inside can only be accessed through methods
217// that take a mutable reference to the inspector, and therefore all accesses
218// to it must be synchronized.
219
220unsafe impl Send for RamInspector {}
221unsafe impl Sync for RamInspector {}
222
223#[non_exhaustive]
224#[derive(Clone, Copy)]
225/// The error type for this library. The variants have self-explanatory names.
226
227pub enum RamInspectError {
228 ProcessTerminated,
229 FailedToOpenProcMaps,
230 FailedToPauseProcess,
231 FailedToResumeProcess,
232
233 FailedToReadMem,
234 FailedToWriteMem,
235 FailedToOpenDeviceFile,
236 FailedToAllocateBuffer,
237 InspectorAlreadyExists,
238}
239
240use core::fmt;
241use core::fmt::Debug;
242use core::fmt::Formatter;
243impl Debug for RamInspectError {
244 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
245 formatter.write_str(match self {
246 RamInspectError::InspectorAlreadyExists => "A `RamInspector` instance already exists for the specified process ID. \
247 Note: If you're in a multi-threaded environment, instead of creating \
248 multiple inspectors you can try accessing one inspector through a \
249 mutex.",
250
251 RamInspectError::FailedToOpenDeviceFile => "Failed to open the raminspect device file! Are you sure the kernel module is currently inserted? If it is, are you running as root?",
252 RamInspectError::FailedToOpenProcMaps => "Failed to access the target processes' memory maps! Are you sure you're running as root? If you are, is the target process running?",
253 RamInspectError::FailedToWriteMem => "Failed to write to the specified memory address! Are you sure the address is in a writable region of the processes' memory?",
254 RamInspectError::FailedToReadMem => "Failed to read from the specified memory address! Are you sure the address is in a readable region of the processes' memory?",
255 RamInspectError::FailedToResumeProcess => "Failed to resume the target process! Are you sure it is currently running?",
256 RamInspectError::FailedToPauseProcess => "Failed to pause the target process! Are you sure it is currently running?",
257 RamInspectError::FailedToAllocateBuffer => "Failed to allocate the specified buffer.",
258 RamInspectError::ProcessTerminated => "The target process unexpectedly terminated.",
259 })
260 }
261}
262
263use core::fmt::Display;
264impl Display for RamInspectError {
265 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
266 Debug::fmt(self, formatter)
267 }
268}
269
270use spin::Mutex;
271static INSPECTED_PIDS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
272
273impl RamInspector {
274 /// Creates a new inspector attached to the specified process ID. This will pause the target process until
275 /// the inspector is dropped or until it's manually resumed using [`RamInspector::resume_process`].
276 ///
277 /// Note that creating two inspectors simultaneously referring to the same process is not supported, and attempting
278 /// to do so will return a [`RamInspectError`] of the kind [`RamInspectError::InspectorAlreadyExists`]. If you want
279 /// to do this you should access the same inspector instead, synchronizing said access if you're in a multithreaded
280 /// environment.
281
282 pub fn new(pid: i32) -> Result<Self, RamInspectError> {
283 unsafe {
284 if INSPECTED_PIDS.lock().contains(&pid) {
285 return Err(RamInspectError::InspectorAlreadyExists);
286 }
287
288 INSPECTED_PIDS.lock().push(pid);
289 let maps_path = format!("/proc/{}/maps\0", pid);
290 let proc_maps_file = fopen(maps_path.as_ptr() as _, "r\0".as_ptr() as _).into_result(
291 RamInspectError::FailedToOpenProcMaps
292 )?;
293
294 // Pause the target process with a SIGSTOP signal
295 if let Err(error) = kill(pid, SIGSTOP).into_result(RamInspectError::FailedToPauseProcess) {
296 fclose(proc_maps_file);
297 return Err(error);
298 }
299
300 let max_iovs = sysconf(_SC_IOV_MAX);
301
302 if max_iovs < 0 {
303 fclose(proc_maps_file);
304 panic!("Unsupported kernel version or platform.");
305 }
306
307 Ok(RamInspector {
308 pid,
309 proc_maps_file,
310 write_requests: Vec::new(),
311 max_iovs: max_iovs as usize,
312 resume_count: Arc::new(AtomicUsize::new(0)),
313 })
314 }
315 }
316
317 /// Resumes the target process, returning a handle that pauses the process again when dropped,
318 /// assuming no other handles currently exist. Use this carefully, since writing to the
319 /// processes' memory while it's resumed may cause data races with the processes' code.
320 ///
321 /// If multiple handles are created before all the others are dropped, the process will remain
322 /// resumed until every one of its resume handles is dropped and dropping an individual handle
323 /// while other handles for the process still exist will have no effect. This ensures
324 /// correctness in multi-threaded contexts.
325
326 pub fn resume_process(&self) -> Result<ResumeHandle, RamInspectError> {
327 if self.resume_count.fetch_add(1, Ordering::SeqCst) == 0 {
328 unsafe {
329 kill(self.pid, SIGCONT).into_result(RamInspectError::FailedToResumeProcess)?;
330 }
331 }
332
333 Ok(ResumeHandle {
334 pid: self.pid,
335 count: Arc::clone(&self.resume_count),
336 })
337 }
338
339 /// Allows for the execution of arbitrary code in the context of the process. This is unsafe
340 /// because there are no checks in place to ensure the provided code is safe. The provided
341 /// code should also be completely position independent, since it could be loaded anywhere.
342 ///
343 /// This function waits for a signal from the shellcode that it is finished executing, given
344 /// by reading exactly one byte from the raminspect device file. It does not time out, so if
345 /// you forget to send the signal you'll have to terminate the hijacked process for this
346 /// function to resume and the shellcode to finish executing.
347 ///
348 /// The second argument is a callback that is called once the shellcode is finished executing
349 /// that takes in a mutable reference to the inspector and the starting address of the loaded
350 /// shellcode as arguments, before the old instructions are restored in memory. This can be
351 /// useful if you want to retrieve information from the shellcode after it's done executing.
352 ///
353 /// Note that this restores the previous register state automatically, so you don't have to
354 /// save and restore registers in your shellcode manually if you're writing it in assembly.
355
356 pub unsafe fn execute_shellcode<F: FnMut(&mut RamInspector, usize) -> Result<(), RamInspectError>>(
357 &mut self,
358 shellcode: &[u8],
359 mut callback: F,
360 ) -> Result<(), RamInspectError> {
361 let device_fd_wrapper = FileWrapper::open("/dev/raminspect\0", O_RDWR, RamInspectError::FailedToOpenDeviceFile)?;
362 let device_fd = device_fd_wrapper.descriptor;
363
364 // Temporarily make the code of the process writable so we can modify it.
365 ioctl(device_fd, TOGGLE_EXEC_WRITE, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
366
367 // Get process instruction pointer. ptrace and /proc/stat don't work here, at least on my machine, so we
368 // rely on the kernel module to do it for us instead.
369
370 let mut inst_ptr_request = InstructionPointerRequest {
371 pid: self.pid,
372 instruction_pointer: 0,
373 };
374
375 ioctl(device_fd, GET_INST_PTR, &mut inst_ptr_request).into_result(RamInspectError::ProcessTerminated)?;
376 let instruction_pointer = inst_ptr_request.instruction_pointer as usize;
377
378 // Save the old code and load the new code
379 let old_code = self.read_vec(instruction_pointer, shellcode.len())?;
380 self.write_to_address(instruction_pointer, shellcode)?;
381
382 // Resume the process and wait for the code to finish executing
383 kill(self.pid, SIGCONT).into_result(RamInspectError::ProcessTerminated)?;
384 ioctl(device_fd, WAIT_FOR_FINISH, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
385
386 // Then pause the process again and call the callback
387 kill(self.pid, SIGSTOP).into_result(RamInspectError::ProcessTerminated)?;
388 callback(self, instruction_pointer)?;
389
390 // Restore the old code and registers
391 self.write_to_address(instruction_pointer, &old_code)?;
392 ioctl(device_fd, RESTORE_REGS, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
393
394 // Leaving the target code as writable when it was originally read-only would present
395 // a fairly big security issue, so we make the modified regions read-only again after
396 // we're done by performing another ioctl.
397
398 ioctl(device_fd, TOGGLE_EXEC_WRITE, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
399 Ok(())
400 }
401
402 /// Allocates a new buffer with the given size for the current process and returns the address
403 /// of it. Currently this only works on x86-64, but PRs to expand it to work on other CPU
404 /// architectures are welcome.
405 ///
406 /// Note that due to the way this is implemented this function is fairly expensive. Don't use this many
407 /// times in a hot loop; try to make a few big allocations instead of many small ones for better
408 /// performance.
409
410 pub fn allocate_buffer(&mut self, size: usize) -> Result<usize, RamInspectError> {
411 assert!(cfg!(target_arch = "x86_64"), "`allocate_buffer` is currently only supported on x86-64.");
412 let mut shellcode: Vec<u8> = include_bytes!("../alloc-blob.bin").to_vec();
413 let alloc_size_offset = shellcode.len() - 8;
414 let out_ptr_offset = shellcode.len() - 16;
415
416 shellcode[alloc_size_offset..alloc_size_offset + 8].copy_from_slice(
417 &size.to_le_bytes()
418 );
419
420 unsafe {
421 let mut addr_bytes = [0; 8];
422 self.execute_shellcode(&shellcode, |this, inst_ptr| {
423 this.read_address(inst_ptr + out_ptr_offset, &mut addr_bytes)
424 })?;
425
426 Ok(u64::from_le_bytes(addr_bytes) as usize)
427 }
428 }
429
430 /// Fills the output buffer with memory read starting from the target address. This can fail
431 /// if the target process was suddenly terminated or if the address used is not part of a
432 /// readable memory region of the process.
433 ///
434 /// Note that this may spuriously fail if the target address is part of a shared memory region
435 /// (e.g. a memory mapped file), in which case you should always handle errors.
436 ///
437 /// If you're making large amounts of small reads, prefer [`RamInspector::read_bulk`] over
438 /// this function, which only performs one I/O syscall.
439
440 pub fn read_address(&mut self, addr: usize, out_buf: &mut [u8]) -> Result<(), RamInspectError> {
441 self.read_bulk(core::iter::once((addr, out_buf)))
442 }
443
444 /// A convenience function that reads the specified amount of bytes from the target address
445 /// and stores the output in a vector. This is shorthand for:
446 ///
447 /// ```rust
448 /// let mut out = vec![0; count];
449 /// inspector.read_address(addr, &mut out);
450 /// ```
451
452 pub fn read_vec(&mut self, addr: usize, count: usize) -> Result<Vec<u8>, RamInspectError> {
453 let mut out = vec![0; count];
454 self.read_address(addr, &mut out)?;
455 Ok(out)
456 }
457
458 // Used internally to simplify bulk reads and writes of data that use iovecs
459 unsafe fn exec_iov_op(&self, local_iovs: Vec<iovec>, remote_iovs: Vec<iovec>, iov_op: unsafe extern "C" fn(
460 pid_t,
461 *const iovec, c_ulong,
462 *const iovec, c_ulong, c_ulong
463 ) -> isize, err: RamInspectError) -> Result<(), RamInspectError> {
464 assert_eq!(local_iovs.len(), remote_iovs.len());
465
466 let mut i = 0;
467 while i < local_iovs.len() {
468 let end_index = (i + self.max_iovs).min(local_iovs.len());
469 let num_iovs = (end_index - i) as _;
470
471 iov_op(
472 self.pid,
473 local_iovs[i..end_index].as_ptr(), num_iovs,
474 remote_iovs[i..end_index].as_ptr(), num_iovs, 0,
475 ).into_result(err)?;
476 i += self.max_iovs;
477 }
478
479 Ok(())
480 }
481
482 /// Performs many memory reads at once in one I/O syscall, taking in an iterator of address / output
483 /// buffer pairs as an argument. This can be much faster than [`RamInspector::read_address`] if
484 /// you're making many small data reads, and should be preferred in that case. This has the
485 /// same failure conditions as `read_address`.
486
487 pub fn read_bulk<T: AsMut<[u8]>, I: Iterator<Item = (usize, T)>>(
488 &mut self,
489 reads: I,
490 ) -> Result<(), RamInspectError> {
491 let mut local_iovs = Vec::with_capacity(reads.size_hint().0);
492 let mut remote_iovs = Vec::with_capacity(reads.size_hint().0);
493
494 for (address, mut buf) in reads {
495 let buf = buf.as_mut();
496 local_iovs.push(iovec {
497 iov_len: buf.len(),
498 iov_base: buf.as_mut_ptr() as _,
499 });
500
501 remote_iovs.push(iovec {
502 iov_len: buf.len(),
503 iov_base: address as _,
504 });
505 }
506
507 unsafe {
508 self.exec_iov_op(
509 local_iovs, remote_iovs,
510 process_vm_readv, RamInspectError::FailedToReadMem,
511 )?;
512 }
513
514 Ok(())
515 }
516
517 /// A convenience function for performing one write of arbitrary data to an arbitrary memory address.
518 /// This does not flush the current write buffer, and is guaranteed to perform exactly one write.
519 ///
520 /// If you're making many writes, use [`RamInspector::queue_write`] in combination with [`RamInspector::flush`]
521 /// instead. This has the same safety constraints as `queue_write`, and is just a thin wrapper around it.
522
523 pub unsafe fn write_to_address(&mut self, addr: usize, buf: &[u8]) -> Result<(), RamInspectError> {
524 let mut old_buffer = Vec::new();
525 core::mem::swap(&mut self.write_requests, &mut old_buffer);
526
527 self.queue_write(addr, buf);
528 let res = self.flush();
529
530 self.write_requests = old_buffer;
531 res
532 }
533
534 /// Queues a write of the specified data to the specified memory address of the target process.
535 /// Writes will fail if the target process unexpectedly terminated, if the specified address is
536 /// not part of a writable region of the target processes' memory, and if the end address (the
537 /// start address plus the written buffers' length) is not part of the same memory region.
538 ///
539 /// This is unsafe since directly writing to an arbitrary address in an arbitrary processes'
540 /// memory is not memory safe at all; it is assumed that the caller knows what they're doing.
541 ///
542 /// Note that this has no effect until the [`RamInspector::flush`] method is called, for
543 /// performance reasons.
544
545 pub unsafe fn queue_write(&mut self, addr: usize, buf: &[u8]) {
546 self.write_requests.push((addr, buf.to_vec()));
547 }
548
549 /// Flushes the current buffer of writes, performing all of them in one I/O syscall. This is unsafe
550 /// for the same reasons that `queue_write` is unsafe, and is called automatically upon dropping
551 /// the inspector. See [`RamInspector::queue_write`] for more information.
552
553 pub unsafe fn flush(&mut self) -> Result<(), RamInspectError> {
554 let local_iovs = self.write_requests.iter().map(|(_addr, buf)| iovec {
555 iov_base: buf.as_ptr() as _,
556 iov_len: buf.len(),
557 }).collect::<Vec<iovec>>();
558
559 let remote_iovs = self.write_requests.iter().map(|(addr, buf)| iovec {
560 iov_base: (*addr) as _,
561 iov_len: buf.len(),
562 }).collect::<Vec<iovec>>();
563
564 self.exec_iov_op(local_iovs, remote_iovs, process_vm_writev, RamInspectError::FailedToWriteMem)?;
565 self.write_requests.clear();
566 Ok(())
567 }
568
569 /// A function that returns an iterator over the target processes' memory regions, generated by reading its
570 /// /proc/maps file. See the documentation of [`MemoryRegion`] for more information.
571
572 pub fn regions(&mut self) -> IntoIter<MemoryRegion> {
573 unsafe {
574 fseek(self.proc_maps_file, 0, SEEK_SET);
575 }
576
577 // For more details about what this calculation in particular means, see the section
578 // for /proc/pid/maps at: https://man7.org/linux/man-pages/man5/proc.5.html
579
580 const MAX_INODE_DIGITS: usize = 16;
581 const MAX_PATH_LENGTH: usize = 4096;
582 const MAX_LINE_LENGTH: usize = "ffffffffffffffff-ffffffffffffffff rwxp ffffffff ff:ff ".len() +
583 MAX_INODE_DIGITS + " ".len() + MAX_PATH_LENGTH;
584
585 let mut regions = Vec::new();
586 let mut line: [u8; MAX_LINE_LENGTH] = [0; MAX_LINE_LENGTH];
587 while unsafe { !fgets(line.as_mut_ptr() as _, line.len() as i32, self.proc_maps_file).is_null() } {
588 let line_str = core::str::from_utf8(
589 &line[..line.iter().position(|byte| *byte == 0).unwrap_or(line.len())]
590 ).unwrap().trim();
591
592 // Skip any bad or unneeded memory regions
593 if line_str.ends_with("(deleted)") || line_str.ends_with("[vvar]") || line_str.ends_with("[vdso]") || line_str.ends_with("[vsyscall]") {
594 continue;
595 }
596
597 let mut chars = line_str.chars();
598 // The lines read from /proc/PID/maps conform to the following format:
599 //
600 // HEX_START_ADDR-HEX_END_ADDR rwx(p or s)... etc
601 //
602 // Where rwx describes whether or not the described memory region can be read from, written
603 // to, and executed. If not the corresponding character will be dashed out. For example,
604 // read-only executable memory areas would show an r-x in the string and write-only
605 // non-executable ones would show a -w-.
606 //
607 // The next character following this (the p or s) describes whether or not the specified region
608 // is private or shared, and cannot be dashed out.
609
610 let start_addr_string = (&mut chars).take_while(char::is_ascii_hexdigit).collect::<String>();
611 let end_addr_string = (&mut chars).take_while(char::is_ascii_hexdigit).collect::<String>();
612 let start_addr = usize::from_str_radix(&start_addr_string, 16).unwrap();
613 let end_addr = usize::from_str_radix(&end_addr_string, 16).unwrap();
614 assert!(end_addr > start_addr);
615
616 regions.push(MemoryRegion {
617 start_addr,
618 length: end_addr - start_addr,
619 readable: chars.next().unwrap() == 'r',
620 writeable: chars.next().unwrap() == 'w',
621 executable: chars.next().unwrap() == 'x',
622 shared: chars.next().unwrap() == 's',
623 });
624
625 line = [0; MAX_LINE_LENGTH];
626 }
627
628 regions.into_iter()
629 }
630
631 /// Searches the target processes' memory for the specified data, and returns a list of
632 /// addresses of found search results and the memory regions that they are contained in.
633 /// This will fail if the process terminated unexpectedly, but it should succeed in
634 /// basically any other case.
635
636 pub fn search_for_term(&mut self, search_term: &[u8]) -> Result<Vec<(usize, MemoryRegion)>, RamInspectError> {
637 if search_term.is_empty() {
638 return Ok(Vec::new());
639 }
640
641 let mut out = Vec::new();
642 for region in self.regions().filter(|region| region.readable) {
643 if region.len() < search_term.len() {
644 continue;
645 }
646
647 if let Ok(data) = region.get_contents(self) {
648 for i in 0..data.len() - search_term.len() {
649 if data[i..].starts_with(search_term) {
650 out.push((region.start_addr + i, region.clone()));
651 }
652 }
653 }
654 }
655
656 Ok(out)
657 }
658}
659
660impl Drop for RamInspector {
661 fn drop(&mut self) {
662 unsafe {
663 // Flush all buffers.
664 let _ = self.flush();
665
666 // Free allocated resources.
667 fclose(self.proc_maps_file);
668
669 // Resume the target process on drop with a SIGCONT. We ignore errors here
670 // since there's no guarantee that the process is still running, so trying
671 // to send a signal to it might fail.
672 kill(self.pid, SIGCONT);
673
674 // Open up the PID for a new inspector.
675 let mut pids = INSPECTED_PIDS.lock();
676 let pos = pids.iter().position(|pid| *pid == self.pid).unwrap();
677 pids.swap_remove(pos);
678 }
679 }
680}
681
682/// A description of a memory region spanning any given address
683/// range with information about its start address, its access
684/// permissions (i.e. whether it's readable, writable, and/or
685/// executable), and whether or not it's shared or private.
686///
687/// You can obtain an iterator over all of a processes' memory
688/// regions using the [`RamInspector::regions`] method.
689
690#[derive(Debug, Clone)]
691pub struct MemoryRegion {
692 start_addr: usize,
693 length: usize,
694
695 executable: bool,
696 writeable: bool,
697 readable: bool,
698 shared: bool,
699}
700
701impl MemoryRegion {
702 /// Attempts to read the contents of the memory region. This fails if the memory region is
703 /// not readable, and may spuriously fail if the memory region is shared (in which case
704 /// you should always handle errors).
705
706 pub fn get_contents(&self, inspector: &mut RamInspector) -> Result<Vec<u8>, RamInspectError> {
707 inspector.read_vec(self.start_addr, self.length)
708 }
709
710 /// Gets the start address of the memory region.
711 pub fn start_addr(&self) -> usize {
712 self.start_addr
713 }
714
715 /// Gets the length of the memory region.
716 pub fn len(&self) -> usize {
717 self.length
718 }
719
720 /// Gets the end address of the memory region. This is equivalent to
721 /// adding the length to the start address.
722
723 pub fn end_addr(&self) -> usize {
724 self.start_addr + self.length
725 }
726
727 /// Checks if the memory region is readable.
728 pub fn readable(&self) -> bool {
729 self.readable
730 }
731
732 /// Checks if the memory region is shared.
733 pub fn shared(&self) -> bool {
734 self.shared
735 }
736
737 /// Checks if the memory region is writable.
738 pub fn writable(&self) -> bool {
739 self.writeable
740 }
741
742 /// Checks if the memory region is executable.
743 pub fn executable(&self) -> bool {
744 self.executable
745 }
746
747 /// Checks whether or not the memory region is both readable and writable.
748 pub fn is_readwrite(&self) -> bool {
749 self.readable && self.writeable
750 }
751}
752
753/// A handle obtained by calling the [`RamInspector::resume_process`] method that
754/// re-pauses the target process when dropped, assuming no other handles for the
755/// process currently exist. See the docs of that method for more information.
756
757#[must_use]
758pub struct ResumeHandle {
759 pid: i32,
760 count: Arc<AtomicUsize>,
761}
762
763impl Drop for ResumeHandle {
764 fn drop(&mut self) {
765 if self.count.fetch_sub(1, Ordering::SeqCst) == 1 {
766 unsafe {
767 kill(self.pid, SIGSTOP);
768 }
769 }
770 }
771}