Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pe rich header #375

Merged
merged 12 commits into from
Sep 20, 2021
5 changes: 5 additions & 0 deletions src/endian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,11 @@ impl<E: Endian> U32Bytes<E> {
pub fn set(&mut self, e: E, n: u32) {
self.0 = e.write_u32_bytes(n);
}

/// Get the underlying representation of this value.
pub fn as_slice(&self) -> &[u8; 4] {
&self.0
}
daladim marked this conversation as resolved.
Show resolved Hide resolved
}

/// An unaligned `u64` value with an externally specified endianness of type `E`.
Expand Down
33 changes: 33 additions & 0 deletions src/pe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,37 @@ pub struct ImageVxdHeader {
pub e32_ddkver: U16<LE>,
}

/// 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. The mask is stored in the associated `read::pe::PeFile` instances
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct MaskedRichHeaderEntry {
pub masked_comp_id: U32<LE>,
pub masked_count: U32<LE>,
daladim marked this conversation as resolved.
Show resolved Hide resolved
}

/// A PE reach header entry after it has been unmasked.
///
/// See [`MaskedRichHeaderEntry`]
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RichHeaderEntry {
pub comp_id: u32,
pub count: u32,
}
daladim marked this conversation as resolved.
Show resolved Hide resolved

/// The checksum of a rich header
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RichCheckSum {
pub checksum: U32<LE>,
}
daladim marked this conversation as resolved.
Show resolved Hide resolved

//
// File header format.
//
Expand Down Expand Up @@ -2954,4 +2985,6 @@ unsafe_impl_pod!(
ImageArchitectureEntry,
ImportObjectHeader,
ImageCor20Header,
MaskedRichHeaderEntry,
RichCheckSum,
);
92 changes: 92 additions & 0 deletions src/read/pe/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use core::{mem, str};

use core::convert::TryInto;

use crate::pe::RichHeaderEntry;
use crate::read::coff::{CoffCommon, CoffSymbol, CoffSymbolIterator, CoffSymbolTable, SymbolTable};
use crate::read::{
self, Architecture, ComdatKind, Error, Export, FileFlags, Import, NoDynamicRelocationIterator,
Expand All @@ -21,6 +22,19 @@ pub type PeFile32<'data, R = &'data [u8]> = PeFile<'data, pe::ImageNtHeaders32,
/// A PE32+ (64-bit) image file.
pub type PeFile64<'data, R = &'data [u8]> = PeFile<'data, pe::ImageNtHeaders64, R>;

/// Extracted infos about a possible Rich Header
#[derive(Debug, Clone, Copy)]
pub struct RichHeaderInfos<'data> {
daladim marked this conversation as resolved.
Show resolved Hide resolved
/// The offset at which the rich header starts
pub start: usize,
/// The length (in bytes) of the rich header
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<LE>,
daladim marked this conversation as resolved.
Show resolved Hide resolved
masked_entries: &'data [pe::MaskedRichHeaderEntry],
}

