use std::{
any::Any,
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::{
analysis::{CilTarget, SsaFunction, SsaOp, SsaVarId},
cilassembly::CleanupRequest,
compiler::{CompilerContext, PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::OpaqueFieldPredicatePass,
techniques::{Detection, Detections, Evidence, Technique, TechniqueCategory},
utils::build_def_map,
},
emulation::{EmValue, Hook, HookPriority, PreHookResult},
metadata::{tables::TableId, token::Token},
CilObject,
};
fn collect_predicate_static_fields(ssa: &SsaFunction) -> HashSet<Token> {
let defs = build_def_map(ssa);
let mut static_fields = HashSet::new();
for block in ssa.blocks() {
let Some(terminator) = block.control_terminator() else {
continue;
};
let condition = match terminator {
SsaOp::Branch { condition, .. } => *condition,
_ => continue,
};
let Some(SsaOp::LoadField { object, .. }) = defs.get(&condition) else {
continue;
};
let Some(SsaOp::LoadStaticField { field, .. }) = defs.get(object) else {
continue;
};
static_fields.insert(field.token());
}
static_fields
}
fn collect_field_stores(ssa: &SsaFunction) -> HashSet<Token> {
let mut stored = HashSet::new();
for block in ssa.blocks() {
for instr in block.instructions() {
match instr.op() {
SsaOp::StoreField { field, .. } | SsaOp::StoreStaticField { field, .. } => {
stored.insert(field.token());
}
_ => {}
}
}
}
stored
}
fn collect_initialization_only_methods(
ctx: &AnalysisContext,
assembly: &CilObject,
) -> HashSet<Token> {
let mut callers: HashMap<Token, HashSet<Token>> = HashMap::new();
let mut methods: Vec<Token> = Vec::new();
for entry in ctx.ssa_functions.iter() {
let caller = *entry.key();
methods.push(caller);
for block in entry.value().blocks() {
for instr in block.instructions() {
let callee = match instr.op() {
SsaOp::Call { method, .. }
| SsaOp::CallVirt { method, .. }
| SsaOp::LoadFunctionPtr { method, .. }
| SsaOp::LoadVirtFunctionPtr { method, .. } => method.token(),
SsaOp::NewObj { ctor, .. } => ctor.token(),
_ => continue,
};
callers.entry(callee).or_default().insert(caller);
}
}
}
let mut init_only: HashSet<Token> = methods
.iter()
.copied()
.filter(|token| is_static_constructor(assembly, *token))
.collect();
loop {
let mut grew = false;
for &method in &methods {
if init_only.contains(&method) {
continue;
}
let Some(method_callers) = callers.get(&method) else {
continue;
};
if !method_callers.is_empty() && method_callers.iter().all(|c| init_only.contains(c)) {
init_only.insert(method);
grew = true;
}
}
if !grew {
break;
}
}
init_only
}
fn is_static_constructor(assembly: &CilObject, token: Token) -> bool {
assembly
.resolve_method_name(token)
.is_some_and(|name| name == ".cctor")
}
fn collect_field_load_sources(ssa: &SsaFunction) -> HashSet<Token> {
let defs = build_def_map(ssa);
let mut static_fields = HashSet::new();
for block in ssa.blocks() {
for instr in block.instructions() {
let SsaOp::LoadField { object, .. } = instr.op() else {
continue;
};
let Some(SsaOp::LoadStaticField { field, .. }) = defs.get(object) else {
continue;
};
static_fields.insert(field.token());
}
}
static_fields
}
fn identify_sentinel_method(ssa: &SsaFunction) -> Option<Token> {
if ssa.block_count() > 2 {
return None;
}
let block = ssa.blocks().first()?;
let terminator = block.control_terminator()?;
let return_var = match terminator {
SsaOp::Return { value: Some(v) } => *v,
_ => return None,
};
let mut defs: HashMap<SsaVarId, &SsaOp> = HashMap::new();
for instr in block.instructions() {
if let Some(dest) = instr.op().dest() {
defs.insert(dest, instr.op());
}
}
let (left, right) = match defs.get(&return_var)? {
SsaOp::Ceq { left, right, .. } => (*left, *right),
_ => return None,
};
let field_token = match (defs.get(&left), defs.get(&right)) {
(Some(SsaOp::LoadStaticField { field, .. }), Some(SsaOp::Const { value, .. }))
if value.is_null() =>
{
field.token()
}
(Some(SsaOp::Const { value, .. }), Some(SsaOp::LoadStaticField { field, .. }))
if value.is_null() =>
{
field.token()
}
_ => return None,
};
if !field_token.is_table(TableId::Field) {
return None;
}
let real_instructions = block
.instructions()
.iter()
.filter(|i| !matches!(i.op(), SsaOp::Nop | SsaOp::Phi { .. }))
.count();
if real_instructions > 6 {
return None;
}
Some(field_token)
}
fn collect_sentinel_info(
ssa_functions: &dashmap::DashMap<Token, SsaFunction>,
) -> (HashMap<Token, Token>, HashSet<Token>) {
let mut sentinel_methods: HashMap<Token, Token> = HashMap::new();
for entry in ssa_functions.iter() {
if let Some(field_token) = identify_sentinel_method(entry.value()) {
sentinel_methods.insert(*entry.key(), field_token);
}
}
if sentinel_methods.is_empty() {
return (sentinel_methods, HashSet::new());
}
let mut call_site_methods: HashSet<Token> = HashSet::new();
for entry in ssa_functions.iter() {
let method_token = *entry.key();
if sentinel_methods.contains_key(&method_token) {
continue;
}
let has_sentinel_call = entry.value().blocks().iter().any(|block| {
block.instructions().iter().any(|instr| {
matches!(instr.op(), SsaOp::Call { method, .. }
if sentinel_methods.contains_key(&method.token()))
})
});
if has_sentinel_call {
call_site_methods.insert(method_token);
}
}
(sentinel_methods, call_site_methods)
}
#[derive(Debug)]
pub struct OpaquePredicateFindings {
pub affected_field_tokens: Vec<Token>,
pub affected_methods: Vec<Token>,
pub owning_type_tokens: Vec<Token>,
pub sentinel_methods: HashMap<Token, Token>,
}
pub struct GenericOpaquePredicates;
impl Technique for GenericOpaquePredicates {
fn id(&self) -> &'static str {
"generic.opaquefields"
}
fn name(&self) -> &'static str {
"Opaque Field Predicates"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Structure
}
fn detect(&self, _assembly: &CilObject) -> Detection {
Detection::new_empty()
}
fn detect_ssa(&self, ctx: &AnalysisContext, assembly: &CilObject) -> Detection {
let mut affected_fields: HashSet<Token> = HashSet::new();
let mut affected_methods: HashSet<Token> = HashSet::new();
let init_only = collect_initialization_only_methods(ctx, assembly);
let mut stored_fields: HashSet<Token> = HashSet::new();
for entry in ctx.ssa_functions.iter() {
if init_only.contains(entry.key()) {
continue;
}
stored_fields.extend(collect_field_stores(entry.value()));
}
for entry in ctx.ssa_functions.iter() {
let method_token = *entry.key();
let predicate_fields = collect_predicate_static_fields(entry.value());
let all_field_loads: HashSet<Token> = collect_field_load_sources(entry.value())
.into_iter()
.filter(|token| !stored_fields.contains(token))
.collect();
let combined: HashSet<Token> =
predicate_fields.union(&all_field_loads).copied().collect();
if !combined.is_empty() {
affected_methods.insert(method_token);
affected_fields.extend(combined);
}
}
let (sentinel_methods, sentinel_call_sites) = collect_sentinel_info(&ctx.ssa_functions);
affected_methods.extend(&sentinel_call_sites);
let sentinel_field_tokens: HashSet<Token> = sentinel_methods.values().copied().collect();
affected_fields.extend(&sentinel_field_tokens);
if affected_methods.is_empty() {
return Detection::new_empty();
}
let variant_a_fields: HashSet<Token> = affected_fields
.difference(&sentinel_field_tokens)
.copied()
.collect();
let mut resolved_fields: HashSet<Token> = HashSet::new();
for token in &variant_a_fields {
resolved_fields.insert(*token);
if token.is_table(TableId::MemberRef) {
if let Some(resolved) = assembly.resolver().resolve_field(*token) {
resolved_fields.insert(resolved);
}
}
}
let mut owning_types: HashSet<Token> = HashSet::new();
let registry = assembly.types();
for entry in registry.iter() {
let type_ref = entry.value();
let mut static_fields = type_ref
.fields
.iter()
.filter(|(_, field)| field.flags.is_static())
.peekable();
if static_fields.peek().is_none() {
continue;
}
if static_fields.all(|(_, field)| resolved_fields.contains(&field.token)) {
owning_types.insert(*entry.key());
}
}
let method_count = affected_methods.len();
let field_count = affected_fields.len();
let sentinel_count = sentinel_methods.len();
let mut evidence = vec![Evidence::Structural(format!(
"{method_count} methods with opaque predicates ({field_count} unique fields)"
))];
if sentinel_count > 0 {
evidence.push(Evidence::Structural(format!(
"{sentinel_count} sentinel null-check methods with {} call sites",
sentinel_call_sites.len()
)));
}
let findings = OpaquePredicateFindings {
affected_field_tokens: affected_fields.into_iter().collect(),
affected_methods: affected_methods.into_iter().collect(),
owning_type_tokens: owning_types.into_iter().collect(),
sentinel_methods,
};
Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
fn initialize(
&self,
ctx: &AnalysisContext,
assembly: &CilObject,
detection: &Detection,
_detections: &Detections,
) {
let Some(findings) = detection.findings::<OpaquePredicateFindings>() else {
return;
};
let mut resolved_fields: HashSet<Token> = HashSet::new();
for token in &findings.affected_field_tokens {
resolved_fields.insert(*token);
if token.is_table(TableId::MemberRef) {
if let Some(resolved) = assembly.resolver().resolve_field(*token) {
resolved_fields.insert(resolved);
}
}
}
let registry = assembly.types();
for entry in registry.iter() {
let type_ref = entry.value();
let owns_needed_field = type_ref.fields.iter().any(|(_, field)| {
field.flags.is_static() && resolved_fields.contains(&field.token)
});
if owns_needed_field {
if let Some(cctor) = type_ref.cctor() {
ctx.register_warmup_method(cctor, vec![]);
}
}
}
ctx.register_emulation_hook("generic.opaquefields", || {
Hook::new("bypass-tamper-verify-hash")
.match_name(
"System.Security.Cryptography",
"RSACryptoServiceProvider",
"VerifyHash",
)
.with_priority(HookPriority::HIGH)
.pre(|_ctx, _thread| PreHookResult::Bypass(Some(EmValue::I32(1))))
});
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Structure)
}
fn create_pass(
&self,
ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass<CilTarget, CompilerContext>>> {
let Some(pool) = ctx.template_pool.get().cloned() else {
return Vec::new();
};
let Some(findings) = detection.findings::<OpaquePredicateFindings>() else {
return Vec::new();
};
let needed_static_fields: HashSet<Token> =
findings.affected_field_tokens.iter().copied().collect();
let affected_methods: HashSet<Token> = findings.affected_methods.iter().copied().collect();
vec![Box::new(OpaqueFieldPredicatePass::new(
pool,
needed_static_fields,
affected_methods,
findings.sentinel_methods.clone(),
))]
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<OpaquePredicateFindings>()?;
let has_types = !findings.owning_type_tokens.is_empty();
let has_sentinel = !findings.sentinel_methods.is_empty();
if !has_types && !has_sentinel {
return None;
}
let mut request = CleanupRequest::new();
for &type_token in &findings.owning_type_tokens {
request.add_type(type_token);
}
request.add_methods(findings.sentinel_methods.keys().copied());
request.add_fields(findings.sentinel_methods.values().copied());
Some(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
compiler::PassPhase,
deobfuscation::techniques::{Technique, TechniqueCategory},
test::helpers::load_sample,
};
#[test]
fn detect_without_ssa_reports_nothing_by_contract() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = GenericOpaquePredicates;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"the non-SSA entry point defers to detect_ssa and must report nothing"
);
assert!(detection.evidence().is_empty());
assert!(detection.findings::<OpaquePredicateFindings>().is_none());
}
#[test]
fn test_technique_metadata() {
let technique = GenericOpaquePredicates;
assert_eq!(technique.id(), "generic.opaquefields");
assert_eq!(technique.name(), "Opaque Field Predicates");
assert_eq!(technique.category(), TechniqueCategory::Structure);
assert!(technique.supersedes().is_empty());
}
#[test]
fn test_technique_ssa_phase() {
let technique = GenericOpaquePredicates;
assert_eq!(
technique.ssa_phase(),
Some(PassPhase::Structure),
"GenericOpaquePredicates should run in the Structure SSA phase"
);
}
#[test]
fn static_constructors_are_identified_for_the_store_scan() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let cctors: Vec<_> = asm
.query_methods()
.static_constructors()
.into_iter()
.collect();
assert!(
!cctors.is_empty(),
"sample must contain at least one .cctor for this test to mean anything"
);
for cctor in &cctors {
assert!(
is_static_constructor(&asm, cctor.token),
"a .cctor must be excluded from the assembly-wide store scan"
);
}
let non_cctors: Vec<_> = asm
.query_methods()
.filter(|m| !m.is_cctor())
.into_iter()
.take(8)
.collect();
assert!(!non_cctors.is_empty());
for method in &non_cctors {
assert!(
!is_static_constructor(&asm, method.token),
"an ordinary method's stores must still invalidate a fold"
);
}
}
}