From 007aec94df108921e212dd884b608d7f74a809c7 Mon Sep 17 00:00:00 2001 From: m4b Date: Sun, 20 Oct 2024 21:40:27 -0700 Subject: [PATCH 01/11] docs: add 1 new contributors; prepare changelog for 0.9.0 --- CHANGELOG.md | 15 +++++++++++++-- README.md | 4 +++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0c1160..db247ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,20 @@ All notable changes to this project will be documented in this file. Before 1.0, this project does not adhere to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -Goblin is now 0.8, which means we will try our best to ease breaking changes. Tracking issue is here: https://github.com/m4b/goblin/issues/97 +Goblin is now 0.9, which means we will try our best to ease breaking changes. Tracking issue is here: https://github.com/m4b/goblin/issues/97 -## [0.8.1] - 2024-04-27 +## [0.9.0] - 2024-10-20 +### Added, Breaking +pe: add TE (terse executable) support, big thanks @Javagedes: https://github.com/m4b/goblin/pull/397 +pe: add support for codeview PDB 2.0, thanks @joschock: https://github.com/m4b/goblin/pull/409 +pe: parse TLS in data directories, thanks @kkent030315: https://github.com/m4b/goblin/pull/404 + +## [0.8.2] - 2024-04-29 +Everything in 0.8.1 except TE support in https://github.com/m4b/goblin/pull/397 was reverted, +due to it being technically a breaking change. +0.8.1 was yanked from crates. + +## [0.8.1] - 2024-04-27 (YANKED) ### Docs pe: document pe header, thanks @JohnScience: https://github.com/m4b/goblin/pull/399 pe, elf: fix doc warnings, thanks @5225225: https://github.com/m4b/goblin/pull/395 diff --git a/README.md b/README.md index af05435f..8ed9c311 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Add to your `Cargo.toml` ```toml [dependencies] -goblin = "0.8" +goblin = "0.9" ``` ### Features @@ -133,6 +133,7 @@ In lexicographic order: - [@Jhynjhiruu] - [@johannst] - [@JohnScience] +- [@joschock] - [@jrmuizel] - [@jsgf] - [@keith] @@ -215,6 +216,7 @@ In lexicographic order: [@Jhynjhiruu]: https://github.com/Jhynjhiruu [@JohnScience]: https://github.com/JohnScience [@johannst]: https://github.com/johannst +[@joschock]: https://github.com/joschock [@jdub]: https://github.com/jdub [@jrmuizel]: https://github.com/jrmuizel [@jsgf]: https://github.com/jsgf From b2505eca690b2e98359842f124cf279513bd6c3b Mon Sep 17 00:00:00 2001 From: m4b Date: Sun, 20 Oct 2024 21:51:02 -0700 Subject: [PATCH 02/11] build: bump version to 0.9.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c609f4da..5a7cf9f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goblin" -version = "0.8.1" +version = "0.9.0" authors = [ "m4b ", "seu ", From 29dcaad36ead323ae498ca5787a22a8f3f1ad8bc Mon Sep 17 00:00:00 2001 From: kkent030315 Date: Fri, 25 Oct 2024 12:17:52 +0900 Subject: [PATCH 03/11] pe: Assume `AddressOfIndex` could not be in raw section, check callback is zero right after it reads from binary (#425) * PE: Assume `AddressOfIndex` could not be in raw section, check callback is zero right after it reads from binary * Simplify the expression with same semantics --- src/pe/tls.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/pe/tls.rs b/src/pe/tls.rs index ad24c50a..2369bb0e 100644 --- a/src/pe/tls.rs +++ b/src/pe/tls.rs @@ -190,15 +190,8 @@ impl<'a> TlsData<'a> { // VA to RVA let rva = itd.address_of_index as usize - image_base; - let offset = - utils::find_offset(rva, sections, file_alignment, opts).ok_or_else(|| { - error::Error::Malformed(format!( - "cannot map tls address_of_index rva ({:#x}) into offset", - rva - )) - })?; - - slot = Some(bytes.pread_with::(offset, scroll::LE)?); + let offset = utils::find_offset(rva, sections, file_alignment, opts); + slot = offset.and_then(|x| bytes.pread_with::(x, scroll::LE).ok()); } // Parse the callbacks if any @@ -227,6 +220,9 @@ impl<'a> TlsData<'a> { } else { bytes.pread_with::(offset + i * 4, scroll::LE)? as u64 }; + if callback == 0 { + break; + } // Each callback is an VA so convert it to RVA let callback_rva = callback as usize - image_base; // Check if the callback is in the image @@ -236,9 +232,6 @@ impl<'a> TlsData<'a> { callback ))); } - if callback == 0 { - break; - } callbacks.push(callback); i += 1; } From 6c5acef628b7354ec522758dc56b889b33f488ce Mon Sep 17 00:00:00 2001 From: m4b Date: Thu, 24 Oct 2024 20:05:18 -0700 Subject: [PATCH 04/11] build: fix rust 2024 edition warning; add stderrlog to rdr --- Cargo.toml | 2 +- examples/rdr.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5a7cf9f6..8d4cafca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ optional = true [dependencies.scroll] version = "0.12" -default_features = false +default-features = false [features] default = ["std", "elf32", "elf64", "mach32", "mach64", "pe32", "pe64", "te", "archive", "endian_fd"] diff --git a/examples/rdr.rs b/examples/rdr.rs index a493a173..48b4cd39 100644 --- a/examples/rdr.rs +++ b/examples/rdr.rs @@ -16,6 +16,7 @@ fn run() -> error::Result<()> { } pub fn main() { + stderrlog::new().verbosity(3).init().unwrap(); match run() { Ok(()) => (), Err(err) => println!("{:#}", err), From 5b0382c051121e325789887fa762b2194dad2f18 Mon Sep 17 00:00:00 2001 From: m4b Date: Thu, 24 Oct 2024 20:22:32 -0700 Subject: [PATCH 05/11] build: bump version to 0.9.1; update changelog --- CHANGELOG.md | 4 ++++ Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db247ffe..40d1fb05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Before 1.0, this project does not adhere to [Semantic Versioning](http://semver. Goblin is now 0.9, which means we will try our best to ease breaking changes. Tracking issue is here: https://github.com/m4b/goblin/issues/97 +## [0.9.1] - 2024-10-24 +### (hot) Fix +pe: fix parsing of tls in certain cases (issue: https://github.com/m4b/goblin/issues/424), thanks @kkent030315: https://github.com/m4b/goblin/pull/425 + ## [0.9.0] - 2024-10-20 ### Added, Breaking pe: add TE (terse executable) support, big thanks @Javagedes: https://github.com/m4b/goblin/pull/397 diff --git a/Cargo.toml b/Cargo.toml index 8d4cafca..19047726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goblin" -version = "0.9.0" +version = "0.9.1" authors = [ "m4b ", "seu ", From f401d18e403db7f2d197cc9793aef9383e8ded73 Mon Sep 17 00:00:00 2001 From: Radmir <33758991+ideeockus@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:38:28 +0300 Subject: [PATCH 06/11] pe.header: add parsing without dos, and fix parsing PE with zero raw_data_size of section (#396) * pe.utils: handle case when section raw_data is zero * pe.header: added ability to parse part of pe header without dos --------- Co-authored-by: Radmir Rezbaev --- src/pe/header.rs | 29 +++++++++++++++++++++-------- src/pe/utils.rs | 2 ++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/pe/header.rs b/src/pe/header.rs index 04ec8a4f..d1b4f36a 100644 --- a/src/pe/header.rs +++ b/src/pe/header.rs @@ -791,14 +791,7 @@ pub struct Header { } impl Header { - pub fn parse(bytes: &[u8]) -> error::Result { - let dos_header = DosHeader::parse(&bytes)?; - let dos_stub = bytes.pread(DOS_STUB_OFFSET as usize).map_err(|_| { - error::Error::Malformed(format!( - "cannot parse DOS stub (offset {:#x})", - DOS_STUB_OFFSET - )) - })?; + fn parse_impl(bytes: &[u8], dos_header: DosHeader, dos_stub: DosStub) -> error::Result { let mut offset = dos_header.pe_pointer as usize; let signature = bytes.gread_with(&mut offset, scroll::LE).map_err(|_| { error::Error::Malformed(format!("cannot parse PE signature (offset {:#x})", offset)) @@ -809,6 +802,7 @@ impl Header { } else { None }; + Ok(Header { dos_header, dos_stub, @@ -817,6 +811,25 @@ impl Header { optional_header, }) } + + /// Parses PE header from the given bytes; this will fail if the DosHeader or DosStub is malformed or missing in some way + pub fn parse(bytes: &[u8]) -> error::Result { + let dos_header = DosHeader::parse(&bytes)?; + let dos_stub = bytes.pread(DOS_STUB_OFFSET as usize).map_err(|_| { + error::Error::Malformed(format!( + "cannot parse DOS stub (offset {:#x})", + DOS_STUB_OFFSET + )) + })?; + + Header::parse_impl(bytes, dos_header, dos_stub) + } + + /// Parses PE header from the given bytes, a default DosHeader and DosStub are generated, and any malformed header or stub is ignored + pub fn parse_without_dos(bytes: &[u8]) -> error::Result { + let dos_header = DosHeader::default(); + Header::parse_impl(bytes, dos_header, DosStub::default()) + } } impl ctx::TryIntoCtx for Header { diff --git a/src/pe/utils.rs b/src/pe/utils.rs index 289ccc52..d4d01726 100644 --- a/src/pe/utils.rs +++ b/src/pe/utils.rs @@ -64,6 +64,8 @@ fn section_read_size(section: §ion_table::SectionTable, file_alignment: u32) if virtual_size == 0 { read_size + } else if read_size == 0 { + virtual_size } else { cmp::min(read_size, round_size(virtual_size)) } From f43bc30023e0634f77d1043f4bce3898e99bcebc Mon Sep 17 00:00:00 2001 From: kkent030315 Date: Mon, 28 Oct 2024 10:37:56 +0900 Subject: [PATCH 07/11] pe: parse rich header and refactor DOS stub parser (#406) * parse rich header * Explicitly isolate the Rich header from DOS stub * Fixed pe pointer issue, added DOS explanation, add Borland tests * impl iterator for `RichMetadata` --- src/pe/header.rs | 633 ++++++++++++++++++++++++++++++++++++++++++++--- src/pe/mod.rs | 3 +- 2 files changed, 601 insertions(+), 35 deletions(-) diff --git a/src/pe/header.rs b/src/pe/header.rs index d1b4f36a..8558194d 100644 --- a/src/pe/header.rs +++ b/src/pe/header.rs @@ -1,8 +1,9 @@ +use core::iter::FusedIterator; + use crate::error; -use crate::pe::{data_directories, optional_header, section_table, symbol}; +use crate::pe::{data_directories, debug, optional_header, section_table, symbol}; use crate::strtab; use alloc::vec::Vec; -use log::debug; use scroll::{ctx, IOread, IOwrite, Pread, Pwrite, SizeWith}; /// In `winnt.h` and `pe.h`, it's `IMAGE_DOS_HEADER`. It's a DOS header present in all PE binaries. @@ -332,7 +333,7 @@ impl DosHeader { offset ); - let pe_pointer = bytes + let pe_pointer: u32 = bytes .pread_with(PE_POINTER_OFFSET as usize, scroll::LE) .map_err(|_| { error::Error::Malformed(format!( @@ -381,8 +382,7 @@ impl DosHeader { } } -#[repr(C)] -#[derive(Debug, PartialEq, Copy, Clone, Pread, Pwrite)] +#[derive(Debug, PartialEq, Copy, Clone)] /// The DOS stub program which should be executed in DOS mode. It prints the message "This program cannot be run in DOS mode" and exits. /// /// ## Position in a modern PE file @@ -391,17 +391,96 @@ impl DosHeader { /// /// * De facto, can be followed by a non-standard ["Rich header"](https://0xrick.github.io/win-internals/pe3/#rich-header). /// * According to the standard, is followed by the [Header::signature] and then the [CoffHeader]. -pub struct DosStub(pub [u8; 0x40]); -impl Default for DosStub { +pub struct DosStub<'a> { + pub data: &'a [u8], +} +impl<'a> Default for DosStub<'a> { + /// This is the very basic DOS program bytecode representation embedded in MSVC linker. + /// + /// An equivalent (Not a equal) DOS program can be follows: + /// + /// ```asm + /// push cs ; 0E Push the code segment onto the stack + /// pop ds ; 1F Pop the top of the stack into the data segment register + /// + /// _start: + /// mov dx, aMessage ; BA 0E 00 Load the address of the message to the DX register + /// mov ah, 09h ; B4 09 DOS function 09h (display string) to print the message at DS:DX + /// int 21h ; CD 21 Call DOS interrupt 21h for displaying the message + /// + /// mov ax, 4C01h ; B8 01 4C DOS function 4Ch (terminate program) with return code 1 + /// int 21h ; CD 21 Call DOS interrupt 21h for program termination + /// + /// aMessage db 'This program cannot be run in DOS mode.' + /// ``` + #[rustfmt::skip] fn default() -> Self { - // "This program cannot be run in DOS mode" error program - Self([ - 0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21, - 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x63, - 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, 0x69, - 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, - 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]) + Self { + data: &[ + 0x0E, // push cs: Setup segment registers + 0x1F, // pop ds: Setup segment registers + 0xBA, 0x0E, 0x00, // mov dx, 0x000E: Load the message address into the DX register + 0xB4, 0x09, // mov ah, 0x09: DOS function to print a string + 0xCD, 0x21, // int 0x21: Trigger DOS interrupt 21h to print the message + 0xB8, 0x01, 0x4C, // mov ax, 0x4C01: Prepare to terminate the program (DOS function 4Ch) + 0xCD, 0x21, // int 0x21: Trigger DOS interrupt 21h to terminate the program + 0x54, 0x68, 0x69, 0x73, // "This" ASCII string "This program cannot be run in DOS mode." + 0x20, 0x70, 0x72, 0x6F, // " pro" Continuation of the ASCII string, + 0x67, 0x72, 0x61, 0x6D, // "gram" Continuation of the ASCII string, + 0x20, 0x63, 0x61, 0x6E, // " can" Continuation of the ASCII string, + 0x6E, 0x6F, 0x74, 0x20, // "not " Continuation of the ASCII string, + 0x62, 0x65, 0x20, 0x72, // "be r" Continuation of the ASCII string, + 0x75, 0x6E, 0x20, 0x69, // "un i" Continuation of the ASCII string, + 0x6E, 0x20, 0x44, 0x4F, // "n DO" Continuation of the ASCII string, + 0x53, 0x20, 0x6D, 0x6F, // "S mo" Continuation of the ASCII string, + 0x64, 0x65, 0x2E, // "DE." Continuation of the ASCII string, ending with a period. + 0x0D, 0x0D, 0x0A, // Carriage return (CR `0x0D, 0x0D`) and line feed (LF `0x0A`) + 0x24, // '$' (End of string marker for DOS function 09h) + 0x00, 0x00, 0x00, 0x00, // Padding bytes (8-byte alignment) + 0x00, 0x00, 0x00, // Padding bytes (8-byte alignment) + ], + } + } +} +impl<'a> ctx::TryIntoCtx for DosStub<'a> { + type Error = error::Error; + + fn try_into_ctx(self, bytes: &mut [u8], _: scroll::Endian) -> Result { + let offset = &mut 0; + bytes.gwrite_with(&*self.data, offset, ())?; + Ok(*offset) + } +} + +impl<'a> DosStub<'a> { + /// Parse the DOS stub. + /// + /// The DOS stub is a small program that prints the message "This program cannot be run in DOS mode" and exits; and + /// is not really read for the PECOFF file format. It's a relic from the MS-DOS era. + pub fn parse(bytes: &'a [u8], pe_pointer: u32) -> error::Result { + let start_offset = DOS_STUB_OFFSET as usize; + let end_offset = pe_pointer as usize; + + // Check if the end_offset is less than or equal to start_offset + if end_offset <= start_offset { + return Err(error::Error::Malformed(format!( + "PE pointer ({:#x}) must be greater than the start offset ({:#x})", + pe_pointer, start_offset + ))); + } + + if bytes.len() < end_offset as usize { + return Err(error::Error::Malformed(format!( + "DOS stub is too short ({} bytes) to contain the PE header pointer ({:#x})", + bytes.len(), + end_offset + ))); + } + + let dos_stub_area = &bytes[start_offset..end_offset]; + Ok(Self { + data: dos_stub_area, + }) } } @@ -774,25 +853,26 @@ impl CoffHeader { /// The PE header is located at the very beginning of the file and /// is followed by the section table and sections. #[derive(Debug, PartialEq, Copy, Clone, Default)] -pub struct Header { +pub struct Header<'a> { pub dos_header: DosHeader, /// DOS program for legacy loaders - pub dos_stub: DosStub, + pub dos_stub: DosStub<'a>, + pub rich_header: Option>, - // Q (JohnScience): should we care about the "rich header"? - // https://0xrick.github.io/win-internals/pe3/#rich-header - // Introducing it would be a breaking change because it would require a new field in the struct - // but it would be a good addition to the library. - // /// PE Magic: PE\0\0, little endian pub signature: u32, pub coff_header: CoffHeader, pub optional_header: Option, } -impl Header { - fn parse_impl(bytes: &[u8], dos_header: DosHeader, dos_stub: DosStub) -> error::Result { +impl<'a> Header<'a> { + fn parse_impl( + bytes: &'a [u8], + dos_header: DosHeader, + dos_stub: DosStub<'a>, + ) -> error::Result { let mut offset = dos_header.pe_pointer as usize; + let rich_header = RichHeader::parse(&bytes)?; let signature = bytes.gread_with(&mut offset, scroll::LE).map_err(|_| { error::Error::Malformed(format!("cannot parse PE signature (offset {:#x})", offset)) })?; @@ -806,6 +886,7 @@ impl Header { Ok(Header { dos_header, dos_stub, + rich_header, signature, coff_header, optional_header, @@ -813,26 +894,21 @@ impl Header { } /// Parses PE header from the given bytes; this will fail if the DosHeader or DosStub is malformed or missing in some way - pub fn parse(bytes: &[u8]) -> error::Result { + pub fn parse(bytes: &'a [u8]) -> error::Result { let dos_header = DosHeader::parse(&bytes)?; - let dos_stub = bytes.pread(DOS_STUB_OFFSET as usize).map_err(|_| { - error::Error::Malformed(format!( - "cannot parse DOS stub (offset {:#x})", - DOS_STUB_OFFSET - )) - })?; + let dos_stub = DosStub::parse(bytes, dos_header.pe_pointer)?; Header::parse_impl(bytes, dos_header, dos_stub) } /// Parses PE header from the given bytes, a default DosHeader and DosStub are generated, and any malformed header or stub is ignored - pub fn parse_without_dos(bytes: &[u8]) -> error::Result { + pub fn parse_without_dos(bytes: &'a [u8]) -> error::Result { let dos_header = DosHeader::default(); Header::parse_impl(bytes, dos_header, DosStub::default()) } } -impl ctx::TryIntoCtx for Header { +impl<'a> ctx::TryIntoCtx for Header<'a> { type Error = error::Error; fn try_into_ctx(self, bytes: &mut [u8], ctx: scroll::Endian) -> Result { @@ -848,6 +924,251 @@ impl ctx::TryIntoCtx for Header { } } +/// The DANS marker is a XOR-decoded version of the string "DanS" and is used to identify the Rich header. +pub const DANS_MARKER: u32 = 0x536E6144; +/// Size of [DANS_MARKER] in bytes +pub const DANS_MARKER_SIZE: usize = core::mem::size_of::(); +/// The Rich marker is a XOR-decoded version of the string "Rich" and is used to identify the Rich header. +pub const RICH_MARKER: u32 = 0x68636952; +/// Size of [RICH_MARKER] in bytes +pub const RICH_MARKER_SIZE: usize = core::mem::size_of::(); + +/// The Rich header is a undocumented header that is used to store information about the build environment. +/// +/// The Rich Header first appeared in Visual Studio 6.0 and contains: a product identifier, build number, and the number of times it was used during the build process. +#[derive(Debug, PartialEq, Copy, Clone, Default)] +pub struct RichHeader<'a> { + /// Key is 32-bit value used for XOR encrypt/decrypt fields + pub key: u32, + /// The Rich header data with the padding. + pub data: &'a [u8], + /// Padding bytes at the prologue of [Self::data] + pub padding_size: usize, + /// Start offset of the Rich header. + pub start_offset: u32, + /// End offset of the Rich header. + pub end_offset: u32, +} + +/// The Rich metadata is a pair of 32-bit values that store the tool version and the use count. +#[repr(C)] +#[derive(Debug, PartialEq, Copy, Clone, Default, Pread, Pwrite)] +pub struct RichMetadata { + /// Build version is a 16-bit value that stores the version of the tool used to build the PE file. + pub build: u16, + /// Product identifier is a 16-bit value that stores the type of tool used to build the PE file. + pub product: u16, + /// The use count is a 32-bit value that stores the number of times the tool was used during the build process. + pub use_count: u32, +} + +impl RichMetadata { + /// Parse [`RichMetadata`] from given bytes + fn parse(bytes: &[u8], key: u32) -> error::Result { + let mut offset = 0; + let build_and_product = bytes.gread_with::(&mut offset, scroll::LE)? ^ key; + let build = (build_and_product & 0xFFFF) as u16; + let product = (build_and_product >> 16) as u16; + let use_count = bytes.gread_with::(&mut offset, scroll::LE)? ^ key; + Ok(Self { + build, + product, + use_count, + }) + } +} + +/// Size of [`RichMetadata`] entries. +const RICH_METADATA_SIZE: usize = 8; + +/// Iterator over [`RichMetadata`] in [`RichHeader`]. +#[derive(Debug)] +pub struct RichMetadataIterator<'a> { + /// The key of [RichHeader::key] + key: u32, + /// The raw data [RichHeader::data] without padding + data: &'a [u8], +} + +impl Iterator for RichMetadataIterator<'_> { + type Item = error::Result; + + fn next(&mut self) -> Option { + if self.data.is_empty() { + return None; + } + + // Data within this iterator should not have padding + Some(match RichMetadata::parse(&self.data, self.key) { + Ok(metadata) => { + self.data = &self.data[RICH_METADATA_SIZE..]; + Ok(metadata) + } + Err(error) => { + self.data = &[]; + Err(error.into()) + } + }) + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.data.len() / RICH_METADATA_SIZE; + (len, Some(len)) + } +} + +impl FusedIterator for RichMetadataIterator<'_> {} +impl ExactSizeIterator for RichMetadataIterator<'_> {} + +impl<'a> RichHeader<'a> { + /// Parse the rich header from the given bytes. + /// + /// To decode the Rich header, + /// - First locate the Rich marker and the subsequent 32-bit encryption key. + /// - Then, work backwards from the Rich marker, XORing the key with the stored 32-bit values until you decode the DanS marker. + /// + /// Between these markers, you'll find pairs of 32-bit values: + /// + /// - the first indicates the Microsoft tool used, and + /// - the second shows the count of linked object files made with that tool. + /// - The upper 16 bits of the tool ID describe the tool type, + /// - while the lower 16 bits specify the tool’s build version. + pub fn parse(bytes: &'a [u8]) -> error::Result> { + // Parse the DOS header; some fields are required to locate the Rich header. + let dos_header = DosHeader::parse(bytes)?; + let dos_header_end_offset = PE_POINTER_OFFSET as usize; + let pe_header_start_offset = dos_header.pe_pointer as usize; + + // The Rich header is not present in all PE files. + if (pe_header_start_offset - dos_header_end_offset) < 8 { + return Ok(None); + } + + // The Rich header is located between the DOS header and the PE header. + let scan_start = dos_header_end_offset + 4; + let scan_end = pe_header_start_offset; + if scan_start > scan_end { + return Err(error::Error::Malformed(format!( + "Rich header scan start ({:#X}) is greater than scan end ({:#X})", + scan_start, scan_end + ))); + } + let scan_stub = &bytes[scan_start..scan_end]; + + // First locate the Rich marker and the subsequent 32-bit encryption key. + let (rich_end_offset, key) = match scan_stub + .windows(8) + .enumerate() + .filter_map( + |(index, window)| match window.pread_with::(0, scroll::LE) { + // Marker matches, then return its index + Ok(marker) if marker == RICH_MARKER => Some(Ok(index)), + // Error reading with scroll + Err(e) => Some(Err(error::Error::from(e))), + // Marker did not match + _ => None, + }, + ) + // Next is the very first element succeeded + .next() + { + Some(Ok(rich_end_offset)) => { + let rich_key = + scan_stub.pread_with::(rich_end_offset + RICH_MARKER_SIZE, scroll::LE)?; + (rich_end_offset, rich_key) + } + // Something went wrong, e.g., reading with scroll + Some(Err(e)) => return Err(e), + // Marker did not found, rich header is assumed it does not exist + None => return Ok(None), + }; + + // Ensure rich_end_offset is within bounds + if rich_end_offset >= scan_stub.len() { + return Err(error::Error::Malformed(format!( + "Rich end offset ({:#X}) exceeds scan stub length ({:#X})", + rich_end_offset, + scan_stub.len() + ))); + } + // Scope the buffer + let rich_header = &scan_stub[..rich_end_offset]; + + // Look for DanS marker + let rich_start_offset = match scan_stub + .windows(4) + .enumerate() + .filter_map( + |(index, window)| match window.pread_with::(0, scroll::LE) { + // If we do found the DanS marker, return the offset + Ok(value) if (value ^ key) == DANS_MARKER => Some(Ok(index + DANS_MARKER_SIZE)), + // This is scroll error, likely malformed rich header + Err(e) => Some(Err(error::Error::from(e))), + // No matching DanS marker found + _ => None, + }, + ) + // Next is the very first element succeeded + .next() + { + // Suceeded + Some(Ok(offset)) => offset, + // Errors such as from scroll reader + Some(Err(e)) => return Err(e), + // DanS marker did not found + None => { + return Err(error::Error::Malformed(format!( + "Rich header does not contain the DanS marker" + ))); + } + }; + + // Ensure rich_start_offset is within bounds + if rich_start_offset >= rich_header.len() { + return Err(error::Error::Malformed(format!( + "Rich start offset ({:#X}) exceeds rich header length ({:#X})", + rich_start_offset, + rich_header.len() + ))); + } + // Scope the buffer + let rich_header = &rich_header[rich_start_offset..]; + + // Skip padding bytes + let padding_size = rich_header + .chunks(4) + .map(|chunk| chunk.pread_with::(0, scroll::LE)) + .collect::, _>>()? + .into_iter() + .take_while(|value| value == &key) + .count() + * core::mem::size_of_val(&key); + + // Extract the Rich header data without the padding + let data = rich_header; + + // Subtract the sizeof DanS marker (u32, 4 bytes) + let start_offset = scan_start as u32 + rich_start_offset as u32 - DANS_MARKER_SIZE as u32; + let end_offset = scan_start as u32 + rich_end_offset as u32; + + Ok(Some(RichHeader { + key, + data, + padding_size, + start_offset, + end_offset, + })) + } + + /// Returns [`RichMetadataIterator`] iterator for [`RichMetadata`] + pub fn metadatas(&self) -> RichMetadataIterator<'a> { + RichMetadataIterator { + key: self.key, + data: &self.data[self.padding_size..], + } + } +} + /// The TE header is a reduced PE32/PE32+ header containing only fields /// required for execution in the Platform Initialization /// ([PI](https://uefi.org/specs/PI/1.8/V1_Introduction.html)) architecture. @@ -1031,7 +1352,11 @@ pub fn machine_to_str(machine: u16) -> &'static str { #[cfg(test)] mod tests { - use super::{machine_to_str, Header, COFF_MACHINE_X86, DOS_MAGIC, PE_MAGIC}; + use crate::{error, pe::header::DosStub}; + + use super::{ + machine_to_str, Header, RichHeader, RichMetadata, COFF_MACHINE_X86, DOS_MAGIC, PE_MAGIC, + }; const CRSS_HEADER: [u8; 688] = [ 0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, @@ -1082,6 +1407,133 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; + const NO_RICH_HEADER: [u8; 262] = [ + 0x4D, 0x5A, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0F, 0x00, 0xFF, 0xFF, 0x00, + 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x1A, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0xBA, 0x10, 0x00, 0x0E, 0x1F, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, + 0x4C, 0xCD, 0x21, 0x90, 0x90, 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, + 0x61, 0x6D, 0x20, 0x6D, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, + 0x75, 0x6E, 0x64, 0x65, 0x72, 0x20, 0x57, 0x69, 0x6E, 0x33, 0x32, 0x0D, 0x0A, 0x24, 0x37, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x50, 0x45, 0x00, 0x00, 0x64, 0x86, + ]; + + const NO_RICH_HEADER_INVALID_PE_POINTER: [u8; 304] = [ + 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, + 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3C, 0xFF, 0x00, 0x00, 0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, + 0x4C, 0xCD, 0x21, 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, + 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, + 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, + 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8D, 0xC7, 0xEA, 0x07, 0xC9, 0xA6, 0x84, + 0x54, 0xC9, 0xA6, 0x84, 0x54, 0xC9, 0xA6, 0x84, 0x54, 0x10, 0xD2, 0x81, 0x55, 0xCB, 0xA6, + 0x84, 0x54, 0xC0, 0xDE, 0x17, 0x54, 0xC1, 0xA6, 0x84, 0x54, 0xDD, 0xCD, 0x80, 0x55, 0xC7, + 0xA6, 0x84, 0x54, 0xDD, 0xCD, 0x87, 0x55, 0xC1, 0xA6, 0x84, 0x54, 0xDD, 0xCD, 0x81, 0x55, + 0x7E, 0xA6, 0x84, 0x54, 0xB9, 0x27, 0x85, 0x55, 0xCA, 0xA6, 0x84, 0x54, 0xC9, 0xA6, 0x85, + 0x54, 0x08, 0xA6, 0x84, 0x54, 0xA5, 0xD2, 0x81, 0x55, 0xE8, 0xA6, 0x84, 0x54, 0xA5, 0xD2, + 0x80, 0x55, 0xD9, 0xA6, 0x84, 0x54, 0xA5, 0xD2, 0x87, 0x55, 0xC0, 0xA6, 0x84, 0x54, 0x10, + 0xD2, 0x80, 0x55, 0x49, 0xA6, 0x84, 0x54, 0x10, 0xD2, 0x84, 0x55, 0xC8, 0xA6, 0x84, 0x54, + 0x10, 0xD2, 0x7B, 0x54, 0xC8, 0xA6, 0x84, 0x54, 0x10, 0xD2, 0x86, 0x55, 0xC8, 0xA6, 0x84, + 0x54, 0x52, 0x69, 0x63, 0x68, 0xC9, 0xA6, 0x84, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x64, 0x86, 0x07, 0x00, 0xEC, 0xA5, 0x5B, 0x66, + 0x00, 0x00, 0x00, 0x00, + ]; + + const CORRECT_RICH_HEADER: [u8; 256] = [ + 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, + 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0x00, 0x00, 0x00, 0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, + 0x4C, 0xCD, 0x21, 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, + 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, + 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, + 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x4C, 0x5B, 0xB1, 0x37, 0x2D, 0x35, + 0xE2, 0x37, 0x2D, 0x35, 0xE2, 0x37, 0x2D, 0x35, 0xE2, 0x44, 0x4F, 0x31, 0xE3, 0x3D, 0x2D, + 0x35, 0xE2, 0x44, 0x4F, 0x36, 0xE3, 0x32, 0x2D, 0x35, 0xE2, 0x44, 0x4F, 0x30, 0xE3, 0x48, + 0x2D, 0x35, 0xE2, 0xEE, 0x4F, 0x36, 0xE3, 0x3E, 0x2D, 0x35, 0xE2, 0xEE, 0x4F, 0x30, 0xE3, + 0x14, 0x2D, 0x35, 0xE2, 0xEE, 0x4F, 0x31, 0xE3, 0x25, 0x2D, 0x35, 0xE2, 0x44, 0x4F, 0x34, + 0xE3, 0x3C, 0x2D, 0x35, 0xE2, 0x37, 0x2D, 0x34, 0xE2, 0xAF, 0x2D, 0x35, 0xE2, 0x37, 0x2D, + 0x35, 0xE2, 0x23, 0x2D, 0x35, 0xE2, 0xFC, 0x4E, 0x37, 0xE3, 0x36, 0x2D, 0x35, 0xE2, 0x52, + 0x69, 0x63, 0x68, 0x37, 0x2D, 0x35, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x64, 0x86, 0x05, + 0x00, + ]; + + const CORRUPTED_RICH_HEADER: [u8; 256] = [ + 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, + 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0x00, 0x00, 0x00, 0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, + 0x4C, 0xCD, 0x21, 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, + 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, + 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, + 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x4C, 0x5B, 0xB1, 0x37, 0x2D, 0x35, + 0xE2, 0x37, 0x2D, 0x35, 0xE2, 0x37, 0x2D, 0x35, 0xE2, 0x44, 0x4F, 0x31, 0xE3, 0x3D, 0x2D, + 0x35, 0xE2, 0x44, 0x4F, 0x36, 0xE3, 0x32, 0x2D, 0x35, 0xE2, 0x44, 0x4F, 0x30, 0xE3, 0x48, + 0x2D, 0x35, 0xE2, 0xEE, 0x4F, 0x36, 0xE3, 0x3E, 0x2D, 0x35, 0xE2, 0xEE, 0x4F, 0x30, 0xE3, + 0x14, 0x2D, 0x35, 0xE2, 0xEE, 0x4F, 0x31, 0xE3, 0x25, 0x2D, 0x35, 0xE2, 0x44, 0x4F, 0x34, + 0xE3, 0x3C, 0x2D, 0x35, 0xE2, 0x37, 0x2D, 0x34, 0xE2, 0xAF, 0x2D, 0x35, 0xE2, 0x37, 0x2D, + 0x35, 0xE2, 0x23, 0x2D, 0x35, 0xE2, 0xFC, 0x4E, 0x37, 0xE3, 0x36, 0x2D, 0x35, 0xE2, 0x52, + 0x69, 0x63, 0x68, 0x37, 0x2D, 0x35, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x64, 0x86, 0x05, + 0x00, + ]; + + const BORLAND_PE32_VALID_NO_RICH_HEADER: [u8; 528] = [ + 0x4D, 0x5A, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0F, 0x00, 0xFF, 0xFF, 0x00, + 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x1A, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0xBA, 0x10, 0x00, 0x0E, 0x1F, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, + 0x4C, 0xCD, 0x21, 0x90, 0x90, 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, + 0x61, 0x6D, 0x20, 0x6D, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, + 0x75, 0x6E, 0x64, 0x65, 0x72, 0x20, 0x57, 0x69, 0x6E, 0x33, 0x32, 0x0D, 0x0A, 0x24, 0x37, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // PE + 0x50, 0x45, 0x00, 0x00, 0x4C, 0x01, 0x08, 0x00, 0xC0, 0x9C, 0x07, 0x67, 0x00, 0x00, 0x00, + 0x00, + ]; + #[test] fn crss_header() { let header = Header::parse(&&CRSS_HEADER[..]).unwrap(); @@ -1091,4 +1543,117 @@ mod tests { assert!(machine_to_str(header.coff_header.machine) == "X86"); println!("header: {:?}", &header); } + + #[test] + fn parse_without_dos() { + let header = Header::parse_without_dos(&BORLAND_PE32_VALID_NO_RICH_HEADER).unwrap(); + assert_eq!(header.dos_stub, DosStub::default()); + assert_eq!(header.rich_header.is_none(), true); + + // DOS stub is default but rich parser still works + let header = Header::parse_without_dos(&CORRECT_RICH_HEADER).unwrap(); + assert_eq!(header.dos_stub, DosStub::default()); + assert_eq!(header.rich_header.is_some(), true); + } + + #[test] + fn parse_borland_weird_dos_stub() { + let dos_stub = DosStub::parse(&BORLAND_PE32_VALID_NO_RICH_HEADER, 0x200).unwrap(); + assert_ne!(dos_stub.data, BORLAND_PE32_VALID_NO_RICH_HEADER.to_vec()); + } + + #[test] + fn parse_borland_no_rich_header() { + let header = RichHeader::parse(&BORLAND_PE32_VALID_NO_RICH_HEADER).unwrap(); + assert_eq!(header, None); + } + + #[test] + fn parse_no_rich_header() { + let header = RichHeader::parse(&NO_RICH_HEADER).unwrap(); + assert_eq!(header, None); + } + + #[test] + fn parse_no_rich_header_invalid_pe_pointer() { + let header = RichHeader::parse(&NO_RICH_HEADER_INVALID_PE_POINTER); + assert_eq!(header.is_err(), true); + if let Err(error::Error::Malformed(msg)) = header { + assert_eq!(msg, "cannot parse PE header signature (offset 0xff3c)"); + } else { + panic!("Expected a Malformed error but got {:?}", header); + } + } + + #[test] + fn parse_correct_rich_header() { + let header = RichHeader::parse(&CORRECT_RICH_HEADER).unwrap(); + assert_ne!(header, None); + let header = header.unwrap(); + let expected = vec![ + RichMetadata { + build: 25203, + product: 260, + use_count: 10, + }, + RichMetadata { + build: 25203, + product: 259, + use_count: 5, + }, + RichMetadata { + build: 25203, + product: 261, + use_count: 127, + }, + RichMetadata { + build: 25305, + product: 259, + use_count: 9, + }, + RichMetadata { + build: 25305, + product: 261, + use_count: 35, + }, + RichMetadata { + build: 25305, + product: 260, + use_count: 18, + }, + RichMetadata { + build: 25203, + product: 257, + use_count: 11, + }, + RichMetadata { + build: 0, + product: 1, + use_count: 152, + }, + RichMetadata { + build: 0, + product: 0, + use_count: 20, + }, + RichMetadata { + build: 25547, + product: 258, + use_count: 1, + }, + ]; + assert_eq!( + header + .metadatas() + .filter_map(Result::ok) + .collect::>(), + expected + ); + } + + #[test] + fn parse_corrupted_rich_header() { + let header_result = RichHeader::parse(&CORRUPTED_RICH_HEADER); + assert_eq!(header_result.is_err(), true); + } } diff --git a/src/pe/mod.rs b/src/pe/mod.rs index 3c8c3c3e..e921f2e2 100644 --- a/src/pe/mod.rs +++ b/src/pe/mod.rs @@ -45,7 +45,7 @@ pub struct PE<'a> { bytes: &'a [u8], authenticode_excluded_sections: Option, /// The PE header - pub header: header::Header, + pub header: header::Header<'a>, /// A list of the sections in this PE binary pub sections: Vec, /// The size of the binary @@ -438,6 +438,7 @@ impl<'a> ctx::TryIntoCtx for PE<'a> { } _ => None, }; + bytes.gwrite_with(self.header, &mut offset, ctx)?; max_offset = max(offset, max_offset); self.write_sections(bytes, &mut offset, file_alignment, ctx)?; From 4ca57cfc2a6d16c98e28ff0e7d7509fd2dc8bd65 Mon Sep 17 00:00:00 2001 From: m4b Date: Sat, 26 Oct 2024 17:44:34 -0700 Subject: [PATCH 08/11] docs: add 1 new contributors; prepare changelog for 0.9.2 --- CHANGELOG.md | 6 ++++++ README.md | 2 ++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40d1fb05..1c06d490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Before 1.0, this project does not adhere to [Semantic Versioning](http://semver. Goblin is now 0.9, which means we will try our best to ease breaking changes. Tracking issue is here: https://github.com/m4b/goblin/issues/97 +## [0.9.2] - 2024-10-26 +### Fixed +pe: fix PE with zero `raw_data_size` of section, thanks @ideeockus: https://github.com/m4b/goblin/pull/396 +### Added +pe: allow parsing pe::Header without dos stubs, `Header::parse_without_dos`, thanks @ideeockus: https://github.com/m4b/goblin/pull/396 + ## [0.9.1] - 2024-10-24 ### (hot) Fix pe: fix parsing of tls in certain cases (issue: https://github.com/m4b/goblin/issues/424), thanks @kkent030315: https://github.com/m4b/goblin/pull/425 diff --git a/README.md b/README.md index 8ed9c311..be3f0834 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ In lexicographic order: - [@glandium] - [@h33p] - [@ibabushkin] +- [@ideeockus] - [@jackcmay] - [@jan-auer] - [@Javagedes] @@ -209,6 +210,7 @@ In lexicographic order: [@glandium]: https://github.com/glandium [@h33p]: https://github.com/h33p [@ibabushkin]: https://github.com/ibabushkin +[@ideeockus]: https://github.com/ideeockus [@jackcmay]: https://github.com/jackcmay [@jan-auer]: https://github.com/jan-auer [@Javagedes]: https://github.com/Javagedes From d096260201158ed34d64728fd1ab0ca125e2f956 Mon Sep 17 00:00:00 2001 From: m4b Date: Sat, 26 Oct 2024 17:45:00 -0700 Subject: [PATCH 09/11] build: bump version to 0.9.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 19047726..32b33fc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goblin" -version = "0.9.1" +version = "0.9.2" authors = [ "m4b ", "seu ", From b9713182a78f0a0ae573837cd9df3cad4afd325d Mon Sep 17 00:00:00 2001 From: kkent030315 Date: Thu, 14 Nov 2024 14:40:09 +0900 Subject: [PATCH 10/11] PE: Fix import parser for non-well-formed import table (#429) * PE: Fix import parser for non-well-formed import table * Fix doc comment --- src/pe/import.rs | 50 +++++++++++++++++-- tests/bins/pe/not_well_formed_import.exe.bin | Bin 0 -> 4096 bytes tests/bins/pe/well_formed_import.exe.bin | Bin 0 -> 3584 bytes 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 tests/bins/pe/not_well_formed_import.exe.bin create mode 100644 tests/bins/pe/well_formed_import.exe.bin diff --git a/src/pe/import.rs b/src/pe/import.rs index 0284fc32..11c3ee7c 100644 --- a/src/pe/import.rs +++ b/src/pe/import.rs @@ -129,7 +129,7 @@ impl<'a> SyntheticImportLookupTableEntry<'a> { use self::SyntheticImportLookupTableEntry::*; if bitfield.is_ordinal() { let ordinal = bitfield.to_ordinal(); - debug!("importing by ordinal {:#x}", ordinal); + debug!("importing by ordinal {:#x} ({})", ordinal, ordinal); OrdinalNumber(ordinal) } else { let rva = bitfield.to_rva(); @@ -171,6 +171,7 @@ pub struct ImportDirectoryEntry { pub const SIZEOF_IMPORT_DIRECTORY_ENTRY: usize = 20; impl ImportDirectoryEntry { + /// Whether the entire fields are set to zero pub fn is_null(&self) -> bool { (self.import_lookup_table_rva == 0) && (self.time_date_stamp == 0) @@ -178,6 +179,13 @@ impl ImportDirectoryEntry { && (self.name_rva == 0) && (self.import_address_table_rva == 0) } + + /// Whether the entry is _possibly_ valid. + /// + /// Both [`Self::name_rva`] and [`Self::import_address_table_rva`] must be non-zero + pub fn is_possibly_valid(&self) -> bool { + self.name_rva != 0 && self.import_address_table_rva != 0 + } } #[derive(Debug)] @@ -342,8 +350,8 @@ impl<'a> ImportData<'a> { loop { let import_directory_entry: ImportDirectoryEntry = bytes.gread_with(offset, scroll::LE)?; - debug!("{:#?}", import_directory_entry); - if import_directory_entry.is_null() { + debug!("{:#?} at {:#x}", import_directory_entry, offset); + if import_directory_entry.is_null() || !import_directory_entry.is_possibly_valid() { break; } else { let entry = SyntheticImportDirectoryEntry::parse_with_opts::( @@ -353,7 +361,7 @@ impl<'a> ImportData<'a> { file_alignment, opts, )?; - debug!("entry {:#?}", entry); + debug!("entry {:#?} at {:#x}", entry, offset); import_data.push(entry); } } @@ -415,3 +423,37 @@ impl<'a> Import<'a> { Ok(imports) } } + +#[cfg(test)] +mod tests { + const NOT_WELL_FORMED_IMPORT: &[u8] = + include_bytes!("../../tests/bins/pe/not_well_formed_import.exe.bin"); + const WELL_FORMED_IMPORT: &[u8] = + include_bytes!("../../tests/bins/pe/well_formed_import.exe.bin"); + + #[test] + fn parse_non_well_formed_import_table() { + let binary = crate::pe::PE::parse(NOT_WELL_FORMED_IMPORT).expect("Unable to parse binary"); + assert_eq!(binary.import_data.is_some(), true); + assert_eq!(binary.imports.len(), 1); + assert_eq!(binary.imports[0].name, "ORDINAL 51398"); + assert_eq!(binary.imports[0].dll, "abcd.dll"); + assert_eq!(binary.imports[0].ordinal, 51398); + assert_eq!(binary.imports[0].offset, 0x7014); + assert_eq!(binary.imports[0].rva, 0); + assert_eq!(binary.imports[0].size, 8); + } + + #[test] + fn parse_well_formed_import_table() { + let binary = crate::pe::PE::parse(WELL_FORMED_IMPORT).expect("Unable to parse binary"); + assert_eq!(binary.import_data.is_some(), true); + assert_eq!(binary.imports.len(), 1); + assert_eq!(binary.imports[0].name, "GetLastError"); + assert_eq!(binary.imports[0].dll, "KERNEL32.dll"); + assert_eq!(binary.imports[0].ordinal, 647); + assert_eq!(binary.imports[0].offset, 0x2000); + assert_eq!(binary.imports[0].rva, 0x21B8); + assert_eq!(binary.imports[0].size, 8); + } +} diff --git a/tests/bins/pe/not_well_formed_import.exe.bin b/tests/bins/pe/not_well_formed_import.exe.bin new file mode 100644 index 0000000000000000000000000000000000000000..1a920d7828de2cfa6f8f9193a774c49de8fe9139 GIT binary patch literal 4096 zcmeHJL2DC16n;%bYi;5l3eBO|iL9U?OK7$9BwK4}!Kk6;AUS1|-PXuvH*B_2f`@p~ zs|SBTj{N~1f*^A`|0&tARx58`_>JMLyTIS7If_a!s$&G+8CnR!FL$%EP(m$8T%yd#IT9Cq)~W7c*}OUq^#)iCv6wJIe5^VJ1tePD+&@lUu4mom8eQZ(7pn^*Xx-LBKzZl zc*iu~?&1($M8;~ZRwJyB*fQ#1-F3p=j$<2yogjwWmT|WTqgq`Fu}#9D7_8ao@->Mp ziTj8L)hA`XCR6~3=K}0ES0TjLAx(0M`GJv@l=wZp&g@!#+35Nu&vQM1`=$MdrSitQ zX4-ag_H+bm#@|1zm3Rk>fgYQJhfHNQE1*oe@7X+Rda4N(&;`%IU}2yP@&n>3{`YDy nAvT3(t^&)Q@JL%|)A;i#$NHfsX&H^C`KND9GE0j5>k9k=g6Wcq literal 0 HcmV?d00001 diff --git a/tests/bins/pe/well_formed_import.exe.bin b/tests/bins/pe/well_formed_import.exe.bin new file mode 100644 index 0000000000000000000000000000000000000000..1336060fa999803dc80186180d300f8a98454e9c GIT binary patch literal 3584 zcmeHJziU%b6h29%wAI86f&oQuDpg zC+e|>IdUF9ed2Wd;_EoQnmgQ%d6Opx)w;_Cm2$C=`G}kI%5z=Pm*33}VZ`UN8pP)VyoJ(5nka*eWn$ zErhJv<(51XVhQbo4qEp!@=RiVgU)GeK&x<$u@6`a6Ak*|_w*#XUJWv(R*?36&nLQ- z&fQ68mX~a|ULUL+A92s*?;rO{{SGc_pVeNL^ChP?nH@-k-}7W%aeAvU9dMM+QFpGJ U=q9_+D*5-SJEmrunwSp!27y+kSO5S3 literal 0 HcmV?d00001 From 48da3d867b47173b19072b30bd3ef21e7d0215ba Mon Sep 17 00:00:00 2001 From: kkent030315 Date: Mon, 18 Nov 2024 16:59:51 +0900 Subject: [PATCH 11/11] PE: Use OFTs for resolving imports without FTs (#430) * PE: Use OFTs for resolving imports without FTs --- src/pe/import.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/pe/import.rs b/src/pe/import.rs index 11c3ee7c..31343234 100644 --- a/src/pe/import.rs +++ b/src/pe/import.rs @@ -271,18 +271,25 @@ impl<'a> SyntheticImportDirectoryEntry<'a> { } }; - let import_address_table_offset = &mut utils::find_offset( - import_directory_entry.import_address_table_rva as usize, - sections, - file_alignment, - opts, - ) - .ok_or_else(|| { - error::Error::Malformed(format!( - "Cannot map import_address_table_rva {:#x} into offset for {}", - import_directory_entry.import_address_table_rva, name - )) - })?; + let rva = match import_directory_entry.import_address_table_rva.is_zero() { + true => import_directory_entry.import_lookup_table_rva, + false => import_directory_entry.import_address_table_rva, + }; + + let import_address_table_offset = + &mut utils::find_offset(rva as usize, sections, file_alignment, opts).ok_or_else( + || { + let target = if import_directory_entry.import_address_table_rva.is_zero() { + "import_lookup_table_rva" + } else { + "import_address_table_rva" + }; + error::Error::Malformed(format!( + "Cannot map {} {:#x} into offset for {}", + target, rva, name + )) + }, + )?; let mut import_address_table = Vec::new(); loop { let import_address = bytes