use std::{
collections::HashSet,
sync::{Arc, OnceLock},
};
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp, SsaVarId},
assembly::{opcodes, Operand},
compiler::{CompilerContext, EventKind, EventLog, SsaPass},
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
obfuscators::confuserex::utils,
},
metadata::token::Token,
CilObject, Result,
};
#[derive(Debug, Clone)]
pub struct AntiDumpMethodInfo {
pub token: Token,
pub calls_virtualprotect: bool,
pub calls_gethinstance: bool,
pub calls_get_module: bool,
pub calls_marshal_copy: bool,
}
#[derive(Debug, Default)]
pub struct AntiDumpDetectionResult {
pub methods: Vec<AntiDumpMethodInfo>,
pub pinvoke_methods: Vec<Token>,
}
impl AntiDumpDetectionResult {
pub fn is_detected(&self) -> bool {
!self.methods.is_empty()
}
}
pub fn detect_antidump(assembly: &CilObject) -> AntiDumpDetectionResult {
AntiDumpDetectionResult {
methods: find_antidump_methods(assembly),
..Default::default()
}
}
pub fn add_antidump_evidence(result: &AntiDumpDetectionResult, score: &DetectionScore) {
if !result.methods.is_empty() {
let locations: boxcar::Vec<Token> = boxcar::Vec::new();
for m in &result.methods {
locations.push(m.token);
}
let confidence = (result.methods.len() * 20).min(40);
score.add(DetectionEvidence::BytecodePattern {
name: format!("ConfuserEx anti-dump ({} methods)", result.methods.len()),
locations,
confidence,
});
}
}
fn find_antidump_methods(assembly: &CilObject) -> Vec<AntiDumpMethodInfo> {
let mut found = Vec::new();
let import_map = utils::build_pinvoke_import_map(assembly);
for method in &assembly.query_methods().has_body() {
let Some(cfg) = method.cfg() else {
continue;
};
let mut calls_virtualprotect = false;
let mut calls_gethinstance = false;
let mut calls_get_module = false;
let mut calls_marshal_copy = false;
for node_id in cfg.node_ids() {
let Some(block) = cfg.block(node_id) else {
continue;
};
for instr in &block.instructions {
if instr.opcode == opcodes::CALL || instr.opcode == opcodes::CALLVIRT {
if let Operand::Token(token) = &instr.operand {
if let Some(name) =
utils::resolve_call_target(assembly, *token, &import_map)
{
match name.as_str() {
"VirtualProtect" => calls_virtualprotect = true,
"GetHINSTANCE" => calls_gethinstance = true,
"get_Module" => calls_get_module = true,
"Copy" => calls_marshal_copy = true,
_ => {}
}
}
}
}
}
}
if calls_virtualprotect && calls_gethinstance && calls_get_module && calls_marshal_copy {
found.push(AntiDumpMethodInfo {
token: method.token,
calls_virtualprotect,
calls_gethinstance,
calls_get_module,
calls_marshal_copy,
});
}
}
found
}
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
let result = detect_antidump(assembly);
for method_info in &result.methods {
findings.anti_dump_methods.push(method_info.token);
}
add_antidump_evidence(&result, score);
}
pub struct ConfuserExAntiDumpPass {
anti_dump_method_tokens: HashSet<Token>,
include_module_cctor: bool,
module_cctor_token: OnceLock<Option<Token>>,
}
impl Default for ConfuserExAntiDumpPass {
fn default() -> Self {
Self::new()
}
}
impl ConfuserExAntiDumpPass {
#[must_use]
pub fn new() -> Self {
Self {
anti_dump_method_tokens: HashSet::new(),
include_module_cctor: false,
module_cctor_token: OnceLock::new(),
}
}
#[must_use]
pub fn with_methods(tokens: impl IntoIterator<Item = Token>) -> Self {
let tokens: HashSet<_> = tokens.into_iter().collect();
let include_cctor = !tokens.is_empty();
Self {
anti_dump_method_tokens: tokens,
include_module_cctor: include_cctor,
module_cctor_token: OnceLock::new(),
}
}
fn get_module_cctor(&self, assembly: &CilObject) -> Option<Token> {
*self
.module_cctor_token
.get_or_init(|| assembly.types().module_cctor())
}
fn is_virtualprotect(method_name: &str) -> bool {
method_name.contains("VirtualProtect")
}
fn is_marshal_copy(method_name: &str) -> bool {
method_name.contains("Marshal") && method_name.contains("Copy")
}
fn is_gethinstance(method_name: &str) -> bool {
method_name.contains("Marshal") && method_name.contains("GetHINSTANCE")
}
fn neutralize_antidump(
ssa: &mut SsaFunction,
method_token: Token,
assembly: &CilObject,
changeset: &mut EventLog,
) {
for block in ssa.blocks_mut() {
for instr in block.instructions_mut() {
match instr.op() {
SsaOp::Call { dest, method, .. } | SsaOp::CallVirt { dest, method, .. } => {
let method_name = utils::get_type_name_from_token(assembly, method.token())
.unwrap_or_else(|| format!("{method}"));
let dest = *dest;
let method_ref = *method;
if Self::is_virtualprotect(&method_name) {
if let Some(dest_var) = dest {
instr.set_op(SsaOp::Const {
dest: dest_var,
value: ConstValue::from_bool(false),
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!(
"Neutralized VirtualProtect: {method_ref} -> false"
));
}
}
else if Self::is_marshal_copy(&method_name) {
let dummy_dest = dest.unwrap_or_else(SsaVarId::new);
instr.set_op(SsaOp::Const {
dest: dummy_dest,
value: ConstValue::Null,
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!("Neutralized Marshal.Copy: {method_ref}"));
}
else if Self::is_gethinstance(&method_name) {
if let Some(dest_var) = dest {
instr.set_op(SsaOp::Const {
dest: dest_var,
value: ConstValue::Null,
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!(
"Neutralized GetHINSTANCE: {method_ref} -> null"
));
}
}
}
_ => {}
}
}
}
}
}
impl SsaPass for ConfuserExAntiDumpPass {
fn name(&self) -> &'static str {
"ConfuserExAntiDump"
}
fn should_run(&self, method_token: Token, _ctx: &CompilerContext) -> bool {
if self.anti_dump_method_tokens.contains(&method_token) {
return true;
}
if self.include_module_cctor {
return true;
}
self.anti_dump_method_tokens.is_empty()
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> Result<bool> {
let is_target_method = self.anti_dump_method_tokens.contains(&method_token);
let is_module_cctor = if self.include_module_cctor && !is_target_method {
let cctor_token = self.get_module_cctor(assembly);
cctor_token == Some(method_token)
} else {
false
};
if !is_target_method && !is_module_cctor && !self.anti_dump_method_tokens.is_empty() {
return Ok(false);
}
let mut changes = EventLog::new();
Self::neutralize_antidump(ssa, method_token, assembly, &mut changes);
let changed = !changes.is_empty();
if changed {
ctx.events.merge(&changes);
}
Ok(changed)
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::obfuscators::confuserex::antidump::detect_antidump, CilObject,
ValidationConfig,
};
const SAMPLES_DIR: &str = "tests/samples/packers/confuserex";
#[test]
fn test_original_no_antidump() -> crate::Result<()> {
let path = format!("{}/original.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antidump(&assembly);
assert!(!result.is_detected(), "Original should have no anti-dump");
Ok(())
}
#[test]
fn test_normal_no_antidump() -> crate::Result<()> {
let path = format!("{}/mkaring_normal.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antidump(&assembly);
assert!(
!result.is_detected(),
"Normal preset should NOT have anti-dump"
);
Ok(())
}
#[test]
fn test_maximum_has_antidump() -> crate::Result<()> {
let path = format!("{}/mkaring_maximum.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antidump(&assembly);
eprintln!("Anti-dump methods found: {}", result.methods.len());
for m in &result.methods {
eprintln!(
" 0x{:08X}: VirtualProtect={}, GetHINSTANCE={}, get_Module={}, Marshal.Copy={}",
m.token.value(),
m.calls_virtualprotect,
m.calls_gethinstance,
m.calls_get_module,
m.calls_marshal_copy,
);
}
assert!(result.is_detected(), "Maximum preset should have anti-dump");
Ok(())
}
}