use std::{
any::Any,
collections::{HashMap, HashSet},
sync::Arc,
};
use log::debug;
use crate::{
analysis::{SsaFunction, SsaOp, SsaVarId},
cilassembly::CleanupRequest,
compiler::{PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::{
count_resolve_method_calli_sites, DelegateProxyResolutionPass, DelegateTypeInfo,
ReflectionDevirtualizationPass,
},
techniques::{Detection, Evidence, Technique, TechniqueCategory},
utils::is_method_named,
},
metadata::token::Token,
CilObject,
};
#[derive(Debug)]
pub struct DelegateProxyFindings {
pub delegate_types: HashMap<Token, DelegateTypeInfo>,
pub affected_methods: HashSet<Token>,
}
#[derive(Debug, Default)]
pub struct ReflectionFindings {
pub affected_methods: HashSet<Token>,
pub site_count: usize,
}
#[derive(Debug)]
pub struct CallIndirectionFindings {
pub delegate: DelegateProxyFindings,
pub reflection: ReflectionFindings,
}
fn traces_to_argument(ssa: &SsaFunction, var: SsaVarId) -> bool {
let mut current = var;
for _ in 0..20 {
if let Some(variable) = ssa.variable(current) {
if variable.origin().is_argument() {
return true;
}
}
if let Some(op) = ssa.get_definition(current) {
match op {
SsaOp::LoadArg { .. } => return true,
SsaOp::Copy { src, .. } => {
current = *src;
continue;
}
_ => return false,
}
}
if let Some((_block_idx, phi)) = ssa.find_phi_defining(current) {
let operands = phi.operands();
if operands.len() == 1 {
current = operands[0].value();
continue;
}
return false;
}
return false;
}
false
}
fn is_delegate_wrapper_ssa(ssa: &SsaFunction, assembly: &CilObject) -> bool {
for block in ssa.blocks() {
for instr in block.instructions() {
let SsaOp::CallVirt { method, args, .. } = instr.op() else {
continue;
};
let Some(method_name) = assembly.resolve_method_name(method.token()) else {
continue;
};
if method_name != "Invoke" {
continue;
}
let Some(&obj_var) = args.first() else {
continue;
};
if traces_to_argument(ssa, obj_var) {
return true;
}
}
}
false
}
fn has_delegate_proxy_calls(ssa: &SsaFunction, wrapper_methods: &HashMap<Token, Token>) -> bool {
for block in ssa.blocks() {
for instr in block.instructions() {
if let SsaOp::Call { method, .. } = instr.op() {
if wrapper_methods.contains_key(&method.token()) {
return true;
}
}
}
}
false
}
pub struct GenericDelegateProxy;
impl Technique for GenericDelegateProxy {
fn id(&self) -> &'static str {
"generic.delegates"
}
fn name(&self) -> &'static str {
"Delegate Proxy Resolution"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Call
}
fn detect(&self, _assembly: &CilObject) -> Detection {
Detection::new_empty()
}
fn detect_ssa(&self, ctx: &AnalysisContext, assembly: &CilObject) -> Detection {
let mut delegate_types: HashMap<Token, DelegateTypeInfo> = HashMap::new();
let mut wrapper_to_delegate: HashMap<Token, Token> = HashMap::new();
let mut delegate_count = 0usize;
let mut has_static_field = 0usize;
let mut has_static_method = 0usize;
let mut has_ssa = 0usize;
let mut is_wrapper = 0usize;
for type_entry in assembly.types().iter() {
let ty = type_entry.value();
if !ty.is_delegate() {
continue;
}
delegate_count += 1;
let Some(singleton_field_token) = ty
.fields
.iter()
.find(|(_, f)| f.flags.is_static())
.map(|(_, f)| f.token)
else {
continue;
};
has_static_field += 1;
let wrapper_method_token = ty.methods.iter().find_map(|(_, method_ref)| {
let method = method_ref.upgrade()?;
if !method.is_static() || method.is_cctor() || method.is_ctor() {
return None;
}
has_static_method += 1;
let ssa_ref = ctx.ssa_functions.get(&method.token);
if ssa_ref.is_none() {
debug!(
"Delegate detect: type {}.{} method 0x{:08X} ({}) has no SSA",
ty.namespace, ty.name, method.token.value(), method.name
);
return None;
}
has_ssa += 1;
let ssa = ssa_ref.unwrap();
if is_delegate_wrapper_ssa(ssa.value(), assembly) {
is_wrapper += 1;
Some(method.token)
} else {
debug!(
"Delegate detect: type {}.{} method 0x{:08X} ({}) SSA did not match wrapper pattern",
ty.namespace, ty.name, method.token.value(), method.name
);
None
}
});
let Some(wrapper_method_token) = wrapper_method_token else {
continue;
};
delegate_types.insert(
ty.token,
DelegateTypeInfo {
singleton_field_token,
wrapper_method_token,
},
);
wrapper_to_delegate.insert(wrapper_method_token, ty.token);
}
debug!(
"Delegate detect: {} delegate types, {} with static fields, {} with static methods, {} with SSA, {} matched wrapper pattern",
delegate_count, has_static_field, has_static_method, has_ssa, is_wrapper
);
let mut delegate_affected: HashSet<Token> = HashSet::new();
let mut reflection_affected: HashSet<Token> = HashSet::new();
let mut reflection_site_count = 0usize;
for entry in ctx.ssa_functions.iter() {
let method_token = *entry.key();
let ssa = entry.value();
if !delegate_types.is_empty() && has_delegate_proxy_calls(ssa, &wrapper_to_delegate) {
delegate_affected.insert(method_token);
}
let calli_count = count_resolve_method_calli_sites(ssa, assembly);
if calli_count > 0 {
reflection_affected.insert(method_token);
reflection_site_count += calli_count;
}
let api_count = count_reflection_api_calls(ssa, assembly);
if api_count > 0 {
reflection_affected.insert(method_token);
reflection_site_count += api_count;
}
}
let has_delegates = !delegate_types.is_empty() && !delegate_affected.is_empty();
let has_reflection = !reflection_affected.is_empty();
if !has_delegates && !has_reflection {
return Detection::new_empty();
}
let mut evidence = Vec::new();
if has_delegates {
let type_count = delegate_types.len();
let method_count = delegate_affected.len();
evidence.push(Evidence::Structural(format!(
"{type_count} delegate proxy types affecting {method_count} methods"
)));
}
if has_reflection {
let method_count = reflection_affected.len();
evidence.push(Evidence::Structural(format!(
"{reflection_site_count} reflection call indirection sites in {method_count} methods"
)));
}
let findings = CallIndirectionFindings {
delegate: DelegateProxyFindings {
delegate_types,
affected_methods: delegate_affected,
},
reflection: ReflectionFindings {
affected_methods: reflection_affected,
site_count: reflection_site_count,
},
};
Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Inline)
}
fn create_pass(
&self,
ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(combined) = detection.findings::<CallIndirectionFindings>() else {
return Vec::new();
};
let mut passes: Vec<Box<dyn SsaPass>> = Vec::new();
let delegate = &combined.delegate;
if !delegate.delegate_types.is_empty() {
if let Some(pool) = ctx.template_pool.get().cloned() {
passes.push(Box::new(DelegateProxyResolutionPass::new(
pool,
delegate.delegate_types.clone(),
delegate.affected_methods.clone(),
)));
}
}
let reflection = &combined.reflection;
if !reflection.affected_methods.is_empty() {
passes.push(Box::new(ReflectionDevirtualizationPass::with_methods(
reflection.affected_methods.clone(),
)));
}
passes
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let combined = detection.findings::<CallIndirectionFindings>()?;
let delegate = &combined.delegate;
if delegate.delegate_types.is_empty() {
return None;
}
let mut request = CleanupRequest::new();
for &type_token in delegate.delegate_types.keys() {
request.add_type(type_token);
}
Some(request)
}
}
fn count_reflection_api_calls(ssa: &SsaFunction, assembly: &CilObject) -> usize {
let mut count = 0;
for block in ssa.blocks() {
for instr in block.instructions() {
let (method_token, arg_count) = match instr.op() {
SsaOp::Call { method, args, .. } | SsaOp::CallVirt { method, args, .. } => {
(method.token(), args.len())
}
_ => continue,
};
let Some(name) = assembly.resolve_method_name(method_token) else {
continue;
};
if name == "Invoke" && arg_count == 3 {
if is_method_named(assembly, method_token, "MethodBase")
|| is_method_named(assembly, method_token, "MethodInfo")
{
count += 1;
continue;
}
}
if name.contains("CreateInstance")
&& is_method_named(assembly, method_token, "Activator")
{
count += 1;
continue;
}
if (name == "GetValue" || name == "SetValue")
&& is_method_named(assembly, method_token, "FieldInfo")
{
count += 1;
}
}
}
count
}
#[cfg(test)]
mod tests {
use crate::{
compiler::PassPhase,
deobfuscation::techniques::{
generic::delegates::{CallIndirectionFindings, GenericDelegateProxy},
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 = GenericDelegateProxy;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"GenericDelegateProxy 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::<CallIndirectionFindings>().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 = GenericDelegateProxy;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"GenericDelegateProxy should not detect anything in an Obfuscar sample"
);
}
#[test]
fn test_technique_metadata() {
let technique = GenericDelegateProxy;
assert_eq!(technique.id(), "generic.delegates");
assert_eq!(technique.name(), "Delegate Proxy Resolution");
assert_eq!(technique.category(), TechniqueCategory::Call);
assert!(technique.supersedes().is_empty());
}
#[test]
fn test_technique_ssa_phase() {
let technique = GenericDelegateProxy;
assert_eq!(
technique.ssa_phase(),
Some(PassPhase::Inline),
"GenericDelegateProxy should run in the Inline SSA phase"
);
}
}