use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PeError {
#[error("{len} bytes at {at:#x} could not be read")]
Unreadable { at: u64, len: usize },
#[error("not a PE image: {reason}")]
NotAnImage { reason: &'static str },
#[error("malformed PE image: {reason}")]
Malformed { reason: &'static str },
#[error("interrupted")]
Interrupted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bitness {
Bits32,
Bits64,
}
impl Bitness {
pub fn pointer(self) -> usize {
match self {
Self::Bits32 => 4,
Self::Bits64 => 8,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Section {
pub name: String,
pub rva: u32,
pub virtual_size: u32,
pub characteristics: u32,
}
impl Section {
pub fn executable(&self) -> bool {
self.characteristics & 0x2000_0000 != 0
}
pub fn discardable(&self) -> bool {
self.characteristics & 0x0200_0000 != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Image {
pub base: u64,
pub bitness: Bitness,
pub machine: u16,
pub size_of_image: u32,
pub section_alignment: u32,
pub sections: Vec<Section>,
pub export_directory: (u32, u32),
pub import_directory: (u32, u32),
pub iat_directory: (u32, u32),
}
impl Image {
pub fn section_at(&self, rva: u32) -> Option<&Section> {
self.sections.iter().find(|section| {
let end = section.rva.saturating_add(section.virtual_size.max(1));
(section.rva..end).contains(&rva)
})
}
pub fn code_sections(&self) -> impl Iterator<Item = &Section> {
self.sections.iter().filter(|section| section.executable())
}
pub fn executable_ranges(&self) -> Vec<std::ops::Range<u64>> {
self.code_sections()
.filter_map(|section| {
let extent = self
.section_alignment
.is_power_of_two()
.then(|| {
section
.virtual_size
.checked_next_multiple_of(self.section_alignment)
})
.flatten()
.unwrap_or(section.virtual_size);
let room = self.size_of_image.saturating_sub(section.rva);
let len = extent.min(room);
let start = self.checked_va(section.rva, len as usize).ok()?;
Some(start..start + u64::from(len))
})
.filter(|range| range.start < range.end)
.collect()
}
pub fn checked_va(&self, rva: u32, len: usize) -> Result<u64, PeError> {
let end = u64::from(rva)
.checked_add(len as u64)
.ok_or(PeError::Malformed {
reason: "an image offset and length overflowed",
})?;
if rva >= self.size_of_image || end > u64::from(self.size_of_image) {
return Err(PeError::Malformed {
reason: "an image offset points outside the image",
});
}
va(self.base, u64::from(rva), len)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportName {
Named(String),
Ordinal(u16),
}
impl std::fmt::Display for ImportName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Named(name) => f.write_str(name),
Self::Ordinal(ordinal) => write!(f, "#{ordinal}"),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ImportTable {
pub imports: Vec<Import>,
pub unnamed_libraries: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Import {
pub library: String,
pub name: ImportName,
pub slot: u64,
}
const MAX_SECTIONS: usize = 96;
const MAX_LIBRARIES: usize = 64;
const MAX_IMPORTS_PER_LIBRARY: usize = 8192;
const MAX_IMPORTS_TOTAL: usize = 16 * 1024;
const MAX_NAME: usize = 512;
const NAME_READ_GRANULE: usize = 0x200;
fn declared_fits(
rva: u32,
len: u32,
size_of_image: u32,
reason: &'static str,
) -> Result<(), PeError> {
match u64::from(rva).saturating_add(u64::from(len)) <= u64::from(size_of_image) {
true => Ok(()),
false => Err(PeError::Malformed { reason }),
}
}
fn va(base: u64, offset: u64, len: usize) -> Result<u64, PeError> {
let start = base.checked_add(offset).ok_or(PeError::Malformed {
reason: "an image address overflowed",
})?;
start.checked_add(len as u64).ok_or(PeError::Malformed {
reason: "an image address overflowed",
})?;
Ok(start)
}
pub fn read_image(
base: u64,
mut read: impl FnMut(u64, usize) -> Option<Vec<u8>>,
) -> Result<Image, PeError> {
let mut at = |offset: u64, len: usize| -> Result<Vec<u8>, PeError> {
let address = va(base, offset, len)?;
read(address, len).ok_or(PeError::Unreadable { at: address, len })
};
let dos = at(0, 0x40)?;
if u16(&dos, 0)? != 0x5a4d {
return Err(PeError::NotAnImage {
reason: "no MZ signature",
});
}
let lfanew = u32(&dos, 0x3c)? as u64;
if lfanew > 0x1000 {
return Err(PeError::NotAnImage {
reason: "e_lfanew is outside the header page",
});
}
let headers = at(lfanew, 0x108)?;
if u32(&headers, 0)? != 0x0000_4550 {
return Err(PeError::NotAnImage {
reason: "no PE signature",
});
}
let machine = u16(&headers, 4)?;
let section_count = u16(&headers, 6)? as usize;
let optional_size = u16(&headers, 20)? as usize;
let optional = 24usize;
let bitness = match u16(&headers, optional)? {
0x10b => Bitness::Bits32,
0x20b => Bitness::Bits64,
_ => {
return Err(PeError::NotAnImage {
reason: "the optional header names neither PE32 nor PE32+",
});
}
};
let implied = match machine {
0x014c | 0x01c4 => Some(Bitness::Bits32), 0x8664 | 0xaa64 | 0x0200 => Some(Bitness::Bits64), _ => None,
};
if implied.is_some_and(|width| width != bitness) {
return Err(PeError::Malformed {
reason: "the machine and the optional header disagree about the image's width",
});
}
let (size_of_image_at, count_at, directories_at) = match bitness {
Bitness::Bits32 => (optional + 56, optional + 92, optional + 96),
Bitness::Bits64 => (optional + 56, optional + 108, optional + 112),
};
let optional_end = optional + optional_size;
if optional_end < count_at + 4 {
return Err(PeError::NotAnImage {
reason: "the optional header is too short to hold its own fields",
});
}
let size_of_image = u32(&headers, size_of_image_at)?;
let section_alignment = u32(&headers, optional + 32)?;
if !section_alignment.is_power_of_two() {
return Err(PeError::Malformed {
reason: "the section alignment is not a unit a loader maps in",
});
}
let declared = u32(&headers, count_at)? as usize;
let directory = |index: usize| -> Result<(u32, u32), PeError> {
let entry = directories_at + index * 8;
if index >= declared || entry + 8 > optional_end {
return Ok((0, 0));
}
Ok((u32(&headers, entry)?, u32(&headers, entry + 4)?))
};
let export_directory = directory(0)?;
let import_directory = directory(1)?;
let iat_directory = directory(12)?;
if section_count > MAX_SECTIONS {
return Err(PeError::Malformed {
reason: "more sections than an image plausibly has",
});
}
let table = lfanew + 24 + optional_size as u64;
let table_rva = u32::try_from(table).map_err(|_| PeError::Malformed {
reason: "the section table lies outside a 32-bit image offset",
})?;
declared_fits(
table_rva,
(section_count * 40) as u32,
size_of_image,
"the section table runs past the end of the image",
)?;
let raw = at(table, section_count * 40)?;
let mut sections = Vec::with_capacity(section_count);
for index in 0..section_count {
let offset = index * 40;
let name = raw
.get(offset..offset + 8)
.ok_or(PeError::Malformed {
reason: "the section table is shorter than its count",
})?
.iter()
.take_while(|&&byte| byte != 0)
.map(|&byte| byte as char)
.collect::<String>();
sections.push(Section {
name,
virtual_size: u32(&raw, offset + 8)?,
rva: u32(&raw, offset + 12)?,
characteristics: u32(&raw, offset + 36)?,
});
}
Ok(Image {
base,
bitness,
machine,
size_of_image,
section_alignment,
sections,
export_directory,
import_directory,
iat_directory,
})
}
fn clipped_reader<'a>(
image: &'a Image,
read: &'a mut impl FnMut(u64, usize) -> Option<Vec<u8>>,
) -> impl FnMut(u32, usize) -> Result<Vec<u8>, PeError> + 'a {
move |rva: u32, len: usize| {
let room = image.size_of_image.saturating_sub(rva) as usize;
let len = len.min(room);
let address = image.checked_va(rva, len)?;
read(address, len).ok_or(PeError::Unreadable { at: address, len })
}
}
fn read_declared(
image: &Image,
read: &mut impl FnMut(u64, usize) -> Option<Vec<u8>>,
rva: u32,
len: usize,
) -> Result<Vec<u8>, PeError> {
if rva == 0 && len > 0 {
return Err(PeError::Malformed {
reason: "a table at RVA zero, which is the image's header rather than a table",
});
}
let address = image.checked_va(rva, len)?;
read(address, len)
.filter(|bytes| bytes.len() == len)
.ok_or(PeError::Unreadable { at: address, len })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IatSlot {
pub slot: u64,
pub value: u64,
}
pub fn read_import_address_table(
image: &Image,
mut read: impl FnMut(u64, usize) -> Option<Vec<u8>>,
) -> Result<Vec<IatSlot>, PeError> {
let (rva, size) = image.iat_directory;
match (rva, size) {
(0, 0) => return Ok(Vec::new()),
(0, _) | (_, 0) => {
return Err(PeError::Malformed {
reason: "the import address table has a size without an address, or the reverse",
});
}
_ => {}
}
let pointer = image.bitness.pointer();
let len = size as usize;
if !len.is_multiple_of(pointer) {
return Err(PeError::Malformed {
reason: "the import address table is not a whole number of slots",
});
}
if len / pointer > MAX_IMPORTS_TOTAL + MAX_LIBRARIES {
return Err(PeError::Malformed {
reason: "more import address slots than an image plausibly has",
});
}
let address = image.checked_va(rva, len)?;
let bytes = read_declared(image, &mut read, rva, len)?;
let mut slots = Vec::new();
for (index, raw) in bytes.chunks_exact(pointer).enumerate() {
let value = match image.bitness {
Bitness::Bits64 => u64_at(raw, 0)?,
Bitness::Bits32 => u64::from(u32(raw, 0)?),
};
if value != 0 {
slots.push(IatSlot {
slot: address + (index * pointer) as u64,
value,
});
}
}
Ok(slots)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExportsAt {
pub library: String,
pub names: BTreeMap<u32, Vec<String>>,
}
const MAX_EXPORTS: usize = 1 << 16;
pub fn read_exports_at(
image: &Image,
mut read: impl FnMut(u64, usize) -> Option<Vec<u8>>,
rvas: &BTreeSet<u32>,
mut halt: impl FnMut() -> bool,
) -> Result<Option<ExportsAt>, PeError> {
let (rva, size) = image.export_directory;
match (rva, size) {
(0, 0) => return Ok(None),
(0, _) | (_, 0) => {
return Err(PeError::Malformed {
reason: "the export directory has a size without an address, or the reverse",
});
}
_ => {}
}
if size < 40 {
return Err(PeError::Malformed {
reason: "the export directory is smaller than its own header",
});
}
declared_fits(
rva,
size,
image.size_of_image,
"the export directory runs past the end of the image",
)?;
let header = read_declared(image, &mut read, rva, 40)?;
let name_rva = u32(&header, 12)?;
let functions = u32(&header, 20)? as usize;
let named = u32(&header, 24)? as usize;
let functions_at = u32(&header, 28)?;
let names_at = u32(&header, 32)?;
let ordinals_at = u32(&header, 36)?;
if functions > MAX_EXPORTS || named > MAX_EXPORTS {
return Err(PeError::Malformed {
reason: "more exports than an ordinal can number",
});
}
if functions == 0 && named > 0 {
return Err(PeError::Malformed {
reason: "export names with no export address table for them to index",
});
}
let library = {
let mut at = clipped_reader(image, &mut read);
read_c_string(name_rva, &mut at)?
};
let mut exports = ExportsAt {
library,
names: BTreeMap::new(),
};
if rvas.is_empty() || functions == 0 || named == 0 {
return Ok(Some(exports));
}
if halt() {
return Err(PeError::Interrupted);
}
let addresses = read_declared(image, &mut read, functions_at, functions * 4)?;
let directory = rva..rva.saturating_add(size);
let wanted: BTreeMap<usize, u32> = (0..functions)
.map(|index| u32(&addresses, index * 4).map(|address| (index, address)))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|(_, address)| rvas.contains(address) && !directory.contains(address))
.collect();
if wanted.is_empty() {
return Ok(Some(exports));
}
if halt() {
return Err(PeError::Interrupted);
}
let ordinals = read_declared(image, &mut read, ordinals_at, named * 2)?;
if halt() {
return Err(PeError::Interrupted);
}
let names = read_declared(image, &mut read, names_at, named * 4)?;
let mut at = clipped_reader(image, &mut read);
for index in 0..named {
let ordinal = usize::from(u16(&ordinals, index * 2)?);
if ordinal >= functions {
return Err(PeError::Malformed {
reason: "an export name's ordinal is past the end of the export address table",
});
}
let Some(&address) = wanted.get(&ordinal) else {
continue;
};
if halt() {
return Err(PeError::Interrupted);
}
let name = read_c_string(u32(&names, index * 4)?, &mut at)?;
exports.names.entry(address).or_default().push(name);
}
Ok(Some(exports))
}
pub fn read_imports(
image: &Image,
mut read: impl FnMut(u64, usize) -> Option<Vec<u8>>,
mut halt: impl FnMut() -> bool,
) -> Result<ImportTable, PeError> {
let (directory, size) = image.import_directory;
match (directory, size) {
(0, 0) => return Ok(ImportTable::default()),
(0, _) | (_, 0) => {
return Err(PeError::Malformed {
reason: "the import directory has a size without an address, or the reverse",
});
}
_ => {}
}
let mut at = clipped_reader(image, &mut read);
if size as usize / 20 > MAX_LIBRARIES + 1 {
return Err(PeError::Malformed {
reason: "the import directory names more libraries than an image plausibly has",
});
}
declared_fits(
directory,
size,
image.size_of_image,
"the import directory runs past the end of the image",
)?;
let descriptors = at(directory, size as usize)?;
let mut table = ImportTable::default();
let mut claimed: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut terminated = false;
for index in 0..(descriptors.len() / 20) {
if halt() {
return Err(PeError::Interrupted);
}
let offset = index * 20;
let lookup = u32(&descriptors, offset)?;
let name_rva = u32(&descriptors, offset + 12)?;
let iat = u32(&descriptors, offset + 16)?;
let stamp = u32(&descriptors, offset + 4)?;
let forwarder = u32(&descriptors, offset + 8)?;
if lookup == 0 && name_rva == 0 && iat == 0 && stamp == 0 && forwarder == 0 {
terminated = true;
break;
}
if name_rva == 0 {
return Err(PeError::Malformed {
reason: "an import descriptor names no library",
});
}
if iat == 0 {
return Err(PeError::Malformed {
reason: "an import descriptor has no import address table",
});
}
let library = read_c_string(name_rva, &mut at)?;
let pointer = image.bitness.pointer();
if lookup == 0 {
image.checked_va(iat, pointer)?;
table.unnamed_libraries.push(library);
continue;
}
let mut library_terminated = false;
for slot_index in 0..MAX_IMPORTS_PER_LIBRARY {
if halt() {
return Err(PeError::Interrupted);
}
let offset = (slot_index * pointer) as u64;
let narrow = |rva: u64, reason: &'static str| -> Result<u32, PeError> {
u32::try_from(rva).map_err(|_| PeError::Malformed { reason })
};
let slot_rva = narrow(
u64::from(iat) + offset,
"an import address table entry lies outside a 32-bit image offset",
)?;
let slot = image.checked_va(slot_rva, pointer)?;
let entry_rva = narrow(
u64::from(lookup) + offset,
"an import lookup table entry lies outside a 32-bit image offset",
)?;
let entry = at(entry_rva, pointer)?;
let value = match image.bitness {
Bitness::Bits32 => u32(&entry, 0)? as u64,
Bitness::Bits64 => u64_at(&entry, 0)?,
};
if value == 0 {
library_terminated = true;
break;
}
let ordinal_flag = match image.bitness {
Bitness::Bits32 => 1u64 << 31,
Bitness::Bits64 => 1u64 << 63,
};
let name = if value & ordinal_flag != 0 {
if value & !(ordinal_flag | 0xffff) != 0 {
return Err(PeError::Malformed {
reason: "an ordinal import sets bits that are neither its flag nor its \
ordinal",
});
}
ImportName::Ordinal((value & 0xffff) as u16)
} else {
let hint = u32::try_from(value).map_err(|_| PeError::Malformed {
reason: "an import name table entry is not a 32-bit image offset",
})?;
let name_rva = hint.checked_add(2).ok_or(PeError::Malformed {
reason: "an import name's hint runs past the end of a 32-bit image offset",
})?;
ImportName::Named(read_c_string(name_rva, &mut at)?)
};
if !claimed.insert(slot) {
return Err(PeError::Malformed {
reason: "two imports claim the same import address table slot",
});
}
if table.imports.len() >= MAX_IMPORTS_TOTAL {
return Err(PeError::Malformed {
reason: "the image imports more functions in total than a plausible one does",
});
}
table.imports.push(Import {
library: library.clone(),
name,
slot,
});
}
if !library_terminated {
return Err(PeError::Malformed {
reason: "a library imports more functions than an image plausibly does",
});
}
}
if !terminated {
return Err(PeError::Malformed {
reason: "the import directory ends without a null descriptor",
});
}
Ok(table)
}
pub fn imports_by_slot(imports: &[Import]) -> BTreeMap<u64, &Import> {
imports.iter().map(|import| (import.slot, import)).collect()
}
fn read_c_string(
rva: u32,
at: &mut impl FnMut(u32, usize) -> Result<Vec<u8>, PeError>,
) -> Result<String, PeError> {
if rva == 0 {
return Err(PeError::Malformed {
reason: "a name at RVA zero, which is the image's header rather than a string",
});
}
let mut raw: Vec<u8> = Vec::new();
while raw.len() < MAX_NAME {
let taken = u32::try_from(raw.len()).map_err(|_| PeError::Malformed {
reason: "an import or library name runs past the length a name may have",
})?;
let here = rva.checked_add(taken).ok_or(PeError::Malformed {
reason: "an import name runs past the end of a 32-bit image offset",
})?;
let want =
(NAME_READ_GRANULE - (here as usize % NAME_READ_GRANULE)).min(MAX_NAME - raw.len());
let chunk = at(here, want)?;
if chunk.is_empty() {
break;
}
if let Some(end) = chunk.iter().position(|&byte| byte == 0) {
raw.extend_from_slice(&chunk[..end]);
return String::from_utf8(raw).map_err(|_| PeError::Malformed {
reason: "an import or library name is not text",
});
}
raw.extend_from_slice(&chunk);
}
Err(PeError::Malformed {
reason: "an import or library name runs past the length a name may have",
})
}
fn u16(bytes: &[u8], offset: usize) -> Result<u16, PeError> {
let raw = bytes.get(offset..offset + 2).ok_or(PeError::Malformed {
reason: "short read of a 16-bit field",
})?;
Ok(u16::from_le_bytes([raw[0], raw[1]]))
}
fn u32(bytes: &[u8], offset: usize) -> Result<u32, PeError> {
let raw = bytes.get(offset..offset + 4).ok_or(PeError::Malformed {
reason: "short read of a 32-bit field",
})?;
Ok(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
}
fn u64_at(bytes: &[u8], offset: usize) -> Result<u64, PeError> {
let raw = bytes.get(offset..offset + 8).ok_or(PeError::Malformed {
reason: "short read of a 64-bit field",
})?;
let mut value = [0u8; 8];
value.copy_from_slice(raw);
Ok(u64::from_le_bytes(value))
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeImage {
base: u64,
bytes: Vec<u8>,
unreadable: Vec<(u64, u64)>,
}
impl FakeImage {
fn read(&self, at: u64, len: usize) -> Option<Vec<u8>> {
let end = at.checked_add(len as u64)?;
if self
.unreadable
.iter()
.any(|&(low, high)| at < high && end > low)
{
return None;
}
let offset = at.checked_sub(self.base)? as usize;
let slice = self.bytes.get(offset..)?;
Some(slice[..slice.len().min(len)].to_vec())
}
}
fn put(bytes: &mut Vec<u8>, offset: usize, value: &[u8]) {
if bytes.len() < offset + value.len() {
bytes.resize(offset + value.len(), 0);
}
bytes[offset..offset + value.len()].copy_from_slice(value);
}
const BASE: u64 = 0xffff_f800_0000_0000;
fn driver_image() -> FakeImage {
let mut bytes = vec![0u8; 0x4000];
put(&mut bytes, 0x00, b"MZ");
put(&mut bytes, 0x3c, &0xe0u32.to_le_bytes());
put(&mut bytes, 0xe0, b"PE\0\0");
put(&mut bytes, 0xe4, &0x8664u16.to_le_bytes()); put(&mut bytes, 0xe6, &3u16.to_le_bytes()); put(&mut bytes, 0xf4, &0xf0u16.to_le_bytes());
put(&mut bytes, 0xf8, &0x20bu16.to_le_bytes()); put(&mut bytes, 0xf8 + 32, &0x1000u32.to_le_bytes()); put(&mut bytes, 0xf8 + 56, &0x4000u32.to_le_bytes()); put(&mut bytes, 0x164, &16u32.to_le_bytes());
put(&mut bytes, 0x170, &0x2000u32.to_le_bytes()); put(&mut bytes, 0x174, &40u32.to_le_bytes());
let mut section = |index: usize, name: &[u8], rva: u32, characteristics: u32| {
let at = 0x1e8 + index * 40;
put(&mut bytes, at, name);
put(&mut bytes, at + 8, &0x1000u32.to_le_bytes()); put(&mut bytes, at + 12, &rva.to_le_bytes()); put(&mut bytes, at + 36, &characteristics.to_le_bytes());
};
section(0, b".text\0\0\0", 0x1000, 0x6000_0020); section(1, b".rdata\0\0", 0x2000, 0x4000_0040); section(2, b".data\0\0\0", 0x3000, 0xc000_0040);
put(&mut bytes, 0x2000, &0x2040u32.to_le_bytes()); put(&mut bytes, 0x200c, &0x2100u32.to_le_bytes()); put(&mut bytes, 0x2010, &0x3000u32.to_le_bytes());
put(&mut bytes, 0x2040, &0x2110u64.to_le_bytes());
put(&mut bytes, 0x2048, &0x2120u64.to_le_bytes());
put(&mut bytes, 0x2050, &0x8000_0000_0000_0007u64.to_le_bytes());
put(&mut bytes, 0x2100, b"ntoskrnl.exe\0");
put(&mut bytes, 0x2112, b"ExAllocatePool2\0");
put(&mut bytes, 0x2122, b"ProbeForRead\0");
FakeImage {
base: BASE,
bytes,
unreadable: Vec::new(),
}
}
#[test]
fn test_a_name_thunk_whose_hint_overflows_is_refused() {
let mut fake = driver_image();
put(
&mut fake.bytes,
0x2040,
&0x0000_0000_ffff_fffeu64.to_le_bytes(),
);
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed {
reason: "an import name's hint runs past the end of a 32-bit image offset",
})
);
}
#[test]
fn test_a_name_thunk_above_the_32_bit_offset_space_is_refused() {
let mut fake = driver_image();
put(
&mut fake.bytes,
0x2040,
&0x0000_0001_0000_1000u64.to_le_bytes(),
);
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed {
reason: "an import name table entry is not a 32-bit image offset",
})
);
}
#[test]
fn test_the_import_walk_stops_at_the_image_bound_before_its_arithmetic_could_wrap() {
let image = Image {
base: BASE,
bitness: Bitness::Bits64,
machine: 0x8664,
size_of_image: 0xffff_ffff,
section_alignment: 0x1000,
sections: Vec::new(),
export_directory: (0, 0),
import_directory: (0x1000, 40),
iat_directory: (0, 0),
};
let read = |address: u64, len: usize| -> Option<Vec<u8>> {
let rva = address.checked_sub(BASE)? as u32;
let mut out = vec![0u8; len];
if rva == 0x1000 {
out[0..4].copy_from_slice(&0xffff_f000u32.to_le_bytes());
out[12..16].copy_from_slice(&0x2000u32.to_le_bytes());
out[16..20].copy_from_slice(&0xffff_f000u32.to_le_bytes());
return Some(out);
}
if rva == 0x2000 {
out[..8].copy_from_slice(b"drv.sys\0");
return Some(out);
}
out[..len.min(8)]
.copy_from_slice(&0x8000_0000_0000_0007u64.to_le_bytes()[..len.min(8)]);
Some(out)
};
let outcome = read_imports(&image, read, || false);
assert_eq!(
outcome,
Err(PeError::Malformed {
reason: "an image offset points outside the image",
}),
"the walk is stopped by the image bound, not by the narrowing -- if this becomes an \
arithmetic refusal, the reachability argument in this test has stopped holding"
);
}
#[test]
fn test_a_section_table_past_the_end_of_the_image_is_refused() {
let mut fake = driver_image();
put(&mut fake.bytes, 0xf8 + 56, &0x200u32.to_le_bytes());
assert_eq!(
read_image(BASE, |at, len| fake.read(at, len)),
Err(PeError::Malformed {
reason: "the section table runs past the end of the image",
})
);
}
#[test]
fn test_a_base_that_would_wrap_the_address_space_is_refused() {
let read = |_address: u64, len: usize| -> Option<Vec<u8>> {
let mut out = vec![0u8; len];
out[0..2].copy_from_slice(b"MZ");
if len > 0x3f {
out[0x3c..0x40].copy_from_slice(&0xe0u32.to_le_bytes());
}
Some(out)
};
assert_eq!(
read_image(u64::MAX - 0x10, read),
Err(PeError::Malformed {
reason: "an image address overflowed",
})
);
}
#[test]
fn test_an_rva_whose_span_wraps_the_address_space_is_refused() {
let image = Image {
base: u64::MAX - 0x1000,
bitness: Bitness::Bits64,
machine: 0x8664,
size_of_image: 0x2000,
section_alignment: 0x1000,
sections: vec![Section {
name: ".text".to_string(),
rva: 0x1000,
virtual_size: 0x1000,
characteristics: 0x6000_0020,
}],
export_directory: (0, 0),
import_directory: (0, 0),
iat_directory: (0, 0),
};
assert_eq!(
image.checked_va(0x1000, 0x1000),
Err(PeError::Malformed {
reason: "an image address overflowed",
})
);
assert!(image.executable_ranges().is_empty());
}
#[test]
fn test_a_name_beside_an_unreadable_page_is_still_read() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x200c, &0x2ff0u32.to_le_bytes());
put(&mut fake.bytes, 0x2ff0, b"ntoskrnl.exe\0");
fake.unreadable.push((BASE + 0x3000, BASE + 0x4000));
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
let table = read_imports(&image, |at, len| fake.read(at, len), || false)
.expect("the name is beside the hole, not inside it");
assert_eq!(table.imports.len(), 3, "{table:#?}");
assert!(
table
.imports
.iter()
.all(|import| import.library == "ntoskrnl.exe"),
"{table:#?}"
);
}
#[test]
fn test_a_name_beside_a_hole_inside_a_page_is_still_read() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x200c, &0x2c8cu32.to_le_bytes());
put(&mut fake.bytes, 0x2c8c, b"ntoskrnl.exe\0");
fake.unreadable.push((BASE + 0x2e00, BASE + 0x3000));
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
let table = read_imports(&image, |at, len| fake.read(at, len), || false)
.expect("the name ends before the hole, inside the same page");
assert_eq!(table.imports.len(), 3, "{table:#?}");
assert!(
table
.imports
.iter()
.all(|import| import.library == "ntoskrnl.exe"),
"{table:#?}"
);
}
#[test]
fn test_the_import_address_table_is_read_slot_by_slot() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x1c8, &0x3000u32.to_le_bytes());
put(&mut fake.bytes, 0x1cc, &0x20u32.to_le_bytes());
put(
&mut fake.bytes,
0x3000,
&0xffff_f801_cdeb_8670u64.to_le_bytes(),
);
put(
&mut fake.bytes,
0x3008,
&0xffff_f801_cdd8_4f30u64.to_le_bytes(),
);
put(
&mut fake.bytes,
0x3018,
&0xffff_f801_cd60_0638u64.to_le_bytes(),
);
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(image.iat_directory, (0x3000, 0x20));
let slots = read_import_address_table(&image, |at, len| fake.read(at, len))
.expect("the table reads");
assert_eq!(
slots,
vec![
IatSlot {
slot: BASE + 0x3000,
value: 0xffff_f801_cdeb_8670
},
IatSlot {
slot: BASE + 0x3008,
value: 0xffff_f801_cdd8_4f30
},
IatSlot {
slot: BASE + 0x3018,
value: 0xffff_f801_cd60_0638
},
]
);
}
#[test]
fn test_an_import_address_table_that_does_not_fit_is_refused() {
let fake = driver_image();
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_import_address_table(&image, |at, len| fake.read(at, len)),
Ok(Vec::new()),
"no directory, no slots"
);
let refused = |rva: u32, size: u32| {
let mut image = image.clone();
image.iat_directory = (rva, size);
read_import_address_table(&image, |at, len| fake.read(at, len))
};
for (rva, size, why) in [
(0x3000, 0, "an address without a size"),
(0, 0x20, "a size without an address"),
(0x3000, 0x1c, "a size that is not a whole number of slots"),
(0x3ff0, 0x20, "a table running past the end of the image"),
] {
assert!(
matches!(refused(rva, size), Err(PeError::Malformed { .. })),
"{why} was read: {:?}",
refused(rva, size)
);
}
}
#[test]
fn test_an_unreadable_import_address_table_says_where() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x1c8, &0x3000u32.to_le_bytes());
put(&mut fake.bytes, 0x1cc, &0x20u32.to_le_bytes());
fake.unreadable.push((BASE + 0x3000, BASE + 0x4000));
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_import_address_table(&image, |at, len| fake.read(at, len)),
Err(PeError::Unreadable {
at: BASE + 0x3000,
len: 0x20
})
);
}
fn exporting_image() -> FakeImage {
let mut fake = driver_image();
put(&mut fake.bytes, 0x168, &0x2200u32.to_le_bytes());
put(&mut fake.bytes, 0x16c, &0x100u32.to_le_bytes());
put(&mut fake.bytes, 0x220c, &0x2240u32.to_le_bytes()); put(&mut fake.bytes, 0x2214, &3u32.to_le_bytes()); put(&mut fake.bytes, 0x2218, &3u32.to_le_bytes()); put(&mut fake.bytes, 0x221c, &0x2260u32.to_le_bytes()); put(&mut fake.bytes, 0x2220, &0x2270u32.to_le_bytes()); put(&mut fake.bytes, 0x2224, &0x2280u32.to_le_bytes()); put(&mut fake.bytes, 0x2240, b"ntoskrnl.exe\0");
for (index, address) in [0x1010u32, 0x1020, 0x1030].into_iter().enumerate() {
put(&mut fake.bytes, 0x2260 + index * 4, &address.to_le_bytes());
}
for (index, (name_at, ordinal, name)) in [
(0x2290u32, 0u16, &b"ExAllocatePoolWithTag\0"[..]),
(0x22b0, 1, b"__C_specific_handler\0"),
(0x22d0, 0, b"AnAliasOfTheFirst\0"),
]
.into_iter()
.enumerate()
{
put(&mut fake.bytes, 0x2270 + index * 4, &name_at.to_le_bytes());
put(&mut fake.bytes, 0x2280 + index * 2, &ordinal.to_le_bytes());
put(&mut fake.bytes, name_at as usize, name);
}
fake
}
#[test]
fn test_exports_are_named_at_the_addresses_asked_about() {
let fake = exporting_image();
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
let asked = BTreeSet::from([0x1010, 0x1020, 0x1030, 0x1040]);
let exports = read_exports_at(&image, |at, len| fake.read(at, len), &asked, || false)
.expect("the exports read")
.expect("the image exports");
assert_eq!(exports.library, "ntoskrnl.exe");
assert_eq!(
exports.names,
BTreeMap::from([
(
0x1010,
vec![
"ExAllocatePoolWithTag".to_string(),
"AnAliasOfTheFirst".to_string()
]
),
(0x1020, vec!["__C_specific_handler".to_string()]),
])
);
let library = read_exports_at(
&image,
|at, len| fake.read(at, len),
&BTreeSet::new(),
|| false,
)
.expect("the directory reads")
.expect("the image exports");
assert_eq!(library.library, "ntoskrnl.exe");
assert!(library.names.is_empty());
let plain = driver_image();
let image = read_image(BASE, |at, len| plain.read(at, len)).expect("the headers read");
assert_eq!(
read_exports_at(&image, |at, len| plain.read(at, len), &asked, || false),
Ok(None)
);
}
#[test]
fn test_a_forwarder_is_not_matched_as_an_address() {
let mut fake = exporting_image();
put(&mut fake.bytes, 0x2264, &0x22f0u32.to_le_bytes());
put(&mut fake.bytes, 0x22f0, b"HAL.Forwarded\0");
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
let exports = read_exports_at(
&image,
|at, len| fake.read(at, len),
&BTreeSet::from([0x22f0, 0x1010]),
|| false,
)
.expect("the exports read")
.expect("the image exports");
assert_eq!(
exports.names.keys().copied().collect::<Vec<_>>(),
vec![0x1010],
"the forwarder's string is not an export's address: {exports:?}"
);
}
#[test]
fn test_a_halt_stops_the_export_walk_before_its_tables() {
let fake = exporting_image();
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_exports_at(
&image,
|at, len| fake.read(at, len),
&BTreeSet::from([0x9999]),
|| true
),
Err(PeError::Interrupted)
);
let polls = std::cell::Cell::new(0);
assert_eq!(
read_exports_at(
&image,
|at, len| fake.read(at, len),
&BTreeSet::from([0x1030]),
|| {
polls.set(polls.get() + 1);
polls.get() >= 3
}
),
Err(PeError::Interrupted)
);
}
#[test]
fn test_an_export_directory_that_contradicts_itself_is_refused() {
type Change = dyn Fn(&mut FakeImage);
let asked = BTreeSet::from([0x1010]);
let refused = |change: &Change| {
let mut fake = exporting_image();
change(&mut fake);
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
read_exports_at(&image, |at, len| fake.read(at, len), &asked, || false)
};
let cases: [(&str, &Change); 10] = [
("a size smaller than the header", &|fake| {
put(&mut fake.bytes, 0x16c, &39u32.to_le_bytes())
}),
("a span past the end of the image", &|fake| {
put(&mut fake.bytes, 0x16c, &0x2000u32.to_le_bytes())
}),
("a library name at RVA zero", &|fake| {
put(&mut fake.bytes, 0x220c, &0u32.to_le_bytes())
}),
("more functions than an ordinal can number", &|fake| {
put(&mut fake.bytes, 0x2214, &0x1_0001u32.to_le_bytes())
}),
("names beside an empty address table", &|fake| {
put(&mut fake.bytes, 0x2214, &0u32.to_le_bytes())
}),
("an ordinal past the address table", &|fake| {
put(&mut fake.bytes, 0x2280, &3u16.to_le_bytes())
}),
("an export name at RVA zero", &|fake| {
put(&mut fake.bytes, 0x2270, &0u32.to_le_bytes())
}),
("an export address table at RVA zero", &|fake| {
put(&mut fake.bytes, 0x221c, &0u32.to_le_bytes())
}),
("an export name table at RVA zero", &|fake| {
fake.bytes.resize(0x6000, 0);
put(&mut fake.bytes, 0x130, &0x6000u32.to_le_bytes()); put(&mut fake.bytes, 4, &0x5000u32.to_le_bytes());
put(&mut fake.bytes, 8, &0x5000u32.to_le_bytes());
put(&mut fake.bytes, 0x2220, &0u32.to_le_bytes())
}),
("an export ordinal table at RVA zero", &|fake| {
put(&mut fake.bytes, 0x2224, &0u32.to_le_bytes())
}),
];
for (why, change) in cases {
assert!(
matches!(refused(change), Err(PeError::Malformed { .. })),
"{why} was read: {:?}",
refused(change)
);
}
}
#[test]
fn test_an_executable_section_past_the_image_is_clipped_rather_than_dropped() {
let image = Image {
base: BASE,
bitness: Bitness::Bits64,
machine: 0x8664,
size_of_image: 0x2000,
section_alignment: 0x1000,
sections: vec![
Section {
name: ".text".to_string(),
rva: 0x1000,
virtual_size: 0x2000, characteristics: 0x6000_0020,
},
Section {
name: ".gone".to_string(),
rva: 0x5000, virtual_size: 0x1000,
characteristics: 0x6000_0020,
},
],
export_directory: (0, 0),
import_directory: (0, 0),
iat_directory: (0, 0),
};
assert_eq!(
image.executable_ranges(),
vec![BASE + 0x1000..BASE + 0x2000],
"the overrunning section keeps the half that is inside the image, and the one that \
starts outside it contributes nothing because none of it is inside"
);
}
#[test]
fn test_an_import_directory_running_past_the_image_is_refused_not_clipped() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x170, &0x3fecu32.to_le_bytes());
put(&mut fake.bytes, 0x174, &40u32.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed {
reason: "the import directory runs past the end of the image",
})
);
}
#[test]
fn test_a_machine_and_magic_that_disagree_are_refused() {
let mut fake = driver_image();
put(&mut fake.bytes, 0xf8, &0x10bu16.to_le_bytes());
assert_eq!(
read_image(BASE, |at, len| fake.read(at, len)),
Err(PeError::Malformed {
reason: "the machine and the optional header disagree about the image's width",
})
);
}
#[test]
fn test_an_unrecognised_machine_is_read_rather_than_refused() {
let mut fake = driver_image();
put(&mut fake.bytes, 0xe4, &0x5032u16.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len))
.expect("an unknown machine is not a contradiction");
assert_eq!(image.machine, 0x5032);
assert_eq!(image.bitness, Bitness::Bits64);
}
#[test]
fn test_a_descriptor_missing_its_name_or_its_address_table_is_refused() {
let mut without_name = driver_image();
put(&mut without_name.bytes, 0x200c, &0u32.to_le_bytes());
let image = read_image(BASE, |at, len| without_name.read(at, len)).expect("headers read");
assert_eq!(
read_imports(&image, |at, len| without_name.read(at, len), || false),
Err(PeError::Malformed {
reason: "an import descriptor names no library",
})
);
let mut without_iat = driver_image();
put(&mut without_iat.bytes, 0x2010, &0u32.to_le_bytes());
let image = read_image(BASE, |at, len| without_iat.read(at, len)).expect("headers read");
assert_eq!(
read_imports(&image, |at, len| without_iat.read(at, len), || false),
Err(PeError::Malformed {
reason: "an import descriptor has no import address table",
})
);
}
#[test]
fn test_a_header_read_running_off_the_address_space_is_refused() {
let mut asked = 0usize;
let outcome = {
let read = |_address: u64, len: usize| -> Option<Vec<u8>> {
asked += 1;
Some(vec![0u8; len])
};
read_image(u64::MAX - 32, read)
};
assert_eq!(
outcome,
Err(PeError::Malformed {
reason: "an image address overflowed",
})
);
assert_eq!(
asked, 0,
"the reader is never asked for a span that cannot exist"
);
}
#[test]
fn test_an_executable_section_covers_the_extent_the_loader_maps() {
let image = |alignment: u32, virtual_size: u32| Image {
base: BASE,
bitness: Bitness::Bits64,
machine: 0x8664,
size_of_image: 0x8000,
section_alignment: alignment,
sections: vec![Section {
name: ".text".to_string(),
rva: 0x1000,
virtual_size,
characteristics: 0x6000_0020,
}],
export_directory: (0, 0),
import_directory: (0, 0),
iat_directory: (0, 0),
};
assert_eq!(
image(0x1000, 0x1234).executable_ranges(),
vec![BASE + 0x1000..BASE + 0x3000],
"0x1234 bytes occupy two aligned pages"
);
assert_eq!(
image(0x1000, 0x1000).executable_ranges(),
vec![BASE + 0x1000..BASE + 0x2000],
"an already-aligned size is not rounded up a page"
);
for bad in [0, 3, 0x1001] {
assert_eq!(
image(bad, 0x1234).executable_ranges(),
vec![BASE + 0x1000..BASE + 0x2234],
"alignment {bad:#x} is not one to round to"
);
}
let mut narrow = image(0x1000, 0x1234);
narrow.size_of_image = 0x2800;
assert_eq!(
narrow.executable_ranges(),
vec![BASE + 0x1000..BASE + 0x2800],
"rounding up does not escape SizeOfImage"
);
}
#[test]
fn test_a_bound_imports_address_table_is_bounded_as_well() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x2000, &0u32.to_le_bytes()); put(&mut fake.bytes, 0x2010, &0x9000u32.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed {
reason: "an image offset points outside the image",
})
);
}
#[test]
fn test_a_section_alignment_that_is_not_a_power_of_two_is_refused() {
for bad in [0u32, 3, 0x1001] {
let mut fake = driver_image();
put(&mut fake.bytes, 0xf8 + 32, &bad.to_le_bytes());
assert_eq!(
read_image(BASE, |at, len| fake.read(at, len)),
Err(PeError::Malformed {
reason: "the section alignment is not a unit a loader maps in",
}),
"alignment {bad:#x}"
);
}
}
#[test]
fn test_a_name_that_is_not_text_is_refused_rather_than_rewritten() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x2112 + 2, &[0x80]);
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed {
reason: "an import or library name is not text",
})
);
}
#[test]
fn test_an_ordinal_thunk_with_reserved_bits_set_is_refused() {
let mut fake = driver_image();
put(
&mut fake.bytes,
0x2040,
&0x8000_0001_0000_2110u64.to_le_bytes(),
);
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed {
reason: "an ordinal import sets bits that are neither its flag nor its ordinal",
})
);
for (thunk, ordinal) in [
(0x8000_0000_0000_0007u64, 7u16),
(0x8000_0000_0000_2110u64, 0x2110),
] {
let mut good = driver_image();
put(&mut good.bytes, 0x2040, &thunk.to_le_bytes());
let image = read_image(BASE, |at, len| good.read(at, len)).expect("the headers read");
let table =
read_imports(&image, |at, len| good.read(at, len), || false).expect("reads");
assert_eq!(table.imports[0].name, ImportName::Ordinal(ordinal));
}
}
#[test]
fn test_imports_are_named_without_reading_the_import_address_table() {
let mut fake = driver_image();
fake.unreadable.push((BASE + 0x3000, BASE + 0x4000));
let image = read_image(BASE, |at, len| fake.read(at, len)).expect("the headers read");
let imports = read_imports(&image, |at, len| fake.read(at, len), || false)
.expect("imports read")
.imports;
assert_eq!(imports.len(), 3, "{imports:#?}");
assert!(imports.iter().all(|i| i.library == "ntoskrnl.exe"));
assert_eq!(
imports
.iter()
.map(|i| i.name.to_string())
.collect::<Vec<_>>(),
vec!["ExAllocatePool2", "ProbeForRead", "#7"]
);
assert_eq!(
imports.iter().map(|i| i.slot).collect::<Vec<_>>(),
vec![BASE + 0x3000, BASE + 0x3008, BASE + 0x3010]
);
}
#[test]
fn test_an_unreadable_lookup_table_is_an_error_rather_than_no_imports() {
let mut fake = driver_image();
fake.unreadable.push((BASE + 0x2040, BASE + 0x2060));
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let error = read_imports(&image, |at, len| fake.read(at, len), || false)
.expect_err("an unreadable lookup table must not read as an empty import table");
assert!(matches!(error, PeError::Unreadable { .. }), "{error:?}");
}
#[test]
fn test_sections_carry_the_flags_a_scan_is_bounded_by() {
let fake = driver_image();
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert_eq!(image.bitness, Bitness::Bits64);
assert_eq!(image.machine, 0x8664);
assert_eq!(image.size_of_image, 0x4000);
assert_eq!(
image
.sections
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
vec![".text", ".rdata", ".data"]
);
let code = image.code_sections().collect::<Vec<_>>();
assert_eq!(code.len(), 1);
assert_eq!(code[0].name, ".text");
assert!(
!code.iter().any(|s| s.name == ".data"),
"a writable section is not code and is never decoded: {code:?}"
);
let ranges = image.executable_ranges();
assert_eq!(ranges, vec![BASE + 0x1000..BASE + 0x2000]);
assert!(ranges.iter().any(|range| range.contains(&(BASE + 0x1500))));
assert!(
!ranges.iter().any(|range| range.contains(&(BASE + 0x2500))),
"`.rdata` is inside the module and is not code"
);
}
#[test]
fn test_an_absent_import_directory_is_no_imports() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x170, &0u32.to_le_bytes());
put(&mut fake.bytes, 0x174, &0u32.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false).unwrap(),
ImportTable::default()
);
}
#[test]
fn test_two_imports_claiming_one_slot_are_refused() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x2014, &0x2060u32.to_le_bytes()); put(&mut fake.bytes, 0x2014 + 12, &0x2100u32.to_le_bytes()); put(&mut fake.bytes, 0x2014 + 16, &0x3000u32.to_le_bytes()); put(&mut fake.bytes, 0x2028, &[0u8; 20]); put(&mut fake.bytes, 0x174, &60u32.to_le_bytes()); put(&mut fake.bytes, 0x2060, &0x2120u64.to_le_bytes());
put(&mut fake.bytes, 0x2068, &0u64.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let read = read_imports(&image, |at, len| fake.read(at, len), || false);
assert!(
matches!(read, Err(PeError::Malformed { .. })),
"a slot claimed twice is refused rather than resolved to whichever came last: {read:?}"
);
}
#[test]
fn test_the_total_number_of_imports_is_bounded_as_the_table_grows() {
let per_library = MAX_IMPORTS_PER_LIBRARY - 1;
assert!(
per_library * 3 > MAX_IMPORTS_TOTAL,
"the fixture must be able to cross the total without crossing the per-library limit"
);
let mut fake = driver_image();
fake.bytes.resize(0x80000, 0);
put(&mut fake.bytes, 0xf8 + 56, &0x80000u32.to_le_bytes()); put(&mut fake.bytes, 0x174, &80u32.to_le_bytes()); for library in 0..3usize {
let descriptor = 0x2000 + library * 20;
let lookup = 0x10000 + library * 0x20000;
put(&mut fake.bytes, descriptor, &(lookup as u32).to_le_bytes());
put(&mut fake.bytes, descriptor + 12, &0x2100u32.to_le_bytes());
put(
&mut fake.bytes,
descriptor + 16,
&((0x60000 + library * 0x8000) as u32).to_le_bytes(),
);
for index in 0..per_library {
put(
&mut fake.bytes,
lookup + index * 8,
&0x2110u64.to_le_bytes(),
);
}
put(
&mut fake.bytes,
lookup + per_library * 8,
&0u64.to_le_bytes(),
);
}
put(&mut fake.bytes, 0x2000 + 3 * 20, &[0u8; 20]);
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let read = read_imports(&image, |at, len| fake.read(at, len), || false);
assert!(
matches!(read, Err(PeError::Malformed { .. })),
"a table this large is refused rather than built: {read:?}"
);
}
#[test]
fn test_the_library_limit_leaves_room_for_the_descriptor_that_ends_the_array() {
fn image_importing(libraries: usize) -> FakeImage {
let mut fake = driver_image();
let size = (libraries + 1) * 20;
put(&mut fake.bytes, 0x170, &0x2000u32.to_le_bytes());
put(&mut fake.bytes, 0x174, &(size as u32).to_le_bytes());
for index in 0..libraries {
let at = 0x2000 + index * 20;
put(&mut fake.bytes, at, &0x2600u32.to_le_bytes()); put(&mut fake.bytes, at + 12, &0x2700u32.to_le_bytes()); let iat = 0x3000 + (index as u32) * 8;
put(&mut fake.bytes, at + 16, &iat.to_le_bytes()); }
put(&mut fake.bytes, 0x2000 + libraries * 20, &[0u8; 20]);
put(&mut fake.bytes, 0x2600, &0x2710u64.to_le_bytes());
put(&mut fake.bytes, 0x2608, &0u64.to_le_bytes());
put(&mut fake.bytes, 0x2700, b"lib.sys\0");
put(&mut fake.bytes, 0x2712, b"Func\0");
fake
}
let fake = image_importing(MAX_LIBRARIES);
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let table = read_imports(&image, |at, len| fake.read(at, len), || false)
.expect("an image with exactly the permitted number of libraries is not malformed");
assert_eq!(table.imports.len(), MAX_LIBRARIES);
let fake = image_importing(MAX_LIBRARIES + 1);
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert!(matches!(
read_imports(&image, |at, len| fake.read(at, len), || false),
Err(PeError::Malformed { .. })
));
}
#[test]
fn test_an_image_declaring_no_data_directories_has_none() {
let mut fake = driver_image();
put(&mut fake.bytes, 0xf4, &112u16.to_le_bytes()); put(&mut fake.bytes, 0x164, &0u32.to_le_bytes()); let section = |bytes: &mut Vec<u8>, index: usize, name: &[u8], rva: u32| {
let at = 0x168 + index * 40;
put(bytes, at, name);
put(bytes, at + 8, &0x1000u32.to_le_bytes()); put(bytes, at + 12, &rva.to_le_bytes()); put(bytes, at + 36, &0x6000_0020u32.to_le_bytes());
};
section(&mut fake.bytes, 0, b".text\0\0\0", 0x1000);
section(&mut fake.bytes, 1, b".rdata\0\0", 0x2000);
section(&mut fake.bytes, 2, b".data\0\0\0", 0x3000);
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert_eq!(
image.import_directory,
(0, 0),
"the first section header is not a data directory"
);
assert_eq!(image.export_directory, (0, 0));
assert_eq!(image.sections.len(), 3, "and the sections still read");
assert_eq!(
read_imports(&image, |at, len| fake.read(at, len), || false).unwrap(),
ImportTable::default(),
"a valid image with no import directory imports nothing, and is not malformed"
);
put(&mut fake.bytes, 0x164, &16u32.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert_eq!(
image.import_directory,
(0, 0),
"a directory the header has no room for is not there, whatever it declares"
);
put(&mut fake.bytes, 0xf4, &108u16.to_le_bytes());
assert!(
matches!(
read_image(BASE, |at, len| fake.read(at, len)),
Err(PeError::NotAnImage { .. })
),
"an optional header too short to hold its own fields is not one"
);
}
#[test]
fn test_an_import_directory_with_one_coordinate_missing_is_refused() {
for (rva, size, what) in [
(0x2000u32, 0u32, "an address with no size"),
(0, 40, "a size with no address"),
] {
let mut fake = driver_image();
put(&mut fake.bytes, 0x170, &rva.to_le_bytes());
put(&mut fake.bytes, 0x174, &size.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let read = read_imports(&image, |at, len| fake.read(at, len), || false);
assert!(
matches!(read, Err(PeError::Malformed { .. })),
"{what} is not an absent directory: {read:?}"
);
}
}
#[test]
fn test_an_import_slot_past_the_end_of_the_image_is_refused_though_it_is_never_read() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x2010, &0x3ff8u32.to_le_bytes());
fake.unreadable.push((BASE + 0x3ff8, BASE + 0x4008));
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert_eq!(image.size_of_image, 0x4000);
let read = read_imports(&image, |at, len| fake.read(at, len), || false);
assert!(
matches!(read, Err(PeError::Malformed { .. })),
"a slot past the image must not be handed back as this image's: {read:?}"
);
}
#[test]
fn test_a_library_with_no_lookup_table_is_reported_rather_than_skipped() {
let mut fake = driver_image();
put(&mut fake.bytes, 0x2000, &0u32.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let table = read_imports(&image, |at, len| fake.read(at, len), || false).unwrap();
assert!(
table.imports.is_empty(),
"nothing can be named without a lookup table: {table:?}"
);
assert_eq!(
table.unnamed_libraries,
vec!["ntoskrnl.exe".to_string()],
"the library must be reported, not dropped: {table:?}"
);
}
#[test]
fn test_an_import_directory_that_overruns_its_bound_is_refused() {
let mut oversized = driver_image();
put(&mut oversized.bytes, 0x174, &(20u32 * 4096).to_le_bytes());
let image = read_image(BASE, |at, len| oversized.read(at, len)).unwrap();
assert!(
matches!(
read_imports(&image, |at, len| oversized.read(at, len), || false),
Err(PeError::Malformed { .. })
),
"an oversized directory must not read as a short one"
);
let mut unterminated = driver_image();
put(&mut unterminated.bytes, 0x174, &20u32.to_le_bytes());
let image = read_image(BASE, |at, len| unterminated.read(at, len)).unwrap();
assert!(
matches!(
read_imports(&image, |at, len| unterminated.read(at, len), || false),
Err(PeError::Malformed { .. })
),
"a directory with no terminator inside its size must not read as complete"
);
}
#[test]
fn test_a_list_or_a_name_that_overruns_its_bound_is_refused() {
let mut endless = driver_image();
let ordinal = 0x8000_0000_0000_0007u64.to_le_bytes();
for slot in 0..(MAX_IMPORTS_PER_LIBRARY + 1) {
put(&mut endless.bytes, 0x2040 + slot * 8, &ordinal);
}
let image = read_image(BASE, |at, len| endless.read(at, len)).unwrap();
assert!(
matches!(
read_imports(&image, |at, len| endless.read(at, len), || false),
Err(PeError::Malformed { .. })
),
"an import list that fills its cap must not read as a complete one"
);
let mut endless_name = driver_image();
for offset in 0..(MAX_NAME + 8) {
put(&mut endless_name.bytes, 0x2112 + offset, b"A");
}
let image = read_image(BASE, |at, len| endless_name.read(at, len)).unwrap();
assert!(
matches!(
read_imports(&image, |at, len| endless_name.read(at, len), || false),
Err(PeError::Malformed { .. })
),
"a name with no terminator must not be accepted truncated"
);
}
#[test]
fn test_an_import_table_pointing_outside_the_image_is_refused() {
let mut outside = driver_image();
put(&mut outside.bytes, 0x2000, &0x9000u32.to_le_bytes());
let image = read_image(BASE, |at, len| outside.read(at, len)).unwrap();
assert_eq!(image.size_of_image, 0x4000);
assert!(
matches!(
read_imports(&image, |at, len| outside.read(at, len), || false),
Err(PeError::Malformed { .. })
),
"an RVA past the image must not be read from whatever is mapped there"
);
}
#[test]
fn test_a_descriptor_is_a_terminator_only_when_every_field_is_zero() {
let mut stamped = driver_image();
put(&mut stamped.bytes, 0x2014 + 4, &1u32.to_le_bytes());
let image = read_image(BASE, |at, len| stamped.read(at, len)).unwrap();
let read = read_imports(&image, |at, len| stamped.read(at, len), || false);
assert!(
matches!(read, Err(PeError::Malformed { .. })),
"a descriptor that is not all-zero must not end the table: {read:?}"
);
}
#[test]
fn test_an_unreadable_header_and_a_non_image_are_told_apart() {
let nothing =
read_image(BASE, |_, _| None).expect_err("a reader answering nothing must not parse");
assert!(matches!(nothing, PeError::Unreadable { .. }), "{nothing:?}");
let mut rubbish = driver_image();
put(&mut rubbish.bytes, 0x00, b"XX");
let refused = read_image(BASE, |at, len| rubbish.read(at, len))
.expect_err("bytes with no MZ must not parse");
assert!(matches!(refused, PeError::NotAnImage { .. }), "{refused:?}");
}
#[test]
fn test_a_32_bit_image_reads_its_directories_at_the_32_bit_offset() {
let mut fake = driver_image();
put(&mut fake.bytes, 0xf8, &0x10bu16.to_le_bytes()); put(&mut fake.bytes, 0xe4, &0x014cu16.to_le_bytes()); put(&mut fake.bytes, 0x154, &16u32.to_le_bytes());
put(&mut fake.bytes, 0x164, &0u32.to_le_bytes());
put(&mut fake.bytes, 0x158 + 8, &0x2000u32.to_le_bytes());
put(&mut fake.bytes, 0x158 + 12, &40u32.to_le_bytes());
put(&mut fake.bytes, 0x170, &0u32.to_le_bytes());
put(&mut fake.bytes, 0x174, &0u32.to_le_bytes());
put(&mut fake.bytes, 0x2040, &0x2110u32.to_le_bytes());
put(&mut fake.bytes, 0x2044, &0x2120u32.to_le_bytes());
put(&mut fake.bytes, 0x2048, &0u32.to_le_bytes());
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
assert_eq!(image.bitness, Bitness::Bits32);
assert_eq!(image.import_directory, (0x2000, 40));
let imports = read_imports(&image, |at, len| fake.read(at, len), || false)
.unwrap()
.imports;
assert_eq!(
imports
.iter()
.map(|i| i.name.to_string())
.collect::<Vec<_>>(),
vec!["ExAllocatePool2", "ProbeForRead"]
);
assert_eq!(
imports.iter().map(|i| i.slot).collect::<Vec<_>>(),
vec![BASE + 0x3000, BASE + 0x3004]
);
}
#[test]
fn test_a_halt_stops_the_import_walk() {
let fake = driver_image();
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let error = read_imports(&image, |at, len| fake.read(at, len), || true)
.expect_err("a halt should stop the walk");
assert_eq!(error, PeError::Interrupted);
}
#[test]
fn test_imports_index_by_the_slot_a_call_goes_through() {
let fake = driver_image();
let image = read_image(BASE, |at, len| fake.read(at, len)).unwrap();
let imports = read_imports(&image, |at, len| fake.read(at, len), || false)
.unwrap()
.imports;
let index = imports_by_slot(&imports);
assert_eq!(
index.get(&(BASE + 0x3008)).map(|i| i.name.to_string()),
Some("ProbeForRead".to_string())
);
assert!(!index.contains_key(&(BASE + 0x3018)));
}
}