use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use dashmap::DashSet;
use crate::{
analysis::{
ConstValue, PhiTaintMode, SsaFunction, SsaOp, SsaVarId, TaintAnalysis, TaintConfig,
},
assembly::Operand,
compiler::CompilerContext,
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
CfgInfo, StateMachineCallSite, StateMachineProvider, StateMachineSemantics,
StateSlotOperation, StateUpdateCall,
},
metadata::{
method::MethodModifiers, signatures::TypeSignature, tables::TableId, token::Token,
typesystem::CilType,
},
prelude::FlowType,
utils::graph::NodeId,
CilObject,
};
struct DetectedCallSite {
caller: Token,
uses_statemachine: bool,
}
impl DetectedCallSite {
fn direct(caller: Token) -> Self {
Self {
caller,
uses_statemachine: false,
}
}
fn statemachine(caller: Token) -> Self {
Self {
caller,
uses_statemachine: true,
}
}
}
#[derive(Debug)]
pub struct ConfuserExStateMachine {
semantics: StateMachineSemantics,
methods: DashSet<Token>,
}
impl ConfuserExStateMachine {
pub fn new(semantics: StateMachineSemantics, methods: impl IntoIterator<Item = Token>) -> Self {
let method_set = DashSet::new();
for method in methods {
method_set.insert(method);
}
Self {
semantics,
methods: method_set,
}
}
}
impl StateMachineProvider for ConfuserExStateMachine {
fn name(&self) -> &'static str {
"ConfuserEx CFGCtx"
}
fn semantics(&self) -> &StateMachineSemantics {
&self.semantics
}
fn applies_to_method(&self, method: Token) -> bool {
self.methods.contains(&method)
}
fn methods(&self) -> Vec<Token> {
self.methods.iter().map(|r| *r).collect()
}
fn find_initializations(
&self,
ssa: &SsaFunction,
ctx: &CompilerContext,
method_token: Token,
_assembly: &Arc<CilObject>,
) -> Vec<(usize, usize, u32)> {
let mut seeds = Vec::new();
let Some(init_method_token) = self.semantics.init_method else {
return seeds;
};
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
match instr.op() {
SsaOp::Call { method, args, .. } if method.token() == init_method_token => {
if args.len() >= 2 {
let seed_var = args[1];
if let Some(ConstValue::I32(seed)) =
self.trace_to_constant(seed_var, ssa, ctx, method_token)
{
#[allow(clippy::cast_sign_loss)]
seeds.push((block_idx, instr_idx, seed as u32));
}
}
}
SsaOp::NewObj { ctor, args, .. } if ctor.token() == init_method_token => {
if args.len() == 1 {
if let Some(ConstValue::I32(seed)) =
self.trace_to_constant(args[0], ssa, ctx, method_token)
{
#[allow(clippy::cast_sign_loss)]
seeds.push((block_idx, instr_idx, seed as u32));
}
}
}
_ => {}
}
}
}
seeds
}
fn find_state_updates(&self, ssa: &SsaFunction) -> Vec<StateUpdateCall> {
let mut updates = Vec::new();
let Some(update_method_token) = self.semantics.update_method else {
return updates;
};
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let SsaOp::Call { method, args, dest } | SsaOp::CallVirt { method, args, dest } =
instr.op()
{
if method.token() == update_method_token {
if args.len() >= 3 {
if let Some(dest) = dest {
updates.push(StateUpdateCall {
block_idx,
instr_idx,
dest: *dest,
flag_var: args[1],
increment_var: args[2],
});
}
}
}
}
}
}
updates
}
fn find_decryptor_call_sites(
&self,
ssa: &SsaFunction,
state_updates: &[StateUpdateCall],
decryptor_tokens: &HashSet<Token>,
assembly: &Arc<CilObject>,
) -> Vec<StateMachineCallSite> {
let mut call_sites = Vec::new();
let mut next_info_map: HashMap<SsaVarId, usize> = HashMap::new();
for (idx, update) in state_updates.iter().enumerate() {
next_info_map.insert(update.dest, idx);
}
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let (call_target, args, dest) = match instr.op() {
SsaOp::Call { method, args, dest } | SsaOp::CallVirt { method, args, dest } => {
(method.token(), args, *dest)
}
_ => continue,
};
let Some(dest) = dest else { continue };
let resolved_target =
resolve_method_spec_to_def(assembly, call_target).unwrap_or(call_target);
if !decryptor_tokens.contains(&resolved_target) {
continue;
}
if args.len() != 1 {
continue;
}
let Some(SsaOp::Xor { left, right, .. }) = ssa.get_definition(args[0]) else {
continue;
};
let (state_var, encoded_var, feeding_idx) =
if let Some(&idx) = next_info_map.get(left) {
(*left, *right, idx)
} else if let Some(&idx) = next_info_map.get(right) {
(*right, *left, idx)
} else {
continue;
};
call_sites.push(StateMachineCallSite {
block_idx,
instr_idx,
dest,
decryptor: resolved_target,
call_target,
state_var,
encoded_var,
feeding_update_idx: feeding_idx,
});
}
}
call_sites
}
fn collect_updates_for_call(
&self,
call_site: &StateMachineCallSite,
all_updates: &[StateUpdateCall],
cfg_info: &CfgInfo<'_>,
) -> Vec<usize> {
let feeding_update = &all_updates[call_site.feeding_update_idx];
let mut updates_by_block: HashMap<usize, Vec<usize>> = HashMap::new();
for (idx, update) in all_updates.iter().enumerate() {
updates_by_block
.entry(update.block_idx)
.or_default()
.push(idx);
}
for indices in updates_by_block.values_mut() {
indices.sort_by_key(|&idx| all_updates[idx].instr_idx);
}
let mut relevant_updates: Vec<usize> = Vec::new();
for (block_idx, update_indices) in &updates_by_block {
if *block_idx >= cfg_info.node_count || feeding_update.block_idx >= cfg_info.node_count
{
continue;
}
if *block_idx == feeding_update.block_idx {
for &idx in update_indices {
if all_updates[idx].instr_idx < feeding_update.instr_idx {
relevant_updates.push(idx);
}
}
} else if cfg_info.dom_tree.strictly_dominates(
NodeId::new(*block_idx),
NodeId::new(feeding_update.block_idx),
) {
relevant_updates.extend(update_indices.iter().copied());
}
}
if relevant_updates.is_empty() && feeding_update.block_idx < cfg_info.predecessors.len() {
let block_preds = &cfg_info.predecessors[feeding_update.block_idx];
if block_preds.len() > 1 {
let mut best_path: Vec<usize> = Vec::new();
for &pred in block_preds {
let mut path_updates: Vec<usize> = Vec::new();
let mut current = pred;
let mut visited: HashSet<usize> = HashSet::new();
while current != cfg_info.entry.index() && visited.insert(current) {
if let Some(indices) = updates_by_block.get(¤t) {
path_updates.extend(indices.iter().copied());
}
if current < cfg_info.node_count
&& cfg_info
.dom_tree
.dominates(cfg_info.entry, NodeId::new(current))
{
if let Some(idom) =
cfg_info.dom_tree.immediate_dominator(NodeId::new(current))
{
current = idom.index();
} else {
break;
}
} else {
break;
}
}
if path_updates.len() > best_path.len() {
best_path = path_updates;
}
}
relevant_updates = best_path;
}
}
relevant_updates.sort_by_key(|&idx| {
let update = &all_updates[idx];
let depth = if update.block_idx < cfg_info.node_count
&& cfg_info
.dom_tree
.dominates(cfg_info.entry, NodeId::new(update.block_idx))
{
cfg_info.dom_tree.depth(NodeId::new(update.block_idx))
} else {
usize::MAX };
(depth, update.block_idx, update.instr_idx)
});
relevant_updates
}
}
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
let decryptors = find_decryptor_methods(assembly);
if decryptors.is_empty() {
return;
}
for token in &decryptors {
findings.decryptor_methods.push(*token);
}
let decryptor_confidence = (decryptors.len() * 20).min(30);
score.add(DetectionEvidence::MetadataPattern {
name: format!(
"Constant decryptor methods ({} with signature string(int32) or T(int32))",
decryptors.len()
),
locations: decryptors.iter().copied().collect(),
confidence: decryptor_confidence,
});
let call_sites = find_call_sites(assembly, &decryptors);
let cfg_mode_methods: HashSet<Token> = call_sites
.iter()
.filter(|site| site.uses_statemachine)
.map(|site| site.caller)
.collect();
if !cfg_mode_methods.is_empty() {
if let Some(semantics) = detect_cfgctx_semantics(assembly) {
#[allow(clippy::cast_possible_truncation)]
let multiplier = semantics.init_constant.unwrap_or(0) as u32;
let method_count = cfg_mode_methods.len();
let provider = ConfuserExStateMachine::new(semantics, cfg_mode_methods.iter().copied());
findings.statemachine_provider = Some(Arc::new(provider));
let cfg_confidence = (method_count * 15).min(25);
score.add(DetectionEvidence::BytecodePattern {
name: format!(
"CFG mode constant encryption ({method_count} methods, multiplier=0x{multiplier:08X})"
),
locations: cfg_mode_methods.into_iter().collect(),
confidence: cfg_confidence,
});
}
}
}
fn detect_cfgctx_semantics(assembly: &CilObject) -> Option<StateMachineSemantics> {
if let Some(module_type) = assembly.types().module_type() {
for (_, nested_ref) in module_type.nested_types.iter() {
let nested_type = nested_ref.upgrade()?;
if let Some(semantics) = try_detect_cfgctx_from_type(assembly, &nested_type) {
return Some(semantics);
}
}
}
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.name.is_ascii() && !cil_type.name.is_empty() {
continue;
}
if let Some(semantics) = try_detect_cfgctx_from_type(assembly, cil_type) {
return Some(semantics);
}
}
None
}
fn try_detect_cfgctx_from_type(
assembly: &CilObject,
cil_type: &CilType,
) -> Option<StateMachineSemantics> {
if !cil_type.is_value_type() {
return None;
}
if cil_type.fields.count() != 4 {
return None;
}
let mut ctor_token: Option<Token> = None;
let mut next_token: Option<Token> = None;
let mut multiplier: Option<u32> = None;
for method in &cil_type.query_methods() {
if method.is_ctor() && ctor_token.is_none() {
if let Some(ssa) = method.ssa(assembly) {
if let Some(mult) = extract_multiplier_from_ssa(&ssa) {
multiplier = Some(mult);
ctor_token = Some(method.token);
}
}
} else if next_token.is_none() {
let sig = &method.signature;
if sig.params.len() == 2 && method.name != ".ctor" {
if let Some(ssa) = method.ssa(assembly) {
let mut has_switch = false;
let mut has_stfld = false;
let mut has_ldfld = false;
for (_, block) in ssa.iter_blocks() {
for instr in block.instructions() {
match instr.op() {
SsaOp::Switch { .. } => has_switch = true,
SsaOp::StoreField { .. } => has_stfld = true,
SsaOp::LoadField { .. } => has_ldfld = true,
_ => {}
}
}
}
if has_switch && has_stfld && has_ldfld {
next_token = Some(method.token);
}
}
}
}
}
let (Some(init_method), Some(update_method), Some(mult)) = (ctor_token, next_token, multiplier)
else {
return None;
};
let field_tokens: Vec<Token> = (0..cil_type.fields.count())
.filter_map(|i| cil_type.fields.get(i))
.map(|f| f.token)
.collect();
let slot_ops = extract_slot_operations(assembly, update_method, &field_tokens)?;
Some(StateMachineSemantics {
type_token: Some(cil_type.token),
init_method: Some(init_method),
update_method: Some(update_method),
slot_count: 4,
slot_ops,
init_ops: vec![
StateSlotOperation::mul(),
StateSlotOperation::mul(),
StateSlotOperation::mul(),
StateSlotOperation::mul(),
],
init_constant: Some(u64::from(mult)),
explicit_flag_bit: 7,
update_slot_mask: 0x03,
get_slot_mask: 0x03,
get_slot_shift: 2,
})
}
fn extract_multiplier_from_ssa(ssa: &SsaFunction) -> Option<u32> {
for (_, block) in ssa.iter_blocks() {
for instr in block.instructions() {
let (left, right) = match instr.op() {
SsaOp::Mul { left, right, .. } | SsaOp::MulOvf { left, right, .. } => {
(*left, *right)
}
_ => continue,
};
for operand in [left, right] {
if let Some(mult) = get_i32_constant(ssa, operand) {
if mult != 0 && mult.abs() > 0x1000 {
#[allow(clippy::cast_sign_loss)]
return Some(mult as u32);
}
}
}
}
}
None
}
fn extract_slot_operations(
assembly: &CilObject,
next_method: Token,
field_tokens: &[Token],
) -> Option<Vec<StateSlotOperation>> {
let method = assembly.method(&next_method)?;
let ssa = method.ssa(assembly)?;
let mut ops_found: Vec<(usize, StateSlotOperation)> = Vec::new();
for (_, block) in ssa.iter_blocks() {
for instr in block.instructions() {
let (field_token, value_var) = match instr.op() {
SsaOp::StoreField { field, value, .. } => (field.token(), *value),
_ => continue,
};
let Some(slot_idx) = field_tokens.iter().position(|t| *t == field_token) else {
continue;
};
if let Some(slot_op) = trace_to_arithmetic_op(&ssa, value_var) {
ops_found.push((slot_idx, slot_op));
}
}
}
ops_found.sort_by_key(|(idx, _)| *idx);
ops_found.dedup_by_key(|(idx, _)| *idx);
if ops_found.len() == 4 && ops_found.iter().enumerate().all(|(i, (idx, _))| *idx == i) {
return Some(ops_found.into_iter().map(|(_, op)| op).collect());
}
None
}
fn trace_to_arithmetic_op(ssa: &SsaFunction, start_var: SsaVarId) -> Option<StateSlotOperation> {
let mut worklist = vec![start_var];
let mut visited: HashSet<SsaVarId> = HashSet::new();
while let Some(var) = worklist.pop() {
if !visited.insert(var) {
continue;
}
let Some(def) = ssa.get_definition(var) else {
continue;
};
match def {
SsaOp::Xor { .. } => return Some(StateSlotOperation::xor()),
SsaOp::Add { .. } | SsaOp::AddOvf { .. } => return Some(StateSlotOperation::add()),
SsaOp::Sub { .. } | SsaOp::SubOvf { .. } => return Some(StateSlotOperation::sub()),
SsaOp::Mul { .. } | SsaOp::MulOvf { .. } => return Some(StateSlotOperation::mul()),
SsaOp::And { .. } => return Some(StateSlotOperation::and()),
SsaOp::Or { .. } => return Some(StateSlotOperation::or()),
SsaOp::Conv { operand, .. } => {
worklist.push(*operand);
}
_ => {
for (_, block) in ssa.iter_blocks() {
for phi in block.phi_nodes() {
if phi.result() == var {
if let Some(operand) = phi.operands().first() {
worklist.push(operand.value());
}
}
}
}
}
}
}
None
}
pub fn find_constants_initializer(assembly: &CilObject) -> Option<Token> {
let module_type = assembly.types().module_type()?;
let cctor_token = assembly.types().module_cctor()?;
let cctor = assembly.method(&cctor_token)?;
let mut init_candidates: Vec<Token> = Vec::new();
for instr in cctor.instructions() {
if instr.flow_type == FlowType::Call {
if let Operand::Token(call_target) = &instr.operand {
if call_target.is_table(TableId::MethodDef) {
init_candidates.push(*call_target);
}
}
}
}
for candidate in init_candidates {
let Some(method) = assembly.method(&candidate) else {
continue;
};
if !method.is_static() {
continue;
}
let sig = &method.signature;
if sig.return_type.base != TypeSignature::Void || !sig.params.is_empty() {
continue;
}
let is_in_module = method
.declaring_type_rc()
.is_some_and(|t| t.name == "<Module>");
if !is_in_module {
continue;
}
let mut has_array_ops = false;
let mut has_field_store = false;
for instr in method.instructions() {
match instr.mnemonic {
"newarr" | "newobj" => has_array_ops = true,
"stsfld" => has_field_store = true,
_ => {}
}
}
if has_array_ops && has_field_store {
return Some(candidate);
}
}
for method in &module_type.query_methods() {
if method.is_cctor() {
continue;
}
if !method.flags_modifiers.contains(MethodModifiers::STATIC) {
continue;
}
let sig = &method.signature;
if sig.return_type.base != TypeSignature::Void || !sig.params.is_empty() {
continue;
}
for instr in method.instructions() {
if instr.flow_type == FlowType::Call {
if let Operand::Token(call_target) = &instr.operand {
if let Some(callee) = assembly.method(call_target) {
if callee.name.contains("Decompress") || callee.name.contains("LZMA") {
return Some(method.token);
}
}
if let Some(memberref) = assembly.member_ref(call_target) {
if memberref.name.contains("Decompress") || memberref.name.contains("LZMA")
{
return Some(method.token);
}
}
}
}
}
}
None
}
pub fn find_decryptor_methods(assembly: &CilObject) -> Vec<Token> {
let mut decryptors = Vec::new();
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
let is_module_type = cil_type.name == "<Module>";
let is_obfuscated_name = !cil_type.name.is_ascii();
if !is_module_type && !is_obfuscated_name {
continue;
}
for i in 0..cil_type.methods.count() {
let Some(method_ref) = cil_type.methods.get(i) else {
continue;
};
let Some(method) = method_ref.upgrade() else {
continue;
};
if !method.is_static() {
continue;
}
let sig = &method.signature;
let is_string_decryptor = sig.param_count_generic == 0
&& sig.return_type.base == TypeSignature::String
&& sig.params.len() == 1
&& sig.params[0].base == TypeSignature::I4;
let is_generic_decryptor = sig.param_count_generic == 1
&& matches!(sig.return_type.base, TypeSignature::GenericParamMethod(0))
&& sig.params.len() == 1
&& sig.params[0].base == TypeSignature::I4;
if is_string_decryptor || is_generic_decryptor {
decryptors.push(method.token);
}
}
}
decryptors
}
fn resolve_method_spec_to_def(assembly: &CilObject, token: Token) -> Option<Token> {
if token.table() != 0x2B {
return None; }
let method_spec = assembly.method_spec(&token)?;
let method_token = method_spec.method.token()?;
if method_token.is_table(TableId::MethodDef) {
Some(method_token)
} else {
None
}
}
fn find_call_sites(assembly: &CilObject, decryptor_tokens: &[Token]) -> Vec<DetectedCallSite> {
let decryptor_set: HashSet<_> = decryptor_tokens.iter().copied().collect();
let call_sites: boxcar::Vec<DetectedCallSite> = boxcar::Vec::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
if decryptor_set.contains(&method.token) {
continue;
}
let Some(ssa) = method.ssa(assembly) else {
continue;
};
for (_, block) in ssa.iter_blocks() {
for instr in block.instructions() {
let (call_target, args) = match instr.op() {
SsaOp::Call {
method: m, args, ..
}
| SsaOp::CallVirt {
method: m, args, ..
} => (m.token(), args),
_ => continue,
};
let resolved_target =
resolve_method_spec_to_def(assembly, call_target).unwrap_or(call_target);
if !decryptor_set.contains(&resolved_target) {
continue;
}
if args.is_empty() {
continue;
}
let arg_var = args[0];
match analyze_argument_dataflow(&ssa, arg_var) {
ArgumentAnalysis::DirectConstant(_) => {
call_sites.push(DetectedCallSite::direct(method.token));
}
ArgumentAnalysis::XorWithConstant(_) => {
call_sites.push(DetectedCallSite::statemachine(method.token));
}
ArgumentAnalysis::FlowsThroughCall { constant } => {
if constant.is_some() {
call_sites.push(DetectedCallSite::statemachine(method.token));
}
}
ArgumentAnalysis::Unknown => {
call_sites.push(DetectedCallSite::direct(method.token));
}
}
}
}
}
call_sites.into_iter().collect()
}
enum ArgumentAnalysis {
DirectConstant(i32),
XorWithConstant(i32),
FlowsThroughCall { constant: Option<i32> },
Unknown,
}
fn analyze_argument_dataflow(ssa: &SsaFunction, arg_var: SsaVarId) -> ArgumentAnalysis {
if let Some(key) = get_i32_constant(ssa, arg_var) {
return ArgumentAnalysis::DirectConstant(key);
}
let config = TaintConfig {
forward: false,
backward: true,
phi_mode: PhiTaintMode::TaintAllOperands,
max_iterations: 50,
};
let mut taint = TaintAnalysis::new(config);
taint.add_tainted_var(arg_var);
taint.propagate(ssa);
let mut has_xor_with_const = None;
let mut has_call = false;
let mut call_const = None;
for var in taint.tainted_variables() {
let Some(def) = ssa.get_definition(*var) else {
continue;
};
match def {
SsaOp::Xor { left, right, .. } => {
if let Some(c) = get_i32_constant(ssa, *left) {
has_xor_with_const = Some(c);
} else if let Some(c) = get_i32_constant(ssa, *right) {
has_xor_with_const = Some(c);
}
}
SsaOp::Call { .. } | SsaOp::CallVirt { .. } => {
has_call = true;
if has_xor_with_const.is_some() {
call_const = has_xor_with_const;
}
}
_ => {}
}
}
if has_call {
ArgumentAnalysis::FlowsThroughCall {
constant: call_const.or(has_xor_with_const),
}
} else if let Some(constant) = has_xor_with_const {
ArgumentAnalysis::XorWithConstant(constant)
} else {
ArgumentAnalysis::Unknown
}
}
fn get_i32_constant(ssa: &SsaFunction, var: SsaVarId) -> Option<i32> {
let value = ssa
.get_var_constant(var)
.or_else(|| match ssa.get_definition(var) {
Some(SsaOp::Const { value, .. }) => Some(value),
_ => None,
})?;
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
value.as_i32().or_else(|| value.as_u64().map(|v| v as i32))
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, sync::Arc};
use crate::{
assembly::Operand,
deobfuscation::{
obfuscators::confuserex::constants::{
detect_cfgctx_semantics, find_call_sites, find_decryptor_methods,
},
SsaOpKind, StateMachineSemantics, StateMachineState,
},
metadata::{tables::TableId, token::Token},
prelude::FlowType,
CilObject, ValidationConfig,
};
#[test]
fn test_state_machine_from_seed() {
let semantics = Arc::new(StateMachineSemantics::confuserex_default());
let state = StateMachineState::from_seed_u32(0x12345678, semantics);
const MULTIPLIER: u32 = 0x2141_2321;
let mut seed = 0x1234_5678_u32;
seed = seed.wrapping_mul(MULTIPLIER);
assert_eq!(state.get_u32(0), seed); seed = seed.wrapping_mul(MULTIPLIER);
assert_eq!(state.get_u32(1), seed); seed = seed.wrapping_mul(MULTIPLIER);
assert_eq!(state.get_u32(2), seed); seed = seed.wrapping_mul(MULTIPLIER);
assert_eq!(state.get_u32(3), seed); }
#[test]
fn test_state_machine_next_incremental() {
let semantics = Arc::new(StateMachineSemantics::confuserex_default());
let mut state = StateMachineState::from_seed_u32(0, semantics);
state.set(0, 0x1000_0000);
state.set(1, 0x2000_0000);
state.set(2, 0x3000_0000);
state.set(3, 0x4000_0000);
let flag = 0b0000_0100;
let result = state.next_u32(flag, 0x0000_1111);
assert_eq!(state.get_u32(0), 0x1000_0000 ^ 0x0000_1111);
assert_eq!(result, state.get_u32(1));
let flag = 0b0000_1001;
let result = state.next_u32(flag, 0x0000_2222);
assert_eq!(state.get_u32(1), 0x2000_0000_u32.wrapping_add(0x0000_2222));
assert_eq!(result, state.get_u32(2));
}
#[test]
fn test_state_machine_next_explicit() {
let semantics = Arc::new(StateMachineSemantics::confuserex_default());
let mut state = StateMachineState::from_seed_u32(0, semantics);
state.set(0, 0x1000_0000);
let flag = 0x80;
let result = state.next_u32(flag, 0xDEAD_BEEF);
assert_eq!(state.get_u32(0), 0xDEAD_BEEF);
assert_eq!(result, 0xDEAD_BEEF);
}
#[test]
fn test_mkaring_constants_detection() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(
decryptors.len(),
5,
"mkaring_constants.exe should have 5 decryptor methods"
);
let expected_tokens: Vec<u32> =
vec![0x06000004, 0x06000005, 0x06000006, 0x06000007, 0x06000008];
for expected in &expected_tokens {
assert!(
decryptors.iter().any(|t| t.value() == *expected),
"Expected decryptor 0x{:08X} not found",
expected
);
}
for token in &decryptors {
let method_entry = assembly
.methods()
.get(token)
.expect("Decryptor should exist");
let method = method_entry.value();
assert_eq!(
method.signature.params.len(),
1,
"Decryptor {:?} should have exactly 1 parameter",
token
);
}
let cfgctx = detect_cfgctx_semantics(&assembly);
assert!(
cfgctx.is_none(),
"mkaring_constants.exe uses Normal mode - should NOT have CFGCtx"
);
}
#[test]
fn test_mkaring_constants_dyncyph_detection() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants_dyncyph.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants_dyncyph.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(
decryptors.len(),
5,
"mkaring_constants_dyncyph.exe should have 5 decryptor methods"
);
let expected_tokens: Vec<u32> =
vec![0x06000004, 0x06000005, 0x06000006, 0x06000007, 0x06000008];
for expected in &expected_tokens {
assert!(
decryptors.iter().any(|t| t.value() == *expected),
"Expected decryptor 0x{:08X} not found in mkaring_constants_dyncyph.exe",
expected
);
}
for token in &decryptors {
let method_entry = assembly
.methods()
.get(token)
.expect("Decryptor should exist");
let method = method_entry.value();
assert_eq!(
method.signature.params.len(),
1,
"Decryptor {:?} should have exactly 1 parameter",
token
);
assert_eq!(
method.signature.param_count_generic, 1,
"Decryptor {:?} should be generic (T<> signature)",
token
);
}
let cfgctx = detect_cfgctx_semantics(&assembly);
assert!(
cfgctx.is_none(),
"mkaring_constants_dyncyph.exe uses Dynamic cipher - should NOT have CFGCtx"
);
}
#[test]
fn test_mkaring_constants_cfg_detection() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants_cfg.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants_cfg.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(
decryptors.len(),
5,
"mkaring_constants_cfg.exe should have 5 decryptor methods"
);
for token in &decryptors {
let method_entry = assembly
.methods()
.get(token)
.expect("Decryptor should exist");
let method = method_entry.value();
assert_eq!(
method.signature.param_count_generic, 1,
"Decryptor {:?} should be generic (T<> signature)",
token
);
}
let semantics = detect_cfgctx_semantics(&assembly)
.expect("CFGCtx should be detected in mkaring_constants_cfg.exe");
assert_eq!(
semantics.type_token.map(|t| t.value()),
Some(0x0200_0011),
"CFGCtx type should be at token 0x02000011"
);
assert_eq!(
semantics.init_method.map(|t| t.value()),
Some(0x0600_004f),
"CFGCtx constructor should be at token 0x0600004f"
);
assert_eq!(
semantics.update_method.map(|t| t.value()),
Some(0x0600_0050),
"CFGCtx.Next method should be at token 0x06000050"
);
assert_eq!(
semantics.init_constant,
Some(0x2141_2321),
"CFGCtx multiplier should be 0x21412321"
);
assert_eq!(semantics.slot_count, 4, "CFGCtx should have 4 slots");
assert_eq!(semantics.slot_ops.len(), 4, "Should have 4 slot operations");
assert_eq!(
semantics.slot_ops[0].op,
SsaOpKind::Xor,
"Slot A should use XOR"
);
assert_eq!(
semantics.slot_ops[1].op,
SsaOpKind::Add,
"Slot B should use ADD"
);
assert_eq!(
semantics.slot_ops[2].op,
SsaOpKind::Xor,
"Slot C should use XOR"
);
assert_eq!(
semantics.slot_ops[3].op,
SsaOpKind::Sub,
"Slot D should use SUB"
);
assert_eq!(
semantics.explicit_flag_bit, 7,
"Explicit flag should be bit 7"
);
assert_eq!(
semantics.update_slot_mask, 0x03,
"Update slot mask should be 0x03"
);
assert_eq!(
semantics.get_slot_mask, 0x03,
"Get slot mask should be 0x03"
);
assert_eq!(semantics.get_slot_shift, 2, "Get slot shift should be 2");
}
#[test]
#[ignore] fn test_mkaring_constants_x86_detection() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants_x86.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants_x86.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(
decryptors.len(),
5,
"mkaring_constants_x86.exe should have 5 decryptor methods"
);
for token in &decryptors {
let method_entry = assembly
.methods()
.get(token)
.expect("Decryptor should exist");
let method = method_entry.value();
assert_eq!(
method.signature.param_count_generic, 1,
"Decryptor {:?} should be generic (T<> signature)",
token
);
}
let cfgctx = detect_cfgctx_semantics(&assembly);
assert!(
cfgctx.is_none(),
"mkaring_constants_x86.exe uses x86 native code - should NOT have CFGCtx"
);
}
fn count_decryptor_calls(assembly: &CilObject, decryptor_set: &HashSet<Token>) -> usize {
let mut total = 0;
for method_entry in assembly.methods() {
let method = method_entry.value();
if decryptor_set.contains(&method.token) {
continue;
}
for instr in method.instructions() {
if instr.flow_type == FlowType::Call {
if let Operand::Token(target) = &instr.operand {
let resolved = match target.table() {
0x06 => Some(*target),
0x2B => assembly
.method_specs()
.get(target)
.and_then(|ms| ms.value().method.token())
.filter(|t| t.is_table(TableId::MethodDef)),
_ => None,
};
if let Some(method_def) = resolved {
if decryptor_set.contains(&method_def) {
total += 1;
}
}
}
}
}
}
total
}
#[test]
fn test_call_sites_mkaring_constants() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(decryptors.len(), 5, "Should find 5 decryptor methods");
let decryptor_set: HashSet<_> = decryptors.iter().copied().collect();
let total_calls = count_decryptor_calls(&assembly, &decryptor_set);
let call_sites = find_call_sites(&assembly, &decryptors);
assert!(
total_calls > 0,
"mkaring_constants.exe should have decryptor calls"
);
assert!(
!call_sites.is_empty(),
"Should detect call sites in Normal mode"
);
for site in &call_sites {
assert!(
!site.uses_statemachine,
"mkaring_constants.exe uses Normal mode - all call sites should not be state machine mode"
);
}
assert_eq!(
total_calls, 37,
"mkaring_constants.exe should have 37 decryptor calls"
);
assert_eq!(
call_sites.len(),
total_calls,
"Should detect all {} call sites in Normal mode (detected {})",
total_calls,
call_sites.len()
);
}
#[test]
fn test_call_sites_mkaring_constants_dyncyph() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants_dyncyph.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants_dyncyph.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(decryptors.len(), 5, "Should find 5 decryptor methods");
let decryptor_set: HashSet<_> = decryptors.iter().copied().collect();
let total_calls = count_decryptor_calls(&assembly, &decryptor_set);
let call_sites = find_call_sites(&assembly, &decryptors);
assert_eq!(
total_calls, 37,
"mkaring_constants_dyncyph.exe should have 37 decryptor calls"
);
assert_eq!(
call_sites.len(),
total_calls,
"Should detect all {} call sites in Dynamic cipher mode (detected {})",
total_calls,
call_sites.len()
);
for site in &call_sites {
assert!(
!site.uses_statemachine,
"mkaring_constants_dyncyph.exe uses Dynamic cipher - all call sites should not be state machine mode"
);
}
}
#[test]
fn test_call_sites_mkaring_constants_cfg() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants_cfg.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants_cfg.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(decryptors.len(), 5, "Should find 5 decryptor methods");
let decryptor_set: HashSet<_> = decryptors.iter().copied().collect();
let total_calls = count_decryptor_calls(&assembly, &decryptor_set);
let call_sites = find_call_sites(&assembly, &decryptors);
assert_eq!(
total_calls, 37,
"mkaring_constants_cfg.exe should have 37 decryptor calls"
);
assert_eq!(
call_sites.len(),
total_calls,
"Should detect all {} call sites in CFG mode (detected {})",
total_calls,
call_sites.len()
);
let state_machine_sites: Vec<_> =
call_sites.iter().filter(|s| s.uses_statemachine).collect();
assert_eq!(
state_machine_sites.len(),
call_sites.len(),
"All call sites in mkaring_constants_cfg.exe should be state machine mode ({} state machine vs {} total)",
state_machine_sites.len(),
call_sites.len()
);
}
#[test]
#[ignore] fn test_call_sites_mkaring_constants_x86() {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_constants_x86.exe",
ValidationConfig::analysis(),
)
.expect("Failed to load mkaring_constants_x86.exe");
let decryptors = find_decryptor_methods(&assembly);
assert_eq!(decryptors.len(), 5, "Should find 5 decryptor methods");
let decryptor_set: HashSet<_> = decryptors.iter().copied().collect();
let total_calls = count_decryptor_calls(&assembly, &decryptor_set);
let call_sites = find_call_sites(&assembly, &decryptors);
assert!(
total_calls > 0,
"mkaring_constants_x86.exe should have decryptor calls"
);
assert!(
!call_sites.is_empty(),
"Should detect call sites in x86 mode"
);
for site in &call_sites {
assert!(
!site.uses_statemachine,
"mkaring_constants_x86.exe uses x86 native code - all call sites should not be state machine mode"
);
}
}
}