use std::collections::{HashMap, HashSet};
use crate::{
analysis::{SsaBlock, SsaFunction, SsaOp, SsaVarId},
deobfuscation::passes::unflattening::tracer::{TraceNode, TraceTerminator, TraceTree},
};
#[derive(Debug)]
pub struct ReconstructionResult {
pub state_transitions_removed: usize,
pub user_branches_preserved: usize,
pub block_count: usize,
}
#[derive(Debug)]
pub struct PatchPlan {
pub dispatcher_block: usize,
pub state_tainted: HashSet<SsaVarId>,
pub redirects: Vec<(usize, usize)>,
pub clone_requests: HashMap<usize, Vec<(usize, usize)>>,
pub execution_order: Vec<usize>,
pub state_transitions_removed: usize,
pub user_branches_preserved: usize,
}
impl PatchPlan {
fn new(dispatcher_block: usize, state_tainted: HashSet<SsaVarId>) -> Self {
Self {
dispatcher_block,
state_tainted,
redirects: Vec::new(),
clone_requests: HashMap::new(),
execution_order: Vec::new(),
state_transitions_removed: 0,
user_branches_preserved: 0,
}
}
fn add_redirect(&mut self, source: usize, target: usize, predecessor: Option<usize>) {
if let Some(&(_, existing_target)) = self.redirects.iter().find(|&&(s, _)| s == source) {
if existing_target != target {
let entry = self.clone_requests.entry(source).or_default();
if let Some(pred) = predecessor {
if !entry.iter().any(|(p, _)| *p == pred) {
entry.push((pred, target));
}
}
}
return; }
self.redirects.push((source, target));
if let Some(pred) = predecessor {
let entry = self.clone_requests.entry(source).or_default();
entry.push((pred, target));
}
}
fn safe_redirects(&self) -> Vec<(usize, usize)> {
self.redirects
.iter()
.filter(|(source, _)| {
self.clone_requests.get(source).is_none_or(|v| v.len() <= 1)
})
.copied()
.collect()
}
fn blocks_to_clone(&self) -> Vec<(usize, Vec<(usize, usize)>)> {
self.clone_requests
.iter()
.filter(|(_, paths)| paths.len() > 1) .map(|(&block, paths)| (block, paths.clone()))
.collect()
}
fn add_to_execution_order(&mut self, block: usize) {
if !self.execution_order.contains(&block) && block != self.dispatcher_block {
self.execution_order.push(block);
}
}
}
pub fn extract_patch_plan(tree: &TraceTree) -> Option<PatchPlan> {
let dispatcher = tree.dispatcher.as_ref()?;
let mut plan = PatchPlan::new(dispatcher.block, tree.state_tainted.clone());
extract_redirects_from_node(&tree.root, dispatcher.block, &mut plan, None);
Some(plan)
}
fn extract_redirects_from_node(
node: &TraceNode,
dispatcher_block: usize,
plan: &mut PatchPlan,
external_predecessor: Option<usize>,
) {
for &block in &node.blocks_visited {
plan.add_to_execution_order(block);
}
match &node.terminator {
TraceTerminator::StateTransition {
target_block,
continues,
..
} => {
let pred_block = node
.blocks_visited
.iter()
.rev()
.find(|&&b| b != dispatcher_block)
.copied();
let predecessor_of_pred = if let Some(pred) = pred_block {
node.blocks_visited
.iter()
.position(|&b| b == pred)
.and_then(|pos| {
if pos > 0 {
Some(node.blocks_visited[pos - 1])
} else {
external_predecessor
}
})
} else {
None
};
if let Some(pred) = pred_block {
plan.add_redirect(pred, *target_block, predecessor_of_pred);
plan.state_transitions_removed += 1;
} else if let Some(ext_pred) = external_predecessor {
plan.add_redirect(ext_pred, *target_block, None);
plan.state_transitions_removed += 1;
}
extract_redirects_from_node(
continues,
dispatcher_block,
plan,
pred_block.or(external_predecessor),
);
}
TraceTerminator::UserBranch {
block,
true_branch,
false_branch,
..
} => {
plan.user_branches_preserved += 1;
extract_redirects_from_node(true_branch, dispatcher_block, plan, Some(*block));
extract_redirects_from_node(false_branch, dispatcher_block, plan, Some(*block));
}
TraceTerminator::UserSwitch {
block,
cases,
default,
..
} => {
plan.user_branches_preserved += 1;
for (_, case_node) in cases {
extract_redirects_from_node(case_node, dispatcher_block, plan, Some(*block));
}
extract_redirects_from_node(default, dispatcher_block, plan, Some(*block));
}
TraceTerminator::Exit { block } => {
plan.add_to_execution_order(*block);
}
TraceTerminator::LoopBack { .. } | TraceTerminator::Stopped { .. } => {
}
}
}
pub fn apply_patch_plan(ssa: &mut SsaFunction, plan: &PatchPlan) -> ReconstructionResult {
let safe = plan.safe_redirects();
let to_clone = plan.blocks_to_clone();
for &(source_block, new_target) in &safe {
if let Some(block) = ssa.block_mut(source_block) {
block.redirect_target(plan.dispatcher_block, new_target);
}
}
let mut clone_map: HashMap<usize, usize> = HashMap::new();
let mut cloned_blocks = Vec::new();
for (merge_block, paths) in &to_clone {
if paths.len() < 2 {
continue;
}
let merge_content = ssa.block(*merge_block).cloned();
let Some(original_block) = merge_content else {
continue;
};
let (_, first_target) = paths[0];
if let Some(block) = ssa.block_mut(*merge_block) {
block.set_target(first_target);
}
for &(pred, target) in paths.iter().skip(1) {
let new_block_idx = ssa.block_count() + cloned_blocks.len();
clone_map.insert(new_block_idx, *merge_block);
let mut cloned = original_block.clone();
cloned.set_id(new_block_idx);
for phi in cloned.phi_nodes_mut() {
phi.operands_mut().retain(|op| op.predecessor() == pred);
}
cloned
.phi_nodes_mut()
.retain(|phi| !phi.operands().is_empty());
cloned.set_target(target);
cloned_blocks.push((pred, cloned, *merge_block));
}
}
for (pred, mut cloned, original_merge) in cloned_blocks {
let new_block_idx = cloned.id();
if let Some(pred_block) = ssa.block_mut(pred) {
pred_block.redirect_target(original_merge, new_block_idx);
}
filter_state_instructions(&mut cloned, &plan.state_tainted, plan.dispatcher_block);
ssa.blocks_mut().push(cloned);
}
let original_block_count = ssa.block_count();
for block_idx in 0..original_block_count {
if let Some(block) = ssa.block_mut(block_idx) {
filter_state_instructions(block, &plan.state_tainted, plan.dispatcher_block);
}
}
if let Some(dispatcher) = ssa.block_mut(plan.dispatcher_block) {
dispatcher.clear();
}
ReconstructionResult {
state_transitions_removed: plan.state_transitions_removed,
user_branches_preserved: plan.user_branches_preserved,
block_count: ssa.block_count(),
}
}
fn filter_state_instructions(
block: &mut SsaBlock,
state_tainted: &HashSet<SsaVarId>,
dispatcher: usize,
) {
block.instructions_mut().retain(|instr| {
if instr.is_terminator() {
if let SsaOp::Jump { target } = instr.op() {
if *target == dispatcher {
return false; }
}
return true;
}
let def_tainted = instr.def().is_some_and(|d| state_tainted.contains(&d));
let uses_tainted = instr.uses().iter().any(|u| state_tainted.contains(u));
!def_tainted && !uses_tainted
});
}
pub fn reconstruct_from_tree(
tree: &TraceTree,
original: &SsaFunction,
) -> Option<ReconstructionResult> {
let plan = extract_patch_plan(tree)?;
Some(ReconstructionResult {
state_transitions_removed: plan.state_transitions_removed,
user_branches_preserved: plan.user_branches_preserved,
block_count: original.block_count(),
})
}
#[cfg(test)]
mod tests {
use crate::{
analysis::{
ConstValue, PhiNode, PhiOperand, SsaBlock, SsaFunction, SsaInstruction, SsaOp,
SsaVarId, VariableOrigin,
},
deobfuscation::passes::unflattening::{
reconstruction::reconstruct_from_tree, tracer::trace_method_tree, UnflattenConfig,
},
};
fn create_simple_cff() -> SsaFunction {
let mut ssa = SsaFunction::new(0, 1);
let state_var = SsaVarId::new();
let const_var = SsaVarId::new();
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: const_var,
value: ConstValue::I32(0),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
let mut phi = PhiNode::new(state_var, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(const_var, 0));
b1.add_phi(phi);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Switch {
value: state_var,
targets: vec![2, 3, 4],
default: 5,
}));
ssa.add_block(b1);
for i in 2..=4 {
let mut b = SsaBlock::new(i);
b.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b);
}
let mut b5 = SsaBlock::new(5);
b5.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b5);
ssa
}
#[test]
fn test_reconstruct_simple_cff() {
let ssa = create_simple_cff();
let config = UnflattenConfig::default();
let tree = trace_method_tree(&ssa, &config, None);
assert!(tree.dispatcher.is_some(), "Should detect dispatcher");
let result = reconstruct_from_tree(&tree, &ssa);
assert!(result.is_some(), "Reconstruction should succeed");
let result = result.unwrap();
println!("=== Reconstruction Result ===");
println!("Blocks: {}", result.block_count);
println!(
"State transitions removed: {}",
result.state_transitions_removed
);
println!(
"User branches preserved: {}",
result.user_branches_preserved
);
assert!(
result.state_transitions_removed > 0,
"Should remove at least one state transition"
);
assert!(result.block_count > 0, "Should have blocks in output");
}
fn create_cff_with_user_branch() -> SsaFunction {
let mut ssa = SsaFunction::new(1, 1); let state_var = SsaVarId::new();
let init_state = SsaVarId::new();
let const_one = SsaVarId::new();
let arg0 = SsaVarId::new();
let user_zero = SsaVarId::new();
let cmp_result = SsaVarId::new();
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: init_state,
value: ConstValue::I32(0),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: arg0,
value: ConstValue::I32(42), }));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
let mut phi = PhiNode::new(state_var, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(init_state, 0)); phi.add_operand(PhiOperand::new(const_one, 3)); phi.add_operand(PhiOperand::new(const_one, 4)); b1.add_phi(phi);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Switch {
value: state_var,
targets: vec![2, 5], default: 6,
}));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: const_one,
value: ConstValue::I32(1),
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: user_zero,
value: ConstValue::I32(0),
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Cgt {
dest: cmp_result,
left: arg0,
right: user_zero,
unsigned: false,
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: cmp_result,
true_target: 3, false_target: 4, }));
ssa.add_block(b2);
let mut b3a = SsaBlock::new(3);
b3a.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b3a);
let mut b3b = SsaBlock::new(4);
b3b.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b3b);
let mut b5 = SsaBlock::new(5);
b5.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b5);
let mut b6 = SsaBlock::new(6);
b6.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b6);
ssa
}
#[test]
fn test_reconstruct_with_user_branch() {
let ssa = create_cff_with_user_branch();
let config = UnflattenConfig::default();
let tree = trace_method_tree(&ssa, &config, None);
assert!(tree.dispatcher.is_some(), "Should detect dispatcher");
assert!(
tree.stats.user_branch_count > 0,
"Should have user branches"
);
let result = reconstruct_from_tree(&tree, &ssa);
assert!(result.is_some(), "Reconstruction should succeed");
let result = result.unwrap();
println!("=== Reconstruction with User Branch ===");
println!("Blocks: {}", result.block_count);
println!(
"State transitions removed: {}",
result.state_transitions_removed
);
println!(
"User branches preserved: {}",
result.user_branches_preserved
);
assert!(
result.user_branches_preserved > 0,
"Should preserve user branches"
);
println!("Block count: {}", result.block_count);
}
}