pub mod parser;
pub mod pe;
mod memory;
mod physical;
use std::path::Path;
use crate::{
utils::align_to,
Error::{self, Goblin, LayoutFailed, Other},
Result,
};
use goblin::pe::PE;
use memory::Memory;
use pe::{constants::COR20_HEADER_SIZE, DataDirectory, DataDirectoryType, Pe};
use physical::Physical;
pub trait Backend: Send + Sync {
fn data_slice(&self, offset: usize, len: usize) -> Result<&[u8]>;
fn data(&self) -> &[u8];
fn len(&self) -> usize;
fn into_data(self: Box<Self>) -> Vec<u8>;
}
pub struct File {
data: Box<dyn Backend>,
pe: Pe,
}
impl File {
pub fn from_path(path: impl AsRef<Path>) -> Result<File> {
let input = Physical::new(path.as_ref())?;
Self::load(input)
}
pub fn from_mem(data: Vec<u8>) -> Result<File> {
let input = Memory::new(data);
Self::load(input)
}
pub fn from_std_file(file: std::fs::File) -> Result<File> {
let input = Physical::from_std_file(file)?;
Self::load(input)
}
pub fn from_reader<R: std::io::Read>(mut reader: R) -> Result<File> {
let mut data = Vec::new();
reader
.read_to_end(&mut data)
.map_err(|e| Other(format!("Failed to read from reader: {e}")))?;
Self::from_mem(data)
}
fn load<T: Backend + 'static>(data: T) -> Result<File> {
if data.len() == 0 {
return Err(Error::NotSupported);
}
let goblin_pe = match PE::parse(data.data()) {
Ok(pe) => pe,
Err(error) => return Err(Goblin(error)),
};
let owned_pe = Pe::from_goblin_pe(&goblin_pe)?;
Ok(File {
data: Box::new(data),
pe: owned_pe,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn imagebase(&self) -> u64 {
self.pe.image_base
}
#[must_use]
pub fn header(&self) -> &pe::CoffHeader {
&self.pe.coff_header
}
#[must_use]
pub fn header_dos(&self) -> &pe::DosHeader {
&self.pe.dos_header
}
#[must_use]
pub fn header_optional(&self) -> &Option<pe::OptionalHeader> {
&self.pe.optional_header
}
#[must_use]
pub fn clr(&self) -> Option<(usize, usize)> {
self.pe
.get_clr_runtime_header()
.map(|clr_dir| (clr_dir.virtual_address as usize, clr_dir.size as usize))
}
#[must_use]
pub fn is_clr(&self) -> bool {
self.clr().is_some()
}
#[must_use]
pub fn sections(&self) -> &[pe::SectionTable] {
&self.pe.sections
}
#[must_use]
pub fn directories(&self) -> Vec<(DataDirectoryType, DataDirectory)> {
self.pe
.data_directories
.iter()
.map(|(&dir_type, &dir)| (dir_type, dir))
.collect()
}
#[must_use]
pub fn get_data_directory(&self, dir_type: DataDirectoryType) -> Option<(u32, u32)> {
self.pe
.get_data_directory(dir_type)
.filter(|directory| directory.virtual_address != 0 && directory.size != 0)
.map(|directory| (directory.virtual_address, directory.size))
}
#[must_use]
pub fn imports(&self) -> Option<&Vec<pe::Import>> {
if self.pe.imports.is_empty() {
None
} else {
Some(&self.pe.imports)
}
}
#[must_use]
pub fn exports(&self) -> Option<&Vec<pe::Export>> {
if self.pe.exports.is_empty() {
None
} else {
Some(&self.pe.exports)
}
}
#[must_use]
pub fn data(&self) -> &[u8] {
self.data.data()
}
pub fn data_slice(&self, offset: usize, len: usize) -> Result<&[u8]> {
self.data.data_slice(offset, len)
}
pub fn va_to_offset(&self, va: usize) -> Result<usize> {
let ib = self.imagebase();
if ib > va as u64 {
return Err(out_of_bounds_error!());
}
let rva_u64 = va as u64 - ib;
let rva = usize::try_from(rva_u64)
.map_err(|_| malformed_error!("RVA too large to fit in usize: {}", rva_u64))?;
self.rva_to_offset(rva)
}
pub fn rva_to_offset(&self, rva: usize) -> Result<usize> {
for section in &self.pe.sections {
let Some(section_max) = section.virtual_address.checked_add(section.virtual_size)
else {
return Err(malformed_error!(
"Section malformed, causing integer overflow - {} + {}",
section.virtual_address,
section.virtual_size
));
};
let rva_u32 = u32::try_from(rva)
.map_err(|_| malformed_error!("RVA too large to fit in u32: {}", rva))?;
if section.virtual_address <= rva_u32 && section_max > rva_u32 {
return Ok(
(rva - section.virtual_address as usize) + section.pointer_to_raw_data as usize
);
}
}
Err(malformed_error!(
"RVA could not be converted to offset - {}",
rva
))
}
pub fn offset_to_rva(&self, offset: usize) -> Result<usize> {
for section in &self.pe.sections {
let Some(section_max) = section
.pointer_to_raw_data
.checked_add(section.size_of_raw_data)
else {
return Err(malformed_error!(
"Section malformed, causing integer overflow - {} + {}",
section.pointer_to_raw_data,
section.size_of_raw_data
));
};
let offset_u32 = u32::try_from(offset)
.map_err(|_| malformed_error!("Offset too large to fit in u32: {}", offset))?;
if section.pointer_to_raw_data <= offset_u32 && section_max > offset_u32 {
return Ok((offset - section.pointer_to_raw_data as usize)
+ section.virtual_address as usize);
}
}
Err(malformed_error!(
"Offset could not be converted to RVA - {}",
offset
))
}
#[must_use]
pub fn section_contains_metadata(&self, section_name: &str) -> bool {
let clr_rva = match self.clr() {
Some((rva, size)) if rva > 0 && size >= COR20_HEADER_SIZE as usize => {
let Ok(rva_u32) = u32::try_from(rva) else {
return false; };
rva_u32
}
_ => return false, };
let Ok(clr_offset) = self.rva_to_offset(clr_rva as usize) else {
return false;
};
let Ok(clr_data) = self.data_slice(clr_offset, COR20_HEADER_SIZE as usize) else {
return false;
};
if clr_data.len() < 12 {
return false;
}
let meta_data_rva =
u32::from_le_bytes([clr_data[8], clr_data[9], clr_data[10], clr_data[11]]);
if meta_data_rva == 0 {
return false; }
for section in self.sections() {
let current_section_name = section.name.as_str();
if current_section_name == section_name {
let section_start = section.virtual_address;
let section_end = section.virtual_address + section.virtual_size;
return meta_data_rva >= section_start && meta_data_rva < section_end;
}
}
false }
pub fn file_alignment(&self) -> Result<u32> {
let optional_header = self.header_optional().as_ref().ok_or_else(|| {
LayoutFailed("Missing optional header for file alignment".to_string())
})?;
Ok(optional_header.windows_fields.file_alignment)
}
pub fn section_alignment(&self) -> Result<u32> {
let optional_header = self.header_optional().as_ref().ok_or_else(|| {
LayoutFailed("Missing optional header for section alignment".to_string())
})?;
Ok(optional_header.windows_fields.section_alignment)
}
pub fn is_pe32_plus_format(&self) -> Result<bool> {
let optional_header = self.header_optional().as_ref().ok_or_else(|| {
LayoutFailed("Missing optional header for PE format detection".to_string())
})?;
Ok(optional_header.standard_fields.magic != 0x10b)
}
fn find_text_section(&self) -> Result<&pe::SectionTable> {
self.sections()
.iter()
.find(|s| s.name.as_str() == ".text" || s.name.starts_with(".text"))
.ok_or_else(|| LayoutFailed("Could not find .text section".to_string()))
}
pub fn text_section_rva(&self) -> Result<u32> {
Ok(self.find_text_section()?.virtual_address)
}
pub fn text_section_file_offset(&self) -> Result<u64> {
Ok(u64::from(self.find_text_section()?.pointer_to_raw_data))
}
pub fn text_section_raw_size(&self) -> Result<u32> {
Ok(self.find_text_section()?.size_of_raw_data)
}
#[must_use]
pub fn file_size(&self) -> u64 {
u64::try_from(self.data().len()).unwrap_or(u64::MAX)
}
pub fn pe_signature_offset(&self) -> Result<u64> {
let data = self.data();
if data.len() < 64 {
return Err(LayoutFailed(
"File too small to contain DOS header".to_string(),
));
}
let pe_offset = u32::from_le_bytes([data[60], data[61], data[62], data[63]]);
Ok(u64::from(pe_offset))
}
pub fn pe_headers_size(&self) -> Result<u64> {
let pe_sig_offset = self.pe_signature_offset()?;
let data = self.data();
let coff_header_offset = pe_sig_offset + 4;
#[allow(clippy::cast_possible_truncation)]
if data.len() < (coff_header_offset + 20) as usize {
return Err(LayoutFailed(
"File too small to contain COFF header".to_string(),
));
}
let opt_header_size_offset = coff_header_offset + 16;
#[allow(clippy::cast_possible_truncation)]
let opt_header_size = u16::from_le_bytes([
data[opt_header_size_offset as usize],
data[opt_header_size_offset as usize + 1],
]);
Ok(4 + 20 + u64::from(opt_header_size)) }
pub fn align_to_file_alignment(&self, offset: u64) -> Result<u64> {
let file_alignment = u64::from(self.file_alignment()?);
Ok(align_to(offset, file_alignment))
}
#[must_use]
pub fn pe(&self) -> &Pe {
&self.pe
}
pub fn pe_mut(&mut self) -> &mut Pe {
&mut self.pe
}
#[must_use]
pub fn into_data(self) -> Vec<u8> {
self.data.into_data()
}
}
#[cfg(test)]
mod tests {
use std::{env, fs, path::PathBuf};
use super::*;
use crate::test::factories::general::file::verify_file;
#[test]
fn load_file() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll");
let file = File::from_path(&path).unwrap();
verify_file(&file);
}
#[test]
fn load_buffer() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll");
let data = fs::read(&path).unwrap();
let file = File::from_mem(data).unwrap();
verify_file(&file);
}
#[test]
fn load_invalid() {
let data = include_bytes!("../../tests/samples/WB_ROOT.bin");
if File::from_mem(data.to_vec()).is_ok() {
panic!("This should not load!")
}
}
#[test]
fn test_get_data_directory() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll");
let file = File::from_path(&path).unwrap();
let clr_dir = file.get_data_directory(DataDirectoryType::ClrRuntimeHeader);
assert!(clr_dir.is_some(), "CLR runtime header should exist");
let (clr_rva, clr_size) = clr_dir.unwrap();
assert!(clr_rva > 0, "CLR RVA should be non-zero");
assert!(clr_size > 0, "CLR size should be non-zero");
let (expected_rva, expected_size) = file.clr().expect("Should have CLR header");
assert_eq!(
clr_rva as usize, expected_rva,
"CLR RVA should match clr() method"
);
assert_eq!(
clr_size as usize, expected_size,
"CLR size should match clr() method"
);
let _base_reloc_dir = file.get_data_directory(DataDirectoryType::BaseRelocationTable);
let tls_dir = file.get_data_directory(DataDirectoryType::TlsTable);
if let Some((tls_rva, tls_size)) = tls_dir {
assert!(
tls_rva > 0 && tls_size > 0,
"If TLS directory exists, it should have valid values"
);
}
}
#[test]
fn test_pe_signature_offset() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/crafted_2.exe");
let file = File::from_path(&path).expect("Failed to load test assembly");
let pe_offset = file
.pe_signature_offset()
.expect("Should get PE signature offset");
assert!(pe_offset > 0, "PE signature offset should be positive");
assert!(pe_offset < 1024, "PE signature offset should be reasonable");
}
#[test]
fn test_pe_headers_size() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/crafted_2.exe");
let file = File::from_path(&path).expect("Failed to load test assembly");
let headers_size = file
.pe_headers_size()
.expect("Should calculate headers size");
assert!(headers_size >= 24, "Headers should be at least 24 bytes");
assert!(headers_size <= 1024, "Headers size should be reasonable");
}
#[test]
fn test_align_to_file_alignment() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/crafted_2.exe");
let file = File::from_path(&path).expect("Failed to load test assembly");
let alignment = file.file_alignment().expect("Should get file alignment");
assert_eq!(file.align_to_file_alignment(0).unwrap(), 0);
assert_eq!(file.align_to_file_alignment(1).unwrap(), alignment as u64);
assert_eq!(
file.align_to_file_alignment(alignment as u64).unwrap(),
alignment as u64
);
assert_eq!(
file.align_to_file_alignment(alignment as u64 + 1).unwrap(),
(alignment * 2) as u64
);
}
}