use std::collections::HashSet;
use rustc_hash::FxHashMap;
use crate::{
assembly::{Instruction, Operand},
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::{DeobfuscationFindings, NativeHelperInfo},
obfuscators::confuserex::{
antidebug, antidump, antitamper, constants, metadata, referenceproxy, resources,
},
},
file::pe::SectionTable,
metadata::{
method::MethodImplCodeType,
signatures::{parse_field_signature, TypeSignature},
streams::TablesHeader,
tables::{FieldRaw, FieldRvaRaw, TypeDefRaw, TypeRefRaw},
token::Token,
typesystem::CilTypeRef,
},
prelude::{FlowType, TableId},
CilObject,
};
pub fn detect_confuserex(assembly: &CilObject) -> (DetectionScore, DeobfuscationFindings) {
let score = DetectionScore::new();
let mut findings = DeobfuscationFindings::new();
metadata::detect(assembly, &score, &mut findings);
constants::detect(assembly, &score, &mut findings);
detect_native_helpers(assembly, &score, &mut findings);
antitamper::detect(assembly, &score, &mut findings);
antidebug::detect(assembly, &score, &mut findings);
antidump::detect(assembly, &score, &mut findings);
resources::detect(assembly, &score, &mut findings);
referenceproxy::detect(assembly, &score, &mut findings);
detect_artifact_sections(assembly, &score, &mut findings);
detect_constant_data_infrastructure(assembly, &score, &mut findings);
detect_protection_infrastructure_types(assembly, &score, &mut findings);
detect_infrastructure_fields(assembly, &score, &mut findings);
(score, findings)
}
fn detect_native_helpers(
assembly: &CilObject,
score: &DetectionScore,
findings: &mut DeobfuscationFindings,
) {
let decryptor_tokens: std::collections::HashSet<Token> =
findings.decryptor_methods.iter().map(|(_, t)| *t).collect();
let mut native_helpers: FxHashMap<Token, NativeHelperInfo> = FxHashMap::default();
if !decryptor_tokens.is_empty() {
for method_entry in assembly.methods() {
let method = method_entry.value();
if !decryptor_tokens.contains(&method.token) {
continue;
}
for instr in method.instructions() {
if instr.flow_type != FlowType::Call {
continue;
}
let Operand::Token(call_target) = &instr.operand else {
continue;
};
let Some(target_method) = assembly.method(call_target) else {
continue;
};
if !target_method
.impl_code_type
.contains(MethodImplCodeType::NATIVE)
{
continue;
}
let sig = &target_method.signature;
let is_int32_to_int32 = sig.return_type.base == TypeSignature::I4
&& sig.params.len() == 1
&& sig.params[0].base == TypeSignature::I4;
if !is_int32_to_int32 {
continue;
}
let Some(rva) = target_method.rva else {
continue;
};
native_helpers
.entry(*call_target)
.or_insert_with(|| NativeHelperInfo::new(*call_target, rva))
.add_caller(method.token);
}
}
}
let module_token = Token::from_parts(TableId::TypeDef, 1);
for method_entry in assembly.methods() {
let method = method_entry.value();
if native_helpers.contains_key(&method.token) {
continue;
}
if !method.impl_code_type.contains(MethodImplCodeType::NATIVE) {
continue;
}
let is_module_method = method
.declaring_type_rc()
.is_some_and(|t| t.token == module_token);
if !is_module_method {
continue;
}
let sig = &method.signature;
let is_int32_to_int32 = sig.return_type.base == TypeSignature::I4
&& sig.params.len() == 1
&& sig.params[0].base == TypeSignature::I4;
if !is_int32_to_int32 {
continue;
}
let Some(rva) = method.rva else {
continue;
};
native_helpers
.entry(method.token)
.or_insert_with(|| NativeHelperInfo::new(method.token, rva));
}
if !native_helpers.is_empty() {
let count = native_helpers.len();
let locations: boxcar::Vec<Token> = boxcar::Vec::new();
for key in native_helpers.keys() {
locations.push(*key);
}
for (_, info) in native_helpers {
findings.native_helpers.push(info);
}
score.add(DetectionEvidence::MetadataPattern {
name: format!("Native x86 helper methods ({count} with signature int32(int32))"),
locations,
confidence: (count * 25).min(40),
});
}
}
fn detect_artifact_sections(
assembly: &CilObject,
score: &DetectionScore,
findings: &mut DeobfuscationFindings,
) {
let file = assembly.file();
let sections = file.sections();
let mut artifact_section_names: HashSet<String> = HashSet::new();
for method in &assembly.query_methods().without_body() {
if let Some(rva) = method.rva {
if rva > 0 {
if let Some(section) = find_section_for_rva(sections, rva) {
if !section.name.starts_with(".text") {
artifact_section_names.insert(section.name.clone());
}
}
}
}
}
let standard_prefixes = [".text", ".rsrc", ".reloc", ".rdata", ".data", ".tls"];
for section in sections {
let is_standard = standard_prefixes
.iter()
.any(|prefix| section.name.starts_with(prefix));
if !is_standard {
artifact_section_names.insert(section.name.clone());
}
}
let section_list: Vec<String> = artifact_section_names.iter().cloned().collect();
for name in §ion_list {
findings.artifact_sections.push(name.clone());
}
if !section_list.is_empty() {
score.add_evidence(DetectionEvidence::ArtifactSections {
sections: section_list,
confidence: 5, });
}
}
fn find_section_for_rva(sections: &[SectionTable], rva: u32) -> Option<&SectionTable> {
sections.iter().find(|s| {
let section_end = s.virtual_address.saturating_add(s.virtual_size);
rva >= s.virtual_address && rva < section_end
})
}
fn detect_constant_data_infrastructure(
assembly: &CilObject,
score: &DetectionScore,
findings: &mut DeobfuscationFindings,
) {
let mut data_field_tokens: HashSet<Token> = HashSet::new();
let mut data_type_tokens: HashSet<Token> = HashSet::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
let instructions: Vec<&Instruction> = method.instructions().collect();
let len = instructions.len();
for i in 0..len.saturating_sub(1) {
let instr = instructions[i];
let next_instr = instructions[i + 1];
if instr.mnemonic != "ldtoken" {
continue;
}
let Operand::Token(operand_token) = &instr.operand else {
continue;
};
let token = Token::new(operand_token.value());
if !token.is_table(TableId::Field) {
continue;
}
if next_instr.flow_type != FlowType::Call {
continue;
}
let Operand::Token(call_operand) = &next_instr.operand else {
continue;
};
let call_token = Token::new(call_operand.value());
let is_init_array = call_token.is_table(TableId::MemberRef)
&& assembly
.refs_members()
.get(&call_token)
.is_some_and(|r| r.value().name == "InitializeArray");
if is_init_array {
data_field_tokens.insert(token);
if let Some(backing_type_token) = find_field_backing_type(assembly, token) {
data_type_tokens.insert(backing_type_token);
}
}
}
}
let obfuscator_type_rids: HashSet<u32> = findings
.obfuscator_type_tokens
.iter()
.map(|(_, t)| t.row())
.collect();
if let Some(tables) = assembly.tables() {
if let Some(fieldrva_table) = tables.table::<FieldRvaRaw>() {
for fieldrva in fieldrva_table {
if fieldrva.rva == 0 {
continue;
}
let field_rid = fieldrva.field;
let field_token = Token::from_parts(TableId::Field, field_rid);
if let Some(declaring_type_rid) = find_field_declaring_type(tables, field_rid) {
if obfuscator_type_rids.contains(&declaring_type_rid) {
data_field_tokens.insert(field_token);
let type_token = Token::from_parts(TableId::TypeDef, declaring_type_rid);
data_type_tokens.insert(type_token);
}
}
if let Some(backing_type) = find_field_backing_type(assembly, field_token) {
let has_class_layout = assembly.types().get(&backing_type).is_some_and(|t| {
t.class_size.get().is_some() || t.packing_size.get().is_some()
});
if has_class_layout {
data_field_tokens.insert(field_token);
data_type_tokens.insert(backing_type);
}
}
}
}
}
let field_count = data_field_tokens.len();
let type_count = data_type_tokens.len();
for token in data_field_tokens {
findings.constant_data_fields.push(token);
}
for token in data_type_tokens {
findings.constant_data_types.push(token);
}
if field_count > 0 || type_count > 0 {
score.add_evidence(DetectionEvidence::ConstantDataFields {
field_count,
type_count,
confidence: 15, });
}
}
fn find_field_backing_type(assembly: &CilObject, field_token: Token) -> Option<Token> {
let field_rid = field_token.row();
let tables = assembly.tables()?;
let field_table = tables.table::<FieldRaw>()?;
let field = field_table.get(field_rid)?;
let blobs = assembly.blob()?;
let sig_data = blobs.get(field.signature as usize).ok()?;
let field_sig = parse_field_signature(sig_data).ok()?;
match &field_sig.base {
TypeSignature::ValueType(type_token) => {
if type_token.is_table(TableId::TypeDef) {
Some(*type_token)
} else {
None
}
}
_ => None,
}
}
fn find_field_declaring_type(tables: &TablesHeader, field_rid: u32) -> Option<u32> {
let typedef_table = tables.table::<TypeDefRaw>()?;
let field_table = tables.table::<FieldRaw>()?;
let type_count = typedef_table.row_count;
let field_count = field_table.row_count;
for type_rid in 1..=type_count {
let typedef = typedef_table.get(type_rid)?;
let field_start = typedef.field_list;
let field_end = if type_rid < type_count {
typedef_table
.get(type_rid + 1)
.map_or(field_count + 1, |next| next.field_list)
} else {
field_count + 1
};
if field_rid >= field_start && field_rid < field_end {
return Some(type_rid);
}
}
None
}
fn detect_protection_infrastructure_types(
assembly: &CilObject,
score: &DetectionScore,
findings: &mut DeobfuscationFindings,
) {
if !findings.has_any_protection() && !findings.has_marker_attributes() {
return;
}
let types = assembly.types();
let already_marked: HashSet<u32> = findings
.obfuscator_type_tokens
.iter()
.map(|(_, t)| t.row())
.chain(findings.constant_data_types.iter().map(|(_, t)| t.row()))
.collect();
let mut infrastructure_types: Vec<Token> = Vec::new();
let module_token = Token::from_parts(TableId::TypeDef, 1);
let types_nested_in_module: Vec<Token> = types
.get(&module_token)
.map(|module_type| {
module_type
.nested_types
.iter()
.filter_map(|(_, type_ref)| type_ref.upgrade())
.map(|t| t.token)
.collect()
})
.unwrap_or_default();
for type_token in &types_nested_in_module {
let type_rid = type_token.row();
if type_rid == 1 {
continue;
}
if already_marked.contains(&type_rid) {
continue;
}
let Some(cil_type) = types.get(type_token) else {
continue;
};
if !cil_type.is_nested_internal() {
continue;
}
if cil_type.has_public_methods() {
continue;
}
infrastructure_types.push(*type_token);
}
let mut more_to_check = true;
while more_to_check {
more_to_check = false;
let infra_tokens: HashSet<Token> = infrastructure_types.iter().copied().collect();
for infra_token in &infra_tokens.clone() {
let Some(infra_type) = types.get(infra_token) else {
continue;
};
for (_, nested_ref) in infra_type.nested_types.iter() {
let Some(nested_type) = nested_ref.upgrade() else {
continue;
};
let nested_token = nested_type.token;
let nested_rid = nested_token.row();
if infra_tokens.contains(&nested_token) {
continue;
}
if already_marked.contains(&nested_rid) {
continue;
}
if !nested_type.is_public() && !nested_type.has_public_methods() {
infrastructure_types.push(nested_token);
more_to_check = true;
}
}
}
}
let infra_count = infrastructure_types.len();
for token in infrastructure_types {
findings.protection_infrastructure_types.push(token);
}
if infra_count > 0 {
score.add_evidence(DetectionEvidence::ProtectionInfrastructure {
count: infra_count,
description: format!("Found {infra_count} types as protection infrastructure"),
confidence: 20,
});
}
}
fn detect_infrastructure_fields(
assembly: &CilObject,
score: &DetectionScore,
findings: &mut DeobfuscationFindings,
) {
if !findings.has_any_protection() && !findings.has_marker_attributes() {
return;
}
let infrastructure_methods: HashSet<Token> = findings.all_protection_method_tokens();
let infrastructure_type_tokens: HashSet<Token> = findings
.protection_infrastructure_types
.iter()
.map(|(_, t)| *t)
.chain(findings.obfuscator_type_tokens.iter().map(|(_, t)| *t))
.collect();
let Some(tables) = assembly.tables() else {
return;
};
let Some(field_table) = tables.table::<FieldRaw>() else {
return;
};
let Some(typedef_table) = tables.table::<TypeDefRaw>() else {
return;
};
let Some(module_typedef) = typedef_table.get(1) else {
return;
};
let module_field_start = module_typedef.field_list;
let module_field_end = if typedef_table.row_count > 1 {
typedef_table
.get(2)
.map_or(field_table.row_count + 1, |next| next.field_list)
} else {
field_table.row_count + 1
};
let mut infrastructure_fields: Vec<Token> = Vec::new();
for field_rid in module_field_start..module_field_end {
let Some(field) = field_table.get(field_rid) else {
continue;
};
let field_token = Token::from_parts(TableId::Field, field_rid);
if findings
.constant_data_fields
.iter()
.any(|(_, t)| *t == field_token)
{
continue;
}
let is_infrastructure_type = is_infrastructure_field_type(assembly, &field);
if !is_infrastructure_type {
continue;
}
let only_infra_access = is_field_only_accessed_by_infrastructure(
assembly,
field_token,
&infrastructure_methods,
&infrastructure_type_tokens,
);
if only_infra_access {
infrastructure_fields.push(field_token);
}
}
let field_count = infrastructure_fields.len();
for token in infrastructure_fields {
findings.infrastructure_fields.push(token);
}
if field_count > 0 {
score.add_evidence(DetectionEvidence::ProtectionInfrastructure {
count: field_count,
description: format!(
"Found {field_count} fields in <Module> as protection infrastructure"
),
confidence: 15,
});
}
}
fn is_infrastructure_field_type(assembly: &CilObject, field: &FieldRaw) -> bool {
let Some(blobs) = assembly.blob() else {
return false;
};
let Ok(sig_data) = blobs.get(field.signature as usize) else {
return false;
};
let Ok(field_sig) = parse_field_signature(sig_data) else {
return false;
};
if let TypeSignature::SzArray(inner) = &field_sig.base {
if matches!(*inner.base, TypeSignature::U1) {
return true;
}
}
if let TypeSignature::Class(type_token) = &field_sig.base {
if type_token.is_table(TableId::TypeRef) {
let Some(tables) = assembly.tables() else {
return false;
};
let Some(typeref_table) = tables.table::<TypeRefRaw>() else {
return false;
};
let Some(strings) = assembly.strings() else {
return false;
};
if let Some(type_ref) = typeref_table.get(type_token.row()) {
let name = strings.get(type_ref.type_name as usize).unwrap_or_default();
let namespace = strings
.get(type_ref.type_namespace as usize)
.unwrap_or_default();
if name == "Assembly" && namespace == "System.Reflection" {
return true;
}
}
}
}
false
}
fn is_field_only_accessed_by_infrastructure(
assembly: &CilObject,
field_token: Token,
infrastructure_methods: &HashSet<Token>,
infrastructure_type_tokens: &HashSet<Token>,
) -> bool {
let cctor_token = assembly.methods().iter().find_map(|entry| {
let method = entry.value();
if method.is_cctor() {
if let Some(owner) = method.declaring_type_rc() {
if owner.name == "<Module>" {
return Some(method.token);
}
}
}
None
});
let mut accessed_by_any = false;
let mut accessed_by_non_infra = false;
for method_entry in assembly.methods() {
let method = method_entry.value();
let method_token_val = method.token;
let accesses_field = method.instructions().any(|instr| {
if instr.mnemonic == "ldsfld"
|| instr.mnemonic == "stsfld"
|| instr.mnemonic == "ldsflda"
{
if let Operand::Token(t) = &instr.operand {
return *t == field_token;
}
}
false
});
if accesses_field {
accessed_by_any = true;
let is_infra_method = infrastructure_methods.contains(&method_token_val)
|| Some(method_token_val) == cctor_token;
let is_in_infra_type = method
.declaring_type
.get()
.and_then(CilTypeRef::upgrade)
.is_some_and(|owner| infrastructure_type_tokens.contains(&owner.token));
if !is_infra_method && !is_in_infra_type {
accessed_by_non_infra = true;
break;
}
}
}
accessed_by_any && !accessed_by_non_infra
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::obfuscators::confuserex::detection::detect_confuserex, CilObject,
ValidationConfig,
};
#[test]
fn test_detect_suppress_ildasm_original() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/original.exe",
ValidationConfig::analysis(),
)?;
let (_, findings) = detect_confuserex(&assembly);
assert!(
!findings.has_suppress_ildasm(),
"Original should not have SuppressIldasm"
);
assert!(findings.suppress_ildasm_token.is_none());
Ok(())
}
#[test]
fn test_detect_suppress_ildasm_minimal() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_minimal.exe",
ValidationConfig::analysis(),
)?;
let (_, findings) = detect_confuserex(&assembly);
println!(
"Minimal: suppress_ildasm={}, token={:?}",
findings.has_suppress_ildasm(),
findings.suppress_ildasm_token
);
Ok(())
}
#[test]
fn test_detect_suppress_ildasm_maximum() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_maximum.exe",
ValidationConfig::analysis(),
)?;
let (score, findings) = detect_confuserex(&assembly);
println!(
"Maximum: suppress_ildasm={}, token={:?}, score={}",
findings.has_suppress_ildasm(),
findings.suppress_ildasm_token,
score.score()
);
assert!(
findings.has_suppress_ildasm(),
"Maximum protection should have SuppressIldasm"
);
assert!(findings.suppress_ildasm_token.is_some());
Ok(())
}
#[test]
fn test_detect_decryptor_methods() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_maximum.exe",
ValidationConfig::analysis(),
)?;
let (_, findings) = detect_confuserex(&assembly);
println!(
"Decryptor methods count: {}",
findings.decryptor_methods.count()
);
Ok(())
}
#[test]
fn test_detect_anti_debug() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_normal.exe",
ValidationConfig::analysis(),
)?;
let (_, findings) = detect_confuserex(&assembly);
println!(
"Anti-debug methods count: {}",
findings.anti_debug_methods.count()
);
assert!(
findings.anti_debug_methods.count() > 0,
"Normal protection should have anti-debug methods"
);
assert!(
findings.needs_anti_debug_patch(),
"Should indicate anti-debug patching is needed"
);
Ok(())
}
#[test]
fn test_detect_antitamper() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_maximum.exe",
ValidationConfig::analysis(),
)?;
let (score, findings) = detect_confuserex(&assembly);
println!(
"Anti-tamper methods: {}, encrypted methods: {}, score: {}",
findings.anti_tamper_methods.count(),
findings.encrypted_method_count,
score.score()
);
assert!(
findings.anti_tamper_methods.count() > 0,
"Maximum protection should have anti-tamper methods"
);
assert!(
findings.encrypted_method_count > 0,
"Maximum protection should have encrypted method bodies"
);
assert!(
findings.needs_anti_tamper_decryption(),
"Should indicate anti-tamper decryption is needed"
);
Ok(())
}
#[test]
fn test_detect_original_no_protections() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/original.exe",
ValidationConfig::analysis(),
)?;
let (score, findings) = detect_confuserex(&assembly);
assert_eq!(
score.score(),
0,
"Original should have zero detection score"
);
assert!(
!findings.has_any_protection(),
"Original should have no protections"
);
assert!(!findings.has_invalid_metadata());
assert!(!findings.has_marker_attributes());
assert!(!findings.has_suppress_ildasm());
assert_eq!(findings.anti_tamper_methods.count(), 0);
assert_eq!(findings.anti_debug_methods.count(), 0);
assert_eq!(findings.decryptor_methods.count(), 0);
Ok(())
}
#[test]
fn test_detect_protection_infrastructure_types() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_maximum.exe",
ValidationConfig::analysis(),
)?;
let (score, findings) = detect_confuserex(&assembly);
assert!(
findings.protection_infrastructure_types.count() >= 8,
"Should detect at least 8 infrastructure types nested in <Module>, found {}",
findings.protection_infrastructure_types.count()
);
assert!(
findings.decryptor_methods.count() >= 5,
"Should detect decryptor methods, found {}",
findings.decryptor_methods.count()
);
assert!(
findings.anti_tamper_methods.count() >= 2,
"Should detect anti-tamper methods, found {}",
findings.anti_tamper_methods.count()
);
assert!(
findings.constant_data_types.count() >= 2,
"Should detect constant data types, found {}",
findings.constant_data_types.count()
);
assert!(
score.score() >= 150,
"Maximum protection should have high score, found {}",
score.score()
);
Ok(())
}
}