use std::{
any::Any,
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::{
analysis::{SsaFunction, SsaOp, SsaVarId},
cilassembly::CleanupRequest,
compiler::{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.terminator_op() 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_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.terminator_op()?;
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();
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 = collect_field_load_sources(entry.value());
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 owns_field = type_ref.fields.iter().any(|(_, field)| {
field.flags.is_static() && resolved_fields.contains(&field.token)
});
if owns_field {
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>> {
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 crate::{
compiler::PassPhase,
deobfuscation::techniques::{
generic::opaquefields::{GenericOpaquePredicates, OpaquePredicateFindings},
Technique, TechniqueCategory,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_negative_confuserex_original() {
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(),
"GenericOpaquePredicates should not detect anything in a ConfuserEx original sample"
);
assert!(
detection.evidence().is_empty(),
"No evidence should be present for a non-obfuscated sample"
);
assert!(
detection.findings::<OpaquePredicateFindings>().is_none(),
"No findings should be present for a non-obfuscated sample"
);
}
#[test]
fn test_detect_negative_obfuscar_sample() {
let asm = load_sample("tests/samples/packers/obfuscar/2.2.50/obfuscar_strings_only.exe");
let technique = GenericOpaquePredicates;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"GenericOpaquePredicates should not detect anything in an Obfuscar sample"
);
}
#[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"
);
}
}