use std::any::Any;
use crate::{
cilassembly::CleanupRequest,
compiler::PassPhase,
deobfuscation::{
context::AnalysisContext,
techniques::{Detection, Detections, Evidence, Technique, TechniqueCategory},
},
metadata::{
signatures::TypeSignature, tables::TypeAttributes, token::Token, typesystem::wellknown,
},
CilObject,
};
#[derive(Debug)]
pub struct ObfuscarStringFindings {
pub helper_type: Token,
pub accessor_methods: Vec<Token>,
pub cctor_token: Option<Token>,
pub data_fields: Vec<Token>,
pub nested_types: Vec<Token>,
pub xor_key: Option<u8>,
}
pub struct ObfuscarStrings;
impl Technique for ObfuscarStrings {
fn id(&self) -> &'static str {
"obfuscar.strings"
}
fn name(&self) -> &'static str {
"Obfuscar String Hiding"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn supersedes(&self) -> &[&'static str] {
&["generic.strings"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if !is_obfuscar_helper_type(&cil_type.namespace) {
continue;
}
let mut has_method_6 = false;
let mut accessor_methods: 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 == wellknown::members::CCTOR {
cctor_token = Some(method.token);
} else if method.name == wellknown::members::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_methods.push(method.token);
}
}
}
if !has_method_6 || accessor_methods.is_empty() {
continue;
}
let mut data_fields: Vec<Token> = Vec::new();
let mut nested_types: Vec<Token> = Vec::new();
let mut evidence = Vec::new();
evidence.push(Evidence::TypePattern(format!(
"Obfuscar helper type '{}.{}' with method 6 and {} accessor methods",
cil_type.namespace,
cil_type.name,
accessor_methods.len()
)));
for (_, field) in cil_type.fields.iter() {
data_fields.push(field.token);
}
for (_, nested_ref) in cil_type.nested_types.iter() {
if let Some(nested_type) = nested_ref.upgrade() {
nested_types.push(nested_type.token);
for (_, field) in nested_type.fields.iter() {
data_fields.push(field.token);
}
if nested_type.flags.layout() == TypeAttributes::EXPLICIT_LAYOUT {
evidence.push(Evidence::Structural(
"Nested ExplicitLayout struct (FieldRVA data source)".to_string(),
));
}
}
}
let xor_key = cctor_token.and_then(|t| extract_xor_key_from_cctor(assembly, t));
if let Some(key) = xor_key {
evidence.push(Evidence::BytecodePattern(format!(
"XOR decryption loop in .cctor (key=0x{key:02X})"
)));
} else if cctor_token.is_some() {
log::warn!(
"Obfuscar: failed to extract XOR key from .cctor — \
.cctor layout may differ from expected xor/ldc.i4/xor pattern"
);
}
if accessor_methods.len() >= 5 {
evidence.push(Evidence::MetadataPattern(format!(
"{} per-string accessor methods",
accessor_methods.len()
)));
}
let nested_type_tokens: Vec<Token> = nested_types.to_vec();
let findings = ObfuscarStringFindings {
helper_type: cil_type.token,
accessor_methods,
cctor_token,
data_fields,
nested_types,
xor_key,
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
detection.cleanup_mut().add_type(cil_type.token);
for token in &nested_type_tokens {
detection.cleanup_mut().add_type(*token);
}
return detection;
}
Detection::new_empty()
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Value)
}
fn initialize(
&self,
ctx: &AnalysisContext,
_assembly: &CilObject,
detection: &Detection,
_detections: &Detections,
) {
let Some(findings) = detection.findings::<ObfuscarStringFindings>() else {
return;
};
if let Some(cctor_token) = findings.cctor_token {
log::info!(
"Obfuscar: registering helper .cctor (0x{:08X}) as warmup",
cctor_token.value()
);
ctx.register_warmup_method(cctor_token, vec![]);
}
for token in &findings.accessor_methods {
ctx.decryptors.register(*token);
}
log::info!(
"Obfuscar: registered {} string accessor method(s) for emulation",
findings.accessor_methods.len()
);
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<ObfuscarStringFindings>()?;
let mut request = CleanupRequest::new();
request.add_type(findings.helper_type);
request.add_types(findings.nested_types.iter().copied());
request.add_methods(findings.accessor_methods.iter().copied());
request.add_fields(findings.data_fields.iter().copied());
if request.has_deletions() {
Some(request)
} else {
None
}
}
}
fn is_obfuscar_helper_type(namespace: &str) -> bool {
let prefix = "<PrivateImplementationDetails>{";
namespace.starts_with(prefix) && namespace.len() > prefix.len()
}
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
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
obfuscar::strings::{is_obfuscar_helper_type, ObfuscarStringFindings, ObfuscarStrings},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive_obfuscar_strings_only() {
let asm = load_sample("tests/samples/packers/obfuscar/2.2.50/obfuscar_strings_only.exe");
let technique = ObfuscarStrings;
let detection = technique.detect(&asm);
assert!(
detection.is_detected(),
"ObfuscarStrings should detect string hiding in obfuscar_strings_only.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should include evidence"
);
let findings = detection
.findings::<ObfuscarStringFindings>()
.expect("Findings should be ObfuscarStringFindings");
assert!(
findings.helper_type.value() != 0,
"Helper type token should be non-zero"
);
assert!(
!findings.accessor_methods.is_empty(),
"Should have at least one accessor method"
);
assert!(
findings.cctor_token.is_some(),
"Should have a .cctor token for the helper type"
);
}
#[test]
fn test_detect_negative_confuserex_original() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ObfuscarStrings;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"ObfuscarStrings should not detect anything in a ConfuserEx original sample"
);
assert!(
detection.evidence().is_empty(),
"No evidence should be present for a non-Obfuscar sample"
);
assert!(
detection.findings::<ObfuscarStringFindings>().is_none(),
"No findings should be present for a non-Obfuscar sample"
);
}
#[test]
fn test_is_obfuscar_helper_type_positive() {
assert!(is_obfuscar_helper_type(
"<PrivateImplementationDetails>{12345678-1234-1234-1234-123456789abc}"
));
assert!(is_obfuscar_helper_type(
"<PrivateImplementationDetails>{ABCDEF00-0000-0000-0000-000000000000}"
));
}
#[test]
fn test_is_obfuscar_helper_type_negative() {
assert!(!is_obfuscar_helper_type(""));
assert!(!is_obfuscar_helper_type("SomeClass"));
assert!(!is_obfuscar_helper_type("<PrivateImplementationDetails>"));
assert!(!is_obfuscar_helper_type("<PrivateImplementationDetails>{"));
}
#[test]
fn test_technique_metadata() {
let technique = ObfuscarStrings;
assert_eq!(technique.id(), "obfuscar.strings");
assert_eq!(technique.name(), "Obfuscar String Hiding");
assert_eq!(
technique.category(),
crate::deobfuscation::techniques::TechniqueCategory::Value
);
assert_eq!(technique.supersedes(), &["generic.strings"]);
}
}