diff options
Diffstat (limited to 'third_party/rust/goblin/src/pe/mod.rs')
-rw-r--r-- | third_party/rust/goblin/src/pe/mod.rs | 249 |
1 files changed, 225 insertions, 24 deletions
diff --git a/third_party/rust/goblin/src/pe/mod.rs b/third_party/rust/goblin/src/pe/mod.rs index 5a7337630a..2336fddc57 100644 --- a/third_party/rust/goblin/src/pe/mod.rs +++ b/third_party/rust/goblin/src/pe/mod.rs @@ -3,7 +3,12 @@ // TODO: panics with unwrap on None for apisetschema.dll, fhuxgraphics.dll and some others +use core::cmp::max; + +use alloc::borrow::Cow; +use alloc::string::String; use alloc::vec::Vec; +use log::warn; pub mod authenticode; pub mod certificate_table; @@ -23,8 +28,11 @@ pub mod utils; use crate::container; use crate::error; +use crate::pe::utils::pad; use crate::strtab; +use scroll::{ctx, Pwrite}; + use log::debug; #[derive(Debug)] @@ -140,7 +148,7 @@ impl<'a> PE<'a> { entry, image_base, is_64 ); let file_alignment = optional_header.windows_fields.file_alignment; - if let Some(export_table) = *optional_header.data_directories.get_export_table() { + if let Some(&export_table) = optional_header.data_directories.get_export_table() { if let Ok(ed) = export::ExportData::parse_with_opts( bytes, export_table, @@ -162,7 +170,7 @@ impl<'a> PE<'a> { } } debug!("exports: {:#?}", exports); - if let Some(import_table) = *optional_header.data_directories.get_import_table() { + if let Some(&import_table) = optional_header.data_directories.get_import_table() { let id = if is_64 { import::ImportData::parse_with_opts::<u64>( bytes, @@ -196,7 +204,7 @@ impl<'a> PE<'a> { import_data = Some(id); } debug!("imports: {:#?}", imports); - if let Some(debug_table) = *optional_header.data_directories.get_debug_table() { + if let Some(&debug_table) = optional_header.data_directories.get_debug_table() { debug_data = Some(debug::DebugData::parse_with_opts( bytes, debug_table, @@ -209,8 +217,8 @@ impl<'a> PE<'a> { if header.coff_header.machine == header::COFF_MACHINE_X86_64 { // currently only x86_64 is supported debug!("exception data: {:#?}", exception_data); - if let Some(exception_table) = - *optional_header.data_directories.get_exception_table() + if let Some(&exception_table) = + optional_header.data_directories.get_exception_table() { exception_data = Some(exception::ExceptionData::parse_with_opts( bytes, @@ -222,26 +230,30 @@ impl<'a> PE<'a> { } } - let certtable = if let Some(certificate_table) = - *optional_header.data_directories.get_certificate_table() - { - certificates = certificate_table::enumerate_certificates( - bytes, - certificate_table.virtual_address, - certificate_table.size, - )?; + // Parse attribute certificates unless opted out of + let certificate_table_size = if opts.parse_attribute_certificates { + if let Some(&certificate_table) = + optional_header.data_directories.get_certificate_table() + { + certificates = certificate_table::enumerate_certificates( + bytes, + certificate_table.virtual_address, + 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 { + 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 { @@ -265,6 +277,192 @@ impl<'a> PE<'a> { certificates, }) } + + pub fn write_sections( + &self, + bytes: &mut [u8], + offset: &mut usize, + file_alignment: Option<usize>, + ctx: scroll::Endian, + ) -> Result<usize, error::Error> { + // sections table and data + debug_assert!( + self.sections + .iter() + .flat_map(|section_a| { + self.sections + .iter() + .map(move |section_b| (section_a, section_b)) + }) + // given sections = (s_1, …, s_n) + // for all (s_i, s_j), i != j, verify that s_i does not overlap with s_j and vice versa. + .all(|(section_i, section_j)| section_i == section_j + || !section_i.overlaps_with(section_j)), + "Overlapping sections were found, this is not supported." + ); + + for section in &self.sections { + let section_data = section.data(&self.bytes)?.ok_or_else(|| { + error::Error::Malformed(format!( + "Section data `{}` is malformed", + section.name().unwrap_or("unknown name") + )) + })?; + let file_section_offset = + usize::try_from(section.pointer_to_raw_data).map_err(|_| { + error::Error::Malformed(format!( + "Section `{}`'s pointer to raw data does not fit in platform `usize`", + section.name().unwrap_or("unknown name") + )) + })?; + let vsize: usize = section.virtual_size.try_into()?; + let ondisk_size: usize = section.size_of_raw_data.try_into()?; + let section_name = String::from(section.name().unwrap_or("unknown name")); + + let mut file_offset = file_section_offset; + // `file_section_offset` is a on-disk offset which can be anywhere in the file. + // Write section data first to avoid the final consumption. + match section_data { + Cow::Borrowed(borrowed) => bytes.gwrite(borrowed, &mut file_offset)?, + Cow::Owned(owned) => bytes.gwrite(owned.as_slice(), &mut file_offset)?, + }; + + // Section tables follows the header. + bytes.gwrite_with(section, offset, ctx)?; + + // for size size_of_raw_data + // if < virtual_size, pad with 0 + // Pad with zeros if necessary + if file_offset < vsize { + bytes.gwrite(vec![0u8; vsize - file_offset].as_slice(), &mut file_offset)?; + } + + // Align on a boundary as per file alignement field. + if let Some(pad) = pad(file_offset - file_section_offset, file_alignment) { + debug!( + "aligning `{}` {:#x} -> {:#x} bytes'", + section_name, + file_offset - file_section_offset, + file_offset - file_section_offset + pad.len() + ); + bytes.gwrite(pad.as_slice(), &mut file_offset)?; + } + + let written_data_size = file_offset - file_section_offset; + if ondisk_size != written_data_size { + warn!("Original PE is inefficient or bug (on-disk data size in PE: {:#x}), we wrote {:#x} bytes", + ondisk_size, + written_data_size); + } + } + + Ok(*offset) + } + + pub fn write_certificates( + &self, + bytes: &mut [u8], + ctx: scroll::Endian, + ) -> Result<usize, error::Error> { + let opt_header = self + .header + .optional_header + .ok_or(error::Error::Malformed(format!( + "This PE binary has no optional header; it is required to write certificates" + )))?; + let mut max_offset = 0; + + if let Some(certificate_directory) = opt_header.data_directories.get_certificate_table() { + let mut certificate_start = certificate_directory.virtual_address.try_into()?; + for certificate in &self.certificates { + bytes.gwrite_with(certificate, &mut certificate_start, ctx)?; + max_offset = max(certificate_start, max_offset); + } + } + + Ok(max_offset) + } +} + +impl<'a> ctx::TryIntoCtx<scroll::Endian> for PE<'a> { + type Error = error::Error; + + fn try_into_ctx(self, bytes: &mut [u8], ctx: scroll::Endian) -> Result<usize, Self::Error> { + let mut offset = 0; + // We need to maintain a `max_offset` because + // we could be writing sections in the wrong order (i.e. not an increasing order for the + // pointer on raw disk) + // and there could be holes between sections. + // If we don't re-layout sections, we cannot fix that ourselves. + // Same can be said about the certificate table, there could be a hole between sections + // and the certificate data. + // To avoid those troubles, we maintain the max over all offsets we see so far. + let mut max_offset = 0; + let file_alignment: Option<usize> = match self.header.optional_header { + Some(opt_header) => { + debug_assert!( + opt_header.windows_fields.file_alignment.count_ones() == 1, + "file alignment should be a power of 2" + ); + Some(opt_header.windows_fields.file_alignment.try_into()?) + } + _ => None, + }; + bytes.gwrite_with(self.header, &mut offset, ctx)?; + max_offset = max(offset, max_offset); + self.write_sections(bytes, &mut offset, file_alignment, ctx)?; + // We want the section offset for which we have the highest pointer on disk. + // The next offset is reserved for debug tables (outside of sections) and/or certificate + // tables. + max_offset = max( + self.sections + .iter() + .max_by_key(|section| section.pointer_to_raw_data as usize) + .map(|section| (section.pointer_to_raw_data + section.size_of_raw_data) as usize) + .unwrap_or(offset), + max_offset, + ); + + // COFF Symbol Table + // Auxiliary Symbol Records + // COFF String Table + assert!( + self.header.coff_header.pointer_to_symbol_table == 0, + "Symbol tables in PE are deprecated and not supported to write" + ); + + // The following data directories are + // taken care inside a section: + // - export table (.edata) + // - import table (.idata) + // - bound import table + // - import address table + // - delay import tables + // - resource table (.rsrc) + // - exception table (.pdata) + // - base relocation table (.reloc) + // - debug table (.debug) <- this one is special, it can be outside of a + // section. + // - load config table + // - tls table (.tls) + // - architecture (reserved, 0 for now) + // - global ptr is a "empty" data directory (header-only) + // - clr runtime header (.cormeta is object-only) + // + // Nonetheless, we need to write the attribute certificate table one. + max_offset = max(max_offset, self.write_certificates(bytes, ctx)?); + + // TODO: we would like to support debug table outside of a section. + // i.e. debug tables that are never mapped in memory + // See https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#debug-directory-image-only + // > The debug directory can be in a discardable .debug section (if one exists), or it can be included in any other section in the image file, or not be in a section at all. + // In case it's not in a section at all, we need to find a way + // to rewrite it again. + // and we need to respect the ordering between attribute certificates + // and debug table. + + Ok(max_offset) + } } /// An analyzed COFF object @@ -274,10 +472,12 @@ pub struct Coff<'a> { pub header: header::CoffHeader, /// A list of the sections in this COFF binary pub sections: Vec<section_table::SectionTable>, - /// The COFF symbol table. - pub symbols: symbol::SymbolTable<'a>, - /// The string table. - pub strings: strtab::Strtab<'a>, + /// The COFF symbol table, they are not guaranteed to exist. + /// For an image, this is expected to be None as COFF debugging information + /// has been deprecated. + pub symbols: Option<symbol::SymbolTable<'a>>, + /// The string table, they don't exist if COFF symbol table does not exist. + pub strings: Option<strtab::Strtab<'a>>, } impl<'a> Coff<'a> { @@ -414,7 +614,7 @@ mod tests { #[test] fn string_table_excludes_length() { let coff = Coff::parse(&&COFF_FILE_SINGLE_STRING_IN_STRING_TABLE[..]).unwrap(); - let string_table = coff.strings.to_vec().unwrap(); + let string_table = coff.strings.unwrap().to_vec().unwrap(); assert!(string_table == vec!["ExitProcess"]); } @@ -422,9 +622,10 @@ mod tests { #[test] fn symbol_name_excludes_length() { let coff = Coff::parse(&COFF_FILE_SINGLE_STRING_IN_STRING_TABLE).unwrap(); - let strings = coff.strings; + let strings = coff.strings.unwrap(); let symbols = coff .symbols + .unwrap() .iter() .filter(|(_, name, _)| name.is_none()) .map(|(_, _, sym)| sym.name(&strings).unwrap().to_owned()) |