From 7686291321290609cee87cf759fb6407daac71a0 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Sun, 31 Mar 2024 17:45:59 -0700 Subject: [PATCH] pe: add Terse Executable (TE) support (#397) Add terse executable (TE) support to the PE module. A terse executable is a PE32/PE32+ binary with a reduced header size containing only the fields necessary for the binary to be properly executed by a PI architecture compliant loader and executor. Terse executables are most commonly used by UEFI compliant firmware to reduce the overall size of the binary. Only the header is replaced, and no other data is changed in a terse executable, resulting in all address values being invalid. The TE parser must take appropriate action to fix up addresses during parsing by adjusting the existing value by the difference between the `stripped_size` and the new size of the header. --- Cargo.toml | 5 +- README.md | 1 + src/lib.rs | 7 +- src/pe/debug.rs | 2 +- src/pe/header.rs | 143 ++++++++++++++++++++++++- src/pe/mod.rs | 92 ++++++++++++++++ tests/bins/te/README.md | 24 +++++ tests/bins/te/test_image.te | Bin 0 -> 28256 bytes tests/bins/te/test_image_loaded.bin | Bin 0 -> 24312 bytes tests/bins/te/test_image_relocated.bin | Bin 0 -> 24312 bytes tests/te.rs | 109 +++++++++++++++++++ 11 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 tests/bins/te/README.md create mode 100644 tests/bins/te/test_image.te create mode 100644 tests/bins/te/test_image_loaded.bin create mode 100644 tests/bins/te/test_image_relocated.bin create mode 100644 tests/te.rs diff --git a/Cargo.toml b/Cargo.toml index ba11f32fb..1b0f1ba3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ include = [ "LICENSE", "README.md", ] -keywords = ["binary", "elf", "mach", "pe", "archive"] +keywords = ["binary", "elf", "mach", "pe", "te", "archive"] license = "MIT" readme = "README.md" repository = "https://github.com/m4b/goblin" @@ -38,7 +38,7 @@ version = "0.12" default_features = false [features] -default = ["std", "elf32", "elf64", "mach32", "mach64", "pe32", "pe64", "archive", "endian_fd"] +default = ["std", "elf32", "elf64", "mach32", "mach64", "pe32", "pe64", "te", "archive", "endian_fd"] std = ["alloc", "scroll/std"] alloc = ["scroll/derive", "log"] endian_fd = ["alloc"] @@ -49,6 +49,7 @@ mach32 = ["alloc", "endian_fd", "archive"] mach64 = ["alloc", "endian_fd", "archive"] pe32 = ["alloc", "endian_fd"] pe64 = ["alloc", "endian_fd"] +te = ["alloc", "endian_fd"] archive = ["alloc"] [badges.travis-ci] diff --git a/README.md b/README.md index 30d30f3c9..e4680c223 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Here are some things you could do with this crate (or help to implement so they * mach32 - 32-bit mach-o `repr(C)` struct defs * pe32 - 32-bit PE `repr(C)` struct defs * pe64 - 64-bit PE `repr(C)` struct defs ++ te - Terse Executable (TE) `repr(C)` struct defs * archive - a Unix Archive parser * endian_fd - parses according to the endianness in the binary * std - to allow `no_std` environments diff --git a/src/lib.rs b/src/lib.rs index ec77f93d5..e811de1ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,6 +229,7 @@ pub enum Hint { Mach(HintData), MachFat(usize), PE, + TE, COFF, Archive, Unknown(u64), @@ -236,7 +237,7 @@ pub enum Hint { macro_rules! if_everything { ($($i:item)*) => ($( - #[cfg(all(feature = "endian_fd", feature = "elf64", feature = "elf32", feature = "pe64", feature = "pe32", feature = "mach64", feature = "mach32", feature = "archive"))] + #[cfg(all(feature = "endian_fd", feature = "elf64", feature = "elf32", feature = "pe64", feature = "pe32", feature = "te", feature = "mach64", feature = "mach32", feature = "archive"))] $i )*) } @@ -262,6 +263,7 @@ if_everything! { } else { match *&bytes[0..2].pread_with::(0, LE)? { pe::header::DOS_MAGIC => Ok(Hint::PE), + pe::header::TE_MAGIC => Ok(Hint::TE), pe::header::COFF_MACHINE_X86 | pe::header::COFF_MACHINE_X86_64 | pe::header::COFF_MACHINE_ARM64 => Ok(Hint::COFF), @@ -290,6 +292,8 @@ if_everything! { Elf(elf::Elf<'a>), /// A PE32/PE32+! PE(pe::PE<'a>), + /// A TE! + TE(pe::TE<'a>), /// A COFF COFF(pe::Coff<'a>), /// A 32/64-bit Mach-o binary _OR_ it is a multi-architecture binary container! @@ -309,6 +313,7 @@ if_everything! { Hint::Mach(_) | Hint::MachFat(_) => Ok(Object::Mach(mach::Mach::parse(bytes)?)), Hint::Archive => Ok(Object::Archive(archive::Archive::parse(bytes)?)), Hint::PE => Ok(Object::PE(pe::PE::parse(bytes)?)), + Hint::TE => Ok(Object::TE(pe::TE::parse(bytes)?)), Hint::COFF => Ok(Object::COFF(pe::Coff::parse(bytes)?)), Hint::Unknown(magic) => Ok(Object::Unknown(magic)), } diff --git a/src/pe/debug.rs b/src/pe/debug.rs index 311bc632c..cae77b4be 100644 --- a/src/pe/debug.rs +++ b/src/pe/debug.rs @@ -92,7 +92,7 @@ impl ImageDebugDirectory { ) } - fn parse_with_opts( + pub(crate) fn parse_with_opts( bytes: &[u8], dd: data_directories::DataDirectory, sections: &[section_table::SectionTable], diff --git a/src/pe/header.rs b/src/pe/header.rs index cb3b08faa..62bfe1906 100644 --- a/src/pe/header.rs +++ b/src/pe/header.rs @@ -1,5 +1,5 @@ use crate::error; -use crate::pe::{optional_header, section_table, symbol}; +use crate::pe::{data_directories, optional_header, section_table, symbol}; use crate::strtab; use alloc::vec::Vec; use log::debug; @@ -837,6 +837,147 @@ impl ctx::TryIntoCtx for Header { } } +/// The TE header is a reduced PE32/PE32+ header containing only fields +/// required for execution in the Platform Initialization +/// ([PI](https://uefi.org/specs/PI/1.8/V1_Introduction.html)) architecture. +/// The TE header is described in this specification: +/// +#[cfg(feature = "te")] +#[repr(C)] +#[derive(Debug, Default, PartialEq, Copy, Clone, Pread, Pwrite)] +pub struct TeHeader { + /// Te signature, always [TE_MAGIC] + pub signature: u16, + /// The machine type + pub machine: u16, + /// The number of sections + pub number_of_sections: u8, + /// The subsystem + pub subsystem: u8, + /// the amount of bytes stripped from the header when converting from a + /// PE32/PE32+ header to a TE header. Used to resolve addresses + pub stripped_size: u16, + /// The entry point of the binary + pub entry_point: u32, + /// The base of the code section + pub base_of_code: u32, + /// The image base + pub image_base: u64, + /// The size and address of the relocation directory + pub reloc_dir: data_directories::DataDirectory, + /// The size and address of the debug directory + pub debug_dir: data_directories::DataDirectory, +} + +#[cfg(feature = "te")] +#[doc(alias("IMAGE_TE_SIGNATURE"))] +pub const TE_MAGIC: u16 = 0x5a56; + +#[cfg(feature = "te")] +impl TeHeader { + /// Parse the TE header from the given bytes. + pub fn parse(bytes: &[u8], offset: &mut usize) -> error::Result { + let mut header: TeHeader = bytes.gread_with(offset, scroll::LE)?; + let adj_offset = header.stripped_size as u32 - core::mem::size_of::() as u32; + header.fixup_header(adj_offset); + Ok(header) + } + + /// Parse the sections from the TE header. + pub fn sections( + &self, + bytes: &[u8], + offset: &mut usize, + ) -> error::Result> { + let adj_offset = self.stripped_size as u32 - core::mem::size_of::() as u32; + let nsections = self.number_of_sections as usize; + + // a section table is at least 40 bytes + if nsections > bytes.len() / 40 { + return Err(error::Error::BufferTooShort(nsections, "sections")); + } + + let mut sections = Vec::with_capacity(nsections); + for i in 0..nsections { + let mut section = section_table::SectionTable::parse(bytes, offset, 0)?; + TeHeader::fixup_section(&mut section, adj_offset); + debug!("({}) {:#?}", i, section); + sections.push(section); + } + Ok(sections) + } + + // Adjust addresses in the header to account for the stripped size + fn fixup_header(&mut self, adj_offset: u32) { + debug!( + "Entry point fixed up from: 0x{:x} to 0x{:X}", + self.entry_point, + self.entry_point.wrapping_sub(adj_offset) + ); + self.entry_point = self.entry_point.wrapping_sub(adj_offset); + + debug!( + "Base of code fixed up from: 0x{:x} to 0x{:X}", + self.base_of_code, + self.base_of_code.wrapping_sub(adj_offset) + ); + self.base_of_code = self.base_of_code.wrapping_sub(adj_offset); + + debug!( + "Relocation Directory fixed up from: 0x{:x} to 0x{:X}", + self.reloc_dir.virtual_address, + self.reloc_dir.virtual_address.wrapping_sub(adj_offset) + ); + self.reloc_dir.virtual_address = self.reloc_dir.virtual_address.wrapping_sub(adj_offset); + + debug!( + "Debug Directory fixed up from: 0x{:x} to 0x{:X}", + self.debug_dir.virtual_address, + self.debug_dir.virtual_address.wrapping_sub(adj_offset) + ); + self.debug_dir.virtual_address = self.debug_dir.virtual_address.wrapping_sub(adj_offset); + } + + // Adjust addresses in the section to account for the stripped size + fn fixup_section(section: &mut section_table::SectionTable, adj_offset: u32) { + debug!( + "Section virtual address fixed up from: 0x{:X} to 0x{:X}", + section.virtual_address, + section.virtual_address.wrapping_sub(adj_offset) + ); + section.virtual_address = section.virtual_address.wrapping_sub(adj_offset); + + if section.pointer_to_linenumbers > 0 { + debug!( + "Section pointer to line numbers fixed up from: 0x{:X} to 0x{:X}", + section.pointer_to_linenumbers, + section.pointer_to_linenumbers.wrapping_sub(adj_offset) + ); + section.pointer_to_linenumbers = + section.pointer_to_linenumbers.wrapping_sub(adj_offset); + } + + if section.pointer_to_raw_data > 0 { + debug!( + "Section pointer to raw data fixed up from: 0x{:X} to 0x{:X}", + section.pointer_to_raw_data, + section.pointer_to_raw_data.wrapping_sub(adj_offset) + ); + section.pointer_to_raw_data = section.pointer_to_raw_data.wrapping_sub(adj_offset); + } + + if section.pointer_to_relocations > 0 { + debug!( + "Section pointer to relocations fixed up from: 0x{:X} to 0x{:X}", + section.pointer_to_relocations, + section.pointer_to_relocations.wrapping_sub(adj_offset) + ); + section.pointer_to_relocations = + section.pointer_to_relocations.wrapping_sub(adj_offset); + } + } +} + /// Convert machine to str representation. Any case of "COFF_UNKNOWN" /// should be expected to change to a more specific value. pub fn machine_to_str(machine: u16) -> &'static str { diff --git a/src/pe/mod.rs b/src/pe/mod.rs index dd9c2c5df..1f2bac7a9 100644 --- a/src/pe/mod.rs +++ b/src/pe/mod.rs @@ -467,6 +467,98 @@ impl<'a> ctx::TryIntoCtx for PE<'a> { } } +/// An analyzed TE binary +/// +/// A TE binary is a PE/PE32+ binary that has had it's header stripped and +/// re-formatted to the TE specification. This presents a challenge for +/// parsing, as all relative addresses (RVAs) are not updated to take this into +/// account, and are thus incorrect. The parsing of a TE must take this into +/// account by using the [header::TeHeader::stripped_size`] field of the TE +/// header to adjust the RVAs during parsing. +#[cfg(feature = "te")] +#[derive(Debug)] +pub struct TE<'a> { + /// The TE header + pub header: header::TeHeader, + /// A list of the sections in this TE binary + pub sections: Vec, + /// Debug information, contained in the PE header + pub debug_data: debug::DebugData<'a>, + /// The offset to apply to addresses not parsed by the TE parser + /// itself: [header::TeHeader::stripped_size] - size_of::<[header::TeHeader]>() + pub rva_offset: usize, +} + +#[cfg(feature = "te")] +impl<'a> TE<'a> { + /// Reads a TE binary from the underlying `bytes` + pub fn parse(bytes: &'a [u8]) -> error::Result { + let opts = &options::ParseOptions { + resolve_rva: false, + parse_attribute_certificates: false, + }; + + let mut offset = 0; + + // Parse the TE header and adjust the offsets + let header = header::TeHeader::parse(bytes, &mut offset)?; + let rva_offset = header.stripped_size as usize - core::mem::size_of::(); + + // Parse the sections and adjust the offsets + let sections = header.sections(bytes, &mut offset)?; + + // Parse the debug data. Must adjust offsets before parsing the image_debug_directory + let mut debug_data = debug::DebugData::default(); + debug_data.image_debug_directory = debug::ImageDebugDirectory::parse_with_opts( + bytes, + header.debug_dir, + §ions, + 0, + opts, + )?; + TE::fixup_debug_data(&mut debug_data, rva_offset as u32); + debug_data.codeview_pdb70_debug_info = debug::CodeviewPDB70DebugInfo::parse_with_opts( + bytes, + &debug_data.image_debug_directory, + opts, + )?; + + Ok(TE { + header, + sections, + debug_data, + rva_offset, + }) + } + + /// Adjust all addresses in the TE binary debug data. + fn fixup_debug_data(dd: &mut debug::DebugData, rva_offset: u32) { + debug!( + "ImageDebugDirectory address of raw data fixed up from: 0x{:X} to 0x{:X}", + dd.image_debug_directory.address_of_raw_data, + dd.image_debug_directory + .address_of_raw_data + .wrapping_sub(rva_offset), + ); + dd.image_debug_directory.address_of_raw_data = dd + .image_debug_directory + .address_of_raw_data + .wrapping_sub(rva_offset); + + debug!( + "ImageDebugDirectory pointer to raw data fixed up from: 0x{:X} to 0x{:X}", + dd.image_debug_directory.pointer_to_raw_data, + dd.image_debug_directory + .pointer_to_raw_data + .wrapping_sub(rva_offset), + ); + dd.image_debug_directory.pointer_to_raw_data = dd + .image_debug_directory + .pointer_to_raw_data + .wrapping_sub(rva_offset); + } +} + /// An analyzed COFF object #[derive(Debug)] pub struct Coff<'a> { diff --git a/tests/bins/te/README.md b/tests/bins/te/README.md new file mode 100644 index 000000000..c60f26333 --- /dev/null +++ b/tests/bins/te/README.md @@ -0,0 +1,24 @@ +# TE binaries + +Binaries located in this directory are precompiled PE32/PE32+ binaries using a +terse executable (TE) header as defined in the Platform Initialization (PI) +specification: [TE](https://uefi.org/specs/PI/1.8/V1_TE_Image.html#te-header). +These binaries were compiled using the +[EDK2](https://github.com/tianocore/edk2) build system. + +## test_image.te + +This binary is a simple Terse executable binary + +## test_image_loaded.bin + +This binary is the same as `test_image.te`, but it has been loaded by a loader, +meaning the sections have been placed in the expected address. Please note that +this particular binary has not been relocated, so no relocations have been +applied + +## test_image_relocated.bin + +This binary is the same as `test_image.te`, but it has been loaded by a loader, +meaning the sections have been placed in the expected address, and any any +relocations have been applied. diff --git a/tests/bins/te/test_image.te b/tests/bins/te/test_image.te new file mode 100644 index 0000000000000000000000000000000000000000..8ac7c77b1e42df24e83bcc7650f4f4755176bfad GIT binary patch literal 28256 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 literal 0 HcmV?d00001 diff --git a/tests/bins/te/test_image_loaded.bin b/tests/bins/te/test_image_loaded.bin new file mode 100644 index 0000000000000000000000000000000000000000..0463f7bf55087874ae32911e96c2dc679d9c3813 GIT binary patch literal 24312 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