diff --git a/src/pe.rs b/src/pe.rs index f42f83b4..e68e1ae5 100644 --- a/src/pe.rs +++ b/src/pe.rs @@ -237,6 +237,20 @@ pub struct ImageVxdHeader { pub e32_ddkver: U16, } +/// A PE rich header entry +/// +/// Rich headers have no official documentation, but have been heavily reversed-engineered and documented in the wild, e.g.: +/// * `http://www.ntcore.com/files/richsign.htm` +/// * `https://www.researchgate.net/figure/Structure-of-the-Rich-Header_fig1_318145388` +/// +/// These data are "masked", i.e. XORed with a rich header mask, that is file-specific. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct MaskedRichHeaderEntry { + pub masked_comp_id: U32, + pub masked_count: U32, +} + // // File header format. // @@ -2954,4 +2968,5 @@ unsafe_impl_pod!( ImageArchitectureEntry, ImportObjectHeader, ImageCor20Header, + MaskedRichHeaderEntry, ); diff --git a/src/read/pe/file.rs b/src/read/pe/file.rs index 15f7d819..4e883dc8 100644 --- a/src/read/pe/file.rs +++ b/src/read/pe/file.rs @@ -5,6 +5,7 @@ use core::{mem, str}; use core::convert::TryInto; use crate::read::coff::{CoffCommon, CoffSymbol, CoffSymbolIterator, CoffSymbolTable, SymbolTable}; +use crate::read::pe::RichHeaderInfos; use crate::read::{ self, Architecture, ComdatKind, Error, Export, FileFlags, Import, NoDynamicRelocationIterator, Object, ObjectComdat, ObjectKind, ReadError, ReadRef, Result, SectionIndex, SymbolIndex, @@ -77,6 +78,11 @@ where self.nt_headers } + /// Returns infos about the rich header of this file (if any) + pub fn rich_header_infos(&self) -> Option { + RichHeaderInfos::parse(self.data, self.dos_header.nt_headers_offset().into()) + } + /// Returns the section table of this binary. pub fn section_table(&self) -> SectionTable<'data> { self.common.sections diff --git a/src/read/pe/mod.rs b/src/read/pe/mod.rs index 6873665c..97c0901a 100644 --- a/src/read/pe/mod.rs +++ b/src/read/pe/mod.rs @@ -22,4 +22,7 @@ pub use export::*; mod import; pub use import::*; +mod rich; +pub use rich::*; + pub use super::coff::{SectionTable, SymbolTable}; diff --git a/src/read/pe/rich.rs b/src/read/pe/rich.rs new file mode 100644 index 00000000..d3bd61ad --- /dev/null +++ b/src/read/pe/rich.rs @@ -0,0 +1,102 @@ +//! PE rich header handling + +use alloc::vec::Vec; + +use crate::{pe, LittleEndian as LE, ReadRef, U32}; + +/// Extracted infos about a possible Rich Header +#[derive(Debug, Clone, Copy)] +pub struct RichHeaderInfos<'data> { + /// The offset at which the rich header starts + pub start: usize, + /// The length (in bytes) of the rich header. + /// This includes the payload, but also the 16-byte start sequence and the 8-byte final "Rich" and XOR key + pub length: usize, + /// The data used to mask the rich header. + /// Unless the file has been tampered with, it should be equal to a checksum of the file header + pub mask: u32, + masked_entries: &'data [pe::MaskedRichHeaderEntry], +} + +/// A PE rich header entry after it has been unmasked. +/// +/// See [`crate::pe::MaskedRichHeaderEntry`] +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct RichHeaderEntry { + /// ID of the component + pub comp_id: u32, + /// Number of times this component has been used when building this PE + pub count: u32, +} + +impl<'data> RichHeaderInfos<'data> { + /// Try to detect a rich header in the current PE file, and locate its [`crate::pe::MaskedRichHeaderEntry`]s + pub fn parse>(data: R, nt_header_offset: u64) -> Option { + const END_MARKER: &[u8] = &[0x52, 0x69, 0x63, 0x68]; // "Rich" + const CLEARTEXT_START_MARKER: u32 = 0x536e6144; // little-endian "DanS" + + // Locate the rich header, if any + // It ends with the ASCII 'Rich' string, before the NT header + // It starts at the start marker (a masked ASCII 'DanS' string) + let all_headers = data.read_bytes_at(0, nt_header_offset).ok()?; + + let dos_and_rich_header = read_bytes_until_u32_sequence(all_headers, END_MARKER).ok()?; + + let xor_key = data + .read_at::>(dos_and_rich_header.len() as u64 + 4) + .ok()?; + + let marker = U32::new(LE, CLEARTEXT_START_MARKER ^ xor_key.get(LE)); + let mut start_sequence: Vec = Vec::with_capacity(16); + start_sequence.extend_from_slice(crate::pod::bytes_of(&marker)); + start_sequence.extend_from_slice(crate::pod::bytes_of(xor_key)); + start_sequence.extend_from_slice(crate::pod::bytes_of(xor_key)); + start_sequence.extend_from_slice(crate::pod::bytes_of(xor_key)); + + let rich_header_start = + match read_bytes_until_u32_sequence(dos_and_rich_header, &start_sequence) { + Err(()) => return None, + Ok(slice) => slice.len(), + }; + let rh_len = dos_and_rich_header.len() - rich_header_start + 8/* for the "Rich" marker and the XOR key */; + + // Extract the contents of the rich header + let items_start = rich_header_start + start_sequence.len(); + let items_len = rh_len - start_sequence.len() - 8; + let item_count = items_len / core::mem::size_of::(); + let items = + match data.read_slice_at::(items_start as u64, item_count) { + Err(()) => return None, + Ok(items) => items, + }; + Some(RichHeaderInfos { + start: rich_header_start, + length: rh_len, + mask: xor_key.get(LE), + masked_entries: items, + }) + } + + /// Creates a new vector of unmasked entries + pub fn unmasked_entries(&self) -> Vec { + self.masked_entries + .iter() + .map(|entry| RichHeaderEntry { + comp_id: entry.masked_comp_id.get(LE) ^ self.mask, + count: entry.masked_count.get(LE) ^ self.mask, + }) + .collect() + } +} + +/// Read bytes until a sequence of u32-aligned values +fn read_bytes_until_u32_sequence<'a>(data: &'a [u8], needle: &[u8]) -> Result<&'a [u8], ()> { + const U32_SIZE: usize = core::mem::size_of::(); + + data.windows(needle.len()) + .step_by(U32_SIZE) + .position(|window| window == needle) + .ok_or(()) + .and_then(|n_steps| data.read_bytes_at(0, (n_steps * U32_SIZE) as u64)) +}