use crate::{
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
obfuscators::utils,
},
metadata::{signatures::TypeSignature, token::Token},
CilObject,
};
pub fn detect_obfuscar(assembly: &CilObject) -> (DetectionScore, DeobfuscationFindings) {
let score = DetectionScore::new();
let mut findings = DeobfuscationFindings::new();
utils::check_suppress_ildasm(assembly, &score, &mut findings, 10);
detect_string_hiding_infrastructure(assembly, &score, &mut findings);
check_null_parameter_names(assembly, &score);
(score, findings)
}
fn detect_string_hiding_infrastructure(
assembly: &CilObject,
score: &DetectionScore,
findings: &mut DeobfuscationFindings,
) {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if !is_obfuscar_helper_type(&cil_type.namespace, &cil_type.name) {
continue;
}
let mut has_method_6 = false;
let mut accessor_method_tokens: Vec<Token> = Vec::new();
let mut cctor_token: Option<Token> = None;
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
if method.name == "6" {
let sig = &method.signature;
let returns_string = matches!(sig.return_type.base, TypeSignature::String);
let has_3_params = sig.params.len() == 3;
if returns_string && has_3_params {
has_method_6 = true;
}
} else if method.name == ".cctor" {
cctor_token = Some(method.token);
} else if method.name == ".ctor" {
} else {
let sig = &method.signature;
let returns_string = matches!(sig.return_type.base, TypeSignature::String);
let no_params = sig.params.is_empty();
if returns_string && no_params {
accessor_method_tokens.push(method.token);
}
}
}
let has_accessors = !accessor_method_tokens.is_empty();
if has_method_6 && has_accessors {
score.add(DetectionEvidence::StructuralPattern {
description: format!(
"Obfuscar string hiding helper type '{}.{}' with method 6 and {} accessor methods",
cil_type.namespace,
cil_type.name,
accessor_method_tokens.len()
),
confidence: 60,
});
findings
.protection_infrastructure_types
.push(cil_type.token);
for (_, field) in cil_type.fields.iter() {
findings.infrastructure_fields.push(field.token);
}
for token in &accessor_method_tokens {
findings.decryptor_methods.push(*token);
}
if accessor_method_tokens.len() >= 5 {
score.add(DetectionEvidence::MetadataPattern {
name: "Obfuscar per-string accessor methods".to_string(),
locations: {
let locs = boxcar::Vec::new();
for token in &accessor_method_tokens {
locs.push(*token);
}
locs
},
confidence: 10,
});
}
if let Some(cctor) = cctor_token {
if let Some(xor_key) = extract_xor_key_from_cctor(assembly, cctor) {
score.add(DetectionEvidence::StructuralPattern {
description: format!(
"XOR decryption loop in .cctor (key=0x{:02X})",
xor_key
),
confidence: 10,
});
}
}
for (_, nested_ref) in cil_type.nested_types.iter() {
if let Some(nested_type) = nested_ref.upgrade() {
findings
.protection_infrastructure_types
.push(nested_type.token);
for (_, field) in nested_type.fields.iter() {
findings.infrastructure_fields.push(field.token);
}
if nested_type.flags & 0x10 != 0 {
score.add(DetectionEvidence::StructuralPattern {
description: "Nested ExplicitLayout struct (FieldRVA data source)"
.to_string(),
confidence: 5,
});
}
}
}
}
}
}
fn extract_xor_key_from_cctor(assembly: &CilObject, cctor_token: Token) -> Option<u8> {
let method = assembly.method(&cctor_token)?;
let instructions: Vec<_> = method.instructions().collect();
for window in instructions.windows(3) {
if window[0].mnemonic == "xor" && window[2].mnemonic == "xor" {
if let Some(val) = window[1].get_i32_operand() {
if (0..=255).contains(&val) {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
return Some(val as u8);
}
}
}
}
None
}
fn is_obfuscar_helper_type(namespace: &str, _name: &str) -> bool {
let prefix = "<PrivateImplementationDetails>{";
if !namespace.starts_with(prefix) {
return false;
}
namespace.len() > prefix.len()
}
fn check_null_parameter_names(assembly: &CilObject, score: &DetectionScore) {
let mut null_param_count = 0usize;
for method_entry in assembly.methods() {
let method = method_entry.value();
if method.name == ".ctor" || method.name == ".cctor" {
continue;
}
for (_, param) in method.params.iter() {
if param.sequence == 0 {
continue; }
match ¶m.name {
None => null_param_count += 1,
Some(name) if name.is_empty() => null_param_count += 1,
_ => {}
}
}
}
if null_param_count >= 5 {
score.add(DetectionEvidence::StructuralPattern {
description: format!("{null_param_count} null/empty parameter names"),
confidence: 5,
});
}
}
#[cfg(test)]
mod tests {
use crate::deobfuscation::obfuscators::obfuscar::detection::is_obfuscar_helper_type;
#[test]
fn test_obfuscar_helper_type_matching() {
assert!(is_obfuscar_helper_type(
"<PrivateImplementationDetails>{549CA519-2B22-4A20-871F-C40FA9335A42}",
"38CC15C4-E7D1-45A1-8841-82A260C99761"
));
assert!(is_obfuscar_helper_type(
"<PrivateImplementationDetails>{ABCDEF}",
"SomeName"
));
assert!(!is_obfuscar_helper_type(
"<PrivateImplementationDetails>",
"SomeName"
));
assert!(!is_obfuscar_helper_type(
"<PrivateImplementationDetails>{",
"SomeName"
));
assert!(!is_obfuscar_helper_type("SomeNamespace", "SomeType"));
assert!(!is_obfuscar_helper_type("", ""));
}
}