use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use rayon::prelude::*;
use crate::{
analysis::{ConstValue, SsaCfg, SsaFunction, SsaOp, SsaVarId, ValueResolver},
compiler::{CompilerContext, EventKind, EventLog, ModificationScope, PassCapability, SsaPass},
deobfuscation::{
context::AnalysisContext,
decryptors::{DecryptorContext, FailureReason},
CfgInfo, EmulationTemplatePool, StateMachineProvider, StateMachineState, StateUpdateCall,
},
emulation::{
EmValue, EmulationError, EmulationOutcome, EmulationProcess, EmulationThread, StepResult,
},
metadata::{
tables::TypeRefRaw,
token::Token,
typesystem::{CilFlavor, CilPrimitive, CilPrimitiveKind, PointerSize},
},
utils::graph::{
algorithms::{compute_dominators, DominatorTree},
GraphBase, NodeId, RootedGraph,
},
CilObject, Error, Result,
};
pub struct DecryptionPass {
template_pool: Option<Arc<EmulationTemplatePool>>,
decryptors: Arc<DecryptorContext>,
statemachine_providers: Arc<boxcar::Vec<Arc<dyn StateMachineProvider>>>,
emulation_max_instructions: u64,
}
struct CfgInfoOwned {
dom_tree: DominatorTree,
predecessors: Vec<Vec<usize>>,
node_count: usize,
entry: NodeId,
}
impl CfgInfoOwned {
fn as_ref(&self) -> CfgInfo<'_> {
CfgInfo {
dom_tree: &self.dom_tree,
predecessors: &self.predecessors,
node_count: self.node_count,
entry: self.entry,
}
}
}
impl DecryptionPass {
#[must_use]
pub fn new(ctx: &AnalysisContext) -> Self {
Self {
template_pool: ctx.template_pool.get().cloned(),
decryptors: Arc::clone(&ctx.decryptors),
statemachine_providers: Arc::clone(&ctx.statemachine_providers),
emulation_max_instructions: ctx.config.emulation.max_instructions,
}
}
fn get_statemachine_provider_for_method(
&self,
method: Token,
) -> Option<Arc<dyn StateMachineProvider>> {
for (_, provider) in self.statemachine_providers.iter() {
if provider.applies_to_method(method) {
return Some(Arc::clone(provider));
}
}
None
}
fn fork_template_process(&self) -> Result<EmulationProcess> {
match &self.template_pool {
Some(pool) => pool.fork(),
None => Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: "no emulation template available".to_string(),
}))),
}
}
fn try_decrypt_at_call(
&self,
decryptor: Token,
call_target: Token,
args: &[ConstValue],
) -> (Option<ConstValue>, Option<FailureReason>) {
if let Some(cached) = self
.decryptors
.with_cached(decryptor, args, ConstValue::clone)
{
return (Some(cached), None);
}
let (result, failure) = self.emulate_call(call_target, args);
if let Some(ref value) = result {
self.decryptors.cache_value(decryptor, args, value.clone());
}
(result, failure)
}
fn emulate_call(
&self,
method: Token,
args: &[ConstValue],
) -> (Option<ConstValue>, Option<FailureReason>) {
let process = match self.fork_template_process() {
Ok(p) => p,
Err(e) => {
return (None, Some(FailureReason::EmulationFailed(e.to_string())));
}
};
let em_args: Vec<EmValue> = args
.iter()
.map(|cv| match cv {
ConstValue::String(us_offset) => {
let resolved = process.assembly().and_then(|asm| {
let us = asm.userstrings()?;
let u16str = us.get(*us_offset as usize).ok()?;
let s = u16str.to_string_lossy();
let href = process.address_space().alloc_string(&s).ok()?;
Some(EmValue::ObjectRef(href))
});
resolved.unwrap_or_else(|| EmValue::from(cv))
}
_ => EmValue::from(cv),
})
.collect();
let captured_value: Arc<Mutex<Option<ConstValue>>> = Arc::new(Mutex::new(None));
let captured_clone = captured_value.clone();
let outcome = match process.emulate_until(
method,
em_args,
move |step_result: &StepResult, thread: &EmulationThread| {
match step_result {
StepResult::Return { .. } => {
if thread.call_depth() != 1 {
return false;
}
if let Ok(em_value) = thread.stack().peek() {
let const_value = Self::emvalue_to_constvalue(em_value, thread);
if const_value.is_some() {
if let Ok(mut guard) = captured_clone.lock() {
*guard = const_value;
}
return true; }
}
false
}
_ => false,
}
},
) {
Ok(o) => o,
Err(e) => {
return (None, Some(FailureReason::EmulationFailed(format!("{e}"))));
}
};
match &outcome {
EmulationOutcome::Stopped { .. } | EmulationOutcome::Completed { .. } => {
let result = captured_value.lock().ok().and_then(|guard| guard.clone());
if result.is_some() {
(result, None)
} else {
(None, Some(FailureReason::InvalidReturnValue))
}
}
EmulationOutcome::UnhandledException { exception, .. } => (
None,
Some(FailureReason::EmulationFailed(format!(
"unhandled exception: {exception:?}"
))),
),
EmulationOutcome::LimitReached { limit, .. } => (
None,
Some(FailureReason::EmulationFailed(format!(
"limit reached: {limit:?}"
))),
),
EmulationOutcome::RequiresSymbolic { reason, .. } => (
None,
Some(FailureReason::EmulationFailed(format!(
"requires symbolic: {reason}"
))),
),
EmulationOutcome::Breakpoint { .. } => {
let result = captured_value.lock().ok().and_then(|guard| guard.clone());
if result.is_some() {
(result, None)
} else {
(
None,
Some(FailureReason::EmulationFailed("breakpoint".to_string())),
)
}
}
}
}
fn resolve_flavor_to_typeref(flavor: &CilFlavor, thread: &EmulationThread) -> Option<Token> {
let kind = CilPrimitiveKind::try_from(flavor.clone()).ok()?;
let primitive = CilPrimitive::new(kind);
let namespace = primitive.namespace();
let name = primitive.name();
let asm = thread.assembly()?;
let tables = asm.tables()?;
let strings = asm.strings()?;
let type_refs = tables.table::<TypeRefRaw>()?;
for row in type_refs {
let row_name = strings.get(row.type_name as usize).ok()?;
let row_ns = strings.get(row.type_namespace as usize).ok()?;
if row_name == name && row_ns == namespace {
return Some(row.token);
}
}
None
}
fn emvalue_to_constvalue(em_value: &EmValue, thread: &EmulationThread) -> Option<ConstValue> {
if matches!(em_value, EmValue::Null) {
log::debug!("Decryption: emulation returned Null — treating as failure");
return None;
}
if let Some(cv) = em_value.to_const_value() {
return Some(cv);
}
if let EmValue::ObjectRef(href) = em_value {
if let Ok(s) = thread.heap().get_string(*href) {
return Some(ConstValue::DecryptedString(s.to_string()));
}
if let Ok(unboxed) = thread.heap().unbox(*href) {
if let Some(cv) = unboxed.to_const_value() {
return Some(cv);
}
}
if let Ok(Some(bytes)) = thread.heap().get_array_as_bytes(*href, PointerSize::Bit32) {
if let Ok(elem_flavor) = thread.heap().get_array_element_type(*href) {
let elem_size = elem_flavor.byte_size(PointerSize::Bit32).unwrap_or(1);
if let Some(token) = Self::resolve_flavor_to_typeref(&elem_flavor, thread) {
return Some(ConstValue::DecryptedArray {
data: bytes,
element_type_token: token,
element_size: elem_size,
});
}
}
}
}
if let EmValue::ValueType { fields, .. } = em_value {
if fields.len() == 1 {
if let EmValue::ObjectRef(href) = &fields[0] {
if let Ok(s) = thread.heap().get_string(*href) {
return Some(ConstValue::DecryptedString(s.to_string()));
}
}
if !matches!(fields[0], EmValue::Null) {
if let Some(cv) = fields[0].to_const_value() {
return Some(cv);
}
}
}
}
None
}
fn get_arg_constants(
args: &[SsaVarId],
resolver: &mut ValueResolver<'_>,
) -> Option<Vec<ConstValue>> {
resolver.resolve_all(args)
}
fn process_state_machine_mode(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
assembly: &CilObject,
provider: &dyn StateMachineProvider,
ptr_size: PointerSize,
) -> (bool, EventLog) {
let changes = EventLog::new();
let semantics = Arc::new(provider.semantics().clone());
let state_updates = provider.find_state_updates(ssa);
if state_updates.is_empty() {
return (false, changes);
}
let cfg_info = Self::build_cfg_info(ssa);
let decryptor_tokens = self.decryptors.all_resolvable_tokens();
let call_sites =
provider.find_decryptor_call_sites(ssa, &state_updates, &decryptor_tokens, assembly);
if call_sites.is_empty() {
return (false, changes);
}
let all_seeds = provider.find_initializations(ssa, ctx, method_token, assembly);
let mut changed = false;
let mut failures: Vec<(Token, usize, FailureReason)> = Vec::new();
for call_site in &call_sites {
let location = call_site.location();
if self.decryptors.is_already_decrypted(method_token, location) {
continue;
}
if self
.decryptors
.has_permanent_failure(method_token, location)
{
continue;
}
let seed = provider
.find_seed_for_call(&all_seeds, call_site, &cfg_info.as_ref())
.unwrap_or(0);
let mut state = StateMachineState::from_seed_u32(seed, Arc::clone(&semantics));
let seed_block = all_seeds
.iter()
.filter(|(_, _, val)| *val == seed)
.map(|(block, _, _)| *block)
.min();
let relevant_updates = provider.collect_updates_for_call(
call_site,
&state_updates,
&cfg_info.as_ref(),
seed_block,
);
let mut resolver = ValueResolver::new(&*ssa, ptr_size).with_path_aware_fallback();
resolver.load_known_values(ctx, method_token);
let simulation_ok = Self::simulate_state_updates(
&mut state,
&relevant_updates,
&state_updates,
&mut resolver,
);
if !simulation_ok {
failures.push((
call_site.decryptor,
location,
FailureReason::NonConstantArgs,
));
continue;
}
let feeding_update = &state_updates[call_site.feeding_update_idx];
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let flag = resolver
.resolve(feeding_update.flag_var)
.and_then(|v| v.as_i64())
.map(|x| x as u8);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let increment = resolver
.resolve(feeding_update.increment_var)
.and_then(|v| v.as_i64())
.map(|x| x as u32);
let (Some(flag), Some(increment)) = (flag, increment) else {
failures.push((
call_site.decryptor,
location,
FailureReason::NonConstantArgs,
));
continue;
};
let state_value = state.next_u32(flag, increment);
let encoded = resolver
.resolve(call_site.encoded_var)
.and_then(|v| v.as_i32());
let Some(encoded) = encoded else {
failures.push((
call_site.decryptor,
location,
FailureReason::NonConstantArgs,
));
continue;
};
let actual_key = provider.compute_key(u64::from(state_value), encoded);
let args = vec![ConstValue::I32(actual_key)];
let (result, failure) =
self.try_decrypt_at_call(call_site.decryptor, call_site.call_target, &args);
if let Some(value) = result {
self.decryptors.record_success(
call_site.decryptor,
method_token,
location,
value.clone(),
);
let event_kind = if value.is_string_like() {
EventKind::StringDecrypted
} else {
EventKind::ConstantDecrypted
};
changes
.record(event_kind)
.at(method_token, location)
.message(format!(
"decrypted (CFG mode, flag=0x{flag:02X}, inc=0x{increment:08X}): {value}"
));
ctx.add_known_value(method_token, call_site.dest, value.clone());
if let Some(block) = ssa.block_mut(call_site.block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(call_site.instr_idx) {
instr.set_op(SsaOp::Const {
dest: call_site.dest,
value,
});
}
}
changed = true;
} else {
failures.push((
call_site.decryptor,
location,
failure.unwrap_or(FailureReason::InvalidReturnValue),
));
}
}
for (decryptor, location, reason) in &failures {
self.decryptors
.record_failure(*decryptor, method_token, *location, reason.clone());
log::warn!(
"CFG mode decryption failed for decryptor 0x{:08X} in method {}: {reason}",
decryptor.value(),
method_token
);
}
let all_resolved = call_sites.iter().all(|cs| {
let loc = cs.location();
self.decryptors.is_already_decrypted(method_token, loc)
|| self.decryptors.has_permanent_failure(method_token, loc)
});
if all_resolved {
let feeding_indices: HashSet<usize> =
call_sites.iter().map(|cs| cs.feeding_update_idx).collect();
let feeding_updates: Vec<StateUpdateCall> = state_updates
.iter()
.enumerate()
.filter(|(i, _)| feeding_indices.contains(i))
.map(|(_, u)| u.clone())
.collect();
Self::cleanup_state_updates(
ssa,
&feeding_updates,
method_token,
&changes,
&mut changed,
);
Self::cleanup_state_initialization(
ssa,
&all_seeds,
method_token,
&changes,
&mut changed,
);
}
(changed, changes)
}
fn build_cfg_info(ssa: &SsaFunction) -> CfgInfoOwned {
let cfg = SsaCfg::from_ssa(ssa);
let node_count = cfg.node_count();
let entry = cfg.entry();
let dom_tree = compute_dominators(&cfg, entry);
let predecessors: Vec<Vec<usize>> = (0..node_count)
.map(|i| cfg.block_predecessors(i).to_vec())
.collect();
CfgInfoOwned {
dom_tree,
predecessors,
node_count,
entry,
}
}
fn simulate_state_updates(
state: &mut StateMachineState,
relevant_updates: &[usize],
all_updates: &[StateUpdateCall],
resolver: &mut ValueResolver<'_>,
) -> bool {
for &idx in relevant_updates {
let update = &all_updates[idx];
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let flag = resolver
.resolve(update.flag_var)
.and_then(|v| v.as_i64())
.map(|x| x as u8);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let inc = resolver
.resolve(update.increment_var)
.and_then(|v| v.as_i64())
.map(|x| x as u32);
if let (Some(flag), Some(inc)) = (flag, inc) {
let _ = state.next_u32(flag, inc);
} else {
return false;
}
}
true
}
fn cleanup_state_updates(
ssa: &mut SsaFunction,
state_updates: &[StateUpdateCall],
method_token: Token,
changes: &EventLog,
changed: &mut bool,
) {
for update in state_updates {
if let Some(block) = ssa.block_mut(update.block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(update.instr_idx) {
instr.set_op(SsaOp::Const {
dest: update.dest,
value: ConstValue::I32(0),
});
*changed = true;
changes
.record(EventKind::InstructionRemoved)
.at(method_token, update.block_idx * 1000 + update.instr_idx)
.message(format!(
"replaced state machine update call with const (result {:?})",
update.dest
));
}
}
}
}
fn cleanup_state_initialization(
ssa: &mut SsaFunction,
seeds: &[(usize, usize, u32)],
method_token: Token,
changes: &EventLog,
changed: &mut bool,
) {
for &(block_idx, instr_idx, _seed) in seeds {
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
instr.set_op(SsaOp::Nop);
*changed = true;
changes
.record(EventKind::InstructionRemoved)
.at(method_token, block_idx * 1000 + instr_idx)
.message("removed state machine initialization call");
}
}
}
}
}
impl SsaPass for DecryptionPass {
fn name(&self) -> &'static str {
"decryption"
}
fn description(&self) -> &'static str {
"Decrypts obfuscated values by emulating registered decryptor methods"
}
fn modification_scope(&self) -> ModificationScope {
ModificationScope::InstructionsOnly
}
fn provides(&self) -> &[PassCapability] {
&[PassCapability::DecryptedStrings]
}
fn initialize(&mut self, _ctx: &CompilerContext) -> Result<()> {
Ok(())
}
fn finalize(&mut self, _ctx: &CompilerContext) -> Result<()> {
Ok(())
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
assembly: &CilObject,
) -> Result<bool> {
if !self.decryptors.has_decryptors() {
return Ok(false);
}
let ptr_size = PointerSize::from_pe(assembly.file().pe().is_64bit);
let mut any_changed = false;
if let Some(provider) = self.get_statemachine_provider_for_method(method_token) {
let (sm_changed, sm_changes) = self.process_state_machine_mode(
ssa,
method_token,
ctx,
assembly,
&*provider,
ptr_size,
);
if sm_changed {
any_changed = true;
}
if !sm_changes.is_empty() {
ctx.events.merge(&sm_changes);
}
}
let changes = EventLog::new();
let mut resolver = ValueResolver::new(&*ssa, ptr_size).with_path_aware_fallback();
resolver.load_known_values(ctx, method_token);
type DecryptionCandidate = (usize, usize, SsaVarId, Token, Token, usize, Vec<ConstValue>);
let mut candidates: Vec<DecryptionCandidate> = Vec::new();
let mut failures: Vec<(Token, usize, FailureReason)> = Vec::new();
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 Some(decryptor) = self.decryptors.resolve_decryptor(call_target) else {
continue;
};
let location = block_idx * 1000 + instr_idx;
if self.decryptors.is_already_decrypted(method_token, location) {
continue;
}
if self
.decryptors
.has_permanent_failure(method_token, location)
{
continue;
}
let Some(arg_constants) = Self::get_arg_constants(args, &mut resolver) else {
failures.push((decryptor, location, FailureReason::NonConstantArgs));
continue;
};
candidates.push((
block_idx,
instr_idx,
dest,
decryptor,
call_target,
location,
arg_constants,
));
}
}
let parallel_failures = Mutex::new(Vec::new());
let successes: Vec<_> = candidates
.into_par_iter()
.filter_map(
|(block_idx, instr_idx, dest, decryptor, call_target, location, args)| {
let (result, failure) = self.try_decrypt_at_call(decryptor, call_target, &args);
if let Some(value) = result {
Some((block_idx, instr_idx, dest, decryptor, location, value))
} else {
if let Ok(mut guard) = parallel_failures.lock() {
guard.push((
decryptor,
location,
failure.unwrap_or(FailureReason::InvalidReturnValue),
));
}
None
}
},
)
.collect();
failures.extend(
parallel_failures
.into_inner()
.map_err(|e| Error::LockError(format!("parallel failures lock: {e}")))?,
);
for (block_idx, instr_idx, dest, decryptor, location, value) in successes {
self.decryptors
.record_success(decryptor, method_token, location, value.clone());
let event_kind = if value.is_string_like() {
EventKind::StringDecrypted
} else {
EventKind::ConstantDecrypted
};
changes
.record(event_kind)
.at(method_token, block_idx * 1000 + instr_idx)
.message(format!("decrypted: {value}"));
ctx.add_known_value(method_token, dest, value.clone());
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
instr.set_op(SsaOp::Const { dest, value });
}
}
}
for (decryptor, location, reason) in &failures {
self.decryptors
.record_failure(*decryptor, method_token, *location, reason.clone());
log::warn!(
"Decryption failed for decryptor 0x{:08X} in method {}: {reason}",
decryptor.value(),
method_token
);
}
let normal_changed = changes.iter().any(|e| e.kind.is_transformation());
if !changes.is_empty() {
ctx.events.merge(&changes);
}
Ok(any_changed || normal_changed)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::{
analysis::{
CallGraph, ConstValue, MethodRef, SsaFunction, SsaFunctionBuilder, SsaType, SsaVarId,
ValueResolver,
},
compiler::SsaPass,
deobfuscation::{
context::AnalysisContext, passes::DecryptionPass, DeobfuscationEngine, EngineConfig,
},
metadata::{token::Token, typesystem::PointerSize},
test::helpers::test_assembly_arc,
};
fn create_test_context() -> AnalysisContext {
let call_graph = Arc::new(CallGraph::new());
AnalysisContext::new(call_graph)
}
#[test]
fn test_pass_creation() {
let ctx = create_test_context();
let pass = DecryptionPass::new(&ctx);
assert_eq!(pass.name(), "decryption");
assert!(!pass.description().is_empty());
}
#[test]
fn test_run_no_decryptors() {
let ctx = create_test_context();
let pass = DecryptionPass::new(&ctx);
let method_token = Token::new(0x06000001);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
let _ = b.const_i32(42);
b.ret();
});
})
.unwrap();
let changed = pass
.run_on_method(&mut ssa, method_token, &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
}
#[test]
fn test_run_with_decryptor_no_calls() {
let ctx = create_test_context();
let decryptor_token = Token::new(0x06000002);
ctx.decryptors.register(decryptor_token);
let pass = DecryptionPass::new(&ctx);
let method_token = Token::new(0x06000001);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
let _ = b.const_i32(42);
b.ret();
});
})
.unwrap();
let changed = pass
.run_on_method(&mut ssa, method_token, &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
}
#[test]
fn test_run_call_to_decryptor_no_dest() {
let ctx = create_test_context();
let decryptor_token = Token::new(0x06000002);
ctx.decryptors.register(decryptor_token);
let pass = DecryptionPass::new(&ctx);
let method_token = Token::new(0x06000001);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
b.call_void(MethodRef::new(decryptor_token), &[]);
b.ret();
});
})
.unwrap();
let changed = pass
.run_on_method(&mut ssa, method_token, &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
}
#[test]
fn test_run_call_to_decryptor_non_constant_args() {
let ctx = create_test_context();
let decryptor_token = Token::new(0x06000002);
ctx.decryptors.register(decryptor_token);
let pass = DecryptionPass::new(&ctx);
let method_token = Token::new(0x06000001);
let mut ssa = SsaFunctionBuilder::new(1, 0)
.build_with(|f| {
let arg0 = f.arg(0, SsaType::I32);
f.block(0, |b| {
let _ = b.call(MethodRef::new(decryptor_token), &[arg0], SsaType::I32);
b.ret();
});
})
.unwrap();
let changed = pass
.run_on_method(&mut ssa, method_token, &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
assert_eq!(ctx.decryptors.total_failed(), 1);
}
#[test]
fn test_run_call_to_non_decryptor() {
let ctx = create_test_context();
let decryptor_token = Token::new(0x06000002);
ctx.decryptors.register(decryptor_token);
let pass = DecryptionPass::new(&ctx);
let method_token = Token::new(0x06000001);
let other_method = Token::new(0x06000003);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
let _ = b.call(MethodRef::new(other_method), &[], SsaType::I32);
b.ret();
});
})
.unwrap();
let changed = pass
.run_on_method(&mut ssa, method_token, &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
}
#[test]
fn test_methodspec_resolution() {
let ctx = create_test_context();
let decryptor_token = Token::new(0x06000002);
let methodspec_token = Token::new(0x2b000001);
ctx.decryptors.register(decryptor_token);
ctx.decryptors
.map_methodspec(methodspec_token, decryptor_token);
let pass = DecryptionPass::new(&ctx);
let method_token = Token::new(0x06000001);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
let c = b.const_i32(42);
let _ = b.call(MethodRef::new(methodspec_token), &[c], SsaType::I32);
b.ret();
});
})
.unwrap();
let var_id = ssa.block(0).unwrap().instructions()[0].op().dest().unwrap();
ctx.add_known_value(method_token, var_id, ConstValue::I32(42));
let _ = pass.run_on_method(&mut ssa, method_token, &ctx, &test_assembly_arc());
assert_eq!(
ctx.decryptors.total_failed(),
1,
"Should record failure for MethodSpec call (recognized but emulation failed)"
);
}
#[test]
fn test_pass_initialize() {
let ctx = create_test_context();
let mut pass = DecryptionPass::new(&ctx);
let result = pass.initialize(&ctx);
assert!(result.is_ok());
}
#[test]
fn test_get_arg_constants_all_known() {
let ctx = create_test_context();
let ssa = SsaFunction::new(0, 0);
let method = Token::new(0x06000001);
let var1 = SsaVarId::from_index(0);
let var2 = SsaVarId::from_index(1);
ctx.add_known_value(method, var1, ConstValue::I32(42));
ctx.add_known_value(method, var2, ConstValue::I32(100));
let mut resolver = ValueResolver::new(&ssa, PointerSize::Bit64);
resolver.load_known_values(&ctx, method);
let args = vec![var1, var2];
let result = DecryptionPass::get_arg_constants(&args, &mut resolver);
assert!(result.is_some());
let constants = result.unwrap();
assert_eq!(constants.len(), 2);
assert_eq!(constants[0], ConstValue::I32(42));
assert_eq!(constants[1], ConstValue::I32(100));
}
#[test]
fn test_get_arg_constants_partial() {
let ctx = create_test_context();
let ssa = SsaFunction::new(0, 0);
let method = Token::new(0x06000001);
let var1 = SsaVarId::from_index(0);
let var2 = SsaVarId::from_index(1);
ctx.add_known_value(method, var1, ConstValue::I32(42));
let mut resolver = ValueResolver::new(&ssa, PointerSize::Bit64);
resolver.load_known_values(&ctx, method);
let args = vec![var1, var2];
let result = DecryptionPass::get_arg_constants(&args, &mut resolver);
assert!(result.is_none());
}
#[test]
fn test_get_arg_constants_empty() {
let ctx = create_test_context();
let ssa = SsaFunction::new(0, 0);
let method = Token::new(0x06000001);
let mut resolver = ValueResolver::new(&ssa, PointerSize::Bit64);
resolver.load_known_values(&ctx, method);
let args: Vec<SsaVarId> = vec![];
let result = DecryptionPass::get_arg_constants(&args, &mut resolver);
assert!(result.is_some());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_constants_decryption_integration() {
const CONSTANTS_PATH: &str = "tests/samples/packers/confuserex/1.6.0/mkaring_constants.exe";
let engine = DeobfuscationEngine::new(EngineConfig::default());
let result = engine.process_file(CONSTANTS_PATH);
match result {
Ok((_deobfuscated, result)) => {
let stats = result.stats();
assert!(
stats.constants_folded > 30,
"Expected at least 30 constants decrypted/folded, got {}",
stats.constants_folded
);
assert!(
stats.methods_transformed > 0,
"Expected some methods to be transformed"
);
assert!(
stats.methods_regenerated > 0,
"Expected some methods to have code regenerated"
);
}
Err(e) => {
panic!("Deobfuscation should succeed: {:?}", e);
}
}
}
}