/// A PE object file.
#[derive(Debug)]
pub struct PeFile<'data, Pe, R = &'data [u8]>
Expand All @@ -29,6 +43,7 @@ where
R: ReadRef<'data>,
{
pub(super) dos_header: &'data pe::ImageDosHeader,
pub(super) rich_header_infos: Option<RichHeaderInfos<'data>>,
pub(super) nt_headers: &'data Pe,
pub(super) data_directories: DataDirectories<'data>,
pub(super) common: CoffCommon<'data, R>,
Expand All @@ -44,13 +59,15 @@ where
pub fn parse(data: R) -> Result<Self> {
let dos_header = pe::ImageDosHeader::parse(data)?;
let mut offset = dos_header.nt_headers_offset().into();
let rich_header_infos = RichHeaderInfos::parse(data, offset);
daladim marked this conversation as resolved.
Show resolved Hide resolved
let (nt_headers, data_directories) = Pe::parse(data, &mut offset)?;
let sections = nt_headers.sections(data, offset)?;
let symbols = nt_headers.symbols(data)?;
let image_base = nt_headers.optional_header().image_base();

Ok(PeFile {
dos_header,
rich_header_infos,
nt_headers,
data_directories,
common: CoffCommon {
Expand All @@ -77,6 +94,11 @@ where
self.nt_headers
}

/// Returns infos about the rich header of this file (if any)
pub fn rich_header_infos(&self) -> Option<&'data RichHeaderInfos> {
self.rich_header_infos.as_ref()
}

/// Returns the section table of this binary.
pub fn section_table(&self) -> SectionTable<'data> {
self.common.sections
Expand Down Expand Up @@ -496,6 +518,76 @@ impl pe::ImageDosHeader {
}
}

impl<'data> RichHeaderInfos<'data> {
/// Try to detect a rich header in the current PE file, and locate its [`MaskedRichHeaderEntry`s]
fn parse<R: ReadRef<'data>>(data: R, nt_header_offset: u64) -> Option<Self> {
const RICH_SEQUENCE: &[u8] = &[0x52, 0x69, 0x63, 0x68]; // "Rich"
const CLEARTEXT_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 dos_and_rich_header =
match data.read_bytes_at_until_sequence(0, RICH_SEQUENCE, nt_header_offset as usize) {
Err(()) => return None,
Ok(slice) => slice,
};
daladim marked this conversation as resolved.
Show resolved Hide resolved

let xor_key = match data.read_at::<pe::RichCheckSum>(dos_and_rich_header.len() as u64 + 4) {
Err(()) => return None,
Ok(key) => key,
};

let mut start_marker: Vec<u8> = U32::<LE>::new(LE, CLEARTEXT_MARKER)
.as_slice()
.iter()
.zip(xor_key.checksum.as_slice())
.map(|(a, b)| *a ^ *b)
.collect();
daladim marked this conversation as resolved.
Show resolved Hide resolved
start_marker.extend_from_slice(xor_key.checksum.as_slice());
start_marker.extend_from_slice(xor_key.checksum.as_slice());
start_marker.extend_from_slice(xor_key.checksum.as_slice());

let rich_header_start =
match data.read_bytes_at_until_sequence(0, &start_marker, nt_header_offset as usize) {
Err(()) => return None,
Ok(slice) => slice.len(),
};
let rh_len = dos_and_rich_header.len() - rich_header_start;

// Extract the contents of the rich header
let items_start = rich_header_start + 16;
let items_len = rh_len - 16;
let item_count = items_len / std::mem::size_of::<pe::MaskedRichHeaderEntry>();
let items =
match data.read_slice_at::<pe::MaskedRichHeaderEntry>(items_start as u64, item_count) {
Err(()) => return None,
Ok(items) => items,
};
Some(RichHeaderInfos {
start: rich_header_start,
length: rh_len,
mask: xor_key.checksum,
masked_entries: items,
})
}

/// Creates a new vector of unmasked entries
pub fn unmasked_entries(&self) -> Vec<pe::RichHeaderEntry> {
self.masked_entries
.iter()
.map(|entry| RichHeaderEntry {
comp_id: Self::unmask_rich_header_item(entry.masked_comp_id, self.mask),
count: Self::unmask_rich_header_item(entry.masked_count, self.mask),
daladim marked this conversation as resolved.
Show resolved Hide resolved
})
.collect()
}

fn unmask_rich_header_item(item: U32<LE>, mask: U32<LE>) -> u32 {
item.get(LE) ^ mask.get(LE)
}
}

/// Find the optional header and read the `optional_header.magic`.
///
/// It can be useful to know this magic value before trying to
Expand Down
18 changes: 18 additions & 0 deletions src/read/read_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ impl<'a, R: Read + Seek> ReadRef<'a> for &'a ReadCache<R> {
// This is OK because we never mutate or remove entries.
Ok(unsafe { mem::transmute::<&[u8], &[u8]>(buf) })
}

fn read_bytes_at_until_sequence(
self,
offset: usize,
needle: &[u8],
max_end: usize,
) -> Result<&'a [u8], ()> {
todo!()
}
}

/// An implementation of `ReadRef` for a range of data in a stream that
Expand Down Expand Up @@ -182,4 +191,13 @@ impl<'a, R: Read + Seek> ReadRef<'a> for ReadCacheRange<'a, R> {
}
Ok(bytes)
}

fn read_bytes_at_until_sequence(
self,
offset: usize,
needle: &[u8],
max_end: usize,
) -> Result<&'a [u8], ()> {
todo!()
}
}
28 changes: 28 additions & 0 deletions src/read/read_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ pub trait ReadRef<'a>: Clone + Copy {
/// not found in the range.
fn read_bytes_at_until(self, range: Range<u64>, delimiter: u8) -> Result<&'a [u8]>;

/// Get a reference to a delimited `u8` slice which starts at range.start, ends before
/// the given final sequence and cannot end after `max_end`
///
/// Does not include the final sequence.
///
/// Returns an error if the range is out of bounds or the final sequence is
/// not found before `max_end`.
fn read_bytes_at_until_sequence(
daladim marked this conversation as resolved.
Show resolved Hide resolved
self,
offset: usize,
seq: &[u8],
max_end: usize,
) -> Result<&'a [u8]>;

/// Get a reference to a `u8` slice at the given offset, and update the offset.
///
/// Returns an error if offset or size are out of bounds.
Expand Down Expand Up @@ -134,4 +148,18 @@ impl<'a> ReadRef<'a> for &'a [u8] {
None => Err(()),
}
}

fn read_bytes_at_until_sequence(
self,
offset: usize,
needle: &[u8],
max_end: usize,
) -> Result<&'a [u8]> {
let sub: &[u8] = self.get(offset..max_end).ok_or(())?;

sub.windows(needle.len())
.position(|window| window == needle)
.ok_or(())
.and_then(|end| self.read_bytes_at(offset as u64, end as u64))
}
}