Skip to content

Commit

Permalink
pe: add Terse Executable (TE) support (#397)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Javagedes committed Apr 1, 2024
1 parent 661180f commit 47ee850
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 5 deletions.
5 changes: 3 additions & 2 deletions Cargo.toml
Expand Up @@ -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"
Expand All @@ -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"]
Expand All @@ -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]
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/lib.rs
Expand Up @@ -229,14 +229,15 @@ pub enum Hint {
Mach(HintData),
MachFat(usize),
PE,
TE,
COFF,
Archive,
Unknown(u64),
}

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
)*)
}
Expand All @@ -262,6 +263,7 @@ if_everything! {
} else {
match *&bytes[0..2].pread_with::<u16>(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),
Expand Down Expand Up @@ -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!
Expand All @@ -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)),
}
Expand Down
2 changes: 1 addition & 1 deletion src/pe/debug.rs
Expand Up @@ -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],
Expand Down
143 changes: 142 additions & 1 deletion 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;
Expand Down Expand Up @@ -837,6 +837,147 @@ impl ctx::TryIntoCtx<scroll::Endian> 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:
/// <https://uefi.org/specs/PI/1.8/V1_TE_Image.html#te-header>
#[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<Self> {
let mut header: TeHeader = bytes.gread_with(offset, scroll::LE)?;
let adj_offset = header.stripped_size as u32 - core::mem::size_of::<TeHeader>() 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<Vec<section_table::SectionTable>> {
let adj_offset = self.stripped_size as u32 - core::mem::size_of::<TeHeader>() 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 {
Expand Down
92 changes: 92 additions & 0 deletions src/pe/mod.rs
Expand Up @@ -467,6 +467,98 @@ impl<'a> ctx::TryIntoCtx<scroll::Endian> 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<section_table::SectionTable>,
/// 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<Self> {
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::<header::TeHeader>();

// 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,
&sections,
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> {
Expand Down
24 changes: 24 additions & 0 deletions 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.
Binary file added tests/bins/te/test_image.te
Binary file not shown.
Binary file added tests/bins/te/test_image_loaded.bin
Binary file not shown.
Binary file added tests/bins/te/test_image_relocated.bin
Binary file not shown.

0 comments on commit 47ee850

Please sign in to comment.