use crate::{
emulation::memory::{AddressSpace, MemoryProtection, MemoryRegion, SectionInfo},
Error, Result,
};
mod reloc_type {
pub const IMAGE_REL_BASED_ABSOLUTE: u16 = 0;
pub const IMAGE_REL_BASED_HIGHLOW: u16 = 3;
pub const IMAGE_REL_BASED_DIR64: u16 = 10;
}
#[derive(Clone, Debug)]
pub struct PeLoaderConfig {
pub base_address: Option<u64>,
pub apply_permissions: bool,
pub apply_relocations: bool,
}
impl Default for PeLoaderConfig {
fn default() -> Self {
Self {
base_address: None,
apply_permissions: true,
apply_relocations: true,
}
}
}
impl PeLoaderConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_base_address(mut self, base: u64) -> Self {
self.base_address = Some(base);
self
}
#[must_use]
pub fn without_permissions(mut self) -> Self {
self.apply_permissions = false;
self
}
#[must_use]
pub fn without_relocations(mut self) -> Self {
self.apply_relocations = false;
self
}
}
#[derive(Clone, Debug)]
pub struct LoadedImage {
pub base_address: u64,
pub size_of_image: u64,
pub entry_point: Option<u64>,
pub is_64_bit: bool,
pub sections: Vec<LoadedSection>,
pub name: String,
pub file_size: usize,
pub clr_header_rva: Option<u32>,
pub clr_header_size: Option<u32>,
}
impl LoadedImage {
#[must_use]
pub fn entry_point_va(&self) -> Option<u64> {
self.entry_point.map(|rva| self.base_address + rva)
}
#[must_use]
pub fn rva_to_va(&self, rva: u32) -> u64 {
self.base_address + u64::from(rva)
}
#[must_use]
pub fn contains_rva(&self, rva: u32) -> bool {
u64::from(rva) < self.size_of_image
}
#[must_use]
pub fn section_for_rva(&self, rva: u32) -> Option<&LoadedSection> {
self.sections.iter().find(|s| {
rva >= s.virtual_address && rva < s.virtual_address + s.virtual_size.max(s.raw_size)
})
}
#[must_use]
pub fn is_dotnet(&self) -> bool {
self.clr_header_rva.is_some()
}
}
#[derive(Clone, Debug)]
pub struct LoadedSection {
pub name: String,
pub virtual_address: u32,
pub virtual_size: u32,
pub raw_size: u32,
pub protection: MemoryProtection,
pub is_code: bool,
pub is_initialized_data: bool,
pub is_uninitialized_data: bool,
}
pub struct PeLoader {
config: PeLoaderConfig,
}
impl PeLoader {
#[must_use]
pub fn new() -> Self {
Self {
config: PeLoaderConfig::default(),
}
}
#[must_use]
pub fn with_config(config: PeLoaderConfig) -> Self {
Self { config }
}
pub fn load(
&self,
pe_bytes: &[u8],
address_space: &AddressSpace,
name: impl Into<String>,
) -> Result<LoadedImage> {
let name = name.into();
let pe = goblin::pe::PE::parse(pe_bytes)?;
let preferred_base = pe.image_base;
let base_address = self.config.base_address.unwrap_or(preferred_base);
let size_of_image = pe
.header
.optional_header
.map_or(0, |oh| u64::from(oh.windows_fields.size_of_image));
let entry_point = pe
.header
.optional_header
.map(|oh| u64::from(oh.standard_fields.address_of_entry_point));
let is_64_bit = pe.is_64;
let (clr_header_rva, clr_header_size) = if let Some(oh) = pe.header.optional_header.as_ref()
{
if let Some(dd) = oh.data_directories.get_clr_runtime_header() {
if dd.size > 0 {
(Some(dd.virtual_address), Some(dd.size))
} else {
(None, None)
}
} else {
(None, None)
}
} else {
(None, None)
};
let mut sections = Vec::new();
let mut section_infos = Vec::new();
#[allow(clippy::cast_possible_truncation)]
let mut image_data = vec![0u8; size_of_image as usize];
let headers_size = pe
.header
.optional_header
.map_or(0x200, |oh| oh.windows_fields.size_of_headers as usize);
if headers_size <= pe_bytes.len() && headers_size <= image_data.len() {
image_data[..headers_size].copy_from_slice(&pe_bytes[..headers_size]);
}
for section in &pe.sections {
let section_name = String::from_utf8_lossy(§ion.name)
.trim_end_matches('\0')
.to_string();
let virtual_address = section.virtual_address;
let virtual_size = section.virtual_size;
let raw_size = section.size_of_raw_data;
let raw_offset = section.pointer_to_raw_data as usize;
let characteristics = section.characteristics;
let mut protection = MemoryProtection::empty();
if characteristics & 0x2000_0000 != 0 {
protection |= MemoryProtection::EXECUTE;
}
if characteristics & 0x4000_0000 != 0 {
protection |= MemoryProtection::READ;
}
if characteristics & 0x8000_0000 != 0 {
protection |= MemoryProtection::WRITE;
}
let is_code = characteristics & 0x20 != 0; let is_initialized_data = characteristics & 0x40 != 0; let is_uninitialized_data = characteristics & 0x80 != 0;
let dest_offset = virtual_address as usize;
let copy_size = raw_size.min(virtual_size) as usize;
if raw_offset + copy_size <= pe_bytes.len()
&& dest_offset + copy_size <= image_data.len()
{
image_data[dest_offset..dest_offset + copy_size]
.copy_from_slice(&pe_bytes[raw_offset..raw_offset + copy_size]);
}
let section_protection = if self.config.apply_permissions {
protection
} else {
MemoryProtection::READ | MemoryProtection::WRITE | MemoryProtection::EXECUTE
};
sections.push(LoadedSection {
name: section_name.clone(),
virtual_address,
virtual_size,
raw_size,
protection: section_protection,
is_code,
is_initialized_data,
is_uninitialized_data,
});
#[allow(clippy::cast_possible_truncation)]
section_infos.push(SectionInfo {
name: section_name,
virtual_address,
virtual_size,
raw_data_offset: raw_offset as u32,
raw_data_size: raw_size,
characteristics,
protection: section_protection,
});
}
let delta = base_address.cast_signed() - preferred_base.cast_signed();
if delta != 0 && self.config.apply_relocations {
Self::apply_relocations(&pe, &mut image_data, delta, is_64_bit)?;
}
let region = MemoryRegion::pe_image(base_address, &image_data, section_infos, name.clone());
address_space.map_at(base_address, region)?;
Ok(LoadedImage {
base_address,
size_of_image,
entry_point,
is_64_bit,
sections,
name,
file_size: pe_bytes.len(),
clr_header_rva,
clr_header_size,
})
}
pub fn load_file(
&self,
path: &std::path::Path,
address_space: &AddressSpace,
) -> Result<LoadedImage> {
let pe_bytes = std::fs::read(path)
.map_err(|e| Error::Other(format!("Failed to read PE file: {e}")))?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
self.load(&pe_bytes, address_space, name)
}
fn apply_relocations(
pe: &goblin::pe::PE,
image_data: &mut [u8],
delta: i64,
is_64_bit: bool,
) -> Result<()> {
let reloc_dir = pe
.header
.optional_header
.as_ref()
.and_then(|oh| oh.data_directories.get_base_relocation_table());
let Some(reloc_dir) = reloc_dir else {
if delta != 0 {
return Err(Error::Other(
"Image requires relocation but has no relocation directory".to_string(),
));
}
return Ok(());
};
if reloc_dir.size == 0 {
if delta != 0 {
return Err(Error::Other(
"Image requires relocation but has empty relocation directory".to_string(),
));
}
return Ok(());
}
let reloc_rva = reloc_dir.virtual_address as usize;
let reloc_size = reloc_dir.size as usize;
if reloc_rva + reloc_size > image_data.len() {
return Err(Error::Other(
"Relocation directory extends beyond image bounds".to_string(),
));
}
let mut offset = reloc_rva;
let end = reloc_rva + reloc_size;
while offset + 8 <= end {
let page_rva = u32::from_le_bytes([
image_data[offset],
image_data[offset + 1],
image_data[offset + 2],
image_data[offset + 3],
]) as usize;
let block_size = u32::from_le_bytes([
image_data[offset + 4],
image_data[offset + 5],
image_data[offset + 6],
image_data[offset + 7],
]) as usize;
if block_size < 8 || offset + block_size > end {
break;
}
let entry_count = (block_size - 8) / 2;
for i in 0..entry_count {
let entry_offset = offset + 8 + i * 2;
if entry_offset + 2 > image_data.len() {
break;
}
let entry =
u16::from_le_bytes([image_data[entry_offset], image_data[entry_offset + 1]]);
let reloc_type = entry >> 12;
let reloc_offset = (entry & 0x0FFF) as usize;
let target_offset = page_rva + reloc_offset;
match reloc_type {
reloc_type::IMAGE_REL_BASED_HIGHLOW => {
if target_offset + 4 <= image_data.len() {
let value = u32::from_le_bytes([
image_data[target_offset],
image_data[target_offset + 1],
image_data[target_offset + 2],
image_data[target_offset + 3],
]);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let new_value = (i64::from(value) + delta) as u32;
image_data[target_offset..target_offset + 4]
.copy_from_slice(&new_value.to_le_bytes());
}
}
reloc_type::IMAGE_REL_BASED_DIR64 if is_64_bit => {
if target_offset + 8 <= image_data.len() {
let value = u64::from_le_bytes([
image_data[target_offset],
image_data[target_offset + 1],
image_data[target_offset + 2],
image_data[target_offset + 3],
image_data[target_offset + 4],
image_data[target_offset + 5],
image_data[target_offset + 6],
image_data[target_offset + 7],
]);
let new_value = (value.cast_signed() + delta).cast_unsigned();
image_data[target_offset..target_offset + 8]
.copy_from_slice(&new_value.to_le_bytes());
}
}
_ => {
}
}
}
offset += block_size;
}
Ok(())
}
}
impl Default for PeLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pe_loader_config() {
let config = PeLoaderConfig::new()
.with_base_address(0x10000000)
.without_permissions();
assert_eq!(config.base_address, Some(0x10000000));
assert!(!config.apply_permissions);
}
#[test]
fn test_loaded_image_methods() {
let image = LoadedImage {
base_address: 0x400000,
size_of_image: 0x10000,
entry_point: Some(0x1000),
is_64_bit: false,
sections: vec![LoadedSection {
name: ".text".to_string(),
virtual_address: 0x1000,
virtual_size: 0x5000,
raw_size: 0x4800,
protection: MemoryProtection::READ | MemoryProtection::EXECUTE,
is_code: true,
is_initialized_data: false,
is_uninitialized_data: false,
}],
name: "test.exe".to_string(),
file_size: 0x8000,
clr_header_rva: Some(0x2000),
clr_header_size: Some(0x48),
};
assert_eq!(image.entry_point_va(), Some(0x401000));
assert_eq!(image.rva_to_va(0x2000), 0x402000);
assert!(image.contains_rva(0x5000));
assert!(!image.contains_rva(0x20000));
assert!(image.is_dotnet());
let section = image.section_for_rva(0x2000);
assert!(section.is_some());
assert_eq!(section.unwrap().name, ".text");
}
#[test]
fn test_loaded_section_protection() {
let section = LoadedSection {
name: ".text".to_string(),
virtual_address: 0x1000,
virtual_size: 0x1000,
raw_size: 0x800,
protection: MemoryProtection::READ | MemoryProtection::EXECUTE,
is_code: true,
is_initialized_data: false,
is_uninitialized_data: false,
};
assert!(section.protection.contains(MemoryProtection::READ));
assert!(section.protection.contains(MemoryProtection::EXECUTE));
assert!(!section.protection.contains(MemoryProtection::WRITE));
assert!(section.is_code);
}
#[test]
fn test_pe_loader_config_without_relocations() {
let config = PeLoaderConfig::new().without_relocations();
assert!(!config.apply_relocations);
assert!(config.apply_permissions);
assert!(config.base_address.is_none());
}
#[test]
fn test_pe_loader_config_defaults() {
let config = PeLoaderConfig::default();
assert!(config.apply_relocations);
assert!(config.apply_permissions);
assert!(config.base_address.is_none());
}
#[test]
fn test_relocation_type_constants() {
assert_eq!(reloc_type::IMAGE_REL_BASED_ABSOLUTE, 0);
assert_eq!(reloc_type::IMAGE_REL_BASED_HIGHLOW, 3);
assert_eq!(reloc_type::IMAGE_REL_BASED_DIR64, 10);
}
fn create_test_pe_bytes(image_base: u64, with_relocations: bool) -> Vec<u8> {
let mut pe = Vec::new();
pe.extend_from_slice(b"MZ"); pe.resize(0x3C, 0); pe.extend_from_slice(&0x80u32.to_le_bytes());
pe.resize(0x80, 0);
pe.extend_from_slice(b"PE\0\0");
pe.extend_from_slice(&0x014Cu16.to_le_bytes()); pe.extend_from_slice(&0x0002u16.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0x00E0u16.to_le_bytes()); pe.extend_from_slice(&0x0103u16.to_le_bytes());
pe.extend_from_slice(&0x010Bu16.to_le_bytes()); pe.extend_from_slice(&[0u8; 2]); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x2000u32.to_le_bytes());
pe.extend_from_slice(&(image_base as u32).to_le_bytes()); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x200u32.to_le_bytes()); pe.extend_from_slice(&0x0006u16.to_le_bytes()); pe.extend_from_slice(&0x0000u16.to_le_bytes()); pe.extend_from_slice(&0u16.to_le_bytes()); pe.extend_from_slice(&0u16.to_le_bytes()); pe.extend_from_slice(&0x0006u16.to_le_bytes()); pe.extend_from_slice(&0x0000u16.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0x4000u32.to_le_bytes()); pe.extend_from_slice(&0x200u32.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&0x0003u16.to_le_bytes()); pe.extend_from_slice(&0x8160u16.to_le_bytes()); pe.extend_from_slice(&0x100000u32.to_le_bytes()); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x100000u32.to_le_bytes()); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0u32.to_le_bytes()); pe.extend_from_slice(&16u32.to_le_bytes());
pe.extend_from_slice(&[0u8; 8]);
pe.extend_from_slice(&[0u8; 8]);
pe.extend_from_slice(&[0u8; 8]);
pe.extend_from_slice(&[0u8; 8]);
pe.extend_from_slice(&[0u8; 8]);
if with_relocations {
pe.extend_from_slice(&0x3000u32.to_le_bytes()); pe.extend_from_slice(&0x10u32.to_le_bytes()); } else {
pe.extend_from_slice(&[0u8; 8]);
}
pe.extend_from_slice(&[0u8; 80]);
pe.extend_from_slice(b".text\0\0\0"); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x200u32.to_le_bytes()); pe.extend_from_slice(&0x200u32.to_le_bytes()); pe.extend_from_slice(&[0u8; 12]); pe.extend_from_slice(&0x60000020u32.to_le_bytes());
pe.extend_from_slice(b".reloc\0\0"); pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x3000u32.to_le_bytes()); pe.extend_from_slice(&0x200u32.to_le_bytes()); pe.extend_from_slice(&0x400u32.to_le_bytes()); pe.extend_from_slice(&[0u8; 12]); pe.extend_from_slice(&0x42000040u32.to_le_bytes());
pe.resize(0x200, 0);
pe.resize(0x400, 0); let abs_addr = (image_base as u32) + 0x2000;
pe[0x210..0x214].copy_from_slice(&abs_addr.to_le_bytes());
if with_relocations {
pe.extend_from_slice(&0x1000u32.to_le_bytes()); pe.extend_from_slice(&0x10u32.to_le_bytes()); let entry1: u16 = (3 << 12) | 0x10; pe.extend_from_slice(&entry1.to_le_bytes());
pe.extend_from_slice(&0u16.to_le_bytes());
pe.extend_from_slice(&0u16.to_le_bytes());
pe.extend_from_slice(&0u16.to_le_bytes());
}
pe.resize(0x600, 0);
pe
}
#[test]
fn test_load_pe_at_preferred_base() {
let pe_bytes = create_test_pe_bytes(0x400000, true);
let address_space = AddressSpace::new();
let loader = PeLoader::new();
let image = loader.load(&pe_bytes, &address_space, "test.exe").unwrap();
assert_eq!(image.base_address, 0x400000);
assert!(!image.is_64_bit);
let data = address_space.read(0x400000 + 0x1010, 4).unwrap();
let value = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
assert_eq!(value, 0x400000 + 0x2000);
}
#[test]
fn test_load_pe_with_relocation() {
let pe_bytes = create_test_pe_bytes(0x400000, true);
let address_space = AddressSpace::new();
let config = PeLoaderConfig::new().with_base_address(0x10000000);
let loader = PeLoader::with_config(config);
let image = loader.load(&pe_bytes, &address_space, "test.exe").unwrap();
assert_eq!(image.base_address, 0x10000000);
let data = address_space.read(0x10000000 + 0x1010, 4).unwrap();
let value = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
assert_eq!(value, 0x10000000 + 0x2000);
}
#[test]
fn test_load_pe_without_relocation_disabled() {
let pe_bytes = create_test_pe_bytes(0x400000, true);
let address_space = AddressSpace::new();
let config = PeLoaderConfig::new()
.with_base_address(0x10000000)
.without_relocations();
let loader = PeLoader::with_config(config);
let image = loader.load(&pe_bytes, &address_space, "test.exe").unwrap();
assert_eq!(image.base_address, 0x10000000);
let data = address_space.read(0x10000000 + 0x1010, 4).unwrap();
let value = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
assert_eq!(value, 0x400000 + 0x2000);
}
#[test]
fn test_load_pe_no_reloc_section_at_preferred_base() {
let pe_bytes = create_test_pe_bytes(0x400000, false);
let address_space = AddressSpace::new();
let loader = PeLoader::new();
let result = loader.load(&pe_bytes, &address_space, "test.exe");
assert!(result.is_ok());
}
}