use std::{
any::Any,
collections::{HashMap, HashSet},
};
use log::{debug, trace};
use crate::{
analysis::SsaOp,
cilassembly::CleanupRequest,
compiler::PassPhase,
deobfuscation::{
context::AnalysisContext,
techniques::{
netreactor::find_resources_referenced_by_methods, Detection, Detections, Evidence,
Technique, TechniqueCategory,
},
utils::{
build_call_site_counts, exclude_cross_calling_candidates, filter_by_call_threshold,
},
},
emulation::EmValue,
metadata::{
signatures::{SignatureParameter, TypeSignature},
tables::TableId,
token::Token,
typesystem::wellknown,
},
CilObject,
};
#[derive(Debug)]
pub struct StringFindings {
pub decryptor_methods: HashSet<Token>,
pub encrypted_resource_tokens: Vec<Token>,
}
pub struct GenericStrings;
impl GenericStrings {
fn collect_candidates(&self, assembly: &CilObject) -> Vec<Token> {
let mut candidates = Vec::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
if !method.is_static() {
continue;
}
if !matches!(method.signature.return_type.base, TypeSignature::String) {
continue;
}
if method.name == wellknown::members::CCTOR || method.name == wellknown::members::CTOR {
continue;
}
let param_count = method.signature.params.len();
let matches_signature = match param_count {
1 => matches!(
method.signature.params[0].base,
TypeSignature::I4 | TypeSignature::U4 | TypeSignature::String
),
2 => method
.signature
.params
.iter()
.all(|p| matches!(p.base, TypeSignature::I4)),
_ => false,
};
if matches_signature {
candidates.push(method.token);
}
}
candidates
}
fn default_warmup_args(params: &[SignatureParameter]) -> Option<Vec<EmValue>> {
match params.len() {
1 => match params[0].base {
TypeSignature::I4 | TypeSignature::U4 => Some(vec![EmValue::I32(0)]),
TypeSignature::String => Some(vec![EmValue::Null]),
_ => None,
},
2 => {
if params.iter().all(|p| matches!(p.base, TypeSignature::I4)) {
Some(vec![EmValue::I32(0), EmValue::I32(0)])
} else {
None
}
}
_ => None,
}
}
}
impl Technique for GenericStrings {
fn id(&self) -> &'static str {
"generic.strings"
}
fn name(&self) -> &'static str {
"Generic String Decryptor Detection"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn detect(&self, assembly: &CilObject) -> Detection {
let candidates = self.collect_candidates(assembly);
if candidates.is_empty() {
return Detection::new_empty();
}
let counts = build_call_site_counts(assembly, candidates.iter().copied());
trace!("GenericStrings: {} candidates found", candidates.len());
for (token, count) in &counts {
if let Some(method) = assembly.method(token) {
trace!(
" candidate {}: {}({}) → string - calls: {}",
token,
method.name,
method
.signature
.params
.iter()
.map(|p| format!("{:?}", p.base))
.collect::<Vec<_>>()
.join(", "),
count
);
}
}
let decryptors = filter_by_call_threshold(candidates, &counts, 3);
let decryptors = exclude_cross_calling_candidates(decryptors, assembly);
if decryptors.is_empty() {
return Detection::new_empty();
}
let count = decryptors.len();
let encrypted_resource_tokens = collect_decryptor_resources(assembly, &decryptors);
let findings = StringFindings {
decryptor_methods: decryptors,
encrypted_resource_tokens,
};
Detection::new_detected(
vec![Evidence::Structural(format!(
"{count} potential string decryptor methods"
))],
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
fn detect_ssa(&self, ctx: &AnalysisContext, assembly: &CilObject) -> Detection {
let candidates = self.collect_candidates(assembly);
if candidates.is_empty() {
return Detection::new_empty();
}
let mut counts: HashMap<Token, usize> = candidates.iter().map(|&t| (t, 0)).collect();
let mut memberref_cache: HashMap<Token, Option<Token>> = HashMap::new();
for entry in ctx.ssa_functions.iter() {
for block in entry.value().blocks() {
for instr in block.instructions() {
let token = match instr.op() {
SsaOp::Call { method, .. } | SsaOp::CallVirt { method, .. } => {
method.token()
}
_ => continue,
};
if let Some(c) = counts.get_mut(&token) {
*c += 1;
continue;
}
if token.is_table(TableId::MemberRef) {
let resolved = memberref_cache
.entry(token)
.or_insert_with(|| assembly.resolver().resolve_memberref_method(token));
if let Some(resolved_token) = resolved {
if let Some(c) = counts.get_mut(resolved_token) {
*c += 1;
}
}
}
}
}
}
let decryptors = filter_by_call_threshold(candidates, &counts, 3);
let decryptors = exclude_cross_calling_candidates(decryptors, assembly);
if decryptors.is_empty() {
return Detection::new_empty();
}
let count = decryptors.len();
debug!(
"GenericStrings SSA detection: {} decryptors found ({} total call sites)",
count,
decryptors
.iter()
.map(|t| counts.get(t).unwrap_or(&0))
.sum::<usize>()
);
let encrypted_resource_tokens = collect_decryptor_resources(assembly, &decryptors);
let findings = StringFindings {
decryptor_methods: decryptors,
encrypted_resource_tokens,
};
Detection::new_detected(
vec![Evidence::Structural(format!(
"{count} string decryptor methods (SSA call-site analysis)"
))],
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
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::<StringFindings>() else {
return;
};
for token in &findings.decryptor_methods {
ctx.decryptors.register(*token);
}
let types = assembly.types();
for entry in types.iter() {
let cil_type = entry.value();
let owns_decryptor = cil_type.methods.iter().any(|(_, method_ref)| {
method_ref
.upgrade()
.is_some_and(|m| findings.decryptor_methods.contains(&m.token))
});
if owns_decryptor {
for (_, method_ref) in cil_type.methods.iter() {
if let Some(m) = method_ref.upgrade() {
if m.is_cctor() {
debug!(
"GenericStrings: registering warmup .cctor 0x{:08X} for {}.{}",
m.token.value(),
cil_type.namespace,
cil_type.name
);
ctx.register_warmup_method(m.token, vec![]);
}
}
}
}
}
for &token in &findings.decryptor_methods {
if let Some(method) = assembly.method(&token) {
let warmup_args = Self::default_warmup_args(&method.signature.params);
if let Some(args) = warmup_args {
debug!(
"GenericStrings: registering warmup call 0x{:08X} ({})",
token.value(),
method.name
);
ctx.register_warmup_method(token, args);
}
}
}
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<StringFindings>()?;
if findings.decryptor_methods.is_empty() {
return None;
}
let mut req = CleanupRequest::new();
for &token in &findings.decryptor_methods {
req.add_method(token);
}
for &token in &findings.encrypted_resource_tokens {
req.add_manifest_resource(token);
}
Some(req)
}
}
fn collect_decryptor_resources(assembly: &CilObject, decryptors: &HashSet<Token>) -> Vec<Token> {
let mut declaring_types: HashSet<Token> = HashSet::new();
for &decryptor in decryptors {
if let Some(method) = assembly.method(&decryptor) {
if let Some(parent) = method.declaring_type_rc() {
declaring_types.insert(parent.token);
}
}
}
let mut method_tokens: Vec<Token> = Vec::new();
let mut stack: Vec<Token> = declaring_types.into_iter().collect();
let mut visited: HashSet<Token> = stack.iter().copied().collect();
while let Some(type_token) = stack.pop() {
let Some(cil_type) = assembly.types().get(&type_token) else {
continue;
};
for method in cil_type.methods() {
method_tokens.push(method.token);
}
for (_, nested_ref) in cil_type.nested_types.iter() {
if let Some(nested) = nested_ref.upgrade() {
if visited.insert(nested.token) {
stack.push(nested.token);
}
}
}
}
if method_tokens.is_empty() {
return Vec::new();
}
find_resources_referenced_by_methods(assembly, &method_tokens)
}
#[cfg(test)]
mod tests {
use crate::{deobfuscation::techniques::Technique, test::helpers::load_sample};
#[test]
fn test_detect_no_panic_on_obfuscated() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_constants.exe");
let technique = super::GenericStrings;
let _detection = technique.detect(&asm);
}
#[test]
fn test_detect_negative() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = super::GenericStrings;
let detection = technique.detect(&asm);
assert!(!detection.is_detected());
}
}