pub mod disasm;
pub mod pe;
pub mod skel;
pub use disasm::{analyze, analyze_opt, reg_params, resolve_thunk, tail_referenced, ArgKind, CallGraph, CallSite};
pub use pe::{FixupStatus, Pe};
pub use skel::{cfg_fingerprint, CfgFingerprint};
use std::collections::BTreeSet;
#[derive(Default)]
pub struct Spec {
pub addr: u64,
calls: Option<(Vec<u64>, bool)>, fields: Vec<u64>,
globals: Vec<u64>,
vtables: Vec<u64>, vcall_slots: Vec<u64>,
indirect_targets: Vec<u64>,
iat: Vec<u64>,
}
type TextStartsCache = std::sync::Mutex<
std::collections::HashMap<(u64, u64, u32, u32), std::sync::OnceLock<TextStarts>>,
>;
type TextStarts = std::sync::Arc<std::collections::BTreeSet<u64>>;
type BranchTargetsCache = std::sync::Mutex<
std::collections::HashMap<(u64, u64, u32, u32), std::sync::OnceLock<BranchTargets>>,
>;
type BranchTargets = std::sync::Arc<std::collections::HashSet<u32>>;
fn branch_targets_cache() -> &'static BranchTargetsCache {
static CACHE: std::sync::OnceLock<BranchTargetsCache> = std::sync::OnceLock::new();
CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
fn text_starts_cache() -> &'static TextStartsCache {
static CACHE: std::sync::OnceLock<TextStartsCache> = std::sync::OnceLock::new();
CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
fn text_instruction_starts(pe: &Pe) -> TextStarts {
let key = || -> Option<(u64, u64, u32, u32)> {
let text = pe.sections().iter().find(|s| s.name == ".text")?;
Some((
pe.id(),
pe.image_base,
text.virtual_address,
text.virtual_size,
))
};
let key = match key() {
Some(k) => k,
None => {
static EMPTY: std::sync::OnceLock<TextStarts> = std::sync::OnceLock::new();
return EMPTY
.get_or_init(|| std::sync::Arc::new(std::collections::BTreeSet::new()))
.clone();
}
};
let cache = text_starts_cache();
let mut lock = cache.lock().unwrap_or_else(|e| e.into_inner());
let entry = lock.entry(key).or_default();
let once = entry.get_or_init(|| {
use iced_x86::{Decoder, DecoderOptions};
let text = pe
.sections()
.iter()
.find(|s| s.name == ".text")
.expect("key was Some");
let text_va = pe.image_base + text.virtual_address as u64;
let bytes = match pe.read(text_va, text.virtual_size as usize) {
Some(b) => b,
None => return std::sync::Arc::new(std::collections::BTreeSet::new()),
};
let mut dec = Decoder::with_ip(32, bytes, text_va, DecoderOptions::NONE);
let mut starts = std::collections::BTreeSet::new();
starts.insert(text_va);
while dec.can_decode() {
let ip = dec.ip();
starts.insert(ip);
let _ = dec.decode();
}
std::sync::Arc::new(starts)
});
once.clone()
}
pub fn has_direct_branch_target(pe: &Pe, target: u64) -> bool {
all_branch_targets(pe).contains(&(target as u32))
}
fn all_branch_targets(pe: &Pe) -> BranchTargets {
let Some(text) = pe.sections().iter().find(|s| s.name == ".text") else {
static EMPTY: std::sync::OnceLock<BranchTargets> = std::sync::OnceLock::new();
return EMPTY
.get_or_init(|| std::sync::Arc::new(std::collections::HashSet::new()))
.clone();
};
let text_va = pe.image_base + text.virtual_address as u64;
let key = (
pe.id(),
pe.image_base,
text.virtual_address,
text.virtual_size,
);
let cache = branch_targets_cache();
let mut lock = cache.lock().unwrap_or_else(|e| e.into_inner());
let entry = lock.entry(key).or_default();
entry
.get_or_init(|| {
let blob = match pe.read(text_va, text.virtual_size as usize) {
Some(b) => b,
None => return std::sync::Arc::new(std::collections::HashSet::new()),
};
let starts = text_instruction_starts(pe);
let mut targets = std::collections::HashSet::new();
let mut i = 0;
while i + 4 < blob.len() {
let rel_at = match blob[i] {
0xE8 | 0xE9 => i + 1, 0x0F if i + 6 <= blob.len() && (0x80..=0x8F).contains(&blob[i + 1]) => i + 2,
_ => {
i += 1;
continue;
}
};
let ip_va = text_va + i as u64;
if !starts.contains(&ip_va) {
i += 1;
continue;
}
if rel_at + 4 > blob.len() {
i += 1;
continue;
}
let rel = i32::from_le_bytes([
blob[rel_at],
blob[rel_at + 1],
blob[rel_at + 2],
blob[rel_at + 3],
]) as i64;
let t = (text_va + (rel_at as u64 + 4)).wrapping_add(rel as u64) as u32;
targets.insert(t);
i += 1;
}
std::sync::Arc::new(targets)
})
.clone()
}
impl Spec {
pub fn new(addr: u64) -> Spec {
Spec {
addr,
..Default::default()
}
}
pub fn calls_exact(mut self, t: &[u64]) -> Spec {
self.calls = Some((t.to_vec(), true));
self
}
pub fn calls_superset(mut self, t: &[u64]) -> Spec {
self.calls = Some((t.to_vec(), false));
self
}
pub fn fields(mut self, t: &[u64]) -> Spec {
self.fields = t.to_vec();
self
}
pub fn globals(mut self, t: &[u64]) -> Spec {
self.globals = t.to_vec();
self
}
pub fn vtables(mut self, t: &[u64]) -> Spec {
self.vtables = t.to_vec();
self
}
pub fn vcall_slots(mut self, t: &[u64]) -> Spec {
self.vcall_slots = t.to_vec();
self
}
pub fn indirect_targets(mut self, t: &[u64]) -> Spec {
self.indirect_targets = t.to_vec();
self
}
pub fn iat(mut self, t: &[u64]) -> Spec {
self.iat = t.to_vec();
self
}
pub fn check(&self, pe: &Pe) -> Result<(), String> {
let cg = analyze(pe, self.addr)?;
let mut v = Vec::new();
if let Some((declared, exact)) = &self.calls {
let real = cg.call_targets();
let dset: BTreeSet<u64> = declared.iter().copied().collect();
for h in dset.difference(&real) {
v.push(format!(
"HALLUCINATED declared CALL {h:#010x} — binary never directly calls it"
));
}
if *exact {
for m in real.difference(&dset) {
v.push(format!(
"MISSING binary CALLs {m:#010x} — not declared"
));
}
}
}
let post = pe.fixup_post_images(
&cg.globals.union(&cg.imm_addrs).copied().collect(),
);
present_check("field offset", &self.fields, &cg.field_offsets, &mut v);
present_check_fixup("global", &self.globals, &cg.globals, &post, &mut v);
present_check_fixup("vtable/imm addr", &self.vtables, &cg.imm_addrs, &post, &mut v);
present_check("vcall slot", &self.vcall_slots, &cg.vcall_slots, &mut v);
for t in &self.indirect_targets {
if !pe.is_executable_section_va(*t) {
v.push(format!(
"indirect target {t:#010x} is not in an executable section"
));
}
}
if !self.indirect_targets.is_empty() && cg.indirect_calls == 0 {
v.push(
"indirect_targets declared, but the binary makes no register/memory \
indirect call at this address"
.to_string(),
);
}
present_check("IAT import", &self.iat, &cg.iat_calls, &mut v);
if v.is_empty() {
Ok(())
} else {
Err(format!(
"{:#010x}: {} violation(s) vs pure-byte ground truth:\n {}\n (indirect calls: {}, indirect jumps: {})",
self.addr,
v.len(),
v.join("\n "),
cg.indirect_calls,
cg.indirect_jumps,
))
}
}
pub fn check_at(&self, manifest_dir: &str, exe: &str) {
let path = std::path::Path::new(manifest_dir).join(exe);
let pe = Pe::load(path.to_str().unwrap())
.unwrap_or_else(|e| panic!("load {}: {e}", path.display()));
if let Err(msg) = self.check(&pe) {
panic!("{msg}");
}
}
}
fn present_check(kind: &str, declared: &[u64], real: &BTreeSet<u64>, out: &mut Vec<String>) {
present_check_fixup(kind, declared, real, &BTreeSet::new(), out)
}
fn present_check_fixup(
kind: &str,
declared: &[u64],
real: &BTreeSet<u64>,
post: &BTreeSet<u64>,
out: &mut Vec<String>,
) {
for d in declared {
if !real.contains(d) && !post.contains(d) {
out.push(format!(
"HALLUCINATED declared {kind} {d:#x} — binary never references it"
));
}
}
}
#[derive(Debug)]
pub struct Diff {
pub entry: u64,
pub hallucinated: Vec<u64>,
pub missing: Vec<u64>,
pub indirect_calls: u32,
pub indirect_jumps: u32,
}
impl Diff {
pub fn is_clean(&self) -> bool {
self.hallucinated.is_empty() && self.missing.is_empty()
}
}
pub fn diff_calls(
pe: &Pe,
entry: u64,
declared: &[u64],
expect_complete: bool,
) -> Result<Diff, String> {
let cg = analyze(pe, entry)?;
let real = cg.call_targets();
let dset: BTreeSet<u64> = declared.iter().copied().collect();
Ok(Diff {
entry,
hallucinated: dset.difference(&real).copied().collect(),
missing: if expect_complete {
real.difference(&dset).copied().collect()
} else {
Vec::new()
},
indirect_calls: cg.indirect_calls,
indirect_jumps: cg.indirect_jumps,
})
}
pub struct CorpusDiff {
pub addr: u64,
pub hallucinated: Vec<(String, Vec<u64>)>,
pub inlined_ok: Vec<String>,
pub vtable_dispatch: Vec<(String, Vec<u64>)>,
pub missing: Vec<u64>,
pub unported: Vec<u64>,
pub indirect_calls: u32,
}
fn is_inlined_into(pe: &Pe, callee: u64, caller_fp: &BTreeSet<u64>) -> bool {
match analyze(pe, callee) {
Ok(cg) => {
let fp = cg.footprint();
fp.len() >= 2 && fp.is_subset(caller_fp)
}
Err(_) => false,
}
}
pub fn diff_body_calls(
pe: &Pe,
own_addrs: &[u64],
corpus: &BTreeSet<u64>,
rust_idents: &[(String, Vec<u64>)],
vtable_methods: &BTreeSet<u64>,
miss_satisfied: &BTreeSet<u64>,
) -> Result<CorpusDiff, String> {
diff_body_calls_ext(
pe,
own_addrs,
corpus,
rust_idents,
vtable_methods,
miss_satisfied,
&BTreeSet::new(),
)
}
pub fn diff_body_calls_ext(
pe: &Pe,
own_addrs: &[u64],
corpus: &BTreeSet<u64>,
rust_idents: &[(String, Vec<u64>)],
vtable_methods: &BTreeSet<u64>,
miss_satisfied: &BTreeSet<u64>,
indirect_targets: &BTreeSet<u64>,
) -> Result<CorpusDiff, String> {
let primary = *own_addrs.first().ok_or("no origin address")?;
let mut bin_calls = BTreeSet::new();
let mut bin_direct = BTreeSet::new();
let mut wide_extras = BTreeSet::new();
let mut footprint = BTreeSet::new();
let mut indirect = 0u32;
let mut tail_targets: BTreeSet<u64> = BTreeSet::new();
let mut wide_calls: BTreeSet<u64> = BTreeSet::new();
let mut wide_tail: BTreeSet<u64> = BTreeSet::new();
for &a in own_addrs {
let cg_d = analyze_opt(pe, a, false)?;
let cg_w = analyze_opt(pe, a, true)?;
bin_direct.extend(cg_d.call_targets());
bin_calls.extend(cg_d.call_targets());
bin_calls.extend(&cg_d.tail_calls);
tail_targets.extend(&cg_d.tail_calls);
wide_calls.extend(cg_w.call_targets());
wide_tail.extend(&cg_w.tail_calls);
wide_extras.extend(
cg_w
.imm_addrs
.iter()
.copied()
.filter(|&x| pe.is_executable_section_va(x)),
);
footprint.extend(cg_w.footprint());
indirect += cg_w.indirect_calls;
}
let mut bin_calls_wide = bin_calls.clone();
bin_calls_wide.extend(wide_calls);
bin_calls_wide.extend(wide_tail);
bin_calls_wide.extend(wide_extras);
bin_calls_wide.extend(indirect_targets.iter().copied());
for t in tail_targets {
if let Ok(tcg) = analyze(pe, t) {
bin_calls_wide.extend(tcg.call_targets());
}
}
let thunked: Vec<u64> = bin_calls_wide
.iter()
.filter_map(|&t| resolve_thunk(pe, t))
.collect();
bin_calls_wide.extend(thunked);
let own: BTreeSet<u64> = own_addrs.iter().copied().collect();
let bin_ported: BTreeSet<u64> = bin_calls
.intersection(corpus)
.copied()
.filter(|a| !own.contains(a))
.collect();
let (mut hallucinated, mut inlined_ok, mut vtable_dispatch, mut resolved) =
(Vec::new(), Vec::new(), Vec::new(), BTreeSet::new());
for (ident, addrs) in rust_idents {
let aset: Vec<u64> = addrs.iter().copied().filter(|a| !own.contains(a)).collect();
if aset.is_empty() {
continue;
}
resolved.extend(&aset);
if aset.iter().any(|a| bin_calls_wide.contains(a)) {
continue; }
if aset.iter().any(|a| is_inlined_into(pe, *a, &footprint)) {
inlined_ok.push(ident.clone());
continue; }
if aset.iter().any(|a| vtable_methods.contains(a)) {
vtable_dispatch.push((ident.clone(), aset));
continue;
}
hallucinated.push((ident.clone(), aset));
}
let mut missing: Vec<u64> = bin_ported
.difference(&resolved)
.filter(|a| !miss_satisfied.contains(a))
.copied()
.collect();
for o in &own {
if bin_direct.contains(o)
&& !rust_idents
.iter()
.any(|(_, addrs)| addrs.contains(o))
{
missing.push(*o);
}
}
let unported: Vec<u64> = bin_calls
.difference(corpus)
.filter(|a| !own.contains(a))
.copied()
.collect();
Ok(CorpusDiff {
addr: primary,
hallucinated,
inlined_ok,
vtable_dispatch,
missing,
unported,
indirect_calls: indirect,
})
}
#[macro_export]
macro_rules! verify_callgraph {
(exe: $exe:expr; $($body:tt)*) => {
$crate::verify_callgraph!(@each $exe; $($body)*);
};
(@each $exe:expr;
$(#[$m:meta])* fn $name:ident @ $addr:expr => exact [ $($t:expr),* $(,)? ] ; $($rest:tt)*) => {
$(#[$m])*
#[test]
fn $name() {
$crate::Spec::new($addr).calls_exact(&[$($t),*]).check_at(env!("CARGO_MANIFEST_DIR"), $exe);
}
$crate::verify_callgraph!(@each $exe; $($rest)*);
};
(@each $exe:expr;
$(#[$m:meta])* fn $name:ident @ $addr:expr => superset [ $($t:expr),* $(,)? ] ; $($rest:tt)*) => {
$(#[$m])*
#[test]
fn $name() {
$crate::Spec::new($addr).calls_superset(&[$($t),*]).check_at(env!("CARGO_MANIFEST_DIR"), $exe);
}
$crate::verify_callgraph!(@each $exe; $($rest)*);
};
(@each $exe:expr;
$(#[$m:meta])* fn $name:ident @ $addr:literal { $($clauses:tt)* } $($rest:tt)*) => {
$(#[$m])*
#[test]
fn $name() {
let spec = $crate::Spec::new($addr);
let spec = $crate::verify_callgraph!(@clauses spec; $($clauses)*);
spec.check_at(env!("CARGO_MANIFEST_DIR"), $exe);
}
$crate::verify_callgraph!(@each $exe; $($rest)*);
};
(@each $exe:expr;) => {};
(@clauses $s:expr; calls: exact [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.calls_exact(&[$($t),*]); $($r)*)
};
(@clauses $s:expr; calls: superset [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.calls_superset(&[$($t),*]); $($r)*)
};
(@clauses $s:expr; fields: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.fields(&[$($t),*]); $($r)*)
};
(@clauses $s:expr; globals: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.globals(&[$($t),*]); $($r)*)
};
(@clauses $s:expr; vtables: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.vtables(&[$($t),*]); $($r)*)
};
(@clauses $s:expr; vcall_slots: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.vcall_slots(&[$($t),*]); $($r)*)
};
(@clauses $s:expr; iat: [ $($t:expr),* $(,)? ] ; $($r:tt)*) => {
$crate::verify_callgraph!(@clauses $s.iat(&[$($t),*]); $($r)*)
};
(@clauses $s:expr;) => { $s };
}