From 1f1885ae7c00e748dc0cc088697309b4a2d5d175 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Fri, 8 Dec 2023 23:42:02 -0800 Subject: [PATCH] authenticode: rework the parser according to the specification --- src/pe/authenticode.rs | 191 ++++++++++++++++++++++++++++++++++++----- src/pe/mod.rs | 13 ++- 2 files changed, 174 insertions(+), 30 deletions(-) diff --git a/src/pe/authenticode.rs b/src/pe/authenticode.rs index 399448357..f465b5381 100644 --- a/src/pe/authenticode.rs +++ b/src/pe/authenticode.rs @@ -8,9 +8,11 @@ // - data directory entry for certtable // - certtable -use core::ops::Range; +use alloc::collections::VecDeque; +use core::{mem, ops::Range}; +use log::debug; -use super::PE; +use super::{section_table::SectionTable, PE}; static PADDING: [u8; 7] = [0; 7]; @@ -31,19 +33,22 @@ impl PE<'_> { pub(super) struct ExcludedSections { checksum: Range, datadir_entry_certtable: Range, - certtable: Option>, + certificate_table_size: usize, + end_image_header: usize, } impl ExcludedSections { pub(super) fn new( checksum: Range, datadir_entry_certtable: Range, - certtable: Option>, + certificate_table_size: usize, + end_image_header: usize, ) -> Self { Self { checksum, datadir_entry_certtable, - certtable, + certificate_table_size, + end_image_header, } } } @@ -56,11 +61,24 @@ pub struct ExcludedSectionsIter<'s> { #[derive(Debug, PartialEq)] enum IterState { Initial, - DatadirEntry(usize), - CertTable(usize), - Final(usize), + ChecksumEnd(usize), + CertificateTableEnd(usize), + HeaderEnd { + end_image_header: usize, + sum_of_bytes_hashed: usize, + }, + Sections { + sections: VecDeque, + tail: usize, + sum_of_bytes_hashed: usize, + }, + Final { + sum_of_bytes_hashed: usize, + }, Padding(usize), Done, + + Pending, } impl Default for IterState { @@ -77,37 +95,164 @@ impl<'s> Iterator for ExcludedSectionsIter<'s> { if let Some(sections) = self.pe.authenticode_excluded_sections.as_ref() { loop { - match self.state { + match mem::replace(&mut self.state, IterState::Pending) { IterState::Initial => { - self.state = IterState::DatadirEntry(sections.checksum.end); - return Some(&bytes[..sections.checksum.start]); + // 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::DatadirEntry(start) => { - self.state = IterState::CertTable(sections.datadir_entry_certtable.end); - return Some(&bytes[start..sections.datadir_entry_certtable.start]); + 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. + 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.state = IterState::Sections { + sections, + tail: end_image_header, + sum_of_bytes_hashed, + }; } - IterState::CertTable(start) => { - if let Some(certtable) = sections.certtable.as_ref() { - self.state = IterState::Final(certtable.end); - return Some(&bytes[start..certtable.start]); + IterState::Sections { + mut 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) = 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 { + sections, + tail, + sum_of_bytes_hashed, + }; + + return Some(buf); } else { - self.state = IterState::Final(start) + self.state = IterState::Final { + sum_of_bytes_hashed, + }; } } - IterState::Final(start) => { - let buf = &bytes[start..]; - self.state = IterState::Padding(buf.len()); - return Some(buf); + 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 > 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]; + + self.state = IterState::Padding(buf.len()); + return Some(buf); + } else { + self.state = IterState::Done; + } } IterState::Padding(hash_size) => { self.state = IterState::Done; if hash_size % 8 != 0 { let pad_size = 8 - hash_size % 8; + + debug!("hashing {hash_size:#x} {pad_size:#x}"); return Some(&PADDING[..pad_size]); } } IterState::Done => return None, + + IterState::Pending => { + panic!("implementation error, state machine left in pending state"); + } } } } else { diff --git a/src/pe/mod.rs b/src/pe/mod.rs index 935136198..2336fddc5 100644 --- a/src/pe/mod.rs +++ b/src/pe/mod.rs @@ -231,7 +231,7 @@ impl<'a> PE<'a> { } // Parse attribute certificates unless opted out of - let certtable = if opts.parse_attribute_certificates { + let certificate_table_size = if opts.parse_attribute_certificates { if let Some(&certificate_table) = optional_header.data_directories.get_certificate_table() { @@ -241,20 +241,19 @@ impl<'a> PE<'a> { certificate_table.size, )?; - let start = certificate_table.virtual_address as usize; - let end = start + certificate_table.size as usize; - Some(start..end) + certificate_table.size as usize } else { - None + 0 } } else { - None + 0 }; authenticode_excluded_sections = Some(authenticode::ExcludedSections::new( checksum, datadir_entry_certtable, - certtable, + certificate_table_size, + optional_header.windows_fields.size_of_headers as usize, )); } Ok(PE {