use crate::disasm::{follow_jump_table, recover_call_site, ArgKind};
use crate::pe::Pe;
use iced_x86::{
Decoder, DecoderOptions, FlowControl, Formatter, Instruction, Mnemonic, NasmFormatter, OpKind,
Register,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
const MAX_SPAN: u64 = 0x10000;
fn decode_all(pe: &Pe, entry: u64) -> (BTreeMap<u64, Instruction>, BTreeSet<u64>) {
let (lo, hi) = (entry, entry.saturating_add(MAX_SPAN));
let mut instrs: BTreeMap<u64, Instruction> = BTreeMap::new();
let mut leaders: BTreeSet<u64> = BTreeSet::new();
leaders.insert(entry);
let mut work = vec![entry];
let mut seen: BTreeSet<u64> = BTreeSet::new();
while let Some(start) = work.pop() {
if !seen.insert(start) {
continue;
}
let Some(bytes) = pe.read(start, 32 * 64) else {
continue;
};
let mut dec = Decoder::with_ip(32, bytes, start, DecoderOptions::NONE);
let mut instr = Instruction::default();
while dec.can_decode() {
let ip = dec.ip();
if ip != start && instrs.contains_key(&ip) {
break;
}
dec.decode_out(&mut instr);
if instr.is_invalid() {
break;
}
instrs.insert(ip, instr);
match instr.flow_control() {
FlowControl::ConditionalBranch => {
let t = instr.near_branch_target();
let n = instr.next_ip();
if (lo..hi).contains(&t) {
leaders.insert(t);
work.push(t);
}
leaders.insert(n);
work.push(n);
break;
}
FlowControl::UnconditionalBranch => {
let t = instr.near_branch_target();
if t < ip {
leaders.insert(t);
work.push(t);
}
break;
}
FlowControl::IndirectBranch => {
let mut cases = Vec::new();
follow_jump_table(&instr, pe, lo, hi, &mut cases);
for c in cases {
leaders.insert(c);
work.push(c);
}
break;
}
FlowControl::Return | FlowControl::Exception => break,
_ => {}
}
}
}
(instrs, leaders)
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CfgFingerprint {
pub loops: usize,
pub switch_cases: usize,
pub conditionals: usize,
pub returns: usize,
}
struct Block {
start: u64,
body: Vec<Instruction>,
term: Option<Instruction>,
succ: Vec<u64>,
}
fn build_cfg(pe: &Pe, entry: u64) -> (BTreeMap<u64, Instruction>, Vec<Block>) {
let (_, _) = (entry, entry.saturating_add(MAX_SPAN));
let (instrs, leaders) = decode_all(pe, entry);
let addrs: Vec<u64> = instrs.keys().copied().collect();
let lo = entry;
let hi = entry.saturating_add(MAX_SPAN);
let mut blocks: Vec<Block> = Vec::new();
for &ld in &leaders {
let mut idx = match addrs.binary_search(&ld) {
Ok(i) => i,
Err(_) => continue,
};
let mut body = Vec::new();
let mut term = None;
let mut succ = Vec::new();
loop {
let a = addrs[idx];
let instr = instrs[&a];
let is_term = !matches!(
instr.flow_control(),
FlowControl::Next
| FlowControl::Call
| FlowControl::IndirectCall
| FlowControl::XbeginXabortXend
);
if is_term {
match instr.flow_control() {
FlowControl::ConditionalBranch => {
let t = instr.near_branch_target();
if (lo..hi).contains(&t) {
succ.push(t);
}
let n = instr.next_ip();
if (lo..hi).contains(&n) {
succ.push(n);
}
}
FlowControl::UnconditionalBranch => {
let t = instr.near_branch_target();
if (lo..hi).contains(&t) {
succ.push(t);
}
}
FlowControl::IndirectBranch => {
let mut cases = Vec::new();
follow_jump_table(&instr, pe, lo, hi, &mut cases);
for c in cases {
if (lo..hi).contains(&c) {
succ.push(c);
}
}
}
_ => {}
}
term = Some(instr);
break;
}
body.push(instr);
idx += 1;
if idx >= addrs.len() {
break;
}
if leaders.contains(&addrs[idx]) {
succ.push(addrs[idx]);
break;
}
}
blocks.push(Block {
start: ld,
body,
term,
succ,
});
}
blocks.sort_by_key(|b| b.start);
(instrs, blocks)
}
fn dominators(entry: u64, blocks: &[Block]) -> BTreeMap<u64, BTreeSet<u64>> {
let starts: BTreeSet<u64> = blocks.iter().map(|b| b.start).collect();
let mut preds: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
for b in blocks {
for &s in &b.succ {
if starts.contains(&s) {
preds.entry(s).or_default().insert(b.start);
}
}
}
let mut dom: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
if !starts.contains(&entry) {
return dom;
}
let mut entry_set: BTreeSet<u64> = BTreeSet::new();
entry_set.insert(entry);
dom.insert(entry, entry_set.clone());
for &s in &starts {
if s != entry {
dom.insert(s, starts.clone());
}
}
let order: Vec<u64> = blocks.iter().map(|b| b.start).collect();
loop {
let mut changed = false;
for &n in &order {
if n == entry {
continue;
}
let empty: BTreeSet<u64> = BTreeSet::new();
let ps = preds.get(&n).unwrap_or(&empty);
if ps.is_empty() {
continue;
}
let first = ps.iter().next().unwrap();
let mut new_dom = dom.get(first).cloned().unwrap_or_else(|| starts.clone());
for &p in ps {
let pd = dom.get(&p).cloned().unwrap_or_else(|| starts.clone());
new_dom = new_dom.intersection(&pd).copied().collect();
}
new_dom.insert(n);
if dom.get(&n) != Some(&new_dom) {
dom.insert(n, new_dom);
changed = true;
}
}
if !changed {
break;
}
}
dom
}
pub fn cfg_fingerprint(pe: &Pe, entry: u64) -> Result<CfgFingerprint, String> {
if !pe.is_executable_va(entry) {
return Err(format!(
"{entry:#010x} is not inside any PE section\n{}",
pe.describe_sections(entry)
));
}
let (lo, hi) = (entry, entry.saturating_add(MAX_SPAN));
let (instrs, blocks) = build_cfg(pe, entry);
let dom = dominators(entry, &blocks);
let mut fp = CfgFingerprint::default();
for b in &blocks {
let Some(instr) = b.term else {
continue;
};
match instr.flow_control() {
FlowControl::ConditionalBranch => {
fp.conditionals += 1;
let t = instr.near_branch_target();
if t <= b.start && dom.get(&b.start).is_some_and(|d| d.contains(&t)) {
fp.loops += 1;
}
}
FlowControl::UnconditionalBranch => {
let t = instr.near_branch_target();
if t <= b.start && dom.get(&b.start).is_some_and(|d| d.contains(&t)) {
fp.loops += 1;
} else {
fp.returns += 1;
}
}
FlowControl::Return | FlowControl::Interrupt => fp.returns += 1,
FlowControl::IndirectBranch => {
let mut cases = Vec::new();
follow_jump_table(&instr, pe, lo, hi, &mut cases);
fp.switch_cases += cases.len();
}
_ => {}
}
let _ = &instrs;
}
Ok(fp)
}
pub fn emit_skeleton(pe: &Pe, entry: u64) -> Result<String, String> {
if !pe.is_executable_va(entry) {
return Err(format!(
"{entry:#010x} is not inside any PE section\n{}",
pe.describe_sections(entry)
));
}
let (instrs, blocks) = build_cfg(pe, entry);
let _ = &instrs;
let dom = dominators(entry, &blocks);
let mut loop_heads: BTreeSet<u64> = BTreeSet::new();
for b in &blocks {
for &s in &b.succ {
if s <= b.start && dom.get(&b.start).is_some_and(|d| d.contains(&s)) {
loop_heads.insert(s);
}
}
}
let mut target_count: BTreeMap<u64, u32> = BTreeMap::new();
for b in &blocks {
for &s in &b.succ {
*target_count.entry(s).or_insert(0) += 1;
}
}
let mut fmt = NasmFormatter::new();
let mut out = String::new();
let _ = writeln!(
out,
"// AUTO-GENERATED reference skeleton from bytes @ {entry:#010x} — NOT compilable; a faithful\n\
// structural hint (ordered calls + recovered args, field accesses, globals, control flow).\n\
unsafe fn sub_{entry:08x}(this: u32) {{"
);
let _ = &target_count; for b in &blocks {
let tag = if loop_heads.contains(&b.start) {
" // loop head"
} else {
""
};
let _ = writeln!(out, " 'L{:08x}: {{{tag}", b.start);
let mut last_cmp: Option<Instruction> = None;
let mut arg_base = 0usize; for (i, instr) in b.body.iter().enumerate() {
if matches!(instr.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
last_cmp = Some(*instr);
}
if let Some(line) = render_instr(pe, instr, &b.body[arg_base..i]) {
let _ = writeln!(out, " {line}");
}
if matches!(
instr.flow_control(),
FlowControl::Call | FlowControl::IndirectCall
) {
arg_base = i + 1;
}
}
if let Some(t) = &b.term {
let _ = writeln!(
out,
" {}",
render_terminator(t, last_cmp.as_ref(), &mut fmt)
);
}
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, "}}");
Ok(out)
}
fn asm(fmt: &mut NasmFormatter, instr: &Instruction) -> String {
let mut s = String::new();
fmt.format(instr, &mut s);
s
}
fn reg(r: Register) -> String {
format!("{r:?}").to_lowercase()
}
fn arg(a: &ArgKind) -> String {
match a {
ArgKind::Imm(v) if *v >= 0x1000 => format!("{v:#x}"),
ArgKind::Imm(v) => format!("{v}"),
ArgKind::Field(o) => format!("rd(_, {o:#x})"),
ArgKind::Global(a) => format!("g({a:#010x})"),
ArgKind::LocalAddr => "&local".to_string(),
ArgKind::Other => "_".to_string(),
}
}
fn render_instr(pe: &Pe, instr: &Instruction, prior: &[Instruction]) -> Option<String> {
match instr.flow_control() {
FlowControl::Call => {
let t = instr.near_branch_target();
if t == 0 {
return Some("(*indirect)(); // indirect call".to_string());
}
let cs = recover_call_site(pe, prior, t);
let mut a: Vec<String> = Vec::new();
if let Some(x) = cs.ecx {
a.push(arg(&x));
}
if let Some(x) = cs.edx {
a.push(arg(&x));
}
a.extend(cs.stack.iter().map(arg));
return Some(format!("sub_{t:08x}({});", a.join(", ")));
}
FlowControl::IndirectCall => {
return Some("(*vtbl[?])(); // indirect/virtual call".to_string())
}
_ => {}
}
if instr.mnemonic() == Mnemonic::Mov {
if instr.op0_kind() == OpKind::Memory {
let src = match instr.op1_kind() {
OpKind::Immediate8
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate16
| OpKind::Immediate32 => format!("{:#x}", instr.immediate(1)),
OpKind::Register => reg(instr.op1_register()),
_ => "_".to_string(),
};
return match mem_of(pe, instr) {
Mem::Field(b, d) => Some(format!("wr({b}, {d:#x}, {src});")),
Mem::Global(va) => Some(format!("s({va:#010x}, {src});")),
Mem::Other => None,
};
}
if instr.op1_kind() == OpKind::Memory && instr.op0_kind() == OpKind::Register {
let dst = reg(instr.op0_register());
return match mem_of(pe, instr) {
Mem::Field(b, d) => Some(format!("let {dst} = rd({b}, {d:#x});")),
Mem::Global(va) => Some(format!("let {dst} = g({va:#010x});")),
Mem::Other => None,
};
}
}
None
}
enum Mem {
Field(String, u64),
Global(u64),
Other,
}
fn mem_of(pe: &Pe, instr: &Instruction) -> Mem {
let base = instr.memory_base();
let disp = instr.memory_displacement64();
if base == Register::None && instr.memory_index() == Register::None {
if pe.section_of(disp).is_some() {
return Mem::Global(disp);
}
return Mem::Other;
}
if base == Register::ESP || base == Register::EBP || base == Register::None {
return Mem::Other;
}
Mem::Field(reg(base), disp)
}
fn render_terminator(
instr: &Instruction,
last_cmp: Option<&Instruction>,
fmt: &mut NasmFormatter,
) -> String {
match instr.flow_control() {
FlowControl::Return | FlowControl::Interrupt => "return;".to_string(),
FlowControl::UnconditionalBranch => {
format!("break 'L{:08x}; // jmp", instr.near_branch_target())
}
FlowControl::IndirectBranch => {
format!("match _ {{ /* switch */ }} // {}", asm(fmt, instr))
}
FlowControl::ConditionalBranch => {
let cond = render_cond(instr, last_cmp, fmt);
format!(
"if {cond} {{ break 'L{:08x}; }} // {}",
instr.near_branch_target(),
asm(fmt, instr)
)
}
_ => format!("// {}", asm(fmt, instr)),
}
}
fn render_cond(
jcc: &Instruction,
last_cmp: Option<&Instruction>,
fmt: &mut NasmFormatter,
) -> String {
let relop = match jcc.mnemonic() {
Mnemonic::Je => "==",
Mnemonic::Jne => "!=",
Mnemonic::Jl | Mnemonic::Jb => "<",
Mnemonic::Jle | Mnemonic::Jbe => "<=",
Mnemonic::Jg | Mnemonic::Ja => ">",
Mnemonic::Jge | Mnemonic::Jae => ">=",
Mnemonic::Js => "< 0 //(sign)",
Mnemonic::Jns => ">= 0 //(sign)",
_ => "?",
};
match last_cmp {
Some(c) if c.mnemonic() == Mnemonic::Test => {
format!("(cond {} {relop} 0)", asm_operands(c, fmt))
}
Some(c) => {
let ops = asm_operands(c, fmt);
if let Some((a, b)) = ops.split_once(", ") {
format!("{a} {relop} {b}")
} else {
format!("({ops}) {relop} 0")
}
}
None => format!("(flags) {relop} 0"),
}
}
fn asm_operands(instr: &Instruction, fmt: &mut NasmFormatter) -> String {
let s = asm(fmt, instr);
s.split_once(' ').map(|x| x.1).unwrap_or(&s).to_string()
}