Bug 1872920 - Introduce the process_reader crate a=diannaS

Original Revision: https://phabricator.services.mozilla.com/D201590

Differential Revision: https://phabricator.services.mozilla.com/D202915
This commit is contained in:
Gabriele Svelto 2024-02-28 19:57:14 +00:00
parent fde823c7b7
commit bce95d658f
6 changed files with 300 additions and 0 deletions

11
Cargo.lock generated
View File

@ -4184,6 +4184,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "process_reader"
version = "0.1.0"
dependencies = [
"goblin",
"memoffset",
"mozilla-central-workspace-hack",
"thiserror",
"winapi",
]
[[package]]
name = "processtools"
version = "0.1.0"

View File

@ -0,0 +1,18 @@
[package]
name = "process_reader"
version = "0.1.0"
authors = ["Gabriele Svelto"]
edition = "2018"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
goblin = { version = "0.6", features = ["elf32", "elf64", "pe32", "pe64"] }
memoffset = "0.8"
mozilla-central-workspace-hack = { path = "../../../build/workspace-hack" }
thiserror = "1.0"
[target."cfg(target_os = \"windows\")".dependencies]
[dependencies.winapi]
version = "0.3"

View File

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProcessReaderError {
#[error("Could not convert address {0}")]
ConvertAddressError(#[from] std::num::TryFromIntError),
#[cfg(target_os = "windows")]
#[error("Cannot enumerate the target process's modules")]
EnumProcessModulesError,
#[error("goblin failed to parse a module")]
GoblinError(#[from] goblin::error::Error),
#[error("Address was out of bounds")]
InvalidAddress,
#[error("Could not read from the target process address space")]
ReadFromProcessError(#[from] ReadError),
#[cfg(target_os = "windows")]
#[error("Section was not found")]
SectionNotFound,
}
#[derive(Debug, Error)]
pub enum ReadError {
#[cfg(target_os = "windows")]
#[error("ReadProcessMemory failed")]
ReadProcessMemoryError,
}

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#[cfg(target_os = "windows")]
type ProcessHandle = winapi::um::winnt::HANDLE;
pub struct ProcessReader {
process: ProcessHandle,
}
mod error;
mod process_reader;

View File

@ -0,0 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#[cfg(target_os = "windows")]
mod windows;

View File

@ -0,0 +1,222 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::{
convert::TryInto,
ffi::OsString,
mem::{size_of, MaybeUninit},
os::windows::ffi::OsStringExt,
ptr::null_mut,
};
use winapi::{
shared::minwindef::{FALSE, HMODULE, MAX_PATH},
um::{
memoryapi::ReadProcessMemory,
psapi::{GetModuleBaseNameW, K32EnumProcessModules, K32GetModuleInformation, MODULEINFO},
},
};
use crate::{
error::{ProcessReaderError, ReadError},
ProcessHandle, ProcessReader,
};
impl ProcessReader {
pub fn new(process: ProcessHandle) -> Result<ProcessReader, ProcessReaderError> {
Ok(ProcessReader { process })
}
pub fn find_section(
&self,
module_name: &str,
section_name: &str,
) -> Result<usize, ProcessReaderError> {
let modules = self.get_module_list()?;
modules
.iter()
.filter(|&&module| {
let name = self.get_module_name(module);
// Crude way of mimicking Windows lower-case comparisons but
// sufficient for our use-cases.
name.is_some_and(|name| name.eq_ignore_ascii_case(module_name))
})
.find_map(|&module| {
self.get_module_info(module).and_then(|info| {
self.find_section_in_module(
section_name,
info.lpBaseOfDll as usize,
info.SizeOfImage as usize,
)
.ok()
})
})
.ok_or(ProcessReaderError::InvalidAddress)
}
fn get_module_list(&self) -> Result<Vec<HMODULE>, ProcessReaderError> {
let mut module_num: usize = 100;
let mut required_buffer_size: u32 = 0;
let mut module_array = Vec::<HMODULE>::with_capacity(module_num);
loop {
let buffer_size: u32 = (module_num * size_of::<HMODULE>()).try_into()?;
let res = unsafe {
K32EnumProcessModules(
self.process,
module_array.as_mut_ptr() as *mut _,
buffer_size,
&mut required_buffer_size as *mut _,
)
};
module_num = required_buffer_size as usize / size_of::<HMODULE>();
if res == 0 {
if required_buffer_size > buffer_size {
module_array = Vec::<HMODULE>::with_capacity(module_num);
} else {
return Err(ProcessReaderError::EnumProcessModulesError);
}
} else {
break;
}
}
// SAFETY: module_array has been filled by K32EnumProcessModules()
unsafe {
module_array.set_len(module_num);
};
Ok(module_array)
}
fn get_module_name(&self, module: HMODULE) -> Option<String> {
let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
let res = unsafe {
GetModuleBaseNameW(
self.process,
module,
(&mut path).as_mut_ptr(),
MAX_PATH.try_into().unwrap(),
)
};
if res == 0 {
None
} else {
let name = OsString::from_wide(&path[0..res as usize]);
let name = name.to_str()?;
Some(name.to_string())
}
}
fn get_module_info(&self, module: HMODULE) -> Option<MODULEINFO> {
let mut info: MaybeUninit<MODULEINFO> = MaybeUninit::uninit();
let res = unsafe {
K32GetModuleInformation(
self.process,
module,
info.as_mut_ptr(),
size_of::<MODULEINFO>() as u32,
)
};
if res == 0 {
None
} else {
let info = unsafe { info.assume_init() };
Some(info)
}
}
fn find_section_in_module(
&self,
section_name: &str,
module_address: usize,
size: usize,
) -> Result<usize, ProcessReaderError> {
// We read only the first page from the module, this should be more than
// enough to read the header and section list. In the future we might do
// this incrementally but for now goblin requires an array to parse
// so we can't do it just yet.
let page_size = 4096;
if size < page_size {
// Don't try to read from the target module if it's too small
return Err(ProcessReaderError::ReadFromProcessError(
ReadError::ReadProcessMemoryError,
));
}
let bytes = self.copy_array(module_address as _, 4096)?;
let header = goblin::pe::header::Header::parse(&bytes)?;
// Skip the PE header so we can parse the sections
let optional_header_offset = header.dos_header.pe_pointer as usize
+ goblin::pe::header::SIZEOF_PE_MAGIC
+ goblin::pe::header::SIZEOF_COFF_HEADER;
let offset =
&mut (optional_header_offset + header.coff_header.size_of_optional_header as usize);
let sections = header.coff_header.sections(&bytes, offset)?;
for section in sections {
if section.name.eq(section_name.as_bytes()) {
let address = module_address.checked_add(section.virtual_address as usize);
return address.ok_or(ProcessReaderError::InvalidAddress);
}
}
Err(ProcessReaderError::SectionNotFound)
}
pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> {
let mut object = MaybeUninit::<T>::uninit();
let res = unsafe {
ReadProcessMemory(
self.process,
src as _,
object.as_mut_ptr() as _,
size_of::<T>(),
null_mut(),
)
};
if res != FALSE {
Ok(object)
} else {
Err(ReadError::ReadProcessMemoryError)
}
}
pub fn copy_object<T>(&self, src: usize) -> Result<T, ReadError> {
let object = self.copy_object_shallow(src)?;
Ok(unsafe { object.assume_init() })
}
pub fn copy_array<T>(&self, src: usize, num: usize) -> Result<Vec<T>, ReadError> {
let num_of_bytes = num * size_of::<T>();
let mut array: Vec<T> = Vec::with_capacity(num);
let res = unsafe {
ReadProcessMemory(
self.process,
src as _,
array.as_mut_ptr() as _,
num_of_bytes,
null_mut(),
)
};
if res != FALSE {
unsafe {
array.set_len(num);
}
Ok(array)
} else {
Err(ReadError::ReadProcessMemoryError)
}
}
}