use std::{any::Any, collections::HashSet, sync::Arc};
use crate::{
cilassembly::CleanupRequest,
compiler::PassPhase,
deobfuscation::{
context::AnalysisContext,
techniques::{
confuserex::{
hooks::{create_anti_tamper_stub_hook, create_lzma_hook},
statemachine::{
detect_cfgctx_semantics, find_call_sites, find_constants_initializer,
ConfuserExStateMachine,
},
tamper::AntiTamperFindings,
},
Detection, Detections, Evidence, Technique, TechniqueCategory,
},
},
metadata::{
signatures::TypeSignature,
tables::{FieldRvaRaw, MethodSpecRaw, TableId},
token::Token,
},
CilObject,
};
const MIN_CALL_SITES: usize = 3;
const LZMA_MAGIC: u8 = 0x5D;
#[derive(Debug)]
pub struct ConstantsFindings {
pub decryptor_tokens: Vec<Token>,
pub uses_cfg_mode: bool,
pub infrastructure_types: Vec<Token>,
pub initializer_token: Option<Token>,
pub methodspec_tokens: Vec<Token>,
pub data_field_tokens: Vec<Token>,
pub backing_type_tokens: Vec<Token>,
pub cfgctx_type_token: Option<Token>,
pub module_state_fields: Vec<Token>,
}
pub struct ConfuserExConstants;
impl Technique for ConfuserExConstants {
fn id(&self) -> &'static str {
"confuserex.constants"
}
fn name(&self) -> &'static str {
"ConfuserEx Constant Decryption"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn supersedes(&self) -> &[&'static str] {
&["generic.strings", "generic.constants"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
let mut decryptor_tokens = Vec::new();
let mut uses_cfg_mode = false;
let mut has_lzma_fieldrva = false;
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
let is_module_type = cil_type.is_module_type();
let is_obfuscated_name = !cil_type.name.is_ascii();
if !is_module_type && !is_obfuscated_name {
continue;
}
for i in 0..cil_type.methods.count() {
let Some(method_ref) = cil_type.methods.get(i) else {
continue;
};
let Some(method) = method_ref.upgrade() else {
continue;
};
if !method.is_static() {
continue;
}
let sig = &method.signature;
let is_string_decryptor = sig.param_count_generic == 0
&& sig.return_type.base == TypeSignature::String
&& sig.params.len() == 1
&& sig.params[0].base == TypeSignature::I4;
let is_generic_decryptor = sig.param_count_generic == 1
&& matches!(sig.return_type.base, TypeSignature::GenericParamMethod(0))
&& sig.params.len() == 1
&& sig.params[0].base == TypeSignature::I4;
if is_string_decryptor || is_generic_decryptor {
decryptor_tokens.push(method.token);
}
}
}
let mut data_field_tokens = Vec::new();
if let Some(tables) = assembly.tables() {
if let Some(fieldrva_table) = tables.table::<FieldRvaRaw>() {
let file = assembly.file();
for row in fieldrva_table {
if row.rva == 0 {
continue;
}
if let Ok(offset) = file.rva_to_offset(row.rva as usize) {
let data = file.data();
if offset < data.len() && data[offset] == LZMA_MAGIC {
has_lzma_fieldrva = true;
data_field_tokens.push(Token::from_parts(TableId::Field, row.field));
}
}
}
}
}
if !decryptor_tokens.is_empty() {
uses_cfg_mode = detect_cfgctx_semantics(assembly).is_some();
}
if decryptor_tokens.is_empty() && !has_lzma_fieldrva {
return Detection::new_empty();
}
let decryptor_set: HashSet<Token> = decryptor_tokens.iter().copied().collect();
let mut infrastructure_types = Vec::new();
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.is_module_type() {
continue;
}
let has_decryptor = (0..cil_type.methods.count()).any(|i| {
cil_type
.methods
.get(i)
.and_then(|r| r.upgrade())
.is_some_and(|m| decryptor_set.contains(&m.token))
});
if has_decryptor {
infrastructure_types.push(cil_type.token);
}
}
let mut methodspec_tokens = Vec::new();
if let Some(tables) = assembly.tables() {
if let Some(methodspec_table) = tables.table::<MethodSpecRaw>() {
for spec in methodspec_table {
let references_decryptor = if decryptor_set.contains(&spec.method.token) {
true
} else if spec.method.token.is_table(TableId::MemberRef) {
resolve_memberref_to_decryptor(assembly, spec.method.token, &decryptor_set)
.is_some()
} else {
false
};
if references_decryptor {
methodspec_tokens.push(spec.token);
}
}
}
}
let data_field_set: HashSet<Token> = data_field_tokens.iter().copied().collect();
let infra_set: HashSet<Token> = infrastructure_types.iter().copied().collect();
let mut backing_type_tokens = Vec::new();
if !data_field_set.is_empty() {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.is_module_type() {
continue;
}
if infra_set.contains(&cil_type.token) {
continue;
}
let field_count = cil_type.fields.iter().count();
let lzma_field_count = cil_type
.fields
.iter()
.filter(|(_, field)| data_field_set.contains(&field.token))
.count();
if field_count > 0 && lzma_field_count == field_count {
backing_type_tokens.push(cil_type.token);
}
}
}
let initializer_token = find_constants_initializer(assembly);
let module_state_fields = collect_module_state_fields(
assembly,
&decryptor_tokens,
initializer_token,
&data_field_set,
);
let cfgctx_type_token = if uses_cfg_mode {
detect_cfgctx_semantics(assembly).and_then(|s| s.type_token)
} else {
None
};
let mut evidence = Vec::new();
if !decryptor_tokens.is_empty() {
evidence.push(Evidence::Structural(format!(
"{} decryptor methods with T(int32) signature",
decryptor_tokens.len(),
)));
}
if has_lzma_fieldrva {
evidence.push(Evidence::Resource(
"LZMA-compressed FieldRVA data blob".to_string(),
));
}
if uses_cfg_mode {
evidence.push(Evidence::Structural(
"CFG mode: order-dependent constant decryption".to_string(),
));
}
let findings = ConstantsFindings {
decryptor_tokens,
uses_cfg_mode,
infrastructure_types,
initializer_token,
methodspec_tokens,
data_field_tokens,
backing_type_tokens,
cfgctx_type_token,
module_state_fields,
};
Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Value)
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<ConstantsFindings>()?;
let mut request = CleanupRequest::new();
for token in &findings.decryptor_tokens {
request.add_method(*token);
}
for token in &findings.infrastructure_types {
request.add_type(*token);
}
for token in &findings.methodspec_tokens {
request.add_methodspec(*token);
}
if let Some(init) = findings.initializer_token {
request.add_method(init);
}
for token in &findings.data_field_tokens {
request.add_field(*token);
}
for token in &findings.module_state_fields {
request.add_field(*token);
}
for token in &findings.backing_type_tokens {
request.add_type(*token);
}
if let Some(cfgctx) = findings.cfgctx_type_token {
request.add_type(cfgctx);
}
if request.has_deletions() {
Some(request)
} else {
None
}
}
fn initialize(
&self,
ctx: &AnalysisContext,
assembly: &CilObject,
detection: &Detection,
detections: &Detections,
) {
let Some(findings) = detection.findings::<ConstantsFindings>() else {
return;
};
if findings.decryptor_tokens.is_empty() {
return;
}
for token in &findings.decryptor_tokens {
ctx.decryptors.register(*token);
}
log::info!(
"Registered {} ConfuserEx decryptor methods (CFG mode: {})",
findings.decryptor_tokens.len(),
findings.uses_cfg_mode,
);
if let Some(init_method) = find_constants_initializer(assembly) {
log::info!(
"Found constants Initialize() method 0x{:08X} — using for targeted warmup",
init_method.value()
);
ctx.register_warmup_method(init_method, vec![]);
}
ctx.register_emulation_hook("confuserex.constants", create_lzma_hook);
if let Some(tamper_findings) =
detections.findings::<AntiTamperFindings>("confuserex.tamper")
{
if let Some(init_token) = tamper_findings.initializer_token {
let mut anti_tamper_tokens = HashSet::new();
anti_tamper_tokens.insert(init_token);
let count = anti_tamper_tokens.len();
ctx.register_emulation_hook("confuserex.antitamper", {
let tokens = anti_tamper_tokens.clone();
move || create_anti_tamper_stub_hook(tokens.clone())
});
log::info!(
"Registered stub hooks for {count} anti-tamper method(s) to prevent \
re-execution during warmup"
);
}
}
register_methodspec_mappings(ctx, assembly, &findings.decryptor_tokens);
if findings.uses_cfg_mode {
let semantics = detect_cfgctx_semantics(assembly);
if let Some(semantics) = semantics {
let call_sites = find_call_sites(assembly, &findings.decryptor_tokens);
let cfg_mode_methods: HashSet<Token> = call_sites
.iter()
.filter(|site| site.uses_statemachine)
.map(|site| site.caller)
.collect();
if !cfg_mode_methods.is_empty() {
let method_count = cfg_mode_methods.len();
let provider =
ConfuserExStateMachine::new(semantics, cfg_mode_methods.iter().copied());
ctx.register_statemachine_provider(Arc::new(provider));
log::info!(
"CFG mode detected: {method_count} methods require order-dependent \
decryption"
);
}
}
}
}
}
fn register_methodspec_mappings(
ctx: &AnalysisContext,
assembly: &CilObject,
decryptor_tokens: &[Token],
) {
let decryptor_set: HashSet<Token> = decryptor_tokens.iter().copied().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) {
resolve_memberref_to_decryptor(assembly, method_token, &decryptor_set)
} else {
None
};
if let Some(decryptor) = base_decryptor {
ctx.decryptors.map_methodspec(methodspec.token, decryptor);
}
}
}
fn collect_module_state_fields(
assembly: &CilObject,
decryptor_tokens: &[Token],
initializer_token: Option<Token>,
data_field_set: &HashSet<Token>,
) -> Vec<Token> {
let mut module_fields = HashSet::new();
for entry in assembly.types().iter() {
if entry.value().is_module_type() {
for field in entry.value().fields() {
module_fields.insert(field.token);
}
break;
}
}
if module_fields.is_empty() {
return Vec::new();
}
let mut state_fields = HashSet::new();
let method_tokens: Vec<Token> = decryptor_tokens
.iter()
.copied()
.chain(initializer_token)
.collect();
for method_token in &method_tokens {
let Some(method) = assembly.method(method_token) else {
continue;
};
for instr in method.instructions() {
if let Some(token) = instr.get_token_operand() {
if token.table() == 0x04
&& module_fields.contains(&token)
&& !data_field_set.contains(&token)
{
state_fields.insert(token);
}
}
}
}
state_fields.into_iter().collect()
}
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
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
confuserex::constants::{ConfuserExConstants, ConstantsFindings},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_constants.exe");
let technique = ConfuserExConstants;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"ConfuserExConstants should detect constants protection in mkaring_constants.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should have evidence"
);
let findings = detection
.findings::<ConstantsFindings>()
.expect("Should have ConstantsFindings");
assert!(
!findings.decryptor_tokens.is_empty(),
"Should have decryptor method tokens"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ConfuserExConstants;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExConstants should not detect constants protection in original.exe"
);
}
}