use std::{any::Any, collections::HashSet};
use crate::{
deobfuscation::{
context::AnalysisContext,
passes::count_resolve_method_calli_sites,
techniques::{Detection, Evidence, Technique, TechniqueCategory},
},
metadata::token::Token,
CilObject,
};
#[derive(Debug)]
pub struct CalliFindings {
pub method_tokens: HashSet<Token>,
pub site_count: usize,
}
pub struct BitMonoCalli;
impl Technique for BitMonoCalli {
fn id(&self) -> &'static str {
"bitmono.calli"
}
fn name(&self) -> &'static str {
"BitMono CallToCalli Reversal"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Structure
}
fn detect(&self, assembly: &CilObject) -> Detection {
let mut method_tokens = HashSet::new();
let mut site_count = 0usize;
for method_entry in assembly.methods() {
let method = method_entry.value();
let instructions: Vec<_> = method.instructions().collect();
let mut method_sites = 0usize;
let mut i = 0;
while i < instructions.len() {
if instructions[i].mnemonic == "calli" {
let window_start = i.saturating_sub(12);
let window = &instructions[window_start..i];
let has_ldtoken = window.iter().any(|instr| instr.mnemonic == "ldtoken");
let has_trampoline_api = window.iter().any(|instr| {
instr
.get_token_operand()
.and_then(|t| assembly.resolve_method_name(t))
.is_some_and(|n| {
n.contains("ResolveMethod") || n.contains("GetFunctionPointer")
})
});
if has_ldtoken && has_trampoline_api {
method_sites += 1;
}
}
i += 1;
}
if method_sites > 0 {
method_tokens.insert(method.token);
site_count += method_sites;
}
}
if site_count == 0 {
return Detection::new_empty();
}
let method_count = method_tokens.len();
Detection::new_detected(
vec![Evidence::BytecodePattern(format!(
"{site_count} CallToCalli conversion sites in {method_count} methods \
(ldtoken + ResolveMethod + calli)"
))],
Some(Box::new(CalliFindings {
method_tokens,
site_count,
}) as Box<dyn Any + Send + Sync>),
)
}
fn detect_ssa(&self, ctx: &AnalysisContext, assembly: &CilObject) -> Detection {
let mut method_tokens = HashSet::new();
let mut site_count = 0usize;
for entry in ctx.ssa_functions.iter() {
let count = count_resolve_method_calli_sites(entry.value(), assembly);
if count > 0 {
site_count += count;
method_tokens.insert(*entry.key());
}
}
if site_count == 0 {
return Detection::new_empty();
}
let method_count = method_tokens.len();
Detection::new_detected(
vec![Evidence::BytecodePattern(format!(
"{site_count} CallToCalli sites in {method_count} methods \
(SSA def-use chain confirmed)"
))],
Some(Box::new(CalliFindings {
method_tokens,
site_count,
}) as Box<dyn Any + Send + Sync>),
)
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{bitmono::BitMonoCalli, Technique},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/bitmono/0.39.0/bitmono_calltocalli.exe");
let technique = BitMonoCalli;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"BitMonoCalli should detect CallToCalli pattern in bitmono_calltocalli.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should include evidence"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = BitMonoCalli;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"BitMonoCalli should not detect CallToCalli in a non-BitMono assembly"
);
}
}