#![no_std]
extern crate alloc;
use libc::*;
use core::sync::atomic::Ordering;
use core::sync::atomic::AtomicUsize;
use alloc::vec;
use alloc::format;
use alloc::vec::Vec;
use alloc::sync::Arc;
use alloc::vec::IntoIter;
use alloc::string::String;
trait IntoResult: Sized {
fn into_result(self, error: RamInspectError) -> Result<Self, RamInspectError>;
}
macro_rules! impl_into_result_for_num {
($num_ty:ty) => {
impl IntoResult for $num_ty {
fn into_result(self, error: RamInspectError) -> Result<Self, RamInspectError> {
if self < 0 {
Err(error)
} else {
Ok(self)
}
}
}
}
}
impl_into_result_for_num!(i32);
impl_into_result_for_num!(i64);
impl_into_result_for_num!(isize);
impl<T> IntoResult for *mut T {
fn into_result(self, error: RamInspectError) -> Result<Self, RamInspectError> {
if self.is_null() {
Err(error)
} else {
Ok(self)
}
}
}
struct FileWrapper {
descriptor: i32
}
impl FileWrapper {
fn open(path: &str, mode: i32, on_err: RamInspectError) -> Result<Self, RamInspectError> {
assert!(path.ends_with('\0'));
Ok(Self {
descriptor: unsafe {
open(path.as_ptr() as _, mode).into_result(on_err)?
}
})
}
}
impl Drop for FileWrapper {
fn drop(&mut self) {
unsafe {
close(self.descriptor);
}
}
}
#[repr(C)]
struct InstructionPointerRequest {
pid: i32,
instruction_pointer: u64,
}
const RESTORE_REGS: c_ulong = 0x40047B03;
const GET_INST_PTR: c_ulong = 0xC0107B02;
const WAIT_FOR_FINISH: c_ulong = 0x40047B00;
const TOGGLE_EXEC_WRITE: c_ulong = 0x40047B01;
pub fn find_processes(name_contains: &str) -> Vec<i32> {
let mut results = Vec::new();
const MAX_LINE_LENGTH: usize = 4096;
unsafe {
let dirp = opendir("/proc\0".as_ptr() as _);
if dirp.is_null() { return results; }
loop {
let entry_ptr = readdir(dirp);
if entry_ptr.is_null() { break; }
let name_bytes: [u8; 256] = core::mem::transmute(core::ptr::read(entry_ptr).d_name);
let end_of_str = name_bytes.iter().position(|byte| *byte == 0).unwrap();
let name = core::str::from_utf8(&name_bytes[..end_of_str]).unwrap();
let pid = match name.parse::<i32>() {
Ok(pid) => pid,
Err(_) => continue,
};
let path = format!("/proc/{}/cmdline\0", pid);
let fd = open(path.as_ptr() as _, O_RDONLY);
if fd < 0 { continue; }
let mut buf = vec![0; MAX_LINE_LENGTH];
if read(fd, buf.as_mut_ptr() as _, buf.len()) < 0 {
close(fd);
continue;
}
let executable_name = core::str::from_utf8(
&buf[..buf.iter().position(|byte| *byte == 0).unwrap_or(buf.len())]
).unwrap();
if executable_name.contains(name_contains) {
results.push(pid);
}
close(fd);
}
}
results
}
pub struct RamInspector {
pid: i32,
max_iovs: usize,
proc_maps_file: *mut FILE,
resume_count: Arc<AtomicUsize>,
write_requests: Vec<(usize, Vec<u8>)>,
}
unsafe impl Send for RamInspector {}
unsafe impl Sync for RamInspector {}
#[non_exhaustive]
#[derive(Clone, Copy)]
pub enum RamInspectError {
ProcessTerminated,
FailedToOpenProcMaps,
FailedToPauseProcess,
FailedToResumeProcess,
FailedToReadMem,
FailedToWriteMem,
FailedToOpenDeviceFile,
FailedToAllocateBuffer,
InspectorAlreadyExists,
}
use core::fmt;
use core::fmt::Debug;
use core::fmt::Formatter;
impl Debug for RamInspectError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
RamInspectError::InspectorAlreadyExists => "A `RamInspector` instance already exists for the specified process ID. \
Note: If you're in a multi-threaded environment, instead of creating \
multiple inspectors you can try accessing one inspector through a \
mutex.",
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?",
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?",
RamInspectError::FailedToWriteMem => "Failed to write to the specified memory address! Are you sure the address is in a writable region of the processes' memory?",
RamInspectError::FailedToReadMem => "Failed to read from the specified memory address! Are you sure the address is in a readable region of the processes' memory?",
RamInspectError::FailedToResumeProcess => "Failed to resume the target process! Are you sure it is currently running?",
RamInspectError::FailedToPauseProcess => "Failed to pause the target process! Are you sure it is currently running?",
RamInspectError::FailedToAllocateBuffer => "Failed to allocate the specified buffer.",
RamInspectError::ProcessTerminated => "The target process unexpectedly terminated.",
})
}
}
use core::fmt::Display;
impl Display for RamInspectError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(self, formatter)
}
}
use spin::Mutex;
static INSPECTED_PIDS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
impl RamInspector {
pub fn new(pid: i32) -> Result<Self, RamInspectError> {
unsafe {
if INSPECTED_PIDS.lock().contains(&pid) {
return Err(RamInspectError::InspectorAlreadyExists);
}
INSPECTED_PIDS.lock().push(pid);
let maps_path = format!("/proc/{}/maps\0", pid);
let proc_maps_file = fopen(maps_path.as_ptr() as _, "r\0".as_ptr() as _).into_result(
RamInspectError::FailedToOpenProcMaps
)?;
if let Err(error) = kill(pid, SIGSTOP).into_result(RamInspectError::FailedToPauseProcess) {
fclose(proc_maps_file);
return Err(error);
}
let max_iovs = sysconf(_SC_IOV_MAX);
if max_iovs < 0 {
fclose(proc_maps_file);
panic!("Unsupported kernel version or platform.");
}
Ok(RamInspector {
pid,
proc_maps_file,
write_requests: Vec::new(),
max_iovs: max_iovs as usize,
resume_count: Arc::new(AtomicUsize::new(0)),
})
}
}
pub fn resume_process(&self) -> Result<ResumeHandle, RamInspectError> {
if self.resume_count.fetch_add(1, Ordering::SeqCst) == 0 {
unsafe {
kill(self.pid, SIGCONT).into_result(RamInspectError::FailedToResumeProcess)?;
}
}
Ok(ResumeHandle {
pid: self.pid,
count: Arc::clone(&self.resume_count),
})
}
pub unsafe fn execute_shellcode<F: FnMut(&mut RamInspector, usize) -> Result<(), RamInspectError>>(
&mut self,
shellcode: &[u8],
mut callback: F,
) -> Result<(), RamInspectError> {
let device_fd_wrapper = FileWrapper::open("/dev/raminspect\0", O_RDWR, RamInspectError::FailedToOpenDeviceFile)?;
let device_fd = device_fd_wrapper.descriptor;
ioctl(device_fd, TOGGLE_EXEC_WRITE, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
let mut inst_ptr_request = InstructionPointerRequest {
pid: self.pid,
instruction_pointer: 0,
};
ioctl(device_fd, GET_INST_PTR, &mut inst_ptr_request).into_result(RamInspectError::ProcessTerminated)?;
let instruction_pointer = inst_ptr_request.instruction_pointer as usize;
let old_code = self.read_vec(instruction_pointer, shellcode.len())?;
self.write_to_address(instruction_pointer, shellcode)?;
kill(self.pid, SIGCONT).into_result(RamInspectError::ProcessTerminated)?;
ioctl(device_fd, WAIT_FOR_FINISH, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
kill(self.pid, SIGSTOP).into_result(RamInspectError::ProcessTerminated)?;
callback(self, instruction_pointer)?;
self.write_to_address(instruction_pointer, &old_code)?;
ioctl(device_fd, RESTORE_REGS, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
ioctl(device_fd, TOGGLE_EXEC_WRITE, self.pid as c_ulong).into_result(RamInspectError::ProcessTerminated)?;
Ok(())
}
pub fn allocate_buffer(&mut self, size: usize) -> Result<usize, RamInspectError> {
assert!(cfg!(target_arch = "x86_64"), "`allocate_buffer` is currently only supported on x86-64.");
let mut shellcode: Vec<u8> = include_bytes!("../alloc-blob.bin").to_vec();
let alloc_size_offset = shellcode.len() - 8;
let out_ptr_offset = shellcode.len() - 16;
shellcode[alloc_size_offset..alloc_size_offset + 8].copy_from_slice(
&size.to_le_bytes()
);
unsafe {
let mut addr_bytes = [0; 8];
self.execute_shellcode(&shellcode, |this, inst_ptr| {
this.read_address(inst_ptr + out_ptr_offset, &mut addr_bytes)
})?;
Ok(u64::from_le_bytes(addr_bytes) as usize)
}
}
pub fn read_address(&mut self, addr: usize, out_buf: &mut [u8]) -> Result<(), RamInspectError> {
self.read_bulk(core::iter::once((addr, out_buf)))
}
pub fn read_vec(&mut self, addr: usize, count: usize) -> Result<Vec<u8>, RamInspectError> {
let mut out = vec![0; count];
self.read_address(addr, &mut out)?;
Ok(out)
}
unsafe fn exec_iov_op(&self, local_iovs: Vec<iovec>, remote_iovs: Vec<iovec>, iov_op: unsafe extern "C" fn(
pid_t,
*const iovec, c_ulong,
*const iovec, c_ulong, c_ulong
) -> isize, err: RamInspectError) -> Result<(), RamInspectError> {
assert_eq!(local_iovs.len(), remote_iovs.len());
let mut i = 0;
while i < local_iovs.len() {
let end_index = (i + self.max_iovs).min(local_iovs.len());
let num_iovs = (end_index - i) as _;
iov_op(
self.pid,
local_iovs[i..end_index].as_ptr(), num_iovs,
remote_iovs[i..end_index].as_ptr(), num_iovs, 0,
).into_result(err)?;
i += self.max_iovs;
}
Ok(())
}
pub fn read_bulk<T: AsMut<[u8]>, I: Iterator<Item = (usize, T)>>(
&mut self,
reads: I,
) -> Result<(), RamInspectError> {
let mut local_iovs = Vec::with_capacity(reads.size_hint().0);
let mut remote_iovs = Vec::with_capacity(reads.size_hint().0);
for (address, mut buf) in reads {
let buf = buf.as_mut();
local_iovs.push(iovec {
iov_len: buf.len(),
iov_base: buf.as_mut_ptr() as _,
});
remote_iovs.push(iovec {
iov_len: buf.len(),
iov_base: address as _,
});
}
unsafe {
self.exec_iov_op(
local_iovs, remote_iovs,
process_vm_readv, RamInspectError::FailedToReadMem,
)?;
}
Ok(())
}
pub unsafe fn write_to_address(&mut self, addr: usize, buf: &[u8]) -> Result<(), RamInspectError> {
let mut old_buffer = Vec::new();
core::mem::swap(&mut self.write_requests, &mut old_buffer);
self.queue_write(addr, buf);
let res = self.flush();
self.write_requests = old_buffer;
res
}
pub unsafe fn queue_write(&mut self, addr: usize, buf: &[u8]) {
self.write_requests.push((addr, buf.to_vec()));
}
pub unsafe fn flush(&mut self) -> Result<(), RamInspectError> {
let local_iovs = self.write_requests.iter().map(|(_addr, buf)| iovec {
iov_base: buf.as_ptr() as _,
iov_len: buf.len(),
}).collect::<Vec<iovec>>();
let remote_iovs = self.write_requests.iter().map(|(addr, buf)| iovec {
iov_base: (*addr) as _,
iov_len: buf.len(),
}).collect::<Vec<iovec>>();
self.exec_iov_op(local_iovs, remote_iovs, process_vm_writev, RamInspectError::FailedToWriteMem)?;
self.write_requests.clear();
Ok(())
}
pub fn regions(&mut self) -> IntoIter<MemoryRegion> {
unsafe {
fseek(self.proc_maps_file, 0, SEEK_SET);
}
const MAX_INODE_DIGITS: usize = 16;
const MAX_PATH_LENGTH: usize = 4096;
const MAX_LINE_LENGTH: usize = "ffffffffffffffff-ffffffffffffffff rwxp ffffffff ff:ff ".len() +
MAX_INODE_DIGITS + " ".len() + MAX_PATH_LENGTH;
let mut regions = Vec::new();
let mut line: [u8; MAX_LINE_LENGTH] = [0; MAX_LINE_LENGTH];
while unsafe { !fgets(line.as_mut_ptr() as _, line.len() as i32, self.proc_maps_file).is_null() } {
let line_str = core::str::from_utf8(
&line[..line.iter().position(|byte| *byte == 0).unwrap_or(line.len())]
).unwrap().trim();
if line_str.ends_with("(deleted)") || line_str.ends_with("[vvar]") || line_str.ends_with("[vdso]") || line_str.ends_with("[vsyscall]") {
continue;
}
let mut chars = line_str.chars();
let start_addr_string = (&mut chars).take_while(char::is_ascii_hexdigit).collect::<String>();
let end_addr_string = (&mut chars).take_while(char::is_ascii_hexdigit).collect::<String>();
let start_addr = usize::from_str_radix(&start_addr_string, 16).unwrap();
let end_addr = usize::from_str_radix(&end_addr_string, 16).unwrap();
assert!(end_addr > start_addr);
regions.push(MemoryRegion {
start_addr,
length: end_addr - start_addr,
readable: chars.next().unwrap() == 'r',
writeable: chars.next().unwrap() == 'w',
executable: chars.next().unwrap() == 'x',
shared: chars.next().unwrap() == 's',
});
line = [0; MAX_LINE_LENGTH];
}
regions.into_iter()
}
pub fn search_for_term(&mut self, search_term: &[u8]) -> Result<Vec<(usize, MemoryRegion)>, RamInspectError> {
if search_term.is_empty() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for region in self.regions().filter(|region| region.readable) {
if region.len() < search_term.len() {
continue;
}
if let Ok(data) = region.get_contents(self) {
for i in 0..data.len() - search_term.len() {
if data[i..].starts_with(search_term) {
out.push((region.start_addr + i, region.clone()));
}
}
}
}
Ok(out)
}
}
impl Drop for RamInspector {
fn drop(&mut self) {
unsafe {
let _ = self.flush();
fclose(self.proc_maps_file);
kill(self.pid, SIGCONT);
let mut pids = INSPECTED_PIDS.lock();
let pos = pids.iter().position(|pid| *pid == self.pid).unwrap();
pids.swap_remove(pos);
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryRegion {
start_addr: usize,
length: usize,
executable: bool,
writeable: bool,
readable: bool,
shared: bool,
}
impl MemoryRegion {
pub fn get_contents(&self, inspector: &mut RamInspector) -> Result<Vec<u8>, RamInspectError> {
inspector.read_vec(self.start_addr, self.length)
}
pub fn start_addr(&self) -> usize {
self.start_addr
}
pub fn len(&self) -> usize {
self.length
}
pub fn end_addr(&self) -> usize {
self.start_addr + self.length
}
pub fn readable(&self) -> bool {
self.readable
}
pub fn shared(&self) -> bool {
self.shared
}
pub fn writable(&self) -> bool {
self.writeable
}
pub fn executable(&self) -> bool {
self.executable
}
pub fn is_readwrite(&self) -> bool {
self.readable && self.writeable
}
}
#[must_use]
pub struct ResumeHandle {
pid: i32,
count: Arc<AtomicUsize>,
}
impl Drop for ResumeHandle {
fn drop(&mut self) {
if self.count.fetch_sub(1, Ordering::SeqCst) == 1 {
unsafe {
kill(self.pid, SIGSTOP);
}
}
}
}