// 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::collections::VecDeque; use core::ops::Range; use log::debug; use super::{section_table::SectionTable, PE}; static PADDING: [u8; 7] = [0; 7]; impl PE<'_> { /// [`authenticode_ranges`] returns the various ranges of the binary that are relevant for /// signature. pub fn authenticode_ranges(&self) -> ExcludedSectionsIter<'_> { ExcludedSectionsIter { pe: self, state: IterState::default(), sections: VecDeque::default(), } } } /// [`ExcludedSections`] holds the various ranges of the binary that are expected to be /// excluded from the authenticode computation. #[derive(Debug, Clone, Default)] pub(super) struct ExcludedSections { checksum: Range, datadir_entry_certtable: Range, certificate_table_size: usize, end_image_header: usize, } impl ExcludedSections { pub(super) fn new( checksum: Range, datadir_entry_certtable: Range, certificate_table_size: usize, end_image_header: usize, ) -> Self { Self { checksum, datadir_entry_certtable, certificate_table_size, end_image_header, } } } pub struct ExcludedSectionsIter<'s> { pe: &'s PE<'s>, state: IterState, sections: VecDeque, } #[derive(Debug, PartialEq)] enum IterState { Initial, ChecksumEnd(usize), CertificateTableEnd(usize), HeaderEnd { end_image_header: usize, sum_of_bytes_hashed: usize, }, Sections { tail: usize, sum_of_bytes_hashed: usize, }, Final { sum_of_bytes_hashed: usize, }, Padding(usize), Done, } impl Default for IterState { fn default() -> Self { Self::Initial } } impl<'s> Iterator for ExcludedSectionsIter<'s> { type Item = &'s [u8]; fn next(&mut self) -> Option { let bytes = &self.pe.bytes; if let Some(sections) = self.pe.authenticode_excluded_sections.as_ref() { loop { match self.state { IterState::Initial => { // 3. Hash the image header from its base to immediately before the start of the // checksum address, as specified in Optional Header Windows-Specific Fields. let out = Some(&bytes[..sections.checksum.start]); debug!("hashing {:#x} {:#x}", 0, sections.checksum.start); // 4. Skip over the checksum, which is a 4-byte field. debug_assert_eq!(sections.checksum.end - sections.checksum.start, 4); self.state = IterState::ChecksumEnd(sections.checksum.end); return out; } IterState::ChecksumEnd(checksum_end) => { // 5. Hash everything from the end of the checksum field to immediately before the start // of the Certificate Table entry, as specified in Optional Header Data Directories. let out = Some(&bytes[checksum_end..sections.datadir_entry_certtable.start]); debug!( "hashing {checksum_end:#x} {:#x}", sections.datadir_entry_certtable.start ); // 6. Get the Attribute Certificate Table address and size from the Certificate Table entry. // For details, see section 5.7 of the PE/COFF specification. // 7. Exclude the Certificate Table entry from the calculation self.state = IterState::CertificateTableEnd(sections.datadir_entry_certtable.end); return out; } IterState::CertificateTableEnd(start) => { // 7. Exclude the Certificate Table entry from the calculation and hash everything from // the end of the Certificate Table entry to the end of image header, including // Section Table (headers). The Certificate Table entry is 8 bytes long, as specified // in Optional Header Data Directories. let end_image_header = sections.end_image_header; let buf = Some(&bytes[start..end_image_header]); debug!("hashing {start:#x} {:#x}", end_image_header - start); // 8. Create a counter called SUM_OF_BYTES_HASHED, which is not part of the signature. // Set this counter to the SizeOfHeaders field, as specified in // Optional Header Windows-Specific Field. let sum_of_bytes_hashed = end_image_header; self.state = IterState::HeaderEnd { end_image_header, sum_of_bytes_hashed, }; return buf; } IterState::HeaderEnd { end_image_header, sum_of_bytes_hashed, } => { // 9. Build a temporary table of pointers to all of the section headers in the // image. The NumberOfSections field of COFF File Header indicates how big // the table should be. Do not include any section headers in the table whose // SizeOfRawData field is zero. // Implementation detail: // We require allocation here because the section table has a variable size and // needs to be sorted. let mut sections: VecDeque = self .pe .sections .iter() .filter(|section| section.size_of_raw_data != 0) .cloned() .collect(); // 10. Using the PointerToRawData field (offset 20) in the referenced SectionHeader // structure as a key, arrange the table's elements in ascending order. In // other words, sort the section headers in ascending order according to the // disk-file offset of the sections. sections .make_contiguous() .sort_by_key(|section| section.pointer_to_raw_data); self.sections = sections; self.state = IterState::Sections { tail: end_image_header, sum_of_bytes_hashed, }; } IterState::Sections { mut tail, mut sum_of_bytes_hashed, } => { // 11. Walk through the sorted table, load the corresponding section into memory, // and hash the entire section. Use the SizeOfRawData field in the SectionHeader // structure to determine the amount of data to hash. if let Some(section) = self.sections.pop_front() { let start = section.pointer_to_raw_data as usize; let end = start + section.size_of_raw_data as usize; tail = end; // 12. Add the section’s SizeOfRawData value to SUM_OF_BYTES_HASHED. sum_of_bytes_hashed += section.size_of_raw_data as usize; debug!("hashing {start:#x} {:#x}", end - start); let buf = &bytes[start..end]; // 13. Repeat steps 11 and 12 for all of the sections in the sorted table. self.state = IterState::Sections { tail, sum_of_bytes_hashed, }; return Some(buf); } else { self.state = IterState::Final { sum_of_bytes_hashed, }; } } IterState::Final { sum_of_bytes_hashed, } => { // 14. Create a value called FILE_SIZE, which is not part of the signature. // Set this value to the image’s file size, acquired from the underlying // file system. If FILE_SIZE is greater than SUM_OF_BYTES_HASHED, the // file contains extra data that must be added to the hash. This data // begins at the SUM_OF_BYTES_HASHED file offset, and its length is: // (File Size) - ((Size of AttributeCertificateTable) + SUM_OF_BYTES_HASHED) // // Note: The size of Attribute Certificate Table is specified in the second // ULONG value in the Certificate Table entry (32 bit: offset 132, // 64 bit: offset 148) in Optional Header Data Directories. let file_size = bytes.len(); // If FILE_SIZE is not a multiple of 8 bytes, the data added to the hash must // be appended with zero padding of length (8 – (FILE_SIZE % 8)) bytes let pad_size = (8 - file_size % 8) % 8; self.state = IterState::Padding(pad_size); if file_size > sum_of_bytes_hashed { let extra_data_start = sum_of_bytes_hashed; let len = file_size - sections.certificate_table_size - sum_of_bytes_hashed; debug!("hashing {extra_data_start:#x} {len:#x}",); let buf = &bytes[extra_data_start..extra_data_start + len]; return Some(buf); } } IterState::Padding(pad_size) => { self.state = IterState::Done; if pad_size != 0 { debug!("hashing {pad_size:#x}"); // NOTE (safety): pad size will be at most 7, and PADDING has a size of 7 // pad_size is computed ~10 lines above. debug_assert!(pad_size <= 7); debug_assert_eq!(PADDING.len(), 7); return Some(&PADDING[..pad_size]); } } 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; } } } } } }