use crate::model::{Binary, Format, SymKind};
use iced_x86::{
Decoder, DecoderOptions, FlowControl, Formatter, Instruction, InstructionInfoFactory,
IntelFormatter, Mnemonic, OpAccess,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
pub const MAX_INSN_BYTES: usize = 15;
#[allow(dead_code)]
#[derive(Serialize, Deserialize)]
pub struct EngineInsn {
pub addr: u64,
pub target: Option<u64>,
pub target_name: Option<Box<str>>,
raw: [u8; MAX_INSN_BYTES],
len: u8,
#[serde(with = "flow_control_serde")]
pub flow: FlowControl,
}
impl EngineInsn {
pub fn new(
addr: u64,
bytes: &[u8],
flow: FlowControl,
target: Option<u64>,
target_name: Option<Box<str>>,
) -> EngineInsn {
let len = bytes.len().min(MAX_INSN_BYTES);
let mut raw = [0u8; MAX_INSN_BYTES];
raw[..len].copy_from_slice(&bytes[..len]);
EngineInsn {
addr,
target,
target_name,
raw,
len: len as u8,
flow,
}
}
pub fn bytes(&self) -> &[u8] {
&self.raw[..self.len as usize]
}
pub fn text(&self, bits: u32, arch: crate::model::Arch) -> String {
if arch.is_x86() {
let mut fmt = IntelFormatter::new();
fmt.options_mut().set_uppercase_hex(false);
fmt.options_mut().set_hex_prefix("0x");
fmt.options_mut().set_hex_suffix("");
fmt.options_mut().set_space_after_operand_separator(true);
let mut decoder = Decoder::with_ip(bits, self.bytes(), self.addr, DecoderOptions::NONE);
if !decoder.can_decode() {
return String::new();
}
let insn = decoder.decode();
let mut s = String::new();
fmt.format(&insn, &mut s);
s
} else {
crate::analysis::aarch64::text(self.bytes(), self.addr)
}
}
}
#[allow(dead_code)]
#[derive(Serialize, Deserialize)]
pub struct BasicBlock {
pub start: u64,
pub end: u64,
pub insns: Vec<EngineInsn>,
pub succ: Vec<u64>,
}
#[derive(Serialize, Deserialize)]
pub struct Function {
pub addr: u64,
pub name: String,
pub blocks: Vec<BasicBlock>,
pub size: u64,
pub incoming: usize,
pub calls: Vec<u64>,
pub named: bool,
pub tables: Vec<u64>,
}
impl Function {
pub fn body_hash(&self) -> u64 {
let mut h = 0x9e37_79b9_7f4a_7c15u64;
for b in &self.blocks {
h = h.rotate_left(5) ^ b.start.wrapping_mul(0x8000_0000_0000_0000);
h ^= b.end;
for i in &b.insns {
for byte in i.bytes() {
h = h.rotate_left(1) ^ u64::from(*byte);
}
h ^= i.addr;
}
}
h
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Xref {
pub from: u64,
pub kind: XrefKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum XrefKind {
Call,
Jump,
Branch,
Data,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ref {
pub to: u64,
pub kind: XrefKind,
}
impl XrefKind {
pub fn label(self) -> &'static str {
match self {
XrefKind::Call => "call",
XrefKind::Jump => "jmp",
XrefKind::Branch => "branch",
XrefKind::Data => "data",
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Analysis {
pub functions: Vec<Function>,
#[serde(skip)]
function_names: BTreeMap<String, usize>,
pub xrefs_to: BTreeMap<u64, Vec<Xref>>,
pub xrefs_from: BTreeMap<u64, Vec<Ref>>,
pub names: BTreeMap<u64, String>,
pub imports: BTreeMap<u64, String>,
pub display_base: u64,
pub bits: u32,
pub arch: crate::model::Arch,
pub truncated: bool,
}
impl Analysis {
pub(crate) fn rebuild_indexes(&mut self) {
self.function_names.clear();
for (index, function) in self.functions.iter().enumerate() {
self.function_names
.entry(function.name.clone())
.or_insert(index);
}
}
pub fn label(&self, addr: u64) -> String {
match self.names.get(&addr).or_else(|| self.imports.get(&addr)) {
Some(n) => n.clone(),
None => format!("sub_{addr:x}"),
}
}
pub fn find_function(&self, addr: u64) -> Option<&Function> {
self.functions
.binary_search_by_key(&addr, |f| f.addr)
.ok()
.map(|i| &self.functions[i])
}
pub fn find_by_name(&self, needle: &str) -> Option<&Function> {
self.function_names.get(needle).map(|&i| &self.functions[i])
}
pub fn function_at(&self, addr: u64) -> Option<&Function> {
let next = self.functions.partition_point(|f| f.addr <= addr);
if next > 0 {
let candidate = &self.functions[next - 1];
if candidate
.blocks
.iter()
.any(|b| addr >= b.start && addr < b.end)
{
return Some(candidate);
}
}
self.functions
.iter()
.find(|f| f.blocks.iter().any(|b| addr >= b.start && addr < b.end))
}
pub fn resolve(&self, sel: &str, literal: Option<u64>) -> Vec<u64> {
let mut out: Vec<u64> = Vec::new();
let push = |a: u64, out: &mut Vec<u64>| {
if !out.contains(&a) {
out.push(a);
}
};
for (addr, name) in &self.names {
if name == sel {
push(*addr, &mut out);
}
}
for (addr, name) in &self.imports {
if crate::analysis::thunks::bare_name(name).eq_ignore_ascii_case(sel) {
push(*addr, &mut out);
}
}
if out.is_empty() {
if let Some(v) = literal {
push(v, &mut out);
}
}
out
}
}
mod flow_control_serde {
use super::*;
pub fn serialize<S>(flow: &FlowControl, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8(*flow as u8)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<FlowControl, D::Error>
where
D: Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
match value {
0 => Ok(FlowControl::Next),
1 => Ok(FlowControl::UnconditionalBranch),
2 => Ok(FlowControl::IndirectBranch),
3 => Ok(FlowControl::ConditionalBranch),
4 => Ok(FlowControl::Return),
5 => Ok(FlowControl::Call),
6 => Ok(FlowControl::IndirectCall),
7 => Ok(FlowControl::Interrupt),
8 => Ok(FlowControl::XbeginXabortXend),
9 => Ok(FlowControl::Exception),
_ => Err(serde::de::Error::custom("invalid cached flow-control kind")),
}
}
}
pub type Path = Vec<u64>;
impl Analysis {
pub fn paths_to(
&self,
target: u64,
roots: &[u64],
max_paths: usize,
strict: bool,
) -> Vec<Path> {
let mut callers: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
for f in &self.functions {
for c in &f.calls {
callers.entry(*c).or_default().insert(f.addr);
}
}
let root_set: BTreeSet<u64> = roots.iter().copied().collect();
let mut out: Vec<Path> = Vec::new();
let mut visited: BTreeSet<u64> = BTreeSet::new();
let mut queue: VecDeque<Path> = VecDeque::new();
queue.push_back(vec![target]);
visited.insert(target);
while let Some(chain) = queue.pop_front() {
if out.len() >= max_paths {
break;
}
let head = chain[0];
let ups = callers.get(&head);
let is_root = root_set.contains(&head);
let unreached = ups.is_none_or(BTreeSet::is_empty);
if is_root || unreached {
if chain.len() > 1 && (is_root || !strict) {
out.push(chain);
}
continue;
}
for up in ups.into_iter().flatten() {
if !visited.insert(*up) {
continue;
}
let mut next = Vec::with_capacity(chain.len() + 1);
next.push(*up);
next.extend_from_slice(&chain);
queue.push_back(next);
}
}
out
}
}
pub fn off_to_va(bin: &Binary, base: u64, off: u64) -> Option<u64> {
bin.sections.iter().find_map(|s| {
(s.file_size > 0 && off >= s.file_off && off < s.file_off + s.file_size)
.then(|| base + s.vaddr + (off - s.file_off))
})
}
pub fn display_base(bin: &Binary) -> u64 {
match bin.format {
Format::Pe => bin.image_base,
_ => 0,
}
}
pub fn va_to_off(bin: &Binary, base: u64, va: u64) -> Option<usize> {
let rva = va.checked_sub(base)?;
for s in &bin.sections {
let span = s.vsize.max(s.file_size);
if s.vaddr != 0 && rva >= s.vaddr && rva < s.vaddr + span {
let delta = rva - s.vaddr;
return (delta < s.file_size).then_some((s.file_off + delta) as usize);
}
}
None
}
fn in_exec(bin: &Binary, base: u64, va: u64) -> bool {
let Some(rva) = va.checked_sub(base) else {
return false;
};
bin.sections.iter().any(|s| {
s.exec && s.file_size > 0 && rva >= s.vaddr && rva < s.vaddr + s.vsize.max(s.file_size)
})
}
fn known_address(bin: &Binary, base: u64, va: u64) -> bool {
va == base || is_mapped(bin, base, va)
}
fn is_mapped(bin: &Binary, base: u64, va: u64) -> bool {
let Some(rva) = va.checked_sub(base) else {
return false;
};
bin.sections
.iter()
.any(|s| s.vaddr != 0 && rva >= s.vaddr && rva < s.vaddr + s.vsize.max(s.file_size))
}
fn data_ref(bin: &Binary, base: u64, insn: &Instruction) -> Option<u64> {
if insn.is_ip_rel_memory_operand() {
let t = insn.ip_rel_memory_address();
return is_mapped(bin, base, t).then_some(t);
}
if insn.memory_base() == iced_x86::Register::None
&& insn.memory_index() == iced_x86::Register::None
{
let d = insn.memory_displacement64();
if d != 0 && is_mapped(bin, base, d) {
return Some(d);
}
}
for i in 0..insn.op_count() {
let v = match insn.op_kind(i) {
iced_x86::OpKind::Immediate32 => insn.immediate32() as u64,
iced_x86::OpKind::Immediate64 => insn.immediate64(),
_ => continue,
};
if v != 0 && !in_exec(bin, base, v) && is_mapped(bin, base, v) {
return Some(v);
}
}
None
}
fn jump_table_base(
bin: &Binary,
insn: &iced_x86::Instruction,
regs: &HashMap<iced_x86::Register, u64>,
) -> Option<u64> {
if !matches!(
bin.arch,
crate::model::Arch::X86 | crate::model::Arch::X86_64
) {
return None;
}
let idx = insn.memory_index();
if idx == iced_x86::Register::None {
return None;
}
let disp = insn.memory_displacement64();
if let Some(t) = regs.get(&idx) {
return Some(t.wrapping_add(disp));
}
if insn.memory_base() == iced_x86::Register::None && disp != 0 {
return Some(disp);
}
None
}
const MAX_TABLE_ENTRIES: usize = 4096;
#[derive(Clone, Copy)]
struct PendingTable {
dst: iced_x86::Register,
index: iced_x86::Register,
base: u64,
table: u64,
ready: bool,
}
fn pending_step(
insn: &Instruction,
regs: &HashMap<iced_x86::Register, u64>,
pending: Option<PendingTable>,
) -> Option<PendingTable> {
if insn.mnemonic() == iced_x86::Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op0_kind() == iced_x86::OpKind::Register
&& insn.op1_kind() == iced_x86::OpKind::Memory
&& insn.memory_index_scale() == 4
&& insn.memory_index() != iced_x86::Register::None
{
if let Some(&b) = regs.get(&insn.memory_base().full_register()) {
return Some(PendingTable {
dst: insn.op0_register().full_register(),
index: insn.memory_index().full_register(),
base: b,
table: b.wrapping_add(insn.memory_displacement64()),
ready: false,
});
}
}
let mut p = pending?;
if !p.ready
&& insn.mnemonic() == iced_x86::Mnemonic::Add
&& insn.op_count() == 2
&& insn.op0_kind() == iced_x86::OpKind::Register
&& insn.op0_register().full_register() == p.dst
&& insn.op1_kind() == iced_x86::OpKind::Register
&& regs.get(&insn.op1_register().full_register()) == Some(&p.base)
{
p.ready = true;
return Some(p);
}
None
}
fn absolute_table(bin: &Binary, base: u64, bytes: &[u8], table: u64) -> Vec<u64> {
let width = if bin.bits == 64 { 8usize } else { 4 };
let mut out = Vec::new();
let mut at = table;
for _ in 0..MAX_TABLE_ENTRIES {
let Some(off) = va_to_off(bin, base, at) else {
break;
};
let Some(e) = bytes.get(off..off + width) else {
break;
};
let t = if width == 8 {
u64::from_le_bytes(e.try_into().expect("eight bytes"))
} else {
u64::from(u32::from_le_bytes(e.try_into().expect("four bytes")))
};
if t == 0 || !in_exec(bin, base, t) {
break;
}
out.push(t);
at += width as u64;
}
out
}
fn offset_table(
bin: &Binary,
base: u64,
bytes: &[u8],
insn: &Instruction,
pending: Option<PendingTable>,
) -> Option<(u64, Vec<u64>)> {
let p = pending?;
if !p.ready
|| insn.op0_kind() != iced_x86::OpKind::Register
|| insn.op0_register().full_register() != p.dst
{
return None;
}
let mut out = Vec::new();
let mut at = p.table;
for _ in 0..MAX_TABLE_ENTRIES {
let Some(off) = va_to_off(bin, base, at) else {
break;
};
let Some(e) = bytes.get(off..off + 4) else {
break;
};
let t = p.base.wrapping_add(u64::from(u32::from_le_bytes(
e.try_into().expect("four bytes"),
)));
if !in_exec(bin, base, t) {
break;
}
out.push(t);
at += 4;
}
Some((p.table, out))
}
fn group_refs<V: Copy, K: Ord + Eq>(
mut flat: Vec<(u64, V)>,
order: impl Fn(&V) -> K,
) -> BTreeMap<u64, Vec<V>> {
flat.sort_unstable_by(|(a_key, a), (b_key, b)| {
a_key.cmp(b_key).then_with(|| order(a).cmp(&order(b)))
});
flat.dedup_by(|(a_key, a), (b_key, b)| a_key == b_key && order(a) == order(b));
let mut groups: Vec<(u64, Vec<V>)> = Vec::new();
for (key, value) in flat {
match groups.last_mut() {
Some((at, run)) if *at == key => run.push(value),
_ => groups.push((key, vec![value])),
}
}
groups.into_iter().collect()
}
pub fn analyze(bin: &Binary, bytes: &[u8], max_insns: usize, db: &crate::db::Db) -> Analysis {
let base = display_base(bin);
let stubs = crate::analysis::thunks::resolve(bin, bytes, base);
let mut names: BTreeMap<u64, String> = BTreeMap::new();
let mut import_slots: BTreeMap<u64, String> = BTreeMap::new();
let mut imports: BTreeMap<u64, String> = BTreeMap::new();
for s in bin.symbols.iter().chain(stubs.iter()) {
let va = s.addr + base;
match s.kind {
SymKind::Import => {
import_slots.insert(va, s.name.clone());
imports.insert(va, s.name.clone());
}
SymKind::Thunk => {
names.entry(va).or_insert_with(|| s.name.clone());
imports.insert(va, s.name.clone());
}
SymKind::Func | SymKind::Export => {
names
.entry(va)
.or_insert_with(|| crate::analysis::demangle::display_name(&s.name));
}
}
}
let entry_va = bin.entry + base;
let mut seeds: VecDeque<u64> = VecDeque::new();
let mut is_seed: BTreeSet<u64> = BTreeSet::new();
if in_exec(bin, base, entry_va) {
seeds.push_back(entry_va);
is_seed.insert(entry_va);
names.entry(entry_va).or_insert_with(|| {
if bin.subsystem.as_deref() == Some("native") {
"DriverEntry".to_string()
} else {
"entry".to_string()
}
});
}
for s in bin.symbols.iter().chain(stubs.iter()) {
let va = s.addr + base;
if matches!(s.kind, SymKind::Func | SymKind::Export | SymKind::Thunk)
&& in_exec(bin, base, va)
&& is_seed.insert(va)
{
seeds.push_back(va);
}
}
for rva in &bin.func_hints {
let va = rva + base;
if in_exec(bin, base, va) && is_seed.insert(va) {
seeds.push_back(va);
}
}
for (rva, name) in &db.names {
let va = rva + base;
names.insert(va, name.clone());
if in_exec(bin, base, va) && is_seed.insert(va) {
seeds.push_back(va);
}
}
let boundaries: BTreeSet<u64> = is_seed.iter().copied().collect();
let mut functions: Vec<Function> = Vec::new();
let mut xrefs_to_flat: Vec<(u64, Xref)> = Vec::new();
let mut xrefs_from_flat: Vec<(u64, Ref)> = Vec::new();
let mut done: BTreeSet<u64> = BTreeSet::new();
let mut budget = max_insns;
let mut truncated = false;
while let Some(func_va) = seeds.pop_front() {
if done.contains(&func_va) {
continue;
}
if budget == 0 {
truncated = true;
break;
}
done.insert(func_va);
let (func, discovered, spent) = build_function(
bin,
bytes,
func_va,
base,
&names,
&import_slots,
&boundaries,
&mut xrefs_to_flat,
&mut xrefs_from_flat,
budget,
);
budget = budget.saturating_sub(spent);
for c in discovered {
if in_exec(bin, base, c) && is_seed.insert(c) {
seeds.push_back(c);
}
}
if func.size > 0 {
functions.push(func);
}
}
let xrefs_to = group_refs(xrefs_to_flat, |x| (x.from, x.kind as u8));
let xrefs_from = group_refs(xrefs_from_flat, |x| (x.to, x.kind as u8));
let mut incoming: BTreeMap<u64, usize> = BTreeMap::new();
for (target, refs) in &xrefs_to {
incoming.insert(*target, refs.len());
}
for f in &mut functions {
f.incoming = incoming.get(&f.addr).copied().unwrap_or(0);
f.named = names.contains_key(&f.addr);
if !f.named {
if let Some(off) = va_to_off(bin, base, f.addr) {
if let Some(sig) = crate::analysis::flirt::identify(bytes, off, f.size as usize) {
names.insert(f.addr, sig.to_string());
f.name = sig.to_string();
f.named = true;
}
}
}
if !f.named {
f.name = format!("sub_{:x}", f.addr);
}
}
functions.sort_by_key(|f| f.addr);
let mut function_names = BTreeMap::new();
for (index, function) in functions.iter().enumerate() {
function_names.entry(function.name.clone()).or_insert(index);
}
Analysis {
functions,
function_names,
xrefs_to,
xrefs_from,
names,
imports,
display_base: 0,
bits: bin.bits,
arch: bin.arch,
truncated,
}
}
#[allow(clippy::too_many_arguments)]
fn build_function(
bin: &Binary,
bytes: &[u8],
entry: u64,
base: u64,
names: &BTreeMap<u64, String>,
import_slots: &BTreeMap<u64, String>,
boundaries: &BTreeSet<u64>,
xrefs_to: &mut Vec<(u64, Xref)>,
xrefs_from: &mut Vec<(u64, Ref)>,
budget: usize,
) -> (Function, Vec<u64>, usize) {
let mut blocks: BTreeMap<u64, BasicBlock> = BTreeMap::new();
let mut worklist: VecDeque<u64> = VecDeque::new();
let mut seen: BTreeSet<u64> = BTreeSet::new();
let mut calls: Vec<u64> = Vec::new();
let mut tables: Vec<u64> = Vec::new();
let mut spent = 0usize;
let mut min_start = entry;
let mut max_end = entry;
let mut regs: HashMap<iced_x86::Register, u64> = HashMap::new();
let queue_block = |t: u64, seen: &mut BTreeSet<u64>, wl: &mut VecDeque<u64>| -> bool {
if t != entry && boundaries.contains(&t) {
return false;
}
if va_to_off(bin, base, t).is_none() {
return false;
}
if seen.insert(t) {
wl.push_back(t);
}
true
};
worklist.push_back(entry);
seen.insert(entry);
while let Some(block_start) = worklist.pop_front() {
if spent >= budget {
break;
}
let Some(off) = va_to_off(bin, base, block_start) else {
continue;
};
let Some(code) = bytes.get(off..) else {
continue;
};
min_start = min_start.min(block_start);
let mut decoder = Decoder::with_ip(bin.bits, code, block_start, DecoderOptions::NONE);
let mut apos = 0usize;
let is_a64 = bin.arch == crate::model::Arch::Aarch64;
let mut insns = Vec::new();
let mut succ = Vec::new();
let mut end = block_start;
let mut pending: Option<PendingTable> = None;
loop {
if spent >= budget {
break;
}
let mut ice: Option<Instruction> = None;
let (addr, len, raw, flow, dtarget) = if is_a64 {
let at = block_start + apos as u64;
let Some(chunk) = bytes.get(off + apos..) else {
break;
};
let Some(w) = crate::analysis::aarch64::decode(chunk, at) else {
break;
};
spent += 1;
let raw = &chunk[..w.len];
apos += w.len;
(w.addr, w.len, raw, w.flow, w.target)
} else {
if !decoder.can_decode() {
break;
}
let pos = decoder.position();
let insn = decoder.decode();
if insn.is_invalid() {
break;
}
spent += 1;
let flow = insn.flow_control();
let dtarget = if matches!(
flow,
FlowControl::Call
| FlowControl::UnconditionalBranch
| FlowControl::ConditionalBranch
) && matches!(
insn.op0_kind(),
iced_x86::OpKind::NearBranch16
| iced_x86::OpKind::NearBranch32
| iced_x86::OpKind::NearBranch64
) {
Some(insn.near_branch_target())
} else {
None
};
let addr = insn.ip();
let len = insn.len();
let raw = &bytes[off + pos..off + pos + len];
ice = Some(insn);
(addr, len, raw, flow, dtarget)
};
end = addr + len as u64;
max_end = max_end.max(end);
let mut target = dtarget;
let mut target_name = None;
if let Some(insn) = &ice {
let mut info = InstructionInfoFactory::new();
let used = info.info(insn).used_registers();
for u in used {
if matches!(
u.access(),
OpAccess::Write
| OpAccess::CondWrite
| OpAccess::ReadWrite
| OpAccess::ReadCondWrite
) {
regs.remove(&u.register().full_register());
regs.remove(&u.register());
}
}
if insn.op0_kind() == iced_x86::OpKind::Register {
let reg = insn.op0_register();
let known = if insn.mnemonic() == iced_x86::Mnemonic::Lea
&& insn.is_ip_rel_memory_operand()
{
let t = insn.ip_rel_memory_address();
known_address(bin, base, t).then_some(t)
} else if insn.mnemonic() == iced_x86::Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op1_kind() == iced_x86::OpKind::Immediate64
{
let t = insn.immediate64();
known_address(bin, base, t).then_some(t)
} else {
None
};
if let Some(t) = known {
regs.insert(reg, t);
}
}
pending = match pending_step(insn, ®s, pending) {
Some(p) => Some(p),
None => pending.filter(|p| {
!used.iter().any(|u| {
matches!(
u.access(),
OpAccess::Write
| OpAccess::CondWrite
| OpAccess::ReadWrite
| OpAccess::ReadCondWrite
) && {
let w = u.register().full_register();
w == p.dst || w == p.index
}
})
}),
};
}
match flow {
FlowControl::Call => {
if let Some(t) = target {
target_name = names.get(&t).map(|n| n.as_str().into());
calls.push(t);
xrefs_to.push((
t,
Xref {
from: addr,
kind: XrefKind::Call,
},
));
xrefs_from.push((
addr,
Ref {
to: t,
kind: XrefKind::Call,
},
));
}
}
FlowControl::UnconditionalBranch => {
if let Some(t) = target {
target_name = names.get(&t).map(|n| n.as_str().into());
let kind = XrefKind::Jump;
xrefs_to.push((t, Xref { from: addr, kind }));
xrefs_from.push((addr, Ref { to: t, kind }));
if boundaries.contains(&t) && t != entry {
calls.push(t); } else if queue_block(t, &mut seen, &mut worklist) {
succ.push(t);
}
}
}
FlowControl::ConditionalBranch => {
if let Some(t) = target {
target_name = names.get(&t).map(|n| n.as_str().into());
xrefs_to.push((
t,
Xref {
from: addr,
kind: XrefKind::Branch,
},
));
xrefs_from.push((
addr,
Ref {
to: t,
kind: XrefKind::Branch,
},
));
if queue_block(t, &mut seen, &mut worklist) {
succ.push(t);
}
}
}
FlowControl::IndirectCall | FlowControl::IndirectBranch => {
if let Some(insn) = &ice {
let slot = if insn.is_ip_rel_memory_operand() {
Some(insn.ip_rel_memory_address())
} else if insn.memory_base() == iced_x86::Register::None
&& insn.memory_index() == iced_x86::Register::None
&& insn.memory_displacement64() != 0
{
Some(insn.memory_displacement64())
} else {
None
};
if let Some(slot) = slot {
if let Some(n) = import_slots.get(&slot) {
target = Some(slot);
target_name = Some(n.as_str().into());
calls.push(slot);
let kind = if flow == FlowControl::IndirectCall {
XrefKind::Call
} else {
XrefKind::Jump
};
xrefs_to.push((slot, Xref { from: addr, kind }));
xrefs_from.push((addr, Ref { to: slot, kind }));
}
}
if target.is_none() && flow == FlowControl::IndirectBranch {
let found = jump_table_base(bin, insn, ®s)
.map(|t| (t, absolute_table(bin, base, bytes, t)))
.or_else(|| offset_table(bin, base, bytes, insn, pending));
if let Some((table, targets)) = found {
if !targets.is_empty() {
tables.push(table);
}
for t in targets {
xrefs_to.push((
t,
Xref {
from: addr,
kind: XrefKind::Jump,
},
));
xrefs_from.push((
addr,
Ref {
to: t,
kind: XrefKind::Jump,
},
));
if queue_block(t, &mut seen, &mut worklist) {
succ.push(t);
}
}
}
}
}
}
_ => {}
}
if target.is_none() {
if let Some(insn) = &ice {
if let Some(t) = data_ref(bin, base, insn) {
xrefs_to.push((
t,
Xref {
from: addr,
kind: XrefKind::Data,
},
));
xrefs_from.push((
addr,
Ref {
to: t,
kind: XrefKind::Data,
},
));
}
}
}
insns.push(EngineInsn::new(addr, raw, flow, target, target_name));
if flow == FlowControl::ConditionalBranch {
if queue_block(end, &mut seen, &mut worklist) {
succ.push(end);
}
break;
}
if end != block_start && seen.contains(&end) {
succ.push(end);
break;
}
let stop = matches!(
flow,
FlowControl::Return
| FlowControl::UnconditionalBranch
| FlowControl::IndirectBranch
| FlowControl::Interrupt
) || ice
.as_ref()
.is_some_and(|i| i.mnemonic() == Mnemonic::Int3)
|| boundaries.contains(&end) && end != entry;
if stop {
break;
}
}
blocks.insert(
block_start,
BasicBlock {
start: block_start,
end,
insns,
succ,
},
);
}
let starts: Vec<u64> = blocks.keys().copied().collect();
for (i, start) in starts.iter().enumerate() {
let Some(&next) = starts.get(i + 1) else {
continue;
};
let Some(b) = blocks.get_mut(start) else {
continue;
};
if b.end > next {
b.insns.retain(|ins| ins.addr < next);
b.end = next;
b.succ = vec![next];
}
}
let name = names
.get(&entry)
.cloned()
.unwrap_or_else(|| format!("sub_{entry:x}"));
let discovered = calls.clone();
let func = Function {
addr: entry,
name,
blocks: blocks.into_values().collect(),
size: max_end.saturating_sub(min_start),
incoming: 0,
calls,
named: names.contains_key(&entry),
tables,
};
(func, discovered, spent)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Db;
use crate::model::{Arch, Section};
fn code_at(vaddr: u64, code: &[u8]) -> (Binary, Vec<u8>) {
let mut bin = Binary::stub(Format::Elf, Arch::X86_64);
bin.entry = vaddr;
bin.sections = vec![Section {
name: ".text".into(),
vaddr,
vsize: code.len() as u64,
file_off: vaddr,
file_size: code.len() as u64,
entropy: 0.0,
read: true,
write: false,
exec: true,
}];
let mut bytes = vec![0u8; vaddr as usize];
bytes.extend_from_slice(code);
(bin, bytes)
}
#[test]
fn a_func_hint_recovers_code_no_control_flow_reaches() {
let code = [
0xc3, 0x90, 0x31, 0xc0, 0xc3, ];
let (mut bin, bytes) = code_at(0x1000, &code);
let bare = analyze(&bin, &bytes, 1000, &Db::default());
assert!(bare.find_function(0x1002).is_none());
bin.func_hints = vec![0x1002];
let with_hint = analyze(&bin, &bytes, 1000, &Db::default());
let f = with_hint
.find_function(0x1002)
.expect("the hinted function should be recovered");
assert!(!f.blocks.is_empty());
assert_eq!(with_hint.label(0x1002), "sub_1002");
assert_eq!(
with_hint
.find_by_name("sub_1002")
.map(|function| function.addr),
Some(0x1002)
);
assert_eq!(
with_hint.function_at(0x1003).map(|function| function.addr),
Some(0x1002)
);
}
#[test]
fn a_branch_into_a_decoded_run_does_not_duplicate_instructions() {
let code = [
0x31, 0xc0, 0x74, 0x02, 0x90, 0x90, 0xc3, ];
let (bin, bytes) = code_at(0x1000, &code);
let an = analyze(&bin, &bytes, 1000, &Db::default());
let f = an.find_function(0x1000).expect("entry recovered");
let mut seen: Vec<u64> = f
.blocks
.iter()
.flat_map(|b| b.insns.iter().map(|i| i.addr))
.collect();
let before = seen.len();
seen.sort_unstable();
seen.dedup();
assert_eq!(before, seen.len(), "every instruction belongs to one block");
let mut spans: Vec<(u64, u64)> = f.blocks.iter().map(|b| (b.start, b.end)).collect();
spans.sort_unstable();
for w in spans.windows(2) {
assert!(
w[0].1 <= w[1].0,
"blocks overlap: {:x?} then {:x?}",
w[0],
w[1]
);
}
}
#[test]
fn a_signature_match_names_the_function() {
let code = [
0xe8, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x05, 0x12, 0x34, 0x56, 0x78, 0x48, 0x3b, 0x04, 0x24, 0x75, 0x02, 0xf3,
0xc3, 0xc3, ];
let (bin, bytes) = code_at(0x1000, &code);
let an = analyze(&bin, &bytes, 1000, &Db::default());
let helper = an
.functions
.iter()
.find(|f| f.addr == 0x1005)
.expect("helper recovered");
assert!(helper.named);
assert_eq!(helper.name, "__security_check_cookie");
}
#[test]
fn a_jump_table_brings_the_cases_in() {
let mut code = vec![0u8; 0x5018];
code[0x0000] = 0x48; code[0x0001] = 0x8d;
code[0x0002] = 0x05;
code[0x0003..0x0007].copy_from_slice(&0xff9i32.to_le_bytes());
code[0x0007] = 0xff; code[0x0008] = 0x24;
code[0x0009] = 0xc5;
code[0x1000..0x1008].copy_from_slice(&0x6000u64.to_le_bytes());
code[0x1008..0x1010].copy_from_slice(&0x6007u64.to_le_bytes());
code[0x1010..0x1018].copy_from_slice(&0x600eu64.to_le_bytes());
code[0x2000] = 0xc3;
code[0x2007] = 0xc3;
code[0x200e] = 0xc3;
let (bin, bytes) = code_at(0x4000, &code);
let an = analyze(&bin, &bytes, 10_000, &Db::default());
let sw = an.find_function(0x4000).expect("entry recovered");
assert_eq!(sw.tables, vec![0x5000], "the table is attributed");
for case in [0x6000u64, 0x6007, 0x600e] {
assert!(
an.xrefs_from.get(&0x4007).is_some_and(|r| r
.iter()
.any(|x| x.to == case && matches!(x.kind, XrefKind::Jump))),
"case {case:#x} reachable from the dispatch"
);
}
for case in [0x6000u64, 0x6007, 0x600e] {
assert!(
sw.blocks.iter().any(|b| b.start == case),
"case {case:#x} became a block"
);
}
}
#[test]
fn an_x64_offset_table_brings_the_cases_in() {
let mut code = vec![0u8; 0x2010];
code[0x0000] = 0x4c; code[0x0001] = 0x8d;
code[0x0002] = 0x35;
code[0x0003..0x0007].copy_from_slice(&(-0x4007i32).to_le_bytes());
code[0x0007] = 0x41; code[0x0008] = 0x8b;
code[0x0009] = 0x8c;
code[0x000a] = 0x86;
code[0x000b..0x000f].copy_from_slice(&0x5000u32.to_le_bytes());
code[0x000f] = 0x49; code[0x0010] = 0x03;
code[0x0011] = 0xce;
code[0x0012] = 0xff; code[0x0013] = 0xe1;
code[0x1000..0x1004].copy_from_slice(&0x6000u32.to_le_bytes());
code[0x1004..0x1008].copy_from_slice(&0x6007u32.to_le_bytes());
code[0x1008..0x100c].copy_from_slice(&0x600eu32.to_le_bytes());
code[0x2000] = 0xc3;
code[0x2007] = 0xc3;
code[0x200e] = 0xc3;
let (bin, bytes) = code_at(0x4000, &code);
let an = analyze(&bin, &bytes, 10_000, &Db::default());
let sw = an.find_function(0x4000).expect("entry recovered");
assert_eq!(sw.tables, vec![0x5000], "the table is attributed");
for case in [0x6000u64, 0x6007, 0x600e] {
assert!(
an.xrefs_from
.get(&0x4012)
.is_some_and(|r| r.iter().any(|x| x.to == case)),
"case 0x{case:x} should be an edge from the jump"
);
}
}
#[test]
fn a_bare_indirect_jump_invents_no_table() {
let mut code = vec![0u8; 0x2010];
code[0x0000] = 0xff; code[0x0001] = 0xe1;
code[0x1000..0x1004].copy_from_slice(&0x6000u32.to_le_bytes());
code[0x2000] = 0xc3;
let (bin, bytes) = code_at(0x4000, &code);
let an = analyze(&bin, &bytes, 10_000, &Db::default());
let f = an.find_function(0x4000).expect("entry recovered");
assert!(
f.tables.is_empty(),
"no table should be claimed: {:?}",
f.tables
);
}
#[test]
fn a_note_annotates_without_renaming() {
let code = [0x90, 0xc3]; let (bin, bytes) = code_at(0x1000, &code);
let mut db = Db::default();
db.set_note(0x1000, "parses the header");
let an = analyze(&bin, &bytes, 1000, &db);
assert_eq!(an.label(0x1000), "entry");
}
#[test]
fn a_name_takes_precedence_over_the_derived_label() {
let code = [0x90, 0xc3];
let (bin, bytes) = code_at(0x1000, &code);
let mut db = Db::default();
db.set_name(0x1000, "parse_header");
let an = analyze(&bin, &bytes, 1000, &db);
assert_eq!(an.label(0x1000), "parse_header");
}
#[test]
fn xrefs_from_mirrors_the_call_edges() {
let code = [
0xe8, 0x01, 0x00, 0x00, 0x00, 0xc3, 0xc3, ];
let (bin, bytes) = code_at(0x1000, &code);
let an = analyze(&bin, &bytes, 1000, &Db::default());
let refs = an.xrefs_from.get(&0x1000).expect("call recorded");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to, 0x1006);
assert_eq!(refs[0].kind, XrefKind::Call);
assert!(an.xrefs_to.get(&0x1006).is_some_and(|r| r
.iter()
.any(|x| x.from == 0x1000 && x.kind == XrefKind::Call)));
}
#[test]
fn xrefs_from_records_a_string_operand() {
let mut bin = Binary::stub(Format::Elf, Arch::X86_64);
bin.entry = 0x1000;
bin.sections = vec![
Section {
name: ".text".into(),
vaddr: 0x1000,
vsize: 8,
file_off: 0,
file_size: 8,
entropy: 0.0,
read: true,
write: false,
exec: true,
},
Section {
name: ".rodata".into(),
vaddr: 0x101e,
vsize: 5,
file_off: 8,
file_size: 5,
entropy: 0.0,
read: true,
write: false,
exec: false,
},
];
let mut bytes = vec![0x48, 0x8d, 0x05, 0x17, 0x00, 0x00, 0x00, 0xc3];
bytes.extend_from_slice(b"hello");
let an = analyze(&bin, &bytes, 1000, &Db::default());
let refs = an.xrefs_from.get(&0x1000).expect("operand recorded");
assert!(refs
.iter()
.any(|r| r.to == 0x101e && r.kind == XrefKind::Data));
}
#[test]
fn the_aarch64_fixture_flows_without_x86() {
let bytes = crate::formats::fixture::elf_aarch64_call();
let bin = crate::formats::analyze("stub", &bytes).expect("parses");
assert_eq!(bin.arch, Arch::Aarch64);
let an = analyze(&bin, &bytes, 1000, &Db::default());
let entry = an.find_function(bin.entry).expect("entry recovered");
assert!(entry.blocks.iter().any(|b| b.start == bin.entry));
let helper = bin.entry + 0x10;
assert!(
an.xrefs_from.get(&bin.entry).is_some_and(|rs| rs
.iter()
.any(|r| r.to == helper && r.kind == XrefKind::Call)),
"call edge into the helper"
);
let h = an.find_function(helper).expect("helper recovered");
assert_eq!(h.blocks.len(), 1);
assert_eq!(h.blocks[0].insns.len(), 3);
let insns = &entry.blocks[0].insns;
assert!(
insns[0].text(bin.bits, bin.arch).starts_with("bl"),
"{}",
insns[0].text(bin.bits, bin.arch)
);
assert_eq!(h.blocks[0].insns[2].text(bin.bits, bin.arch), "ret");
}
#[test]
fn the_aarch64_linear_disassembler_names_the_common_forms() {
let bytes = crate::formats::fixture::elf_aarch64_call();
let bin = crate::formats::analyze("stub", &bytes).unwrap();
let insns = crate::analysis::disasm::disassemble(
&bytes, bin.entry, bin.entry, bin.bits, bin.arch, 8,
);
let texts: Vec<&str> = insns.iter().map(|i| i.text.as_str()).collect();
assert_eq!(texts.len(), 8, "{texts:?}");
assert!(texts[0].starts_with("bl"), "{texts:?}");
assert_eq!(texts[1], "movz x0, #0x2a", "{texts:?}");
assert_eq!(texts[2], "ret", "{texts:?}");
assert_eq!(texts[3], "nop", "{texts:?}");
assert_eq!(texts[4], "add x0, x1, x2", "{texts:?}");
assert_eq!(texts[5], "ldr x1, [x0, #0]", "{texts:?}");
assert_eq!(texts[6], "ret", "{texts:?}");
assert!(texts[7].starts_with("dword"), "{texts:?}");
}
}