use crate::{
assembly::{Instruction, Operand},
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
},
metadata::token::Token,
prelude::FlowType,
CilObject,
};
const MAX_PROXY_INSTRUCTIONS: usize = 10;
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
let entry_point_token = assembly.cor20header().entry_point_token;
let entry_token = if entry_point_token != 0 {
Some(Token::new(entry_point_token))
} else {
None
};
let mut proxy_count = 0usize;
let locations: boxcar::Vec<Token> = boxcar::Vec::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
if method.is_ctor() || method.is_cctor() {
continue;
}
if entry_token.is_some_and(|t| t == method.token) {
continue;
}
let instructions: Vec<&Instruction> = method.instructions().collect();
if instructions.len() > MAX_PROXY_INSTRUCTIONS {
continue;
}
if instructions.is_empty() {
continue;
}
if is_mild_proxy(&instructions) || is_strong_proxy(&instructions) {
findings.proxy_methods.push(method.token);
locations.push(method.token);
proxy_count += 1;
}
}
if proxy_count > 0 {
score.add(DetectionEvidence::BytecodePattern {
name: format!("ReferenceProxy methods ({proxy_count} call forwarders)"),
locations,
confidence: (proxy_count * 5).min(30),
});
}
}
fn is_mild_proxy(instructions: &[&Instruction]) -> bool {
if instructions.len() < 2 {
return false;
}
let last = instructions.last().unwrap();
if last.mnemonic != "ret" {
return false;
}
let call_instr = instructions[instructions.len() - 2];
if call_instr.mnemonic != "call" || call_instr.flow_type != FlowType::Call {
return false;
}
if !matches!(call_instr.operand, Operand::Token(_)) {
return false;
}
for instr in &instructions[..instructions.len() - 2] {
if !instr.mnemonic.starts_with("ldarg") {
return false;
}
}
true
}
fn is_strong_proxy(instructions: &[&Instruction]) -> bool {
if instructions.len() < 3 {
return false;
}
let first = instructions[0];
if first.mnemonic != "ldsfld" {
return false;
}
let last = instructions.last().unwrap();
if last.mnemonic != "ret" {
return false;
}
let call_instr = instructions[instructions.len() - 2];
if call_instr.mnemonic != "callvirt" || call_instr.flow_type != FlowType::Call {
return false;
}
if !matches!(call_instr.operand, Operand::Token(_)) {
return false;
}
for instr in &instructions[1..instructions.len() - 2] {
if !instr.mnemonic.starts_with("ldarg") {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::obfuscators::confuserex::{detection::detect_confuserex, referenceproxy},
CilObject, ValidationConfig,
};
#[test]
fn test_no_proxy_in_original() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/original.exe",
ValidationConfig::analysis(),
)?;
let (score, mut findings) = detect_confuserex(&assembly);
referenceproxy::detect(&assembly, &score, &mut findings);
println!("Original proxy count: {}", findings.proxy_methods.count());
Ok(())
}
#[test]
fn test_proxy_in_normal() -> crate::Result<()> {
let assembly = CilObject::from_path_with_validation(
"tests/samples/packers/confuserex/mkaring_normal.exe",
ValidationConfig::analysis(),
)?;
let (score, mut findings) = detect_confuserex(&assembly);
referenceproxy::detect(&assembly, &score, &mut findings);
println!("Normal proxy count: {}", findings.proxy_methods.count());
assert!(
findings.proxy_methods.count() > 0,
"Normal preset should have ReferenceProxy methods"
);
assert!(
findings.needs_proxy_inlining(),
"Should indicate proxy inlining is needed"
);
Ok(())
}
}