procmem_linux/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3use std::{
4    fs::{File, OpenOptions, read_dir, read_link},
5    io::{BufRead, BufReader, Error, ErrorKind},
6    os::unix::fs::FileExt,
7    path::Path,
8};
9
10use bytemuck::{AnyBitPattern, NoUninit};
11use error::{MemoryError, ProcessError};
12use libc::{EFAULT, EPERM, ESRCH, iovec, process_vm_readv, process_vm_writev};
13
14mod elf;
15/// errors for process handling and memory operations
16pub mod error;
17
18/// mode used to read and write memory.
19#[derive(PartialEq)]
20pub enum MemoryMode {
21    /// use file i/o on `/proc/{pid}/mem`.
22    File,
23    /// use `process_vm_readv` and `process_vm_writev` syscalls.
24    Syscall,
25}
26
27/// represents a process handle for memory operations.
28pub struct Process {
29    pid: i32,
30    memory: File,
31    mode: MemoryMode,
32}
33
34impl Process {
35    fn find_pid<S: AsRef<str>>(name: S) -> Result<i32, ProcessError> {
36        for dir in read_dir("/proc").unwrap() {
37            let entry = match dir {
38                Ok(entry) => entry,
39                Err(_) => continue,
40            };
41
42            match entry.file_type() {
43                Ok(file_type) => {
44                    if !file_type.is_dir() {
45                        continue;
46                    }
47                }
48                Err(_) => continue,
49            }
50
51            let pid_osstr = entry.file_name();
52            let pid = match pid_osstr.to_str() {
53                Some(pid) => pid,
54                None => continue,
55            };
56
57            if !pid.chars().all(|char| char.is_numeric()) {
58                continue;
59            }
60
61            let Ok(exe_path) = read_link(format!("/proc/{}/exe", pid)) else {
62                continue;
63            };
64
65            let exe_name = match exe_path.file_name() {
66                Some(exe_name) => exe_name,
67                None => continue,
68            };
69
70            if exe_name == name.as_ref() {
71                let pid = pid
72                    .parse::<i32>()
73                    .map_err(|error| ProcessError::InvalidPid(error.kind().clone()))?;
74                return Ok(pid);
75            }
76        }
77        Err(ProcessError::NotFound)
78    }
79
80    fn map_mem_error(error: Error) -> MemoryError {
81        if error.kind() == ErrorKind::PermissionDenied {
82            MemoryError::PermissionDenied
83        } else {
84            MemoryError::Unknown
85        }
86    }
87
88    /// open a process given its executable name.
89    ///
90    /// this will use the first process with the given name.
91    ///
92    /// # example
93    /// ```rust
94    /// let process = Process::open_exe_name("bash").unwrap();
95    /// ```
96    pub fn open_exe_name<S: AsRef<str>>(name: S) -> Result<Process, ProcessError> {
97        let pid = Process::find_pid(name)?;
98        Process::open_pid(pid)
99    }
100
101    /// open a process from its pid.
102    ///
103    /// determines availability of `process_vm_*` syscalls and chooses the right mode.
104    pub fn open_pid(pid: i32) -> Result<Process, ProcessError> {
105        // test whether process_vm_readv is a valid syscall
106        // call it with dummy data and see what happens
107        let mut dummy_data = [0u8; 1];
108        let iov = iovec {
109            iov_base: dummy_data.as_mut_ptr() as *mut libc::c_void,
110            iov_len: 1,
111        };
112
113        let has_proc_read = unsafe { process_vm_readv(pid, &iov, 1, &iov, 1, 0) } > 0;
114
115        let memory = OpenOptions::new()
116            .read(true)
117            .write(true)
118            .open(format!("/proc/{pid}/mem"))
119            .map_err(|error| {
120                if error.kind() == ErrorKind::PermissionDenied {
121                    ProcessError::PermissionDenied(pid)
122                } else {
123                    ProcessError::FileOpenError(pid)
124                }
125            })?;
126
127        Ok(Process {
128            pid,
129            memory,
130            mode: if has_proc_read {
131                MemoryMode::Syscall
132            } else {
133                MemoryMode::File
134            },
135        })
136    }
137
138    /// switch between `Syscall` and `File` mode at runtime.
139    pub fn set_mode(&mut self, mode: MemoryMode) {
140        self.mode = mode;
141    }
142
143    /// check if the process is still running and valid
144    pub fn is_running(&self) -> bool {
145        Path::new(&format!("/proc/{}/mem", self.pid)).exists()
146    }
147
148    /// get the pid of the target process.
149    pub fn pid(&self) -> i32 {
150        self.pid
151    }
152
153    /// read a value T from the specified address.
154    ///
155    /// the type must implement [`bytemuck::AnyBitPattern`].
156    /// in Syscall mode uses `process_vm_readv`, in File mode uses FileExt::read_at.
157    pub fn read<T: AnyBitPattern>(&self, address: usize) -> Result<T, MemoryError> {
158        let mut buffer = vec![0u8; std::mem::size_of::<T>()];
159        if self.mode == MemoryMode::File {
160            self.memory
161                .read_at(&mut buffer, address as u64)
162                .map_err(Process::map_mem_error)?;
163        } else {
164            let local_iov = iovec {
165                iov_base: buffer.as_mut_ptr() as *mut libc::c_void,
166                iov_len: buffer.len(),
167            };
168            let remote_iov = iovec {
169                iov_base: address as *mut libc::c_void,
170                iov_len: buffer.len(),
171            };
172
173            let bytes_read =
174                unsafe { process_vm_readv(self.pid, &local_iov, 1, &remote_iov, 1, 0) };
175            if bytes_read < 0 {
176                let os_error = Error::last_os_error().raw_os_error();
177                return Err(match os_error {
178                    Some(EFAULT) => MemoryError::OutOfRange,
179                    Some(ESRCH) => MemoryError::ProcessQuit,
180                    Some(EPERM) => MemoryError::PermissionDenied,
181                    _ => MemoryError::Unknown,
182                });
183            } else if (bytes_read as usize) < buffer.len() {
184                return Err(MemoryError::PartialTransfer(
185                    bytes_read as usize,
186                    buffer.len(),
187                ));
188            }
189        }
190
191        match bytemuck::try_from_bytes::<T>(&buffer).cloned() {
192            Ok(value) => Ok(value),
193            Err(_) => Err(MemoryError::InvalidData(std::any::type_name::<T>())),
194        }
195    }
196
197    /// write a value T to the specified address.
198    ///
199    /// the type must implement [`bytemuck::NoUninit`].
200    /// in Syscall mode uses `process_vm_writev`, in File mode uses FileExt::write_at.
201    pub fn write<T: NoUninit>(&self, address: usize, value: &T) -> Result<(), MemoryError> {
202        let mut buffer = bytemuck::bytes_of(value).to_vec();
203        if self.mode == MemoryMode::File {
204            self.memory
205                .write_at(&buffer, address as u64)
206                .map_err(Process::map_mem_error)?;
207        } else {
208            let local_iov = iovec {
209                iov_base: buffer.as_mut_ptr() as *mut libc::c_void,
210                iov_len: buffer.len(),
211            };
212            let remote_iov = iovec {
213                iov_base: address as *mut libc::c_void,
214                iov_len: buffer.len(),
215            };
216
217            let bytes_written =
218                unsafe { process_vm_writev(self.pid, &local_iov, 1, &remote_iov, 1, 0) };
219            if bytes_written < 0 {
220                let os_error = Error::last_os_error().raw_os_error();
221                return Err(match os_error {
222                    Some(EFAULT) => MemoryError::OutOfRange,
223                    Some(ESRCH) => MemoryError::ProcessQuit,
224                    Some(EPERM) => MemoryError::PermissionDenied,
225                    _ => MemoryError::Unknown,
226                });
227            } else if (bytes_written as usize) < buffer.len() {
228                return Err(MemoryError::PartialTransfer(
229                    bytes_written as usize,
230                    buffer.len(),
231                ));
232            }
233        }
234
235        Ok(())
236    }
237
238    /// reads `count` bytes starting at `address`, using File mode.
239    ///
240    /// process_vm_readv does not work for very large reads,
241    /// which is why File mode is always used.
242    /// it will not switch the mode for other reads and writes.
243    pub fn read_bytes(&self, address: usize, count: usize) -> Result<Vec<u8>, MemoryError> {
244        let mut buffer = vec![0u8; count];
245        self.memory
246            .read_at(&mut buffer, address as u64)
247            .map_err(Process::map_mem_error)?;
248        Ok(buffer)
249    }
250
251    /// writes `count` bytes starting at `address`, using File mode.
252    ///
253    /// process_vm_writev does not work for very large writes,
254    /// which is why File mode is always used.
255    /// it will not switch the mode for other reads and writes.
256    pub fn write_bytes(&self, address: usize, value: &[u8]) -> Result<(), MemoryError> {
257        self.memory
258            .write_at(value, address as u64)
259            .map_err(Process::map_mem_error)?;
260        Ok(())
261    }
262
263    /// reads a c-style null-terminated string starting at `address`
264    /// until a `0` byte.
265    pub fn read_terminated_string(&self, address: usize) -> Result<String, MemoryError> {
266        const MAX_BYTES: usize = 1024;
267        const SIZE: usize = 32;
268        let mut buffer = Vec::with_capacity(SIZE);
269        let mut current_address = address;
270        let mut bytes_read = 0;
271        loop {
272            let chunk = self.read_bytes(current_address, SIZE)?;
273            bytes_read += SIZE;
274
275            if let Some(null_pos) = chunk.iter().position(|&b| b == 0) {
276                buffer.extend_from_slice(&chunk[..null_pos]);
277                return Ok(String::from_utf8_lossy(&buffer).to_string());
278            }
279
280            buffer.extend_from_slice(&chunk);
281            current_address += SIZE;
282
283            if bytes_read >= MAX_BYTES {
284                return Err(MemoryError::StringTooLong);
285            }
286        }
287    }
288
289    /// reads a utf-8 encoded string starting at `address` with a given length.
290    pub fn read_string(&self, address: usize, length: usize) -> Result<String, MemoryError> {
291        let bytes = self.read_bytes(address, length)?;
292        String::from_utf8(bytes)
293            .map_err(|_| MemoryError::InvalidData(std::any::type_name::<String>()))
294    }
295
296    /// writes any string-like starting at `address`
297    pub fn write_string<S: AsRef<str>>(&self, address: usize, value: S) -> Result<(), MemoryError> {
298        self.write_bytes(address, value.as_ref().as_bytes())
299    }
300
301    /// parses `/proc/{pid}/maps` to locate the base address of a loaded
302    /// library with name matching `library`.
303    pub fn find_library<S: AsRef<str>>(&self, library: S) -> Result<usize, MemoryError> {
304        let maps =
305            File::open(format!("/proc/{}/maps", self.pid)).map_err(Process::map_mem_error)?;
306        for line in BufReader::new(maps).lines() {
307            let Ok(line) = line else {
308                continue;
309            };
310            let Some((line, file_name)) = line.rsplit_once('/') else {
311                continue;
312            };
313            if !file_name.contains(library.as_ref()) {
314                continue;
315            }
316            let Some((address, _)) = line.split_once('-') else {
317                return Err(MemoryError::Unknown);
318            };
319            let address = usize::from_str_radix(address, 16)
320                .map_err(|_| MemoryError::InvalidData(std::any::type_name::<usize>()))?;
321            return Ok(address);
322        }
323        Err(MemoryError::NotFound)
324    }
325
326    /// returns the size of a library at `address`, in bytes.
327    pub fn library_size(&self, address: usize) -> Result<usize, MemoryError> {
328        // check if elf header is present
329        let header = self.read::<u32>(address)?;
330        if header != 0x464C457F && header != 0x7F454C46 {
331            return Err(MemoryError::OutOfRange);
332        }
333        let section_header_offset = self.read::<usize>(address + elf::SECTION_HEADER_OFFSET)?;
334        let section_header_entry_size =
335            self.read::<u16>(address + elf::SECTION_HEADER_ENTRY_SIZE)? as usize;
336        let section_header_num_entries =
337            self.read::<u16>(address + elf::SECTION_HEADER_NUM_ENTRIES)? as usize;
338
339        Ok(section_header_offset + section_header_entry_size * section_header_num_entries)
340    }
341
342    /// dump a library at base address `address`.
343    /// this will return a complete copy of the library, as it is loaded into memory.
344    pub fn dump_library(&self, address: usize) -> Result<Vec<u8>, MemoryError> {
345        // check if elf header is present
346        let header = self.read::<u32>(address)?;
347        if header != 0x464C457F && header != 0x7F454C46 {
348            return Err(MemoryError::OutOfRange);
349        }
350        let lib_size = self.library_size(address)?;
351        self.read_bytes(address, lib_size)
352    }
353
354    /// scan a pattern in library at `address`, using `pattern`.
355    ///
356    /// the pattern accepted is a normal ida pattern.
357    ///
358    /// # example
359    ///
360    /// ```rust
361    /// let process = Process::open_exe_name("bash").unwrap();
362    /// process.scan_pattern("12 34 ? ? 56 78", 0x12345678);
363    /// ```
364    ///
365    /// this scans the ida pattern `12 34 ? ? 56 78`.
366    pub fn scan_pattern<S: AsRef<str>>(
367        &self,
368        pattern: S,
369        address: usize,
370    ) -> Result<usize, MemoryError> {
371        let pattern_string = pattern.as_ref();
372        let mut pattern = Vec::with_capacity(pattern_string.len());
373        let mut mask = Vec::with_capacity(pattern_string.len());
374
375        for c in pattern_string.split(' ') {
376            match u8::from_str_radix(c, 16) {
377                Ok(c) => {
378                    pattern.push(c);
379                    mask.push(1);
380                }
381                Err(_) => {
382                    pattern.push(0);
383                    mask.push(0);
384                }
385            }
386        }
387
388        let module = self.dump_library(address)?;
389        if module.len() < 500 {
390            return Err(MemoryError::InvalidLibrary);
391        }
392
393        let pattern_length = pattern.len();
394        let stop_index = module.len() - pattern_length;
395        'outer: for i in 0..stop_index {
396            for j in 0..pattern_length {
397                if mask[j] != 0 && module[i + j] != pattern[j] {
398                    continue 'outer;
399                }
400            }
401            return Ok(address + i);
402        }
403        Err(MemoryError::NotFound)
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use crate::error::MemoryError;
410
411    use super::Process;
412
413    /// get own process pid.
414    fn pid() -> i32 {
415        std::process::id() as i32
416    }
417
418    #[test]
419    fn create() {
420        assert!(Process::open_pid(pid()).is_ok());
421    }
422
423    #[test]
424    fn read() -> Result<(), MemoryError> {
425        let process = Process::open_pid(pid()).unwrap();
426        let buffer = [0x55u8];
427        let value = process.read::<u8>(buffer.as_ptr() as usize)?;
428        assert!(value == buffer[0]);
429        Ok(())
430    }
431
432    #[test]
433    fn write() -> Result<(), MemoryError> {
434        let process = Process::open_pid(pid()).unwrap();
435        let buffer = [0x55u8];
436        const VALUE: u8 = 0x66;
437        process.write::<u8>(buffer.as_ptr() as usize, &VALUE)?;
438        assert!(buffer[0] == VALUE);
439        Ok(())
440    }
441
442    #[test]
443    fn read_bytes() -> Result<(), MemoryError> {
444        let process = Process::open_pid(pid()).unwrap();
445        let buffer: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
446        let value = process.read_bytes(buffer.as_ptr() as usize, 4)?;
447        assert!(value == buffer);
448        Ok(())
449    }
450
451    #[test]
452    fn write_bytes() -> Result<(), MemoryError> {
453        let process = Process::open_pid(pid()).unwrap();
454        let buffer: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
455        const VALUE: [u8; 4] = [0x55, 0x66, 0x77, 0x88];
456        process.write_bytes(buffer.as_ptr() as usize, &VALUE)?;
457        assert!(buffer == VALUE);
458        Ok(())
459    }
460
461    #[test]
462    fn read_terminated_string() -> Result<(), MemoryError> {
463        let process = Process::open_pid(pid()).unwrap();
464        const STRING: &str = "Hello World";
465        let buffer = std::ffi::CString::new(STRING).unwrap();
466        let value = process.read_terminated_string(buffer.as_ptr() as usize)?;
467        assert!(value == *STRING);
468        Ok(())
469    }
470
471    #[test]
472    fn read_string() -> Result<(), MemoryError> {
473        let process = Process::open_pid(pid()).unwrap();
474        const STRING: &str = "Hello World";
475        let value = process.read_string(STRING.as_ptr() as usize, STRING.len())?;
476        assert!(value == STRING);
477        Ok(())
478    }
479
480    #[test]
481    fn scan_pattern() -> Result<(), MemoryError> {
482        let process = Process::open_pid(pid()).unwrap();
483        const STRING: &str = "Hello World";
484
485        // find loaded process elf
486        let exe_path = std::env::current_exe().unwrap();
487        let exe_name = exe_path.file_name().unwrap().to_str().unwrap();
488        let lib = process.find_library(exe_name)?;
489
490        // convert hello world string to ida pattern
491        let pattern = STRING
492            .as_bytes()
493            .iter()
494            .map(|c| format!("{:02x}", c))
495            .collect::<Vec<String>>()
496            .join(" ");
497
498        let value = process.scan_pattern(pattern, lib)?;
499        assert!(value == STRING.as_ptr() as usize);
500        Ok(())
501    }
502}