use std::{collections::HashMap, sync::Arc};
use crate::{
compiler::{PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::{CffDetector, CffReconstructionPass, Dispatcher, UnflattenConfig},
techniques::{Detection, Evidence, Technique, TechniqueCategory},
},
metadata::token::Token,
CilObject,
};
#[derive(Debug)]
pub struct FlatteningFindings {
pub dispatchers: HashMap<Token, Vec<Dispatcher>>,
}
pub struct GenericFlattening;
impl Technique for GenericFlattening {
fn id(&self) -> &'static str {
"generic.flattening"
}
fn name(&self) -> &'static str {
"Control Flow Flattening Reconstruction"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Structure
}
fn requires(&self) -> &[&'static str] {
&["generic.predicates"]
}
fn detect(&self, _assembly: &CilObject) -> Detection {
Detection::new_empty()
}
fn detect_ssa(&self, ctx: &AnalysisContext, _assembly: &CilObject) -> Detection {
let min_confidence = UnflattenConfig::default().min_confidence;
let mut dispatchers_by_method: HashMap<Token, Vec<Dispatcher>> = HashMap::new();
let mut total_dispatchers = 0usize;
for entry in ctx.ssa_functions.iter() {
let method_token = *entry.key();
let ssa = entry.value();
let mut detector = CffDetector::new(ssa);
let all_dispatchers = detector.detect_all_dispatchers();
let has_high_confidence = all_dispatchers
.iter()
.any(|d| d.confidence >= min_confidence);
let effective_threshold = if has_high_confidence {
min_confidence * 0.75
} else {
min_confidence
};
let method_dispatchers: Vec<Dispatcher> = all_dispatchers
.into_iter()
.filter(|d| d.confidence >= effective_threshold)
.collect();
if !method_dispatchers.is_empty() {
total_dispatchers += method_dispatchers.len();
dispatchers_by_method.insert(method_token, method_dispatchers);
}
}
if dispatchers_by_method.is_empty() {
return Detection::new_empty();
}
let method_count = dispatchers_by_method.len();
let findings = FlatteningFindings {
dispatchers: dispatchers_by_method,
};
Detection::new_detected(
vec![Evidence::Structural(format!(
"{total_dispatchers} CFF dispatchers in {method_count} methods"
))],
Some(Box::new(findings) as Box<dyn std::any::Any + Send + Sync>),
)
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Structure)
}
fn create_pass(
&self,
ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let cff_config = UnflattenConfig {
max_states: ctx.config.unflattening.max_states_per_case,
max_tree_depth: ctx.config.unflattening.max_trace_iterations,
..UnflattenConfig::default()
};
let mut cff_pass = CffReconstructionPass::new(ctx, cff_config);
if let Some(findings) = detection.findings::<FlatteningFindings>() {
cff_pass = cff_pass.with_pre_detected(findings.dispatchers.clone());
}
vec![Box::new(cff_pass)]
}
}
#[cfg(test)]
mod tests {
use crate::{deobfuscation::techniques::Technique, test::helpers::load_sample};
#[test]
fn test_detect_is_noop() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_controlflow.exe");
let technique = super::GenericFlattening;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"IL-level detect() should be a no-op — detection happens in detect_ssa()"
);
}
#[test]
fn test_detect_negative() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = super::GenericFlattening;
let detection = technique.detect(&asm);
assert!(!detection.is_detected());
}
}