use std::collections::HashSet;
use crate::{
analysis::{SsaFunction, SsaOp, SsaVarId},
deobfuscation::passes::unflattening::{
dispatcher::{analyze_switch_dispatcher, Dispatcher, DispatcherInfo},
statevar::{identify_state_variable, StateVariable},
UnflattenConfig,
},
utils::graph::{
algorithms::{compute_dominators, DominatorTree},
GraphBase, NodeId, Successors,
},
};
#[derive(Debug, Clone)]
pub struct EntryPoint {
pub block: usize,
pub initial_state: Option<i64>,
pub condition: Option<EntryCondition>,
pub state_var: Option<SsaVarId>,
}
#[derive(Debug, Clone)]
pub enum EntryCondition {
Compare {
var: SsaVarId,
value: i64,
is_equal: bool,
},
SwitchCase(i64),
Boolean {
var: SsaVarId,
when_true: bool,
},
Argument {
index: u16,
expected: Option<i64>,
},
}
impl EntryPoint {
#[must_use]
pub fn new(block: usize) -> Self {
Self {
block,
initial_state: None,
condition: None,
state_var: None,
}
}
#[must_use]
pub fn with_state(block: usize, initial_state: i64) -> Self {
Self {
block,
initial_state: Some(initial_state),
condition: None,
state_var: None,
}
}
pub fn with_condition(mut self, condition: EntryCondition) -> Self {
self.condition = Some(condition);
self
}
pub fn with_state_var(mut self, var: SsaVarId) -> Self {
self.state_var = Some(var);
self
}
#[must_use]
pub fn is_unconditional(&self) -> bool {
self.condition.is_none()
}
#[must_use]
pub fn has_known_state(&self) -> bool {
self.initial_state.is_some()
}
}
struct SsaGraphAdapter<'a> {
ssa: &'a SsaFunction,
}
impl<'a> SsaGraphAdapter<'a> {
fn new(ssa: &'a SsaFunction) -> Self {
Self { ssa }
}
}
impl GraphBase for SsaGraphAdapter<'_> {
fn node_count(&self) -> usize {
self.ssa.block_count()
}
fn node_ids(&self) -> impl Iterator<Item = NodeId> {
(0..self.ssa.block_count()).map(NodeId::new)
}
}
impl Successors for SsaGraphAdapter<'_> {
fn successors(&self, node: NodeId) -> impl Iterator<Item = NodeId> {
self.ssa
.block_successors(node.index())
.into_iter()
.map(NodeId::new)
}
}
#[derive(Debug, Clone)]
pub struct CffPattern {
pub dispatcher_block: usize,
pub dispatcher: DispatcherInfo,
pub state_var: Option<StateVariable>,
pub case_blocks: HashSet<usize>,
pub entry_block: Option<usize>,
pub entry_points: Vec<EntryPoint>,
pub exit_blocks: HashSet<usize>,
pub confidence: f64,
}
impl CffPattern {
#[must_use]
pub fn case_count(&self) -> usize {
self.dispatcher.case_count()
}
#[must_use]
pub fn is_confuserex_style(&self) -> bool {
matches!(&self.dispatcher, DispatcherInfo::Switch { transform, .. }
if transform.modulo_divisor().is_some())
}
#[must_use]
pub fn has_multiple_entries(&self) -> bool {
self.entry_points.len() > 1
}
#[must_use]
pub fn entry_count(&self) -> usize {
self.entry_points.len()
}
#[must_use]
pub fn primary_entry(&self) -> Option<&EntryPoint> {
self.entry_points.first()
}
#[must_use]
pub fn entries_with_states(&self) -> Vec<&EntryPoint> {
self.entry_points
.iter()
.filter(|e| e.has_known_state())
.collect()
}
#[must_use]
pub fn initial_states(&self) -> Vec<i64> {
self.entry_points
.iter()
.filter_map(|e| e.initial_state)
.collect()
}
}
pub struct CffDetector<'a> {
ssa: &'a SsaFunction,
config: UnflattenConfig,
dom_tree: Option<DominatorTree>,
}
impl<'a> CffDetector<'a> {
#[must_use]
pub fn new(ssa: &'a SsaFunction) -> Self {
Self {
ssa,
config: UnflattenConfig::default(),
dom_tree: None,
}
}
#[must_use]
pub fn with_config(ssa: &'a SsaFunction, config: &UnflattenConfig) -> Self {
Self {
ssa,
config: config.clone(),
dom_tree: None,
}
}
fn get_dom_tree(&mut self) -> &DominatorTree {
let ssa = self.ssa;
self.dom_tree.get_or_insert_with(|| {
let adapter = SsaGraphAdapter::new(ssa);
compute_dominators(&adapter, NodeId::new(0))
})
}
pub fn detect_best(&mut self) -> Option<Dispatcher> {
let pattern = self.detect()?;
match &pattern.dispatcher {
DispatcherInfo::Switch {
block,
switch_var,
cases,
default,
transform,
} => {
let mut dispatcher = Dispatcher::new(*block, *switch_var, cases.clone(), *default);
if let Some(ref state_var) = pattern.state_var {
if let Some(phi_var) = state_var.dispatcher_var {
dispatcher = dispatcher.with_state_phi(phi_var);
}
}
dispatcher = dispatcher
.with_transform(transform.clone())
.with_confidence(pattern.confidence);
Some(dispatcher)
}
DispatcherInfo::IfElseChain {
head_block,
state_var,
comparisons,
default,
} => {
let cases: Vec<usize> = comparisons.iter().map(|(_, target)| *target).collect();
let default_block = default.unwrap_or(*head_block);
let mut dispatcher = Dispatcher::new(*head_block, *state_var, cases, default_block);
if let Some(ref state_var_info) = pattern.state_var {
if let Some(phi_var) = state_var_info.dispatcher_var {
dispatcher = dispatcher.with_state_phi(phi_var);
}
}
dispatcher = dispatcher.with_confidence(pattern.confidence);
Some(dispatcher)
}
DispatcherInfo::ComputedJump {
block,
target_var,
jump_table,
..
} => {
let default_block = jump_table.first().copied().unwrap_or(0);
let mut dispatcher =
Dispatcher::new(*block, *target_var, jump_table.clone(), default_block);
if let Some(ref state_var_info) = pattern.state_var {
if let Some(phi_var) = state_var_info.dispatcher_var {
dispatcher = dispatcher.with_state_phi(phi_var);
}
}
dispatcher = dispatcher.with_confidence(pattern.confidence);
Some(dispatcher)
}
}
}
pub fn detect(&mut self) -> Option<CffPattern> {
if self.ssa.block_count() < 4 {
return None;
}
let candidates = self.find_dispatcher_candidates();
if candidates.is_empty() {
return None;
}
let mut best_pattern: Option<CffPattern> = None;
let mut best_score = 0.0;
for block_idx in candidates {
if let Some(pattern) = self.analyze_dispatcher_candidate(block_idx) {
if pattern.confidence > best_score {
best_score = pattern.confidence;
best_pattern = Some(pattern);
}
}
}
best_pattern
}
fn find_dispatcher_candidates(&self) -> Vec<usize> {
let mut candidates = Vec::new();
for block_idx in 0..self.ssa.block_count() {
if self.is_dispatcher_candidate(block_idx) {
candidates.push(block_idx);
}
}
candidates
}
fn is_dispatcher_candidate(&self, block_idx: usize) -> bool {
let Some(block) = self.ssa.block(block_idx) else {
return false;
};
if block.instructions().is_empty() {
return false;
}
let has_switch = block
.instructions()
.iter()
.any(|instr| matches!(instr.op(), SsaOp::Switch { .. }));
if has_switch {
let pred_count = self.ssa.block_predecessors(block_idx).len();
let has_self_loop = block
.instructions()
.iter()
.any(|i| i.op().successors().contains(&block_idx));
let effective_preds = pred_count + usize::from(has_self_loop);
return effective_preds >= 2;
}
let has_branch = block
.instructions()
.iter()
.any(|instr| matches!(instr.op(), SsaOp::Branch { .. }));
if has_branch {
let pred_count = self.ssa.block_predecessors(block_idx).len();
return pred_count >= 3;
}
false
}
fn analyze_dispatcher_candidate(&mut self, block_idx: usize) -> Option<CffPattern> {
let dispatcher = analyze_switch_dispatcher(self.ssa, block_idx)?;
let state_var = identify_state_variable(self.ssa, block_idx, dispatcher.dispatch_var());
let case_blocks: HashSet<usize> = dispatcher.all_targets().into_iter().collect();
let exit_blocks = self.find_exit_blocks(block_idx, &case_blocks);
let entry_block = self.find_entry_block(block_idx);
let entry_points = self.find_entry_points(block_idx, &case_blocks, state_var.as_ref());
let confidence = self.compute_confidence(
block_idx,
&dispatcher,
state_var.as_ref(),
&case_blocks,
&exit_blocks,
);
Some(CffPattern {
dispatcher_block: block_idx,
dispatcher,
state_var,
case_blocks,
entry_block,
entry_points,
exit_blocks,
confidence,
})
}
fn find_exit_blocks(
&self,
dispatcher_block: usize,
case_blocks: &HashSet<usize>,
) -> HashSet<usize> {
let mut exits = HashSet::new();
for &case_block in case_blocks {
for succ in self.ssa.block_successors(case_block) {
if succ != dispatcher_block && !case_blocks.contains(&succ) {
exits.insert(succ);
}
}
}
exits
}
fn find_entry_block(&self, dispatcher_block: usize) -> Option<usize> {
let preds = self.ssa.block_predecessors(dispatcher_block);
for pred in preds {
if pred == 0 {
return Some(pred);
}
}
if dispatcher_block == 0 {
return None;
}
None
}
fn find_entry_points(
&self,
dispatcher_block: usize,
case_blocks: &HashSet<usize>,
state_var: Option<&StateVariable>,
) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let preds = self.ssa.block_predecessors(dispatcher_block);
let entry_blocks: Vec<usize> = preds
.iter()
.filter(|&&pred| !case_blocks.contains(&pred))
.copied()
.collect();
if entry_blocks.is_empty() {
let entry = EntryPoint::new(dispatcher_block);
entries.push(entry);
return entries;
}
for &entry_block in &entry_blocks {
let mut entry = EntryPoint::new(entry_block);
if let Some(initial) = self.extract_initial_state(entry_block, state_var) {
entry.initial_state = Some(initial);
}
if entry_blocks.len() > 1 {
if let Some(condition) = self.extract_entry_condition(entry_block, &entry_blocks) {
entry.condition = Some(condition);
}
}
if let Some(sv) = state_var {
if let Some(ssa_var) = sv.var.as_ssa_var() {
entry.state_var = Some(ssa_var);
}
}
entries.push(entry);
}
entries
}
fn extract_initial_state(
&self,
block_idx: usize,
state_var: Option<&StateVariable>,
) -> Option<i64> {
let block = self.ssa.block(block_idx)?;
for instr in block.instructions() {
if let SsaOp::Const { dest, value } = instr.op() {
if let Some(sv) = state_var {
if let Some(ssa_var) = sv.var.as_ssa_var() {
if *dest == ssa_var {
return value.as_i64();
}
}
}
}
if let SsaOp::Copy { dest, src } = instr.op() {
if let Some(sv) = state_var {
if let Some(ssa_var) = sv.var.as_ssa_var() {
if *dest == ssa_var {
if let Some(SsaOp::Const { value, .. }) = self.ssa.get_definition(*src)
{
return value.as_i64();
}
}
}
}
}
}
None
}
fn extract_entry_condition(
&self,
entry_block: usize,
_all_entries: &[usize],
) -> Option<EntryCondition> {
let preds = self.ssa.block_predecessors(entry_block);
for pred in preds {
let Some(pred_block) = self.ssa.block(pred) else {
continue;
};
for instr in pred_block.instructions() {
if let SsaOp::Branch {
condition,
true_target,
false_target,
} = instr.op()
{
let is_true_branch = *true_target == entry_block;
let is_false_branch = *false_target == entry_block;
if is_true_branch || is_false_branch {
if let Some(SsaOp::Ceq { left, right, .. }) =
self.ssa.get_definition(*condition)
{
if let Some(SsaOp::Const { value, .. }) =
self.ssa.get_definition(*right)
{
if let Some(val) = value.as_i64() {
return Some(EntryCondition::Compare {
var: *left,
value: val,
is_equal: is_true_branch,
});
}
}
}
return Some(EntryCondition::Boolean {
var: *condition,
when_true: is_true_branch,
});
}
}
}
}
None
}
fn compute_confidence(
&mut self,
dispatcher_block: usize,
dispatcher: &DispatcherInfo,
state_var: Option<&StateVariable>,
case_blocks: &HashSet<usize>,
exit_blocks: &HashSet<usize>,
) -> f64 {
let mut score = 0.0;
let case_count = case_blocks.len();
if case_count >= 3 {
score += 0.10;
}
if case_count >= 5 {
score += 0.05;
}
if case_count >= 10 {
score += 0.05;
}
if let Some(sv) = state_var {
if sv.dispatcher_var.is_some() {
score += 0.15;
}
if sv.def_count() >= case_count.saturating_sub(1) {
score += 0.10;
}
}
let pred_count = self.ssa.block_predecessors(dispatcher_block).len();
if pred_count >= case_count / 2 {
score += 0.10;
}
let back_edge_count = case_blocks
.iter()
.filter(|&&b| self.ssa.block_successors(b).contains(&dispatcher_block))
.count();
#[allow(clippy::cast_precision_loss)]
let back_edge_ratio = back_edge_count as f64 / case_count.max(1) as f64;
score += back_edge_ratio * 0.10;
if !exit_blocks.is_empty() {
score += 0.05;
}
if matches!(dispatcher, DispatcherInfo::Switch { .. }) {
score += 0.10;
}
let transform = dispatcher.transform();
if transform.modulo_divisor().is_some() {
score += 0.10;
}
let dominance_score = self.compute_dominance_score(dispatcher_block, case_blocks);
score += dominance_score * 0.20;
score.min(1.0)
}
fn compute_dominance_score(
&mut self,
dispatcher_block: usize,
case_blocks: &HashSet<usize>,
) -> f64 {
if case_blocks.is_empty() {
return 0.0;
}
let dom_tree = self.get_dom_tree();
let dispatcher_node = NodeId::new(dispatcher_block);
let dominated_count = case_blocks
.iter()
.filter(|&&case_block| {
if case_block == dispatcher_block {
return true;
}
let case_node = NodeId::new(case_block);
dom_tree.dominates(dispatcher_node, case_node)
})
.count();
#[allow(clippy::cast_precision_loss)]
let ratio = dominated_count as f64 / case_blocks.len() as f64;
ratio
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{
analysis::SsaVarId,
deobfuscation::passes::unflattening::dispatcher::{DispatcherInfo, StateTransform},
};
use super::{CffPattern, EntryCondition, EntryPoint};
#[test]
fn test_cff_pattern_case_count() {
let pattern = CffPattern {
dispatcher_block: 0,
dispatcher: DispatcherInfo::Switch {
block: 0,
switch_var: SsaVarId::new(),
cases: vec![1, 2, 3, 4, 5],
default: 6,
transform: StateTransform::Modulo(5),
},
state_var: None,
case_blocks: HashSet::from([1, 2, 3, 4, 5, 6]),
entry_block: None,
entry_points: vec![EntryPoint::with_state(0, 42)],
exit_blocks: HashSet::new(),
confidence: 0.8,
};
assert_eq!(pattern.case_count(), 5);
assert!(pattern.is_confuserex_style());
assert_eq!(pattern.entry_count(), 1);
assert!(!pattern.has_multiple_entries());
}
#[test]
fn test_entry_point() {
let entry = EntryPoint::new(0);
assert!(entry.is_unconditional());
assert!(!entry.has_known_state());
let entry_with_state = EntryPoint::with_state(1, 100);
assert!(entry_with_state.has_known_state());
assert_eq!(entry_with_state.initial_state, Some(100));
let var = SsaVarId::new();
let entry_with_condition = EntryPoint::new(2).with_condition(EntryCondition::Boolean {
var,
when_true: true,
});
assert!(!entry_with_condition.is_unconditional());
}
#[test]
fn test_cff_pattern_multiple_entries() {
let pattern = CffPattern {
dispatcher_block: 1,
dispatcher: DispatcherInfo::Switch {
block: 1,
switch_var: SsaVarId::new(),
cases: vec![2, 3],
default: 4,
transform: StateTransform::Identity,
},
state_var: None,
case_blocks: HashSet::from([2, 3, 4]),
entry_block: None,
entry_points: vec![EntryPoint::with_state(0, 10), EntryPoint::with_state(5, 20)],
exit_blocks: HashSet::from([6]),
confidence: 0.7,
};
assert!(pattern.has_multiple_entries());
assert_eq!(pattern.entry_count(), 2);
assert_eq!(pattern.initial_states(), vec![10, 20]);
let primary = pattern.primary_entry().unwrap();
assert_eq!(primary.block, 0);
assert_eq!(primary.initial_state, Some(10));
}
}