use std::io::{Cursor, Read, Write};
use std::{iter, mem};
use goblin::pe::optional_header::OptionalHeader;
use goblin::pe::section_table::SectionTable;
use goblin::pe::PE;
use tracing::{instrument, Span};
use crate::mem::pe::base_relocations;
use crate::{debug, log_then_return, Result};
const IMAGE_REL_BASED_DIR64: u8 = 10;
const IMAGE_REL_BASED_ABSOLUTE: u8 = 0;
const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664;
const CHARACTERISTICS_RELOCS_STRIPPED: u16 = 0x0001;
const CHARACTERISTICS_EXECUTABLE_IMAGE: u16 = 0x0002;
pub(crate) struct PEInfo {
pub(crate) payload: Vec<u8>,
optional_header: OptionalHeader,
reloc_section: Option<SectionTable>,
}
impl PEInfo {
pub fn new(pe_bytes: impl Into<Vec<u8>>) -> Result<Self> {
let mut pe_bytes: Vec<u8> = pe_bytes.into();
let mut pe = PE::parse(&pe_bytes)?;
if pe.header.coff_header.machine != IMAGE_FILE_MACHINE_AMD64 {
log_then_return!("unsupported PE file, contents is not a x64 File")
}
if !pe.is_64 {
log_then_return!("unsupported PE file, not a PE32+ formatted file")
}
if (pe.header.coff_header.characteristics & CHARACTERISTICS_EXECUTABLE_IMAGE)
!= CHARACTERISTICS_EXECUTABLE_IMAGE
{
log_then_return!("unsupported PE file, not an executable image")
}
let optional_header = pe
.header
.optional_header
.expect("unsupported PE file, missing optional header entry");
if optional_header.windows_fields.dll_characteristics & 0x0040 == 0 {
log_then_return!("unsupported PE file, not built with /DYNAMICBASE")
}
if optional_header.windows_fields.section_alignment
!= optional_header.windows_fields.file_alignment
{
log_then_return!("unsupported PE file, section alignment does not match file alignment make sure to link the .exe with /FILEALIGN and /ALIGN options set to the same value")
}
if (pe.header.coff_header.characteristics & CHARACTERISTICS_RELOCS_STRIPPED)
== CHARACTERISTICS_RELOCS_STRIPPED
{
log_then_return!("unsupported PE file, relocations have been removed")
}
let mut data_section_additional_bytes = 0;
let mut end_of_data_index = 0;
let mut data_section_raw_pointer = 0;
for (i, section) in pe.sections.iter().enumerate() {
let name = section.name().unwrap_or("Unknown");
let virtual_size = section.virtual_size;
let raw_size = section.size_of_raw_data;
debug!(
"Section: {}, Virtual Size: {}, On-Disk Size: {}",
name, virtual_size, raw_size
);
if virtual_size > raw_size {
if name == ".data" {
assert_eq!(
data_section_additional_bytes, 0,
"Hyperlight currently only supports one .data section"
);
data_section_raw_pointer = section.pointer_to_raw_data;
data_section_additional_bytes = virtual_size - raw_size;
debug!(
"Resizing the data section - Additional bytes required: {}",
data_section_additional_bytes
);
debug!(
"Resizing the data section - Existing PE File Size: {} New PE File Size: {}",
pe_bytes.len(),
pe_bytes.len() + data_section_additional_bytes as usize,
);
debug!(
"Resizing the data section - Data Section Raw Pointer: {}",
data_section_raw_pointer
);
end_of_data_index =
(section.pointer_to_raw_data + section.size_of_raw_data) as usize;
debug!("End of data index: {}", end_of_data_index);
let next_section = pe.sections.get(i + 1);
if let Some(next_section) = next_section {
debug!(
"Start of section after data index: {}",
next_section.pointer_to_raw_data
);
} else {
debug!("No more sections after the .data section");
}
} else {
log_then_return!(
"Section {} has a virtual size {} greater than the on-disk size {}",
name,
virtual_size,
raw_size
);
}
}
}
if data_section_additional_bytes > 0 {
for section in pe.sections.iter_mut() {
if section.pointer_to_raw_data > data_section_raw_pointer {
section.pointer_to_raw_data += data_section_additional_bytes;
}
}
}
let reloc_section = pe
.sections
.iter()
.find(|section| section.name().unwrap_or_default() == ".reloc")
.cloned();
pe_bytes.splice(
end_of_data_index..end_of_data_index,
iter::repeat(0).take(data_section_additional_bytes as usize),
);
Ok(Self {
payload: pe_bytes,
optional_header,
reloc_section,
})
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(super) fn entry_point_offset(&self) -> u64 {
self.optional_header.standard_fields.address_of_entry_point
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(super) fn preferred_load_address(&self) -> u64 {
self.optional_header.windows_fields.image_base
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn stack_reserve(&self) -> u64 {
self.optional_header.windows_fields.size_of_stack_reserve
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(super) fn stack_commit(&self) -> u64 {
self.optional_header.windows_fields.size_of_stack_commit
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn heap_reserve(&self) -> u64 {
self.optional_header.windows_fields.size_of_heap_reserve
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(super) fn heap_commit(&self) -> u64 {
self.optional_header.windows_fields.size_of_heap_commit
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn apply_relocation_patches(
&mut self,
patches: Vec<RelocationPatch>,
) -> Result<usize> {
let payload_len = self.payload.len();
let mut cur = Cursor::new(&mut self.payload);
let mut applied: usize = 0;
for patch in patches {
if patch.offset + mem::size_of::<u64>() > payload_len {
log_then_return!("invalid offset is larger than the payload");
}
cur.set_position(patch.offset as u64);
cur.write_all(&patch.relocated_virtual_address.to_le_bytes())
.expect("failed to write patch to pe file contents");
applied += 1;
}
Ok(applied)
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn get_exe_relocation_patches(
&self,
address_to_load_at: usize,
) -> Result<Vec<RelocationPatch>> {
let addr_diff = (address_to_load_at as u64).wrapping_sub(self.preferred_load_address());
if addr_diff == 0 {
return Ok(Vec::new());
}
let relocations =
base_relocations::get_base_relocations(&self.payload, &self.reloc_section)
.expect("error parsing base relocations");
let mut patches = Vec::with_capacity(relocations.len());
for reloc in relocations {
match reloc.typ {
IMAGE_REL_BASED_DIR64 => {
let offset = reloc.page_base_rva as u64 + (reloc.page_offset as u64);
let mut cur = Cursor::new(&self.payload);
cur.set_position(offset);
let mut bytes = [0; 8];
cur.read_exact(&mut bytes)?;
let original_address = u64::from_le_bytes(bytes);
let relocated_virtual_address = original_address.wrapping_add(addr_diff);
patches.push(RelocationPatch {
offset: offset as usize,
relocated_virtual_address,
});
}
IMAGE_REL_BASED_ABSOLUTE => (),
_ => {
log_then_return!("unsupported relocation type {}", reloc.typ);
}
}
}
Ok(patches)
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct RelocationPatch {
offset: usize,
relocated_virtual_address: u64,
}
#[cfg(test)]
mod tests {
use hyperlight_testing::{callback_guest_exe_as_string, simple_guest_exe_as_string};
use crate::mem::exe::ExeInfo;
use crate::{new_error, Result};
#[allow(dead_code)]
struct PEFileTest {
path: String,
stack_size: u64,
heap_size: u64,
load_address: u64,
num_relocations: Vec<usize>,
}
fn pe_files() -> Result<Vec<PEFileTest>> {
let simple_guest_pe_file_test = if cfg!(debug_assertions) {
PEFileTest {
path: simple_guest_exe_as_string()
.map_err(|e| new_error!("Simple Guest Path Error {}", e))?,
stack_size: 65536,
heap_size: 131072,
load_address: 5368709120,
num_relocations: (900..1200).collect(),
}
} else {
PEFileTest {
path: simple_guest_exe_as_string()
.map_err(|e| new_error!("Simple Guest Path Error {}", e))?,
stack_size: 65536,
heap_size: 131072,
load_address: 5368709120,
num_relocations: (600..900).collect(),
}
};
let callback_guest_pe_file_test = if cfg!(debug_assertions) {
PEFileTest {
path: callback_guest_exe_as_string()
.map_err(|e| new_error!("Callback Guest Path Error {}", e))?,
stack_size: 65536,
heap_size: 131072,
load_address: 5368709120,
num_relocations: (600..900).collect(),
}
} else {
PEFileTest {
path: callback_guest_exe_as_string()
.map_err(|e| new_error!("Callback Guest Path Error {}", e))?,
stack_size: 65536,
heap_size: 131072,
load_address: 5368709120,
num_relocations: (400..800).collect(),
}
};
Ok(vec![simple_guest_pe_file_test, callback_guest_pe_file_test])
}
#[test]
fn load_pe_info() -> Result<()> {
for test in pe_files()? {
let pe_path = test.path;
let pe_info = match ExeInfo::from_file(&pe_path)? {
ExeInfo::PE(pe_info) => pe_info,
_ => panic!("{pe_path} did not load as a PE"),
};
assert_eq!(
test.stack_size,
pe_info.stack_reserve(),
"unexpected stack reserve for {pe_path}",
);
assert_eq!(
test.stack_size,
pe_info.stack_commit(),
"unexpected stack commit for {pe_path}"
);
assert_eq!(
pe_info.heap_reserve(),
test.heap_size,
"unexpected heap reserve for {pe_path}",
);
assert_eq!(
pe_info.heap_commit(),
test.heap_size,
"unexpected heap commit for {pe_path}",
);
assert_eq!(
pe_info.preferred_load_address(),
test.load_address,
"unexpected load address for {pe_path}"
);
let patches = pe_info
.get_exe_relocation_patches(0)
.unwrap_or_else(|_| panic!("wrong # of relocation patches returned for {pe_path}"));
let num_patches = patches.len();
assert!(
test.num_relocations.contains(&num_patches),
"unexpected number ({num_patches}) of relocations for {pe_path}"
);
}
Ok(())
}
}