From a2f65e32dcfcfcd99e4051ac98079ba44257fcff Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Thu, 9 Mar 2023 21:54:55 -0800 Subject: [PATCH] pe: add authenticode support Authenticode is the hashing format used to sign PE binaries. This provides the hash to be signed. --- Cargo.toml | 3 + src/pe/authenticode.rs | 129 ++++++++++++++++++++++++++++++++++++++ src/pe/mod.rs | 75 +++++++++++++++++++++- src/pe/optional_header.rs | 4 ++ 4 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 src/pe/authenticode.rs diff --git a/Cargo.toml b/Cargo.toml index d1cc797c..0d95bcfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ description = "An impish, cross-platform, ELF, Mach-o, and PE binary parsing and [dependencies] plain = "0.2.3" +digest = { version = "0.10.6", optional = true } [dependencies.log] version = "0.4" @@ -50,6 +51,8 @@ mach64 = ["alloc", "endian_fd", "archive"] pe32 = ["alloc", "endian_fd"] pe64 = ["alloc", "endian_fd"] archive = ["alloc"] +pe_source = [] +pe_authenticode = ["pe_source", "digest"] [badges.travis-ci] branch = "master" diff --git a/src/pe/authenticode.rs b/src/pe/authenticode.rs new file mode 100644 index 00000000..bb7bdca2 --- /dev/null +++ b/src/pe/authenticode.rs @@ -0,0 +1,129 @@ +// Reference: +// https://learn.microsoft.com/en-us/windows-hardware/drivers/install/authenticode +// https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx + +// Authenticode works by omiting sections of the PE binary from the digest +// those sections are: +// - checksum +// - data directory entry for certtable +// - certtable + +use alloc::{boxed::Box, vec::Vec}; +use core::ops::Range; +use digest::{Digest, Output}; + +use super::PE; + +impl PE<'_> { + /// [`authenticode_ranges`] returns the various ranges of the binary that are relevant for + /// signature. + fn authenticode_ranges(&self) -> ExcludedAuthenticodeSectionsIter<'_> { + ExcludedAuthenticodeSectionsIter { + pe: self, + state: IterState::default(), + } + } + + /// [`authenticode_digest`] returns the result of the provided hash algorithm. + pub fn authenticode_digest(&self) -> Output { + let mut digest = D::new(); + + for chunk in self.authenticode_ranges() { + digest.update(chunk); + } + + digest.finalize() + } + + /// [`authenticode_slice`] is intended for convenience when signing a binary with a PKCS#11 + /// interface (HSM interface). + /// Some algorithms (RSA-PKCS at least) for signature require the non-prehashed slice to be provided. + pub fn authenticode_slice(&self) -> Box<[u8]> { + // PE may be 70-80MB large (especially for linux UKIs). We'll get the length beforehand as + // it's cheaper than getting Vec to realloc and move stuff around multiple times. + let mut length = 0; + for chunk in self.authenticode_ranges() { + length += chunk.len(); + } + + let mut out = Vec::with_capacity(length); + for chunk in self.authenticode_ranges() { + out.extend_from_slice(chunk); + } + + out.into() + } +} + +/// [`ExcludedAuthenticodeSections`] holds the various ranges of the binary that are expected to be +/// excluded from the authenticode computation. +#[derive(Debug, Clone, Default)] +pub(super) struct ExcludedAuthenticodeSections { + pub checksum: Range, + pub datadir_entry_certtable: Range, + pub certtable: Option>, +} + +pub struct ExcludedAuthenticodeSectionsIter<'s> { + pe: &'s PE<'s>, + state: IterState, +} + +#[derive(Default, Debug, PartialEq)] +enum IterState { + #[default] + Initial, + DatadirEntry(usize), + CertTable(usize), + Final(usize), + Done, +} + +impl<'s> Iterator for ExcludedAuthenticodeSectionsIter<'s> { + type Item = &'s [u8]; + + fn next(&mut self) -> Option { + let bytes = &self.pe.bytes; + + if let Some(sections) = self.pe.excluded_authenticode_sections.as_ref() { + loop { + match self.state { + IterState::Initial => { + self.state = IterState::DatadirEntry(sections.checksum.end); + return Some(&bytes[..sections.checksum.start]); + } + IterState::DatadirEntry(start) => { + self.state = IterState::CertTable(sections.datadir_entry_certtable.end); + return Some(&bytes[start..sections.datadir_entry_certtable.start]); + } + IterState::CertTable(start) => { + if let Some(certtable) = sections.certtable.as_ref() { + self.state = IterState::Final(certtable.end); + return Some(&bytes[start..certtable.start]); + } else { + self.state = IterState::Final(start) + } + } + IterState::Final(start) => { + self.state = IterState::Done; + return Some(&bytes[start..]); + } + IterState::Done => return None, + } + } + } else { + loop { + match self.state { + IterState::Initial => { + self.state = IterState::Done; + return Some(bytes); + } + IterState::Done => return None, + _ => { + self.state = IterState::Done; + } + } + } + } + } +} diff --git a/src/pe/mod.rs b/src/pe/mod.rs index f0df3c7e..2808c2f5 100644 --- a/src/pe/mod.rs +++ b/src/pe/mod.rs @@ -5,6 +5,8 @@ use alloc::vec::Vec; +#[cfg(feature = "pe_authenticode")] +pub mod authenticode; pub mod certificate_table; pub mod characteristic; pub mod data_directories; @@ -29,6 +31,11 @@ use log::debug; #[derive(Debug)] /// An analyzed PE32/PE32+ binary pub struct PE<'a> { + #[cfg(feature = "pe_source")] + /// Underlying bytes + bytes: &'a [u8], + #[cfg(feature = "pe_authenticode")] + excluded_authenticode_sections: Option, /// The PE header pub header: header::Header, /// A list of the sections in this PE binary @@ -72,11 +79,16 @@ impl<'a> PE<'a> { /// Reads a PE binary from the underlying `bytes` pub fn parse_with_opts(bytes: &'a [u8], opts: &options::ParseOptions) -> error::Result { let header = header::Header::parse(bytes)?; + #[cfg(feature = "pe_authenticode")] + let mut excluded_authenticode_sections = None; + debug!("{:#?}", header); - let offset = &mut (header.dos_header.pe_pointer as usize + let optional_header_offset = header.dos_header.pe_pointer as usize + header::SIZEOF_PE_MAGIC - + header::SIZEOF_COFF_HEADER - + header.coff_header.size_of_optional_header as usize); + + header::SIZEOF_COFF_HEADER; + let offset = + &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize); + let sections = header.coff_header.sections(bytes, offset)?; let is_lib = characteristic::is_dll(header.coff_header.characteristics); let mut entry = 0; @@ -92,6 +104,47 @@ impl<'a> PE<'a> { let mut certificates = Default::default(); let mut is_64 = false; if let Some(optional_header) = header.optional_header { + #[cfg(feature = "pe_authenticode")] + // Sections we are assembling through the parsing, eventually, it will be passed + // to the authenticode_sections attribute of `PE`. + let mut excluded_sections = { + let (checksum, datadir_entry_certtable) = + match optional_header.standard_fields.magic { + optional_header::MAGIC_32 => { + let standard_field_offset = + optional_header_offset + optional_header::SIZEOF_STANDARD_FIELDS_32; + let checksum_field_offset = standard_field_offset + + optional_header::OFFSET_WINDOWS_FIELDS_32_CHECKSUM; + ( + checksum_field_offset..checksum_field_offset + 4, + optional_header_offset + 128..optional_header_offset + 136, + ) + } + optional_header::MAGIC_64 => { + let standard_field_offset = + optional_header_offset + optional_header::SIZEOF_STANDARD_FIELDS_64; + let checksum_field_offset = standard_field_offset + + optional_header::OFFSET_WINDOWS_FIELDS_64_CHECKSUM; + + ( + checksum_field_offset..checksum_field_offset + 4, + optional_header_offset + 144..optional_header_offset + 152, + ) + } + magic => { + return Err(error::Error::Malformed(format!( + "Unsupported header magic ({magic:#x})" + ))) + } + }; + + authenticode::ExcludedAuthenticodeSections { + checksum, + datadir_entry_certtable, + certtable: None, + } + }; + entry = optional_header.standard_fields.address_of_entry_point as usize; image_base = optional_header.windows_fields.image_base as usize; is_64 = optional_header.container()? == container::Container::Big; @@ -190,9 +243,25 @@ impl<'a> PE<'a> { certificate_table.virtual_address, certificate_table.size, )?; + + #[cfg(feature = "pe_authenticode")] + { + let start = certificate_table.virtual_address as usize; + let end = start + certificate_table.size as usize; + excluded_sections.certtable = Some(start..end); + } + } + + #[cfg(feature = "pe_authenticode")] + { + excluded_authenticode_sections = Some(excluded_sections); } } Ok(PE { + #[cfg(feature = "pe_source")] + bytes, + #[cfg(feature = "pe_authenticode")] + excluded_authenticode_sections, header, sections, size: 0, diff --git a/src/pe/optional_header.rs b/src/pe/optional_header.rs index 56523c53..b1d31927 100644 --- a/src/pe/optional_header.rs +++ b/src/pe/optional_header.rs @@ -120,6 +120,8 @@ pub struct WindowsFields32 { } pub const SIZEOF_WINDOWS_FIELDS_32: usize = 68; +/// Offset of the `check_sum` field in [`WindowsFields32`] +pub const OFFSET_WINDOWS_FIELDS_32_CHECKSUM: usize = 36; /// 64-bit Windows specific fields #[repr(C)] @@ -149,6 +151,8 @@ pub struct WindowsFields64 { } pub const SIZEOF_WINDOWS_FIELDS_64: usize = 88; +/// Offset of the `check_sum` field in [`WindowsFields64`] +pub const OFFSET_WINDOWS_FIELDS_64_CHECKSUM: usize = 40; // /// Generic 32/64-bit Windows specific fields // #[derive(Debug, PartialEq, Copy, Clone, Default)]