mod antidebug;
mod antidump;
mod antitamper;
mod candidates;
mod constants;
mod detection;
mod hooks;
mod metadata;
mod referenceproxy;
mod resources;
mod utils;
mod cleanup;
pub use detection::detect_confuserex;
pub use hooks::{create_anti_tamper_stub_hook, create_lzma_hook};
pub use utils::find_encrypted_methods;
use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
use crate::{
cilassembly::{CilAssembly, CleanupRequest, GeneratorConfig},
compiler::{EventLog, InliningPass, SsaPass},
deobfuscation::{
config::EngineConfig, context::AnalysisContext, detection::DetectionScore,
findings::DeobfuscationFindings, obfuscators::Obfuscator,
passes::NativeMethodConversionPass,
},
emulation::TracingConfig,
metadata::{
tables::{MethodSpecRaw, TableId},
token::Token,
validation::ValidationConfig,
},
CilObject, Result,
};
pub struct ConfuserExObfuscator {
tracing: RwLock<Option<TracingConfig>>,
}
impl Default for ConfuserExObfuscator {
fn default() -> Self {
Self::new()
}
}
impl ConfuserExObfuscator {
#[must_use]
pub fn new() -> Self {
Self {
tracing: RwLock::new(None),
}
}
fn tracing(&self) -> Option<TracingConfig> {
self.tracing.read().ok().and_then(|t| t.clone())
}
fn register_methodspec_mappings(
ctx: &AnalysisContext,
assembly: &CilObject,
findings: &DeobfuscationFindings,
) {
let decryptor_set: HashSet<_> = findings
.decryptor_methods
.iter()
.map(|(_, token)| *token)
.collect();
let Some(tables) = assembly.tables() else {
return;
};
let Some(methodspec_table) = tables.table::<MethodSpecRaw>() else {
return;
};
for methodspec in methodspec_table {
let method_token = methodspec.method.token;
let base_decryptor = if decryptor_set.contains(&method_token) {
Some(method_token)
} else if method_token.is_table(TableId::MemberRef) {
Self::resolve_memberref_to_decryptor(assembly, method_token, &decryptor_set)
} else {
None
};
if let Some(decryptor) = base_decryptor {
let methodspec_token = methodspec.token;
ctx.decryptors.map_methodspec(methodspec_token, decryptor);
}
}
}
fn resolve_memberref_to_decryptor(
assembly: &CilObject,
memberref_token: Token,
decryptor_set: &HashSet<Token>,
) -> Option<Token> {
let memberref = assembly.member_ref(&memberref_token)?;
for decryptor_token in decryptor_set {
if let Some(method) = assembly.method(decryptor_token) {
if method.name == memberref.name {
return Some(*decryptor_token);
}
}
}
None
}
fn find_type_cctor(assembly: &CilObject, method_token: Token) -> Option<Token> {
let method = assembly.method(&method_token)?;
let cil_type = method.declaring_type_rc()?;
let result = cil_type
.query_methods()
.static_constructors()
.find_first()
.map(|m| m.token);
result
}
fn run_deobfuscation(
&self,
assembly: CilObject,
events: &mut EventLog,
findings: &mut DeobfuscationFindings,
) -> Result<CilObject> {
let mut current = assembly;
if findings.needs_resource_decryption() {
let (returned, result) = resources::decrypt_resources(current, events)?;
current = returned;
if result.has_assemblies() {
events.info(format!(
"Extracted {} embedded assemblies from resource protection",
result.assembly_count()
));
}
}
let had_anti_tamper = findings.needs_anti_tamper_decryption();
if had_anti_tamper {
current = antitamper::decrypt_bodies(current, events, self.tracing())?;
}
if had_anti_tamper {
let preserved_name = findings.obfuscator_name.take();
let preserved_anti_tamper: Vec<Token> = findings
.anti_tamper_methods
.iter()
.map(|(_, &t)| t)
.collect();
let (_, mut updated) = detection::detect_confuserex(¤t);
let native_count = updated.native_helpers.count();
let decryptor_count = updated.decryptor_methods.count();
updated.obfuscator_name = preserved_name;
for token in preserved_anti_tamper {
updated.anti_tamper_methods.push(token);
}
*findings = updated;
events.info(format!(
"Re-ran detection after anti-tamper decryption: {} decryptors, {} native helpers, {} anti-tamper methods preserved",
decryptor_count, native_count, findings.anti_tamper_methods.count()
));
}
if findings.needs_metadata_fix() {
current = metadata::fix_invalid_metadata(current)?;
events.info("Fixed invalid ConfuserEx metadata markers (0x7fff7fff)");
}
if let Some(token) = findings.suppress_ildasm_token {
current = metadata::remove_suppress_ildasm(¤t, token)?;
events.info(format!(
"Removed SuppressIldasmAttribute (0x{:08x})",
token.value()
));
}
if findings.has_marker_attributes() {
let tokens: Vec<_> = findings
.marker_attribute_tokens
.iter()
.map(|(_, t)| *t)
.collect();
if !tokens.is_empty() {
let count = tokens.len();
current = metadata::remove_confuser_attributes(current, tokens)?;
events.info(format!("Removed {count} ConfuserEx marker attributes"));
}
}
if findings.needs_native_conversion() {
current = Self::convert_native_helpers(¤t, findings, events)?;
}
Ok(current)
}
fn convert_native_helpers(
assembly: &CilObject,
findings: &DeobfuscationFindings,
events: &mut EventLog,
) -> Result<CilObject> {
let file = assembly.file();
let mut cil_assembly = CilAssembly::from_bytes_with_validation(
file.data().to_vec(),
ValidationConfig::analysis(),
)?;
let mut converter = NativeMethodConversionPass::new();
for (_, helper) in &findings.native_helpers {
converter.register_target(helper.token);
}
let stats = converter.run(&mut cil_assembly, file)?;
if stats.failed > 0 {
events.warn(format!(
"Converted {}/{} native x86 methods to CIL (failures: {})",
stats.converted,
stats.converted + stats.failed,
stats.errors.join(", ")
));
} else {
events.info(format!(
"Converted {} native x86 method(s) to CIL",
stats.converted
));
}
if stats.converted > 0 {
cil_assembly
.into_cilobject_with(ValidationConfig::analysis(), GeneratorConfig::default())
} else {
CilAssembly::from_bytes_with_validation(
file.data().to_vec(),
ValidationConfig::analysis(),
)?
.into_cilobject_with(ValidationConfig::analysis(), GeneratorConfig::default())
}
}
}
impl Obfuscator for ConfuserExObfuscator {
fn id(&self) -> String {
"confuserex".to_string()
}
fn name(&self) -> String {
"ConfuserEx".to_string()
}
fn detect(&self, assembly: &CilObject, findings: &mut DeobfuscationFindings) -> DetectionScore {
let (score, detected) = detection::detect_confuserex(assembly);
*findings = detected;
score
}
fn passes(&self, findings: &DeobfuscationFindings) -> Vec<Box<dyn SsaPass>> {
let mut passes: Vec<Box<dyn SsaPass>> = Vec::new();
if findings.needs_anti_debug_patch() {
let anti_debug_tokens: Vec<_> = findings
.anti_debug_methods
.iter()
.map(|(_, t)| *t)
.collect();
passes.push(Box::new(antidebug::ConfuserExAntiDebugPass::with_methods(
anti_debug_tokens,
)));
}
if findings.needs_anti_dump_patch() {
let anti_dump_tokens: Vec<_> =
findings.anti_dump_methods.iter().map(|(_, t)| *t).collect();
passes.push(Box::new(antidump::ConfuserExAntiDumpPass::with_methods(
anti_dump_tokens,
)));
}
if findings.needs_proxy_inlining() {
passes.push(Box::new(InliningPass::new(0, true)));
}
passes
}
fn deobfuscate(
&self,
assembly: CilObject,
events: &mut EventLog,
findings: &mut DeobfuscationFindings,
) -> Result<CilObject> {
self.run_deobfuscation(assembly, events, findings)
}
fn set_config(&self, config: &EngineConfig) {
if let Ok(mut tracing) = self.tracing.write() {
tracing.clone_from(&config.tracing);
}
}
fn initialize_context(
&self,
ctx: &AnalysisContext,
assembly: &CilObject,
findings: &DeobfuscationFindings,
) {
let decryptor_count = findings.decryptor_methods.count();
if decryptor_count > 0 {
if let Some(init_method) = constants::find_constants_initializer(assembly) {
ctx.events.info(format!(
"Found constants Initialize() method 0x{:08X} - using for targeted warmup",
init_method.value()
));
ctx.register_warmup_method(init_method);
}
for (_, token) in &findings.decryptor_methods {
ctx.decryptors.register(*token);
}
Self::register_methodspec_mappings(ctx, assembly, findings);
ctx.events.info(format!(
"Registered {decryptor_count} ConfuserEx decryptor method(s)"
));
ctx.register_emulation_hook(hooks::create_lzma_hook);
if findings.anti_tamper_methods.count() > 0 {
let anti_tamper_tokens: std::collections::HashSet<Token> = findings
.anti_tamper_methods
.iter()
.map(|(_, &t)| t)
.collect();
let count = anti_tamper_tokens.len();
ctx.register_emulation_hook({
let tokens = anti_tamper_tokens.clone();
move || hooks::create_anti_tamper_stub_hook(tokens.clone())
});
ctx.events.info(format!(
"Registered stub hooks for {count} anti-tamper method(s) to prevent re-execution during warmup"
));
}
}
if let Some(ref provider) = findings.statemachine_provider {
let method_count = provider.methods().len();
ctx.register_statemachine_provider(Arc::clone(provider));
ctx.events.info(format!(
"CFG mode detected: {method_count} methods require order-dependent decryption"
));
}
}
fn cleanup_request(
&self,
assembly: &CilObject,
ctx: &AnalysisContext,
findings: &DeobfuscationFindings,
) -> Result<Option<CleanupRequest>> {
Ok(cleanup::build_request(assembly, ctx, findings))
}
fn supported_versions(&self) -> &[&str] {
&["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"]
}
fn description(&self) -> &'static str {
"ConfuserEx open-source obfuscator - supports name obfuscation, control flow, string encryption, anti-tamper, and more"
}
}
#[cfg(test)]
mod tests {
use crate::{
compiler::EventLog,
deobfuscation::{
findings::DeobfuscationFindings,
obfuscators::{confuserex::ConfuserExObfuscator, Obfuscator},
},
CilObject, Result, ValidationConfig,
};
#[test]
fn test_detect_confuserex_samples() -> Result<()> {
let obfuscator = ConfuserExObfuscator::new();
let obfuscated_samples = [
"tests/samples/packers/confuserex/mkaring_minimal.exe",
"tests/samples/packers/confuserex/mkaring_normal.exe",
"tests/samples/packers/confuserex/mkaring_maximum.exe",
"tests/samples/packers/confuserex/mkaring_constants.exe",
"tests/samples/packers/confuserex/mkaring_controlflow.exe",
"tests/samples/packers/confuserex/mkaring_resources.exe",
];
for path in obfuscated_samples {
let assembly =
CilObject::from_path_with_validation(path, ValidationConfig::analysis())?;
let mut findings = DeobfuscationFindings::new();
let score = obfuscator.detect(&assembly, &mut findings);
assert!(
score.score() > 0,
"{}: Should detect ConfuserEx (score: {}, evidence: {})",
path,
score.score(),
score.evidence_summary()
);
println!("{}: score={}, evidence:", path, score.score());
for evidence in score.evidence() {
println!(" - {:?}", evidence);
}
}
let assembly = CilObject::from_path("tests/samples/packers/confuserex/original.exe")?;
let mut findings = DeobfuscationFindings::new();
let score = obfuscator.detect(&assembly, &mut findings);
println!(
"original.exe: score={}, evidence={}",
score.score(),
score.evidence_summary()
);
assert_eq!(
score.score(),
0,
"Original should not be detected as ConfuserEx"
);
Ok(())
}
#[test]
fn test_detect_populates_findings() -> Result<()> {
let obfuscator = ConfuserExObfuscator::new();
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_normal.exe",
ValidationConfig::analysis(),
)?;
let mut findings = DeobfuscationFindings::new();
let _score = obfuscator.detect(&assembly, &mut findings);
assert!(
findings.has_marker_attributes(),
"Normal protection should have marker attributes"
);
assert!(
findings.decryptor_methods.count() > 0,
"Normal protection should have decryptor methods"
);
Ok(())
}
#[test]
fn test_deobfuscate_with_findings() -> Result<()> {
let obfuscator = ConfuserExObfuscator::new();
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_normal.exe",
ValidationConfig::analysis(),
)?;
let mut findings = DeobfuscationFindings::new();
let _score = obfuscator.detect(&assembly, &mut findings);
let mut events = EventLog::new();
let result = obfuscator.deobfuscate(assembly, &mut events, &mut findings);
assert!(result.is_ok());
Ok(())
}
}