use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
};
use rayon::prelude::*;
use crate::{
analysis::{ConstValue, SsaCfg, SsaFunction, SsaOp, SsaVarId, ValueResolver},
compiler::{CompilerContext, EventKind, EventLog, SsaPass},
deobfuscation::{
context::AnalysisContext,
decryptors::{DecryptorContext, FailureReason},
CfgInfo, StateMachineProvider, StateMachineState, StateUpdateCall,
},
emulation::{
EmValue, EmulationError, EmulationOutcome, EmulationProcess, EmulationThread, Hook,
ProcessBuilder, StepResult,
},
metadata::{token::Token, typesystem::PointerSize},
utils::graph::{
algorithms::{compute_dominators, DominatorTree},
GraphBase, NodeId, RootedGraph,
},
CilObject, Error, Result,
};
pub type HookFactory = Box<dyn Fn() -> Hook + Send + Sync>;
pub struct DecryptionPass {
template_process: RwLock<Option<EmulationProcess>>,
warmup_failed: AtomicBool,
decryptors: Arc<DecryptorContext>,
emulation_hooks: Arc<boxcar::Vec<HookFactory>>,
warmup_methods: Arc<boxcar::Vec<Token>>,
statemachine_providers: Arc<boxcar::Vec<Arc<dyn StateMachineProvider>>>,
emulation_max_instructions: u64,
tracing_config: Option<crate::emulation::TracingConfig>,
}
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 Default for DecryptionPass {
fn default() -> Self {
Self {
template_process: RwLock::new(None),
warmup_failed: AtomicBool::new(false),
decryptors: Arc::new(DecryptorContext::new()),
emulation_hooks: Arc::new(boxcar::Vec::new()),
warmup_methods: Arc::new(boxcar::Vec::new()),
statemachine_providers: Arc::new(boxcar::Vec::new()),
emulation_max_instructions: 5_000_000,
tracing_config: None,
}
}
}
impl DecryptionPass {
#[must_use]
pub fn new(ctx: &AnalysisContext) -> Self {
Self {
template_process: RwLock::new(None),
warmup_failed: AtomicBool::new(false),
decryptors: Arc::clone(&ctx.decryptors),
emulation_hooks: Arc::clone(&ctx.emulation_hooks),
warmup_methods: Arc::clone(&ctx.warmup_methods),
statemachine_providers: Arc::clone(&ctx.statemachine_providers),
emulation_max_instructions: ctx.config.emulation_max_instructions,
tracing_config: ctx.config.tracing.clone(),
}
}
fn create_template_process(
&self,
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> Result<EmulationProcess> {
let warmup_instruction_limit = self.emulation_max_instructions.max(10_000_000);
let mut builder = ProcessBuilder::new()
.assembly_arc(Arc::clone(assembly))
.with_max_instructions(warmup_instruction_limit)
.with_max_call_depth(100)
.name("decryption_template");
if let Some(ref tracing) = self.tracing_config {
let decryption_tracing = tracing.clone().with_context("decryption");
builder = builder.with_tracing(decryption_tracing);
}
for (_, factory) in self.emulation_hooks.iter() {
builder = builder.hook(factory());
}
let process = builder.build()?;
let warmup_methods: Vec<Token> = self.warmup_methods.iter().map(|(_, &m)| m).collect();
if !warmup_methods.is_empty() {
for warmup_token in &warmup_methods {
match process.execute_method(*warmup_token, vec![]) {
Ok(EmulationOutcome::Completed { .. }) => {
}
Ok(EmulationOutcome::UnhandledException { exception, .. }) => {
ctx.events.warn(format!(
"Warmup method 0x{:08X} threw unhandled exception: {:?} - aborting decryption emulation",
warmup_token.value(),
exception
));
return Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: format!(
"warmup method 0x{:08X} threw unhandled exception",
warmup_token.value()
),
})));
}
Ok(outcome) => {
ctx.events.warn(format!(
"Warmup method 0x{:08X} did not complete: {} - aborting decryption emulation",
warmup_token.value(),
outcome
));
return Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: format!(
"warmup method 0x{:08X} did not complete: {}",
warmup_token.value(),
outcome
),
})));
}
Err(e) => {
ctx.events.warn(format!(
"Warmup method 0x{:08X} failed: {} - aborting decryption emulation",
warmup_token.value(),
e
));
return Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: format!(
"warmup method 0x{:08X} failed: {}",
warmup_token.value(),
e
),
})));
}
}
}
}
Ok(process)
}
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,
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> Result<EmulationProcess> {
if self.warmup_failed.load(Ordering::Relaxed) {
return Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: "warmup failed - decryption disabled".to_string(),
})));
}
{
let guard = self
.template_process
.read()
.map_err(|e| Error::LockError(format!("template process read lock: {e}")))?;
if let Some(ref template) = *guard {
return Ok(template.fork());
}
}
let mut guard = self
.template_process
.write()
.map_err(|e| Error::LockError(format!("template process write lock: {e}")))?;
if let Some(ref template) = *guard {
return Ok(template.fork());
}
if self.warmup_failed.load(Ordering::Relaxed) {
return Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: "warmup failed - decryption disabled".to_string(),
})));
}
match self.create_template_process(ctx, assembly) {
Ok(process) => {
let forked = process.fork();
*guard = Some(process);
Ok(forked)
}
Err(e) => {
self.warmup_failed.store(true, Ordering::Relaxed);
Err(e)
}
}
}
fn try_decrypt_at_call(
&self,
decryptor: Token,
args: &[ConstValue],
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> (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(decryptor, args, ctx, assembly);
if let Some(ref value) = result {
self.decryptors.cache_value(decryptor, args, value.clone());
}
(result, failure)
}
fn emulate_call(
&self,
method: Token,
args: &[ConstValue],
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> (Option<ConstValue>, Option<FailureReason>) {
let process = match self.fork_template_process(ctx, assembly) {
Ok(p) => p,
Err(e) => {
return (None, Some(FailureReason::EmulationFailed(e.to_string())));
}
};
let em_args: Vec<EmValue> = args.iter().map(EmValue::from).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 emvalue_to_constvalue(em_value: &EmValue, thread: &EmulationThread) -> Option<ConstValue> {
if matches!(em_value, EmValue::Null) {
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 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: &Arc<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.registered_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 relevant_updates =
provider.collect_updates_for_call(call_site, &state_updates, &cfg_info.as_ref());
let mut resolver = ValueResolver::new(&*ssa, ptr_size).with_path_aware_fallback();
resolver.load_known_values(ctx, method_token);
Self::simulate_state_updates(
&mut state,
&relevant_updates,
&state_updates,
&mut resolver,
);
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, &args, ctx, assembly);
if let Some(value) = result {
self.decryptors.record_success(
call_site.decryptor,
method_token,
location,
value.clone(),
);
changes
.record(EventKind::ConstantDecrypted)
.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());
changes
.record(EventKind::Warning)
.at(method_token, *location)
.message(format!(
"CFG mode decryption failed for decryptor 0x{:08X}: {reason}",
decryptor.value()
));
}
Self::cleanup_state_updates(ssa, &state_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<'_>,
) {
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);
}
}
}
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 initialize(&mut self, _ctx: &CompilerContext) -> Result<()> {
Ok(())
}
fn finalize(&mut self, _ctx: &CompilerContext) -> Result<()> {
*self
.template_process
.write()
.map_err(|e| Error::LockError(format!("template process write lock: {e}")))? = None;
Ok(())
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> Result<bool> {
if !self.decryptors.has_decryptors() {
return Ok(false);
}
if self.warmup_failed.load(Ordering::Relaxed) {
return Ok(false);
}
let ptr_size = PointerSize::from_pe(assembly.file().pe().is_64bit);
let mut state_machine_changed = false;
if let Some(provider) = self.get_statemachine_provider_for_method(method_token) {
let (changed, sm_changes) = self.process_state_machine_mode(
ssa,
method_token,
ctx,
assembly,
&*provider,
ptr_size,
);
if !sm_changes.is_empty() {
ctx.events.merge(&sm_changes);
}
state_machine_changed = changed;
}
let changes = EventLog::new();
let mut resolver = ValueResolver::new(&*ssa, ptr_size);
resolver.load_known_values(ctx, method_token);
let mut candidates: Vec<(usize, usize, SsaVarId, Token, usize, Vec<ConstValue>)> =
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,
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, location, args)| {
let (result, failure) = self.try_decrypt_at_call(decryptor, &args, ctx, assembly);
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());
changes
.record(EventKind::ConstantDecrypted)
.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());
changes
.record(EventKind::Warning)
.at(method_token, *location)
.message(format!(
"Decryption failed for decryptor 0x{:08X}: {reason}",
decryptor.value()
));
}
let normal_mode_changed = changes.iter().any(|e| !e.kind.is_diagnostic());
if !changes.is_empty() {
ctx.events.merge(&changes);
}
Ok(state_machine_changed || normal_mode_changed)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::{
analysis::{
CallGraph, ConstValue, MethodRef, SsaFunction, SsaFunctionBuilder, 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_pass_default() {
let pass = DecryptionPass::default();
assert_eq!(pass.name(), "decryption");
}
#[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();
});
});
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();
});
});
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();
});
});
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);
f.block(0, |b| {
let _ = b.call(MethodRef::new(decryptor_token), &[arg0]);
b.ret();
});
});
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), &[]);
b.ret();
});
});
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]);
b.ret();
});
});
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::new();
let var2 = SsaVarId::new();
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::new();
let var2 = SsaVarId::new();
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/mkaring_constants.exe";
let mut 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);
}
}
}
}