From c33bf5a06816ca71d798924045002b177d732e5f Mon Sep 17 00:00:00 2001 From: Philip Craig Date: Tue, 17 Aug 2021 12:11:07 +1000 Subject: [PATCH] read/pe: add ExportTable (#353) Also: - fix parsing of export tables that have no names - add parsing of strings for forwarded exports - add more support in readobj example - some arbitrary style changes --- examples/readobj.rs | 45 +++++- src/read/mod.rs | 2 +- src/read/pe/export.rs | 369 ++++++++++++++++++++++++++++++------------ src/read/pe/file.rs | 56 ++++--- src/read/traits.rs | 3 +- 5 files changed, 351 insertions(+), 124 deletions(-) diff --git a/examples/readobj.rs b/examples/readobj.rs index 428c70a0..710985da 100644 --- a/examples/readobj.rs +++ b/examples/readobj.rs @@ -4667,7 +4667,7 @@ mod pe { }); } - fn print_export_dir(p: &mut Printer, _dir: &ImageDataDirectory, dir_data: &[u8]) { + fn print_export_dir(p: &mut Printer, dir: &ImageDataDirectory, dir_data: &[u8]) { if let Ok((export_dir, _)) = object::from_bytes::(dir_data) { p.group("ImageExportDirectory", |p| { p.field_hex("Characteristics", export_dir.characteristics.get(LE)); @@ -4687,7 +4687,48 @@ mod pe { "AddressOfNameOrdinals", export_dir.address_of_name_ordinals.get(LE), ); - // TODO: display tables + if let Ok(export_table) = ExportTable::parse(dir_data, dir.virtual_address.get(LE)) + { + // TODO: the order of the name pointers might be interesting? + let mut names = vec![None; export_table.addresses().len()]; + for (name_pointer, ordinal) in export_table.name_iter() { + if let Some(name) = names.get_mut(ordinal as usize) { + *name = Some(name_pointer); + } + } + + let ordinal_base = export_table.ordinal_base(); + for (ordinal, address) in export_table.addresses().iter().enumerate() { + p.group("Export", |p| { + p.field("Ordinal", ordinal_base.wrapping_add(ordinal as u32)); + if let Some(name_pointer) = names[ordinal] { + p.field_string( + "Name", + name_pointer, + export_table.name_from_pointer(name_pointer).ok(), + ); + } + p.field_hex("Address", address.get(LE)); + if let Ok(target) = export_table.target_from_address(address.get(LE)) { + match target { + ExportTarget::Address(_) => {} + ExportTarget::ForwardByOrdinal(library, ordinal) => { + p.field_inline_string("ForwardLibrary", library); + p.field("ForwardOrdinal", ordinal); + } + ExportTarget::ForwardByName(library, name) => { + p.field_inline_string("ForwardLibrary", library); + p.field_inline_string("ForwardName", name); + } + } + } else if let Ok(Some(forward)) = + export_table.forward_string(address.get(LE)) + { + p.field_inline_string("Forward", forward); + } + }); + } + } }); } } diff --git a/src/read/mod.rs b/src/read/mod.rs index 17f1ec32..b47780b4 100644 --- a/src/read/mod.rs +++ b/src/read/mod.rs @@ -476,7 +476,7 @@ impl<'data> Import<'data> { /// An exported symbol. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Export<'data> { - // TODO: and ordinal + // TODO: and ordinal? name: ByteString<'data>, address: u64, } diff --git a/src/read/pe/export.rs b/src/read/pe/export.rs index fba1f765..f413d4b2 100644 --- a/src/read/pe/export.rs +++ b/src/read/pe/export.rs @@ -1,33 +1,51 @@ use alloc::vec::Vec; -use core::convert::TryInto; use core::fmt::Debug; -use crate::read::ReadError; -use crate::read::Result; -use crate::ByteString; -use crate::{pe, Bytes, LittleEndian as LE, U16Bytes, U32Bytes}; +use crate::read::{ByteString, Bytes, Error, ReadError, Result}; +use crate::{pe, LittleEndian as LE, U16Bytes, U32Bytes}; -/// Where an export is pointing to -#[derive(Clone)] +/// Where an export is pointing to. +#[derive(Clone, Copy)] pub enum ExportTarget<'data> { - /// The export points at a RVA in the file - Local(u64), - /// The export is "forwarded" to another DLL + /// The address of the export, relative to the image base. + Address(u32), + /// Forwarded to an export ordinal in another DLL. /// - /// for example, "MYDLL.expfunc" or "MYDLL.#27" - Forwarded(&'data [u8]), + /// This gives the name of the DLL, and the ordinal. + ForwardByOrdinal(&'data [u8], u32), + /// Forwarded to an export name in another DLL. + /// + /// This gives the name of the DLL, and the export name. + ForwardByName(&'data [u8], &'data [u8]), } -/// An export from a PE file +impl<'data> ExportTarget<'data> { + /// Returns true if the target is an address. + pub fn is_address(&self) -> bool { + match self { + ExportTarget::Address(_) => true, + _ => false, + } + } + + /// Returns true if the export is forwarded to another DLL. + pub fn is_forward(&self) -> bool { + !self.is_address() + } +} + +/// An export from a PE file. /// -/// There are multiple kinds of PE exports (with or without a name, and local or exported) -#[derive(Clone)] +/// There are multiple kinds of PE exports (with or without a name, and local or forwarded). +#[derive(Clone, Copy)] pub struct Export<'data> { - /// The ordinal of the export + /// The ordinal of the export. + /// + /// These are sequential, starting at a base specified in the DLL. pub ordinal: u32, - /// The name of the export, if ever the PE file has named it + /// The name of the export, if known. pub name: Option<&'data [u8]>, - /// The target of this export + /// The target of this export. pub target: ExportTarget<'data>, } @@ -44,109 +62,262 @@ impl<'a> Debug for Export<'a> { impl<'a> Debug for ExportTarget<'a> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> { match self { - ExportTarget::Local(addr) => f.write_fmt(format_args!("Local({:#x})", addr)), - ExportTarget::Forwarded(forward) => { - f.write_fmt(format_args!("Forwarded({:?})", ByteString(forward))) - } + ExportTarget::Address(address) => write!(f, "Address({:#x})", address), + ExportTarget::ForwardByOrdinal(library, ordinal) => write!( + f, + "ForwardByOrdinal({:?}.#{})", + ByteString(library), + ordinal + ), + ExportTarget::ForwardByName(library, name) => write!( + f, + "ForwardByName({:?}.{:?})", + ByteString(library), + ByteString(name) + ), } } } -impl<'data, Pe, R> super::PeFile<'data, Pe, R> -where - Pe: super::ImageNtHeaders, - R: crate::ReadRef<'data>, -{ - /// Returns the exports of this PE file - /// - /// See also the [`PeFile::exports`] function, which only returns a subset of these exports. - pub fn export_table(&self) -> Result>> { - let data_dir = match self.data_directory(pe::IMAGE_DIRECTORY_ENTRY_EXPORT) { - Some(data_dir) => data_dir, - None => return Ok(Vec::new()), - }; - let export_va = data_dir.virtual_address.get(LE); - let export_size = data_dir.size.get(LE); - let export_data = data_dir.data(self.data, &self.common.sections).map(Bytes)?; - let export_dir = export_data +/// A partially parsed PE export table. +#[derive(Debug, Clone)] +pub struct ExportTable<'data> { + data: Bytes<'data>, + virtual_address: u32, + directory: &'data pe::ImageExportDirectory, + addresses: &'data [U32Bytes], + names: &'data [U32Bytes], + name_ordinals: &'data [U16Bytes], +} + +impl<'data> ExportTable<'data> { + /// Parse the export table given its section data and address. + pub fn parse(data: &'data [u8], virtual_address: u32) -> Result { + let data = Bytes(data); + let directory = data .read_at::(0) .read_error("Invalid PE export dir size")?; - let addresses = export_data + let addresses = data .read_slice_at::>( - export_dir + directory .address_of_functions .get(LE) - .wrapping_sub(export_va) as usize, - export_dir.number_of_functions.get(LE) as usize, + .wrapping_sub(virtual_address) as usize, + directory.number_of_functions.get(LE) as usize, ) .read_error("Invalid PE export address table")?; - let number = export_dir.number_of_names.get(LE) as usize; - let names = export_data - .read_slice_at::>( - export_dir.address_of_names.get(LE).wrapping_sub(export_va) as usize, - number, - ) - .read_error("Invalid PE export name table")?; - let base_ordinal = export_dir.base.get(LE); - let ordinals = export_data - .read_slice_at::>( - export_dir - .address_of_name_ordinals - .get(LE) - .wrapping_sub(export_va) as usize, - number, - ) - .read_error("Invalid PE export ordinal table")?; + let mut names = &[][..]; + let mut name_ordinals = &[][..]; + let number = directory.number_of_names.get(LE) as usize; + if number != 0 { + names = data + .read_slice_at::>( + directory + .address_of_names + .get(LE) + .wrapping_sub(virtual_address) as usize, + number, + ) + .read_error("Invalid PE export name pointer table")?; + name_ordinals = data + .read_slice_at::>( + directory + .address_of_name_ordinals + .get(LE) + .wrapping_sub(virtual_address) as usize, + number, + ) + .read_error("Invalid PE export ordinal table")?; + } + Ok(ExportTable { + data, + virtual_address, + directory, + addresses, + names, + name_ordinals, + }) + } - // First, let's list all exports... - let mut exports = Vec::new(); - for (i, address) in addresses.iter().enumerate() { - // Convert from an array index to an ordinal - // The MSDN documentation is wrong here, see https://stackoverflow.com/a/40001778/721832 - let ordinal: u32 = match i.try_into() { - Err(_err) => continue, - Ok(index) => index, - }; - let ordinal = ordinal + base_ordinal; - let address = address.get(LE); - - // is it a regular or forwarded export? - if address < export_va || (address - export_va) >= export_size { - exports.push(Export { - ordinal: ordinal, - target: ExportTarget::Local( - self.common.image_base.wrapping_add(address as u64), - ), - name: None, // might be populated later - }); - } else { - let forwarded_to = export_data - .read_string_at(address.wrapping_sub(export_va) as usize) - .read_error("Invalid target for PE forwarded export")?; - exports.push(Export { - ordinal: ordinal, - target: ExportTarget::Forwarded(forwarded_to), - name: None, // might be populated later - }); + /// Returns the header of the export table. + pub fn directory(&self) -> &'data pe::ImageExportDirectory { + self.directory + } + + /// Returns the base value of ordinals. + /// + /// Adding this to an address index will give an ordinal. + pub fn ordinal_base(&self) -> u32 { + self.directory.base.get(LE) + } + + /// Returns the unparsed address table. + /// + /// An address table entry may be a local address, or the address of a forwarded export entry. + /// See [`Self::is_forward`] and [`Self::target_from_address`]. + pub fn addresses(&self) -> &'data [U32Bytes] { + self.addresses + } + + /// Returns the unparsed name pointer table. + /// + /// A name pointer table entry can be used with [`Self::name_from_pointer`]. + pub fn name_pointers(&self) -> &'data [U32Bytes] { + self.names + } + + /// Returns the unparsed ordinal table. + /// + /// An ordinal table entry is a 0-based index into the address table. + /// See [`Self::address_by_index`] and [`Self::target_by_index`]. + pub fn name_ordinals(&self) -> &'data [U16Bytes] { + self.name_ordinals + } + + /// Returns an iterator for the entries in the name pointer table and ordinal table. + /// + /// A name pointer table entry can be used with [`Self::name_from_pointer`]. + /// + /// An ordinal table entry is a 0-based index into the address table. + /// See [`Self::address_by_index`] and [`Self::target_by_index`]. + pub fn name_iter(&self) -> impl Iterator + 'data { + self.names + .iter() + .map(|x| x.get(LE)) + .zip(self.name_ordinals.iter().map(|x| x.get(LE))) + } + + /// Returns the export address table entry at the given address index. + /// + /// This may be a local address, or the address of a forwarded export entry. + /// See [`Self::is_forward`] and [`Self::target_from_address`]. + /// + /// `index` is a 0-based index into the export address table. + pub fn address_by_index(&self, index: u32) -> Result { + Ok(self + .addresses + .get(index as usize) + .read_error("Invalid PE export address index")? + .get(LE)) + } + + /// Returns the export address table entry at the given ordinal. + /// + /// This may be a local address, or the address of a forwarded export entry. + /// See [`Self::is_forward`] and [`Self::target_from_address`]. + pub fn address_by_ordinal(&self, ordinal: u32) -> Result { + self.address_by_index(ordinal.wrapping_sub(self.ordinal_base())) + } + + /// Returns the target of the export at the given address index. + /// + /// `index` is a 0-based index into the export address table. + pub fn target_by_index(&self, index: u32) -> Result> { + self.target_from_address(self.address_by_index(index)?) + } + + /// Returns the target of the export at the given ordinal. + pub fn target_by_ordinal(&self, ordinal: u32) -> Result> { + self.target_from_address(self.address_by_ordinal(ordinal)?) + } + + /// Convert an export address table entry into a target. + pub fn target_from_address(&self, address: u32) -> Result> { + Ok(if let Some(forward) = self.forward_string(address)? { + let i = forward + .iter() + .position(|x| *x == b'.') + .read_error("Missing PE forwarded export separator")?; + let library = &forward[..i]; + match &forward[i + 1..] { + [b'#', digits @ ..] => { + let ordinal = + parse_ordinal(digits).read_error("Invalid PE forwarded export ordinal")?; + ExportTarget::ForwardByOrdinal(library, ordinal) + } + [] => { + return Err(Error("Missing PE forwarded export name")); + } + name => ExportTarget::ForwardByName(library, name), } + } else { + ExportTarget::Address(address) + }) + } + + fn forward_offset(&self, address: u32) -> Option { + let offset = address.wrapping_sub(self.virtual_address) as usize; + if offset < self.data.len() { + Some(offset) + } else { + None + } + } + + /// Return true if the export address table entry is a forward. + pub fn is_forward(&self, address: u32) -> bool { + self.forward_offset(address).is_some() + } + + /// Return the forward string if the export address table entry is a forward. + pub fn forward_string(&self, address: u32) -> Result> { + if let Some(offset) = self.forward_offset(address) { + self.data + .read_string_at(offset) + .read_error("Invalid PE forwarded export address") + .map(Some) + } else { + Ok(None) } + } - // Now, check whether some (or all) of them have an associated name - for (name_ptr, ordinal_index) in names.iter().zip(ordinals.iter()) { - // Items in the ordinal array are biased. - // The MSDN documentation is wrong regarding this bias, see https://stackoverflow.com/a/40001778/721832 - let ordinal_index = ordinal_index.get(LE) as u32; + /// Convert an export name pointer table entry into a name. + pub fn name_from_pointer(&self, name_pointer: u32) -> Result<&'data [u8]> { + let offset = name_pointer.wrapping_sub(self.virtual_address); + self.data + .read_string_at(offset as usize) + .read_error("Invalid PE export name pointer") + } - let name = export_data - .read_string_at(name_ptr.get(LE).wrapping_sub(export_va) as usize) - .read_error("Invalid PE export name entry")?; + /// Returns the parsed exports in this table. + pub fn exports(&self) -> Result>> { + // First, let's list all exports. + let mut exports = Vec::new(); + let ordinal_base = self.ordinal_base(); + for (i, address) in self.addresses.iter().enumerate() { + // Convert from an array index to an ordinal. + let ordinal = ordinal_base.wrapping_add(i as u32); + let target = self.target_from_address(address.get(LE))?; + exports.push(Export { + ordinal, + target, + // Might be populated later. + name: None, + }); + } + // Now, check whether some (or all) of them have an associated name. + // `ordinal_index` is a 0-based index into `addresses`. + for (name_pointer, ordinal_index) in self.name_iter() { + let name = self.name_from_pointer(name_pointer)?; exports .get_mut(ordinal_index as usize) - .ok_or_else(|| crate::read::Error("Invalid PE export ordinal"))? + .read_error("Invalid PE export ordinal")? .name = Some(name); } Ok(exports) } } + +fn parse_ordinal(digits: &[u8]) -> Option { + if digits.is_empty() { + return None; + } + let mut result: u32 = 0; + for &c in digits { + let x = (c as char).to_digit(10)?; + result = result.checked_mul(10)?.checked_add(x)?; + } + Some(result) +} diff --git a/src/read/pe/file.rs b/src/read/pe/file.rs index 5a190898..eeae1d5f 100644 --- a/src/read/pe/file.rs +++ b/src/read/pe/file.rs @@ -11,7 +11,9 @@ use crate::read::{ }; use crate::{pe, ByteString, Bytes, CodeView, LittleEndian as LE, Pod, U32, U64}; -use super::{PeSection, PeSectionIterator, PeSegment, PeSegmentIterator, SectionTable}; +use super::{ + ExportTable, PeSection, PeSectionIterator, PeSegment, PeSegmentIterator, SectionTable, +}; /// A PE32 (32-bit) image file. pub type PeFile32<'data, R = &'data [u8]> = PeFile<'data, pe::ImageNtHeaders32, R>; @@ -59,8 +61,9 @@ where }) } - pub(super) fn section_alignment(&self) -> u64 { - u64::from(self.nt_headers.optional_header().section_alignment()) + /// Returns this binary data. + pub fn data(&self) -> R { + self.data } /// Return the DOS header of this file @@ -85,13 +88,25 @@ where .filter(|d| d.virtual_address.get(LE) != 0) } - fn data_at(&self, va: u32) -> Option> { - self.common.sections.pe_data_at(self.data, va).map(Bytes) + /// Returns the export table of this file. + /// + /// The export table is located using the data directory. + pub fn export_table(&self) -> Result>> { + let data_dir = match self.data_directory(pe::IMAGE_DIRECTORY_ENTRY_EXPORT) { + Some(data_dir) => data_dir, + None => return Ok(None), + }; + let export_data = data_dir.data(self.data, &self.common.sections)?; + let export_va = data_dir.virtual_address.get(LE); + ExportTable::parse(export_data, export_va).map(Some) } - /// Returns this binary data. - pub fn data(&self) -> R { - self.data + pub(super) fn section_alignment(&self) -> u64 { + u64::from(self.nt_headers.optional_header().section_alignment()) + } + + fn data_at(&self, va: u32) -> Option> { + self.common.sections.pe_data_at(self.data, va).map(Bytes) } } @@ -309,21 +324,20 @@ where } fn exports(&self) -> Result>> { - self.export_table().map(|pe_exports| { - let mut exports = Vec::new(); - for pe_export in pe_exports { - match (pe_export.name, pe_export.target) { - (Some(name), super::export::ExportTarget::Local(address)) => { - exports.push(Export { - name: ByteString(name), - address, - }) - } - _ => continue, + let mut exports = Vec::new(); + if let Some(export_table) = self.export_table()? { + for (name_pointer, address_index) in export_table.name_iter() { + let name = export_table.name_from_pointer(name_pointer)?; + let address = export_table.address_by_index(address_index.into())?; + if !export_table.is_forward(address) { + exports.push(Export { + name: ByteString(name), + address: self.common.image_base.wrapping_add(address.into()), + }) } } - exports - }) + } + Ok(exports) } fn pdb_info(&self) -> Result> { diff --git a/src/read/traits.rs b/src/read/traits.rs index e28bd8ff..2581d497 100644 --- a/src/read/traits.rs +++ b/src/read/traits.rs @@ -177,7 +177,8 @@ pub trait Object<'data: 'file, 'file>: read::private::Sealed { /// Get the exported symbols that expose both a name and an address. /// - /// Some file formats may provide other kinds of symbols, that can be retrieved using the lower-level API + /// Some file formats may provide other kinds of symbols, that can be retrieved using + /// the lower-level API. fn exports(&self) -> Result>>; /// Return true if the file contains debug information sections, false if not.