use std::{collections::BTreeMap, ops::Not};
use anyhow::Result;
use log::debug;
#[cfg(feature = "disassembler")]
use crate::analysis::dis;
use crate::{
aspace::AddressSpace,
loader::pe::{
imports,
imports::{read_best_thunk_data, IMAGE_THUNK_DATA},
PE,
},
module::Permissions,
RVA, VA,
};
#[cfg(feature = "disassembler")]
use std::collections::BTreeSet;
#[cfg(feature = "disassembler")]
pub mod call_targets;
pub mod control_flow_guard;
pub mod entrypoints;
pub mod exports;
pub mod patterns;
pub mod pointers;
pub mod runtime_functions;
pub mod safeseh;
#[cfg(feature = "disassembler")]
pub mod noret_imports;
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub enum ImportedSymbol {
Ordinal(u32),
Name(String),
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub struct Import {
pub address: VA,
pub dll: String,
pub symbol: ImportedSymbol,
}
impl std::fmt::Display for Import {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.symbol {
ImportedSymbol::Ordinal(ord) => write!(f, "{}!#{}", self.dll, ord),
ImportedSymbol::Name(name) => write!(f, "{}!{}", self.dll, name),
}
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub enum ThunkTarget {
Import(Import),
Function(VA),
}
impl std::fmt::Display for ThunkTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
ThunkTarget::Import(import) => write!(f, "Import({})", import),
ThunkTarget::Function(va) => write!(f, "Function(0x{:x})", va),
}
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Thunk {
pub address: VA,
pub target: ThunkTarget,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Function {
Local(VA),
Thunk(Thunk),
Import(Import),
}
pub fn get_imports(pe: &PE) -> Result<BTreeMap<VA, Import>> {
let mut imports: BTreeMap<VA, Import> = Default::default();
if let Some(import_directory) = imports::get_import_directory(pe)? {
let base_address = pe.module.address_space.base_address;
let psize = pe.module.arch.pointer_size();
for import_descriptor in imports::read_import_descriptors(pe, import_directory) {
let dll = pe
.module
.address_space
.relative
.read_ascii(import_descriptor.name, 1)?
.to_lowercase();
for i in 0.. {
let oft = base_address + import_descriptor.original_first_thunk + (i * psize) as RVA;
let ft = base_address + import_descriptor.first_thunk + (i * psize) as RVA;
if pe.module.read_rva_at_va(ft)? == 0x0 {
break;
}
let symbol = match read_best_thunk_data(pe, oft, ft) {
Ok(IMAGE_THUNK_DATA::Function(name_rva)) => {
let name = pe.module.address_space.relative.read_ascii(name_rva + 2, 1)?;
ImportedSymbol::Name(name)
}
Ok(IMAGE_THUNK_DATA::Ordinal(ord)) => ImportedSymbol::Ordinal(ord),
Err(e) => {
debug!("imports: error reading thunk: {}", e);
continue;
}
};
imports.insert(
ft,
Import {
address: ft,
dll: dll.to_string(),
symbol,
},
);
}
}
}
Ok(imports)
}
#[cfg(feature = "disassembler")]
pub fn find_thunks(pe: &PE, imports: &BTreeMap<VA, Import>, functions: &BTreeSet<VA>) -> Result<BTreeMap<VA, Thunk>> {
use super::dis::get_operand_xref;
let mut thunks: BTreeMap<VA, Thunk> = Default::default();
let decoder = dis::get_disassembler(&pe.module)?;
for &function in functions.iter() {
if let Ok(insn_buf) = pe.module.address_space.read_bytes(function, 0x10) {
if let Ok(Some(insn)) = dis::decode(&decoder, &insn_buf) {
if insn.mnemonic != zydis::Mnemonic::JMP {
continue;
}
let op = dis::get_first_operand(&insn).expect("JMP has no target");
if let Ok(Some(target)) = get_operand_xref(&pe.module, function, &insn, op) {
match target {
dis::Target::Direct(target) => {
let thunk = if let Some(import) = imports.get(&target) {
Thunk {
address: function,
target: ThunkTarget::Import(import.clone()),
}
} else if functions.contains(&target) {
Thunk {
address: function,
target: ThunkTarget::Function(target),
}
} else {
if pe.module.probe_va(target, Permissions::X).not() {
continue;
}
Thunk {
address: function,
target: ThunkTarget::Function(target),
}
};
debug!("thunk: {:#x} -> {:}", thunk.address, thunk.target);
thunks.insert(thunk.address, thunk);
}
dis::Target::Indirect(_) => {
continue;
}
}
}
}
}
}
Ok(thunks)
}
#[cfg(feature = "disassembler")]
pub fn find_functions(pe: &PE) -> Result<Vec<Function>> {
use crate::analysis::heuristics;
let imports = get_imports(pe)?;
debug!("imports: found {} imports", imports.len());
for (va, import) in imports.iter() {
debug!("imports: {va:#x}: {import}");
}
let mut function_starts: BTreeSet<VA> = Default::default();
function_starts.extend(crate::analysis::pe::entrypoints::find_pe_entrypoint(pe)?);
function_starts.extend(crate::analysis::pe::exports::find_pe_exports(pe)?);
function_starts.extend(crate::analysis::pe::safeseh::find_pe_safeseh_handlers(pe)?);
function_starts.extend(crate::analysis::pe::runtime_functions::find_pe_runtime_functions(pe)?);
function_starts.extend(crate::analysis::pe::control_flow_guard::find_pe_cfguard_functions(pe)?);
let decoder = dis::get_disassembler(&pe.module)?;
function_starts.extend(
crate::analysis::pe::call_targets::find_pe_call_targets(pe)?
.into_iter()
.filter(|&va| heuristics::is_probably_code(&pe.module, &decoder, va)),
);
function_starts.extend(
crate::analysis::pe::patterns::find_function_prologues(pe)?
.into_iter()
.filter(|&va| heuristics::is_probably_code(&pe.module, &decoder, va)),
);
let mut thunk_candidates = function_starts.clone();
let mut thunks: BTreeMap<VA, Thunk> = Default::default();
while thunk_candidates.is_empty().not() {
let confirmed_thunks = find_thunks(pe, &imports, &thunk_candidates)?;
let function_thunk_targets = confirmed_thunks
.values()
.filter_map(|thunk| match thunk.target {
ThunkTarget::Function(target) => Some(target),
ThunkTarget::Import(_) => None,
})
.collect::<Vec<VA>>();
thunks.extend(confirmed_thunks);
let mut next_candidates: BTreeSet<VA> = Default::default();
for &target in function_thunk_targets.iter() {
if function_starts.insert(target) {
debug!("found new function candidate from thunk target: 0x{target:x}");
next_candidates.insert(target);
}
}
debug!("found {} thunk candidates this round", next_candidates.len());
thunk_candidates = next_candidates;
}
debug!("functions: found {} function candidates", function_starts.len());
debug!("functions: found {} thunks", thunks.len());
for function_start in function_starts.iter() {
debug!("functions: function candidate: {function_start:#x}");
}
let function_starts: Vec<_> = function_starts
.difference(&thunks.keys().cloned().collect())
.cloned()
.collect();
debug!("functions: found {} functions", function_starts.len());
let mut functions: Vec<Function> = Default::default();
functions.extend(function_starts.iter().map(|&f| Function::Local(f)));
functions.extend(thunks.values().cloned().map(Function::Thunk));
functions.extend(imports.values().cloned().map(Function::Import));
functions.sort_unstable();
Ok(functions)
}
#[cfg(feature = "disassembler")]
pub fn find_function_starts(pe: &PE) -> Result<Vec<VA>> {
Ok(find_functions(pe)?
.into_iter()
.filter_map(|f| match f {
Function::Local(va) => Some(va),
Function::Thunk(Thunk { address: va, .. }) => Some(va),
_ => None,
})
.collect())
}