use pcode_ir::{AddressSpaceId, Instruction, PcodeOp, Varnode};
use std::collections::HashSet;
pub type Memory<'a> = &'a dyn Fn(u64, usize) -> Option<Vec<u8>>;
#[derive(Debug, Clone)]
pub struct JumpTable {
pub jump: u64,
pub index: Varnode,
pub copy_before: Option<u64>,
pub targets: Vec<u64>,
pub first: i64,
}
const MAX_ENTRIES: usize = 256;
const MAX_GUARDED_ENTRIES: usize = 4096;
const WINDOW: usize = 512;
struct Op<'a> {
op: &'a PcodeOp,
address: u64,
}
enum Entries {
Relative { base: u64, signed: bool },
Absolute(usize),
}
pub fn find(instructions: &[(u64, Instruction)], memory: Memory) -> Vec<JumpTable> {
let mut sorted: Vec<&(u64, Instruction)> = instructions.iter().collect();
sorted.sort_by_key(|(a, _)| *a);
let starts: HashSet<u64> = sorted.iter().map(|(a, _)| *a).collect();
let mut found = Vec::new();
for (i, (address, inst)) in sorted.iter().enumerate() {
let Some(PcodeOp::BranchInd { dest }) = inst.ops.last() else {
continue;
};
let mut first = i;
while first > 0
&& i - first < WINDOW
&& sorted[first - 1].0 + sorted[first - 1].1.len == sorted[first].0
{
first -= 1;
}
let window: Vec<Op> = sorted[first..=i]
.iter()
.flat_map(|(address, inst)| {
inst.ops.iter().map(|op| Op {
op,
address: *address,
})
})
.collect();
let end = window.len() - 1;
let Some((table, entries, index_pos, index)) = dispatch(&window, end, *dest) else {
continue;
};
let floor = signed_floor(&window, index_pos, index);
let Some((index, copy_before)) = stable_index(&window, index_pos, index, *address) else {
continue;
};
let bound = guard_count(&sorted[i.saturating_sub(16)..i])
.filter(|count| *count <= MAX_GUARDED_ENTRIES);
let width = match entries {
Entries::Relative { .. } => 4,
Entries::Absolute(w) => w,
};
let entry = |k: i64| -> Option<u64> {
let bytes = memory(table.wrapping_add((k * width as i64) as u64), width)?;
let raw = bytes
.iter()
.rev()
.fold(0u64, |acc, b| (acc << 8) | *b as u64);
let target = match entries {
Entries::Relative { base, signed: true } => {
base.wrapping_add(raw as u32 as i32 as i64 as u64)
}
Entries::Relative {
base,
signed: false,
} => base.wrapping_add(raw as u32 as u64),
Entries::Absolute(_) => raw,
};
starts.contains(&target).then_some(target)
};
let mut targets = Vec::new();
for k in 0..bound.unwrap_or(MAX_ENTRIES) {
let Some(target) = entry(k as i64) else {
if bound.is_some() {
targets.clear();
}
break;
};
targets.push(target);
}
if targets.is_empty() {
continue;
}
let mut first = 0i64;
if let (None, Some(floor)) = (bound, floor) {
while first > floor && targets.len() < MAX_ENTRIES {
let Some(target) = entry(first - 1) else {
break;
};
targets.insert(0, target);
first -= 1;
}
}
found.push(JumpTable {
jump: *address,
index,
copy_before,
targets,
first,
});
}
found
}
fn stable_index(
window: &[Op],
start: usize,
start_vn: Varnode,
jump: u64,
) -> Option<(Varnode, Option<u64>)> {
let (mut pos, mut vn) = (start, start_vn);
for _ in 0..6 {
let overwritten = window[pos..].iter().any(|o| writes(o.op, vn));
if !overwritten && vn.space != AddressSpaceId::Const {
return Some((vn, None));
}
let Some(d) = def(window, pos, vn) else { break };
match window[d].op {
PcodeOp::Copy { input, .. }
| PcodeOp::IntZext { input, .. }
| PcodeOp::Subpiece { input, lsb: 0, .. } => (vn, pos) = (*input, d),
_ => break,
}
}
let at = window[start].address;
let changed_first = window[..start]
.iter()
.any(|o| o.address == at && writes(o.op, start_vn));
(start_vn.space == AddressSpaceId::Register && at != jump && !changed_first)
.then_some((start_vn, Some(at)))
}
fn signed_floor(window: &[Op], pos: usize, vn: Varnode) -> Option<i64> {
let (mut at, mut vn) = (pos, vn);
for _ in 0..6 {
let d = def(window, at, vn)?;
match window[d].op {
PcodeOp::Copy { input, .. } => (vn, at) = (*input, d),
PcodeOp::IntSext { input, out } if input.size < out.size && input.size <= 4 => {
return Some(-(1i64 << (input.size * 8 - 1)));
}
_ => return None,
}
}
None
}
fn writes(op: &PcodeOp, vn: Varnode) -> bool {
pcode_ir::get_output(op).is_some_and(|out| out.space == vn.space && out.offset == vn.offset)
}
fn def(window: &[Op], end: usize, vn: Varnode) -> Option<usize> {
window[..end].iter().rposition(|o| {
pcode_ir::get_output(o.op)
.is_some_and(|out| out.space == vn.space && out.offset == vn.offset)
})
}
fn constant(window: &[Op], end: usize, vn: Varnode, depth: u32) -> Option<u64> {
if vn.space == AddressSpaceId::Const {
return Some(vn.offset);
}
if depth == 0 {
return None;
}
let d = def(window, end, vn)?;
match window[d].op {
PcodeOp::Copy { input, .. }
| PcodeOp::IntZext { input, .. }
| PcodeOp::IntSext { input, .. }
| PcodeOp::Subpiece { input, lsb: 0, .. } => constant(window, d, *input, depth - 1),
PcodeOp::IntAdd { left, right, .. } => Some(
constant(window, d, *left, depth - 1)?.wrapping_add(constant(
window,
d,
*right,
depth - 1,
)?),
),
_ => None,
}
}
fn scaled(window: &[Op], end: usize, vn: Varnode, depth: u32) -> Option<(Varnode, usize, u64)> {
if depth == 0 {
return None;
}
let d = def(window, end, vn)?;
match window[d].op {
PcodeOp::IntMult { left, right, .. } => {
let scale = constant(window, d, *right, 4)?;
Some((*left, d, scale))
}
PcodeOp::IntLsl { left, right, .. } => {
let shift = constant(window, d, *right, 4)?;
(shift < 4).then(|| (*left, d, 1 << shift))
}
PcodeOp::Copy { input, .. }
| PcodeOp::IntZext { input, .. }
| PcodeOp::IntSext { input, .. } => scaled(window, d, *input, depth - 1),
_ => None,
}
}
fn table_address(
window: &[Op],
end: usize,
vn: Varnode,
depth: u32,
) -> Option<(u64, Varnode, usize, u64)> {
if depth == 0 {
return None;
}
let d = def(window, end, vn)?;
match window[d].op {
PcodeOp::IntAdd { left, right, .. } => [(*left, *right), (*right, *left)]
.into_iter()
.find_map(|(base, offset)| {
let table = constant(window, d, base, 6)?;
if let Some((index, pos, scale)) = scaled(window, d, offset, 4) {
return Some((table, index, pos, scale));
}
let (rest, index, pos, scale) = table_address(window, d, offset, depth - 1)?;
Some((table.wrapping_add(rest), index, pos, scale))
}),
PcodeOp::Copy { input, .. } => table_address(window, d, *input, depth - 1),
_ => None,
}
}
fn dispatch(window: &[Op], end: usize, dest: Varnode) -> Option<(u64, Entries, usize, Varnode)> {
if dest.space == AddressSpaceId::Ram {
let address = Varnode {
space: AddressSpaceId::Unique,
..dest
};
let (table, index, pos, scale) = table_address(window, end, address, 4)?;
return matches!(scale, 4 | 8)
.then(|| (table, Entries::Absolute(scale as usize), pos, index));
}
let (mut vn, mut at) = (dest, end);
for _ in 0..8 {
let d = def(window, at, vn)?;
match window[d].op {
PcodeOp::Copy { input, .. } => (vn, at) = (*input, d),
PcodeOp::Load { ptr, out, .. } => {
let (table, index, pos, scale) = table_address(window, d, *ptr, 4)?;
let width = out.size as u64;
return (scale == width && matches!(width, 4 | 8))
.then(|| (table, Entries::Absolute(width as usize), pos, index));
}
PcodeOp::IntAdd { left, right, .. } => {
return [(*left, *right), (*right, *left)]
.into_iter()
.find_map(|(base, entry)| {
let base = constant(window, d, base, 6)?;
let (mut ev, mut eat, mut signed) = (entry, d, true);
for _ in 0..6 {
let ed = def(window, eat, ev)?;
match window[ed].op {
PcodeOp::IntSext { input, .. } | PcodeOp::Copy { input, .. } => {
(ev, eat) = (*input, ed)
}
PcodeOp::IntZext { input, .. } => {
(ev, eat, signed) = (*input, ed, false)
}
PcodeOp::Load { ptr, out, .. } if out.size == 4 => {
let (table, index, pos, scale) =
table_address(window, ed, *ptr, 4)?;
return (scale == 4).then_some((
table,
Entries::Relative { base, signed },
pos,
index,
));
}
_ => return None,
}
}
None
});
}
_ => return None,
}
}
None
}
fn guard_count(before: &[&(u64, Instruction)]) -> Option<usize> {
let mut inclusive = None;
for (_, inst) in before.iter().rev() {
let text = inst.disassembly.trim().to_ascii_uppercase();
let mnemonic = text.split_whitespace().next().unwrap_or("");
if inclusive.is_none() {
inclusive = match mnemonic {
"JA" | "JNBE" => Some(true),
"JAE" | "JNB" | "JNC" => Some(false),
_ => continue,
};
continue;
}
if mnemonic == "CMP" {
let imm = text.rsplit(',').next()?.trim();
let n = match imm.strip_prefix("0X") {
Some(hex) => u64::from_str_radix(hex, 16).ok()?,
None => imm.parse().ok()?,
};
let count = if inclusive? { n + 1 } else { n };
return usize::try_from(count).ok().filter(|c| *c >= 1);
}
}
None
}
pub struct ImageMemory<'a> {
data: &'a [u8],
ranges: Vec<(u64, u64, u64)>,
}
impl<'a> ImageMemory<'a> {
pub fn new(data: &'a [u8]) -> Self {
let ranges = match goblin::Object::parse(data) {
Ok(goblin::Object::Elf(elf)) => elf
.program_headers
.iter()
.filter(|ph| ph.p_type == goblin::elf::program_header::PT_LOAD)
.map(|ph| (ph.p_vaddr, ph.p_filesz, ph.p_offset))
.collect(),
Ok(goblin::Object::PE(pe)) => pe
.sections
.iter()
.map(|s| {
(
pe.image_base as u64 + s.virtual_address as u64,
s.size_of_raw_data
.min(s.virtual_size.max(s.size_of_raw_data))
as u64,
s.pointer_to_raw_data as u64,
)
})
.collect(),
Ok(goblin::Object::Mach(goblin::mach::Mach::Binary(macho))) => macho
.segments
.iter()
.map(|seg| (seg.vmaddr, seg.filesize, seg.fileoff))
.collect(),
_ => Vec::new(),
};
Self { data, ranges }
}
pub fn read(&self, va: u64, len: usize) -> Option<Vec<u8>> {
let (start, _, offset) = self
.ranges
.iter()
.find(|(start, size, _)| va >= *start && va + len as u64 <= start + size)?;
let from = usize::try_from(offset + (va - start)).ok()?;
self.data
.get(from..from.checked_add(len)?)
.map(<[u8]>::to_vec)
}
}