diff --git a/Cargo.toml b/Cargo.toml index 6b0b94d..b47c5c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,21 @@ rust-version = "1.82" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -# Note that not enabling this feature does not make the package no_std or not use allocations at all; -# several of the dependencies (i.e. goblin) will always need to allocate memory. -alloc = [] +# Callers must be able to provide the PE image as a span of bytes. Opening the file is not supported in no_std. +std = ["dep:memmap2"] +default = ["std"] [dependencies] -goblin = {version="0.10", default-features = false, features=["pe32", "pe64"]} thiserror = { version="2", default-features = false } -memmap2 = "0.9" +memmap2 = { version="0.9", optional = true } either = { version="1.8", default-features = false } zerocopy = "0.8" zerocopy-derive = "0.8" +[[bin]] +required-features = ["std"] +name = "main" + [profile.release] opt-level = "s" debug = false diff --git a/src/lib.rs b/src/lib.rs index 86af36b..8f0fda2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(feature = "std"), no_std)] + mod rsrc { use core::fmt::Write; use thiserror::Error; @@ -12,19 +14,97 @@ mod rsrc { #[error("PE file does not contain a resource table")] NoResourceTable(), + #[cfg(feature = "std")] #[error("Invalid resource string: {0}")] BadResourceString(String), #[error("Resource with the provided name / ID not found")] ResourceNameNotFound(), - #[error("An error was returned when parsing the PE: {0}")] - GoblinError(goblin::error::Error), - #[error("The resource data is too small to contain the expected structure (size {0}, offset {1}, required at least {2})")] BufferSizeError(usize, usize, usize), } + // Win32 PE/COFF header constants and magic values from winnt.h + pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; // "MZ" + pub const IMAGE_NT_SIGNATURE: u32 = 0x0000_4550; // "PE\0\0" + pub const IMAGE_NT_OPTIONAL_HDR32_MAGIC: u16 = 0x010B; + pub const IMAGE_NT_OPTIONAL_HDR64_MAGIC: u16 = 0x020B; + + // Offsets of the DataDirectory array within IMAGE_OPTIONAL_HEADER{32,64}. + // The standard fields up to and including NumberOfRvaAndSizes take up + // 96 bytes for PE32 and 112 bytes for PE32+ (the only difference is that + // several pointer-sized fields are u32 vs u64). + pub const OPTIONAL_HEADER32_DATA_DIRECTORY_OFFSET: usize = 96; + pub const OPTIONAL_HEADER64_DATA_DIRECTORY_OFFSET: usize = 112; + + pub const IMAGE_DIRECTORY_ENTRY_RESOURCE: usize = 2; + pub const IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR: usize = 14; // CLR runtime header + + // struct _IMAGE_DOS_HEADER, winnt.h + #[repr(C)] + #[derive(FromBytes, Immutable, KnownLayout)] + pub struct _ImageDosHeader { + pub e_magic: u16, + pub e_cblp: u16, + pub e_cp: u16, + pub e_crlc: u16, + pub e_cparhdr: u16, + pub e_minalloc: u16, + pub e_maxalloc: u16, + pub e_ss: u16, + pub e_sp: u16, + pub e_csum: u16, + pub e_ip: u16, + pub e_cs: u16, + pub e_lfarlc: u16, + pub e_ovno: u16, + pub e_res: [u16; 4], + pub e_oemid: u16, + pub e_oeminfo: u16, + pub e_res2: [u16; 10], + pub e_lfanew: u32, // Win32 LONG; offset to the IMAGE_NT_HEADERS signature + } + + // struct _IMAGE_FILE_HEADER, winnt.h + #[repr(C)] + #[derive(FromBytes, Immutable, KnownLayout)] + pub struct _ImageFileHeader { + pub machine: u16, + pub number_of_sections: u16, + pub time_date_stamp: u32, + pub pointer_to_symbol_table: u32, + pub number_of_symbols: u32, + pub size_of_optional_header: u16, + pub characteristics: u16, + } + + // struct _IMAGE_DATA_DIRECTORY, winnt.h + #[repr(C)] + #[derive(FromBytes, Immutable, KnownLayout)] + pub struct _ImageDataDirectory { + pub virtual_address: u32, + pub size: u32, + } + + // struct _IMAGE_SECTION_HEADER, winnt.h. + // The first union (Misc) is represented here by its virtual_size variant, + // which is what every modern PE actually stores. + #[repr(C)] + #[derive(FromBytes, Immutable, KnownLayout)] + pub struct _ImageSectionHeader { + pub name: [u8; 8], + pub virtual_size: u32, + pub virtual_address: u32, + pub size_of_raw_data: u32, + pub pointer_to_raw_data: u32, + pub pointer_to_relocations: u32, + pub pointer_to_line_numbers: u32, + pub number_of_relocations: u16, + pub number_of_line_numbers: u16, + pub characteristics: u32, + } + // struct _IMAGE_RESOURCE_DIRECTORY, winnt.h #[repr(C)] pub struct _ImageResourceDirectory { @@ -226,12 +306,29 @@ pub mod parser { use zerocopy::FromBytes; #[derive(Debug)] - pub struct ImageResource<'a> { - image_file: memmap2::Mmap, + enum ImageSource<'data> { + Borrowed(&'data [u8]), + #[cfg(feature = "std")] + Mapped(memmap2::Mmap), + } + + impl<'data> ImageSource<'data> { + fn as_slice(&self) -> &[u8] { + match self { + ImageSource::Borrowed(buf) => buf, + + #[cfg(feature = "std")] + ImageSource::Mapped(mapped) => &mapped[..], + } + } + } + + #[derive(Debug)] + pub struct ImageResource<'data> { + image_file: ImageSource<'data>, rva_to_va_offset: usize, resource_table_offset: usize, resource_table_end: usize, - _phantom: core::marker::PhantomData<&'a u8>, } #[derive(Debug)] @@ -248,10 +345,13 @@ pub mod parser { pub data: ResourceData<'a>, } - impl<'a> ImageResource<'a> { + impl<'data> ImageResource<'data> { + fn image(&self) -> &[u8] { + self.image_file.as_slice() + } // Win32 FindResourceW // Wrapper around ImageResourceEntry::find that returns only the buffer slice for the found resource - pub fn find(&'a self, name: &T, id: &U) -> Result, PEError> + pub fn find<'a, T, U>(&'a self, name: &T, id: &U) -> Result, PEError> where ResourceIdType<'a>: PartialEq, ResourceIdType<'a>: PartialEq, @@ -267,13 +367,10 @@ pub mod parser { Err(PEError::ResourceNameNotFound()) } - pub fn to_chars<'x>( + pub fn to_chars<'a>( &'a self, resource_id: ResourceIdType<'a>, - ) -> impl Iterator + use<'a> - where - 'a: 'x, - { + ) -> impl Iterator + use<'a> { match resource_id { ResourceIdType::Name(name) => either::Left(name.clone().chars()), ResourceIdType::Id(id) => unsafe { @@ -312,7 +409,7 @@ pub mod parser { } // Since we are lazy-parsing, this can fail if the resource is malformed - pub fn try_into_iter(&'a self) -> Result, PEError> { + pub fn try_into_iter<'a>(&'a self) -> Result, PEError> { ImageResourceEnumerator::try_parse(self) } } @@ -346,14 +443,17 @@ pub mod parser { } pub struct ImageResourceEnumerator<'a> { - image_resource: &'a ImageResource<'a>, + image_file: &'a [u8], + rva_to_va_offset: usize, + resource_table_offset: usize, + resource_table_end: usize, current_index: usize, // Current index into cur_dir cur_dir: [CurrentDirectoryState<'a>; 3], // Arbitrary depth limit of 3 nested directories } impl<'a> ImageResourceEnumerator<'a> { pub fn try_parse( - image_resource: &'a ImageResource, + image_resource: &'a ImageResource<'_>, ) -> Result, PEError> { // Should be a compile-time assert, but Rust doesn't have those yet. debug_assert!(size_of::<_ImageResourceDirectory>() >= 16); @@ -368,7 +468,8 @@ pub mod parser { )); } - let buf: &[u8] = &image_resource.image_file + let image_file = image_resource.image(); + let buf: &[u8] = &image_file [image_resource.resource_table_offset..image_resource.resource_table_end]; let num_named_entries = match u16::ref_from_bytes(&buf[12..(12 + size_of::())]) { @@ -386,7 +487,10 @@ pub mod parser { }; Ok(ImageResourceEnumerator { - image_resource, + image_file, + rva_to_va_offset: image_resource.rva_to_va_offset, + resource_table_offset: image_resource.resource_table_offset, + resource_table_end: image_resource.resource_table_end, current_index: 0, cur_dir: [ CurrentDirectoryState { @@ -434,8 +538,7 @@ pub mod parser { } } - let buf: &[u8] = &self.image_resource.image_file - [self.image_resource.resource_table_offset..self.image_resource.resource_table_end]; + let buf: &[u8] = &self.image_file[self.resource_table_offset..self.resource_table_end]; let directory_offset = self.cur_dir[self.current_index].directory_offset; let i = self.cur_dir[self.current_index].current_child_index; @@ -490,9 +593,8 @@ pub mod parser { let rva_to_data = entry_data.offset_to_data as usize; let data_size = entry_data.size as usize; - let data = &self.image_resource.image_file[rva_to_data - - self.image_resource.rva_to_va_offset - ..rva_to_data - self.image_resource.rva_to_va_offset + data_size]; + let data_offset = rva_to_data - self.rva_to_va_offset; + let data = &self.image_file[data_offset..data_offset + data_size]; let mut rsrc_name = ResourceIdType::Id(0); let mut rsrc_id = ResourceIdType::Id(0); @@ -557,80 +659,219 @@ pub mod parser { } } - pub fn find_resource_directory_from_pe(filename: &str) -> Result, PEError> { + #[cfg(feature = "std")] + pub fn find_resource_directory_from_pe( + filename: &str, + ) -> Result, PEError> { let file = std::fs::File::open(filename).map_err(|e| PEError::BadResourceString(e.to_string()))?; let mapped = unsafe { memmap2::Mmap::map(&file).map_err(|e| PEError::BadResourceString(e.to_string()))? }; - let buf: &[u8] = &mapped; - if buf.len() < 0x10 { - panic!("file too small: {}", filename); + find_resource_directory_from_source(ImageSource::Mapped(mapped)) + } + + pub fn find_resource_directory_from_bytes<'a>( + buf: &'a [u8], + ) -> Result, PEError> { + find_resource_directory_from_source(ImageSource::Borrowed(buf)) + } + + fn find_resource_directory_from_source<'data>( + image_file: ImageSource<'data>, + ) -> Result, PEError> { + let buf = image_file.as_slice(); + + // ---- IMAGE_DOS_HEADER ---- + let dos_header_size = size_of::<_ImageDosHeader>(); + if buf.len() < dos_header_size { + return Err(PEError::BufferSizeError(buf.len(), 0, dos_header_size)); } - let mut pe_opts = goblin::pe::options::ParseOptions::default() - .with_parse_mode(goblin::pe::options::ParseMode::Permissive); - pe_opts.parse_attribute_certificates = false; - pe_opts.parse_tls_data = false; + let dos = _ImageDosHeader::ref_from_bytes(&buf[..dos_header_size]) + .map_err(|_| PEError::BufferSizeError(buf.len(), 0, dos_header_size))?; + if dos.e_magic != IMAGE_DOS_SIGNATURE { + return Err(PEError::FormatNotSupported("not a PE/MZ executable")); + } - let _pe: Result = - match goblin::pe::PE::parse_with_opts(buf, &pe_opts) - .map_err(|e| PEError::BadResourceString(e.to_string())) - { - Ok(pe) => { - if let Some(opt) = pe.header.optional_header { - if opt.data_directories.get_clr_runtime_header().is_some() { - return Err(PEError::FormatNotSupported(".NET assembly")); - } - } - Ok(pe) - } - _ => Err(PEError::FormatNotSupported("unknown")), - }; + // ---- IMAGE_NT_HEADERS (signature + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER) ---- + let nt_offset = dos.e_lfanew as usize; + let file_header_offset = nt_offset + .checked_add(size_of::()) + .ok_or(PEError::FormatNotSupported("e_lfanew overflow"))?; + let optional_header_offset = file_header_offset + .checked_add(size_of::<_ImageFileHeader>()) + .ok_or(PEError::FormatNotSupported("e_lfanew overflow"))?; + + if buf.len() < optional_header_offset { + return Err(PEError::BufferSizeError( + buf.len(), + nt_offset, + size_of::() + size_of::<_ImageFileHeader>(), + )); + } - let pe = _pe?; + let signature = *u32::ref_from_bytes(&buf[nt_offset..nt_offset + size_of::()]) + .map_err(|_| PEError::BufferSizeError(buf.len(), nt_offset, size_of::()))?; + if signature != IMAGE_NT_SIGNATURE { + return Err(PEError::FormatNotSupported("not a PE executable")); + } + + let file_header = _ImageFileHeader::ref_from_bytes( + &buf[file_header_offset..file_header_offset + size_of::<_ImageFileHeader>()], + ) + .map_err(|_| { + PEError::BufferSizeError(buf.len(), file_header_offset, size_of::<_ImageFileHeader>()) + })?; + + let size_of_optional_header = file_header.size_of_optional_header as usize; + let optional_header_end = optional_header_offset + .checked_add(size_of_optional_header) + .ok_or(PEError::FormatNotSupported("optional header overflow"))?; + if buf.len() < optional_header_end { + return Err(PEError::BufferSizeError( + buf.len(), + optional_header_offset, + size_of_optional_header, + )); + } - let optional_header = pe.header.optional_header.unwrap(); + let optional_header = &buf[optional_header_offset..optional_header_end]; - let resource_table = optional_header - .data_directories - .get_resource_table() + // ---- IMAGE_OPTIONAL_HEADER magic + DataDirectory ---- + if optional_header.len() < size_of::() { + return Err(PEError::BufferSizeError( + optional_header.len(), + 0, + size_of::(), + )); + } + let magic = *u16::ref_from_bytes(&optional_header[..size_of::()]) + .map_err(|_| PEError::BufferSizeError(optional_header.len(), 0, size_of::()))?; + + let data_directory_offset = match magic { + IMAGE_NT_OPTIONAL_HDR32_MAGIC => OPTIONAL_HEADER32_DATA_DIRECTORY_OFFSET, + IMAGE_NT_OPTIONAL_HDR64_MAGIC => OPTIONAL_HEADER64_DATA_DIRECTORY_OFFSET, + _ => { + return Err(PEError::FormatNotSupported( + "unknown PE optional header magic", + )); + } + }; + + // NumberOfRvaAndSizes is the u32 immediately preceding the DataDirectory array. + let num_rva_offset = data_directory_offset - size_of::(); + if optional_header.len() < data_directory_offset { + return Err(PEError::BufferSizeError( + optional_header.len(), + num_rva_offset, + size_of::(), + )); + } + let number_of_rva_and_sizes = *u32::ref_from_bytes( + &optional_header[num_rva_offset..num_rva_offset + size_of::()], + ) + .map_err(|_| { + PEError::BufferSizeError(optional_header.len(), num_rva_offset, size_of::()) + })? as usize; + + let data_dirs = &optional_header[data_directory_offset..]; + let total_dirs_bytes = number_of_rva_and_sizes + .checked_mul(size_of::<_ImageDataDirectory>()) + .ok_or(PEError::FormatNotSupported("data directory count overflow"))?; + if data_dirs.len() < total_dirs_bytes { + return Err(PEError::BufferSizeError( + optional_header.len(), + data_directory_offset, + total_dirs_bytes, + )); + } + + let read_data_directory = |index: usize| -> Option<&_ImageDataDirectory> { + if index >= number_of_rva_and_sizes { + return None; + } + let start = index * size_of::<_ImageDataDirectory>(); + _ImageDataDirectory::ref_from_bytes( + &data_dirs[start..start + size_of::<_ImageDataDirectory>()], + ) + .ok() + }; + + // Reject .NET / CLR images, mirroring the previous goblin-based guard. + if let Some(clr) = read_data_directory(IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR) { + if clr.virtual_address != 0 && clr.size != 0 { + return Err(PEError::FormatNotSupported(".NET assembly")); + } + } + + let resource_table = read_data_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE) + .filter(|d| d.virtual_address != 0 && d.size != 0) .ok_or(PEError::NoResourceTable())?; let resource_table_start = resource_table.virtual_address as usize; let resource_table_end = resource_table_start + resource_table.size as usize; - let resource_section_table = pe - .sections - .iter() - .find(|section| { - section.virtual_address as usize >= resource_table_start - && (section.virtual_address + section.virtual_size) as usize - <= resource_table_end - }) - .ok_or(PEError::NoResourceTable())?; + // ---- IMAGE_SECTION_HEADERs ---- + let section_headers_offset = optional_header_end; + let num_sections = file_header.number_of_sections as usize; + let sections_total = num_sections + .checked_mul(size_of::<_ImageSectionHeader>()) + .ok_or(PEError::FormatNotSupported("section count overflow"))?; + let sections_end = section_headers_offset + .checked_add(sections_total) + .ok_or(PEError::FormatNotSupported("section table overflow"))?; + if buf.len() < sections_end { + return Err(PEError::BufferSizeError( + buf.len(), + section_headers_offset, + sections_total, + )); + } - // offset will almost always == resource_section_table.pointer_to_raw_data, - // because the resource table will start will start exactly at the start of the section - let offset = resource_table_start - resource_section_table.virtual_address as usize - + resource_section_table.pointer_to_raw_data as usize; - let end = offset + resource_section_table.virtual_size as usize; + // Locate the section containing the resource table. Note: the original + // goblin-based predicate looked for a section whose VA range lies + // entirely *within* the resource directory's RVA range, which happens + // to work because the .rsrc section's contents normally match the + // resource directory exactly. Preserve that behavior to keep results + // identical to the previous implementation. + let mut resource_section: Option<&_ImageSectionHeader> = None; + for i in 0..num_sections { + let off = section_headers_offset + i * size_of::<_ImageSectionHeader>(); + let section = _ImageSectionHeader::ref_from_bytes( + &buf[off..off + size_of::<_ImageSectionHeader>()], + ) + .map_err(|_| { + PEError::BufferSizeError(buf.len(), off, size_of::<_ImageSectionHeader>()) + })?; + + if section.virtual_address as usize >= resource_table_start + && (section.virtual_address as usize + section.virtual_size as usize) + <= resource_table_end + { + resource_section = Some(section); + break; + } + } + + let resource_section = resource_section.ok_or(PEError::NoResourceTable())?; + + // offset will almost always == resource_section.pointer_to_raw_data, + // because the resource table will start exactly at the start of the section. + let offset = resource_table_start - resource_section.virtual_address as usize + + resource_section.pointer_to_raw_data as usize; + let end = offset + resource_section.virtual_size as usize; // Since the RVA is relative to the loaded image layout rather than the raw image on disk, // we need to adjust the RVA by the difference between those two layouts. - let rva_to_va_offset = (resource_section_table.virtual_address - - resource_section_table.pointer_to_raw_data) as usize; - - let _section_name = resource_section_table - .name() - .map_err(|e| PEError::BadResourceString(e.to_string()))?; + let rva_to_va_offset = + (resource_section.virtual_address - resource_section.pointer_to_raw_data) as usize; Ok(ImageResource { - image_file: mapped, + image_file, rva_to_va_offset, resource_table_offset: offset, resource_table_end: end, - _phantom: core::marker::PhantomData {}, }) } } diff --git a/tests/no_std.rs b/tests/no_std.rs new file mode 100644 index 0000000..85108e9 --- /dev/null +++ b/tests/no_std.rs @@ -0,0 +1,60 @@ +extern crate pe_resource; +extern crate std; + +#[cfg(test)] +mod functional { + use core::iter::FromIterator; + use pe_resource::parser; + + fn read_pe_image(filename: &str) -> std::vec::Vec { + std::fs::read(filename).expect("failed to read test PE image") + } + + #[test] + #[cfg(target_os = "windows")] + fn wevtapi() -> Result<(), parser::PEError> { + let image = read_pe_image("C:\\windows\\system32\\wevtapi.dll"); + let resource = parser::find_resource_directory_from_bytes(&image)?; + + let pmres_data = resource.find(&"WEVT_TEMPLATE", &"#1")?; + + assert_eq!(core::str::from_utf8(&pmres_data.buf[0..4]).unwrap(), "CRIM"); + assert!(pmres_data.id.eq(&0x409u16)); + + Ok(()) + } + + #[test] + #[cfg(target_os = "windows")] + fn wevtsvc() -> Result<(), parser::PEError> { + let image = read_pe_image("C:\\windows\\system32\\wevtsvc.dll"); + let resource = parser::find_resource_directory_from_bytes(&image)?; + + let pmres_data = resource.find(&"WEVT_TEMPLATE", &"#1")?; + + assert_eq!(core::str::from_utf8(&pmres_data.buf[0..4]).unwrap(), "CRIM"); + assert!(pmres_data.id.eq(&0x409u16)); + + Ok(()) + } + + #[test] + #[cfg(target_os = "windows")] + fn wevtapi_enum() -> Result<(), parser::PEError> { + let image = read_pe_image("C:\\windows\\system32\\wevtapi.dll"); + let resources = parser::find_resource_directory_from_bytes(&image)?; + + let resource_iter = resources.try_into_iter()?; + for resource in resource_iter { + let resource = resource?; + std::println!( + "Enumerated resource: {}/{}/{}", + resource.name, + resource.id, + std::string::String::from_iter(resources.to_chars(resource.data.id)) + ); + } + + Ok(()) + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 9c0b79e..1abdb88 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "std")] + #[cfg(test)] mod functional { use ::pe_resource::*;