use std::collections::{BTreeMap, VecDeque};
use anyhow::Result;
use log::debug;
use smallvec::{smallvec, SmallVec};
use crate::{
analysis::dis,
aspace::AddressSpace,
module::{Module, Permissions},
util, VA,
};
#[derive(Debug, Clone, Copy)]
pub enum Flow {
Fallthrough(VA),
Call(VA),
UnconditionalJump(VA),
ConditionalJump(VA),
ConditionalMove(VA),
}
impl Flow {
pub fn va(&self) -> VA {
match *self {
Flow::Fallthrough(va) => va,
Flow::Call(va) => va,
Flow::UnconditionalJump(va) => va,
Flow::ConditionalJump(va) => va,
Flow::ConditionalMove(va) => va,
}
}
pub fn swap(&self, va: VA) -> Flow {
match *self {
Flow::Fallthrough(_) => Flow::Fallthrough(va),
Flow::Call(_) => Flow::Call(va),
Flow::UnconditionalJump(_) => Flow::UnconditionalJump(va),
Flow::ConditionalJump(_) => Flow::ConditionalJump(va),
Flow::ConditionalMove(_) => Flow::ConditionalMove(va),
}
}
}
type Flows = SmallVec<[Flow; 2]>;
#[derive(Debug, Clone)]
pub struct BasicBlock {
pub address: VA,
pub length: u64,
pub predecessors: Flows,
pub successors: Flows,
}
pub struct CFG {
pub basic_blocks: BTreeMap<VA, BasicBlock>,
}
pub fn does_insn_fallthrough(insn: &zydis::DecodedInstruction) -> bool {
match insn.mnemonic {
zydis::Mnemonic::JMP => false,
zydis::Mnemonic::RET => false,
zydis::Mnemonic::IRET => false,
zydis::Mnemonic::IRETD => false,
zydis::Mnemonic::IRETQ => false,
zydis::Mnemonic::INT3 => false,
zydis::Mnemonic::INT => {
match insn.operands[0].imm.value {
0x29 => false,
0x2C => false,
_ => {
debug!("{:#x?}", insn);
true
}
}
}
zydis::Mnemonic::CALL => true,
_ => true,
}
}
fn va_add_signed(va: VA, rva: i64) -> Option<VA> {
if rva >= 0 {
va.checked_add(rva as u64)
} else if i64::abs(rva) as u64 > va {
None
} else {
Some(va - i64::abs(rva) as u64)
}
}
fn print_op(_op: &zydis::DecodedOperand) {
println!("op: TODO(print_op)");
}
pub fn get_first_operand(insn: &zydis::DecodedInstruction) -> Option<&zydis::DecodedOperand> {
insn.operands
.iter()
.find(|op| op.visibility == zydis::OperandVisibility::EXPLICIT)
}
#[allow(clippy::if_same_then_else)]
pub fn get_memory_operand_xref(
module: &Module,
va: VA,
insn: &zydis::DecodedInstruction,
op: &zydis::DecodedOperand,
) -> Result<Option<VA>> {
if op.mem.base == zydis::Register::NONE
&& op.mem.index == zydis::Register::NONE
&& op.mem.scale == 0
&& op.mem.disp.has_displacement
{
if op.mem.disp.displacement < 0 {
return Ok(None);
}
let ptr: VA = op.mem.disp.displacement as u64;
let dst = match module.read_va_at_va(ptr) {
Ok(dst) => dst,
Err(_) => return Ok(None),
};
if !module.probe_va(dst, Permissions::X) {
return Ok(None);
};
Ok(Some(dst))
} else if op.mem.base == zydis::Register::RIP
&& op.mem.index == zydis::Register::NONE
&& op.mem.scale == 0
&& op.mem.disp.has_displacement
{
let ptr = match va_add_signed(va + insn.length as u64, op.mem.disp.displacement as i64) {
None => return Ok(None),
Some(ptr) => ptr,
};
let dst = match module.read_va_at_va(ptr) {
Ok(dst) => dst,
Err(_) => return Ok(None),
};
if !module.probe_va(dst, Permissions::X) {
return Ok(None);
};
Ok(Some(dst))
} else if op.mem.base != zydis::Register::NONE {
Ok(None)
} else if op.mem.scale == 0x4 {
Ok(None)
} else {
println!("{:#x}: get mem op xref", va);
print_op(op);
panic!("not supported");
}
}
pub fn get_pointer_operand_xref(op: &zydis::DecodedOperand) -> Result<Option<VA>> {
Ok(Some(op.ptr.offset as u64))
}
pub fn get_immediate_operand_xref(
module: &Module,
va: VA,
insn: &zydis::DecodedInstruction,
op: &zydis::DecodedOperand,
) -> Result<Option<VA>> {
if op.imm.is_relative {
let imm = if op.imm.is_signed {
util::u64_i64(op.imm.value)
} else {
op.imm.value as i64
};
let dst = match va_add_signed(va + insn.length as u64, imm) {
None => return Ok(None),
Some(dst) => dst,
};
if module.probe_va(dst, Permissions::X) {
Ok(Some(dst))
} else {
Ok(None)
}
} else {
println!("get imm op xref");
println!("not implemented: immediate absolute address");
print_op(op);
panic!("not implemented");
}
}
fn get_operand_xref(
module: &Module,
va: VA,
insn: &zydis::DecodedInstruction,
op: &zydis::DecodedOperand,
) -> Result<Option<VA>> {
match op.ty {
zydis::OperandType::MEMORY => get_memory_operand_xref(module, va, insn, op),
zydis::OperandType::POINTER => get_pointer_operand_xref(op),
zydis::OperandType::IMMEDIATE => get_immediate_operand_xref(module, va, insn, op),
zydis::OperandType::REGISTER => Ok(None),
zydis::OperandType::UNUSED => Ok(None),
}
}
pub fn get_call_insn_flow(module: &Module, va: VA, insn: &zydis::DecodedInstruction) -> Result<Flows> {
let op = get_first_operand(insn).expect("CALL has no operand");
match get_operand_xref(module, va, insn, op)? {
None => Ok(smallvec![]),
Some(dst) => Ok(smallvec![Flow::Call(dst)]),
}
}
pub fn get_jmp_insn_flow(module: &Module, va: VA, insn: &zydis::DecodedInstruction) -> Result<Flows> {
let op = get_first_operand(insn).expect("JMP has no target");
if op.ty == zydis::OperandType::MEMORY
&& op.mem.scale == 0x4
&& op.mem.base == zydis::Register::NONE
&& op.mem.disp.has_displacement
{
Ok(smallvec![])
} else {
match get_operand_xref(module, va, insn, op)? {
None => Ok(smallvec![]),
Some(dst) => Ok(smallvec![Flow::UnconditionalJump(dst)]),
}
}
}
pub fn get_cjmp_insn_flow(module: &Module, va: VA, insn: &zydis::DecodedInstruction) -> Result<Flows> {
let op = get_first_operand(insn).expect("CJMP has no target");
match get_operand_xref(module, va, insn, op)? {
None => Ok(smallvec![]),
Some(dst) => Ok(smallvec![Flow::ConditionalJump(dst)]),
}
}
pub fn get_cmov_insn_flow(va: VA, insn: &zydis::DecodedInstruction) -> Result<Flows> {
let next = va + insn.length as u64;
Ok(smallvec![Flow::ConditionalMove(next)])
}
pub fn get_insn_flow(module: &Module, va: VA, insn: &zydis::DecodedInstruction) -> Result<Flows> {
let mut flows = match insn.mnemonic {
zydis::Mnemonic::CALL => get_call_insn_flow(module, va, insn)?,
zydis::Mnemonic::JMP => get_jmp_insn_flow(module, va, insn)?,
zydis::Mnemonic::RET | zydis::Mnemonic::IRET | zydis::Mnemonic::IRETD | zydis::Mnemonic::IRETQ => smallvec![],
zydis::Mnemonic::JB
| zydis::Mnemonic::JBE
| zydis::Mnemonic::JCXZ
| zydis::Mnemonic::JECXZ
| zydis::Mnemonic::JKNZD
| zydis::Mnemonic::JKZD
| zydis::Mnemonic::JL
| zydis::Mnemonic::JLE
| zydis::Mnemonic::JNB
| zydis::Mnemonic::JNBE
| zydis::Mnemonic::JNL
| zydis::Mnemonic::JNLE
| zydis::Mnemonic::JNO
| zydis::Mnemonic::JNP
| zydis::Mnemonic::JNS
| zydis::Mnemonic::JNZ
| zydis::Mnemonic::JO
| zydis::Mnemonic::JP
| zydis::Mnemonic::JRCXZ
| zydis::Mnemonic::JS
| zydis::Mnemonic::JZ => get_cjmp_insn_flow(module, va, insn)?,
zydis::Mnemonic::CMOVB
| zydis::Mnemonic::CMOVBE
| zydis::Mnemonic::CMOVL
| zydis::Mnemonic::CMOVLE
| zydis::Mnemonic::CMOVNB
| zydis::Mnemonic::CMOVNBE
| zydis::Mnemonic::CMOVNL
| zydis::Mnemonic::CMOVNLE
| zydis::Mnemonic::CMOVNO
| zydis::Mnemonic::CMOVNP
| zydis::Mnemonic::CMOVNS
| zydis::Mnemonic::CMOVNZ
| zydis::Mnemonic::CMOVO
| zydis::Mnemonic::CMOVP
| zydis::Mnemonic::CMOVS
| zydis::Mnemonic::CMOVZ => get_cmov_insn_flow(va, insn)?,
_ => smallvec![],
};
if does_insn_fallthrough(&insn) {
flows.push(Flow::Fallthrough(va + insn.length as u64))
}
Ok(flows)
}
struct InstructionDescriptor {
length: u64,
successors: Flows,
}
fn fallthrough_flows<'a>(flows: &'a Flows) -> Box<dyn Iterator<Item = &'a Flow> + 'a> {
Box::new(flows.iter().filter(|flow| matches!(flow, Flow::Fallthrough(_))))
}
fn non_fallthrough_flows<'a>(flows: &'a Flows) -> Box<dyn Iterator<Item = &'a Flow> + 'a> {
Box::new(flows.iter().filter(|flow| !matches!(flow, Flow::Fallthrough(_))))
}
fn empty<'a, T>(mut i: Box<dyn Iterator<Item = T> + 'a>) -> bool {
i.next().is_none()
}
fn read_insn_descriptors(module: &Module, va: VA) -> Result<BTreeMap<VA, InstructionDescriptor>> {
let decoder = dis::get_disassembler(module)?;
let mut insn_buf = [0u8; 16];
let mut queue: VecDeque<VA> = Default::default();
queue.push_back(va);
let mut insns: BTreeMap<VA, InstructionDescriptor> = Default::default();
loop {
let va = match queue.pop_back() {
None => break,
Some(va) => va,
};
if insns.contains_key(&va) {
continue;
}
if module.address_space.read_into(va, &mut insn_buf).is_ok() {
if let Ok(Some(insn)) = decoder.decode(&insn_buf) {
let successors: Flows = get_insn_flow(module, va, &insn)?
.into_iter()
.filter(|succ| !matches!(succ, Flow::Call(_)))
.collect();
for target in successors.iter() {
queue.push_back(target.va());
}
let desc = InstructionDescriptor {
length: insn.length as u64,
successors,
};
insns.insert(va, desc);
}
}
}
Ok(insns)
}
fn compute_successors(insns: &BTreeMap<VA, InstructionDescriptor>) -> BTreeMap<VA, Flows> {
let mut successors: BTreeMap<VA, Flows> = Default::default();
for &va in insns.keys() {
successors.insert(va, smallvec![]);
}
for (&va, desc) in insns.iter() {
successors
.entry(va)
.and_modify(|l: &mut Flows| l.extend(desc.successors.clone()));
}
successors
}
fn compute_predecessors(insns: &BTreeMap<VA, InstructionDescriptor>) -> BTreeMap<VA, Flows> {
let mut predecessors: BTreeMap<VA, Flows> = Default::default();
for &va in insns.keys() {
predecessors.insert(va, smallvec![]);
}
for (&va, desc) in insns.iter() {
for succ in desc.successors.iter() {
let flow = succ.swap(va);
predecessors.entry(succ.va()).and_modify(|l: &mut Flows| l.push(flow));
}
}
predecessors
}
fn compute_basic_blocks(
insns: &BTreeMap<VA, InstructionDescriptor>,
predecessors: &BTreeMap<VA, Flows>,
successors: &BTreeMap<VA, Flows>,
) -> BTreeMap<VA, BasicBlock> {
let starts: Vec<VA> = insns
.keys()
.filter(|&va| {
let preds = &predecessors[va];
if preds.is_empty() {
return true;
}
if !empty(non_fallthrough_flows(preds)) {
return true;
}
for pred in fallthrough_flows(preds) {
if !empty(non_fallthrough_flows(&successors[&pred.va()])) {
return true;
}
}
false
})
.cloned()
.collect();
let mut basic_blocks: BTreeMap<VA, BasicBlock> = Default::default();
let mut basic_blocks_by_last_insn: BTreeMap<VA, VA> = Default::default();
for &start in starts.iter() {
let mut va = start;
let mut insn = &insns[&va];
let mut bb = BasicBlock {
address: va,
length: 0,
predecessors: Default::default(),
successors: Default::default(),
};
loop {
let flows = &successors[&va];
if empty(fallthrough_flows(flows)) {
break;
}
if !empty(non_fallthrough_flows(flows)) {
break;
}
let next_va = va + insn.length;
if !predecessors.contains_key(&next_va) {
log::warn!("cfg: {:#x}: no subsequent instruction at {:#x}", va, next_va);
break;
}
if !empty(non_fallthrough_flows(&predecessors[&next_va])) {
break;
}
bb.length += insn.length;
va = next_va;
insn = &insns[&next_va];
}
bb.length += insn.length;
bb.successors = successors.get(&va).unwrap_or(&smallvec![]).clone();
basic_blocks.insert(start, bb);
basic_blocks_by_last_insn.insert(va, start);
}
for (va, bb) in basic_blocks.iter_mut() {
bb.predecessors.extend(
predecessors
.get(&va)
.unwrap_or(&smallvec![])
.iter()
.filter(|pred| basic_blocks_by_last_insn.contains_key(&pred.va()))
.map(|pred| pred.swap(basic_blocks_by_last_insn[&pred.va()])),
)
}
basic_blocks
}
pub fn build_cfg(module: &Module, va: VA) -> Result<CFG> {
debug!("cfg: {:#x}", va);
let insns = read_insn_descriptors(module, va)?;
debug!("cfg: {:#x}: {} instructions", va, insns.len());
let successors = compute_successors(&insns);
let predecessors = compute_predecessors(&insns);
let bbs = compute_basic_blocks(&insns, &predecessors, &successors);
debug!("cfg: {:#x}: {} basic blocks", va, bbs.len());
Ok(CFG { basic_blocks: bbs })
}
#[cfg(test)]
mod tests {
use crate::{analysis::cfg::build_cfg, rsrc::*};
use anyhow::Result;
#[test]
fn k32() -> Result<()> {
let buf = get_buf(Rsrc::K32);
let pe = crate::loader::pe::PE::from_bytes(&buf)?;
let cfg = build_cfg(&pe.module, 0x1800527B0)?;
assert_eq!(cfg.basic_blocks.len(), 4);
Ok(())
}
}