use crate::{
assembly::{opcodes, Operand},
metadata::{tables::TableId, token::Token},
CilObject,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtectionType {
AntiTamper,
Resources,
Constants,
}
#[derive(Debug, Clone)]
pub struct ProtectionCandidate {
pub token: Token,
pub score: u32,
pub reasons: Vec<&'static str>,
pub called_from_cctor: bool,
}
impl ProtectionCandidate {
fn new(token: Token) -> Self {
Self {
token,
score: 0,
reasons: Vec::new(),
called_from_cctor: false,
}
}
fn add_score(&mut self, points: u32, reason: &'static str) {
self.score += points;
self.reasons.push(reason);
}
}
#[derive(Debug)]
pub struct CandidateDetectionResult {
pub candidates: Vec<ProtectionCandidate>,
pub cctor_token: Option<Token>,
}
impl CandidateDetectionResult {
#[must_use]
pub fn best(&self) -> Option<&ProtectionCandidate> {
self.candidates.first()
}
pub fn iter(&self) -> impl Iterator<Item = &ProtectionCandidate> {
self.candidates.iter()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.candidates.is_empty()
}
}
pub fn find_candidates(
assembly: &CilObject,
protection: ProtectionType,
) -> CandidateDetectionResult {
let cctor_token = assembly.types().module_cctor();
let cctor_callees = cctor_token
.map(|t| extract_direct_callees(assembly, t))
.unwrap_or_default();
let mut candidates: Vec<ProtectionCandidate> = cctor_callees
.iter()
.filter_map(|&token| {
let method = assembly.method(&token)?;
if method.is_cctor() {
return None;
}
let mut candidate = ProtectionCandidate::new(token);
candidate.called_from_cctor = true;
candidate.add_score(10, "called from .cctor");
Some(candidate)
})
.collect();
for candidate in &mut candidates {
score_candidate(assembly, candidate, protection);
}
if candidates.is_empty() {
candidates = global_search(assembly, protection);
}
candidates.sort_by(|a, b| {
b.score
.cmp(&a.score)
.then_with(|| a.token.value().cmp(&b.token.value()))
});
candidates.retain(|c| c.score > 0);
CandidateDetectionResult {
candidates,
cctor_token,
}
}
fn extract_direct_callees(assembly: &CilObject, method_token: Token) -> Vec<Token> {
let mut callees = Vec::new();
let Some(method) = assembly.method(&method_token) else {
return callees;
};
let Some(cfg) = method.cfg() else {
return callees;
};
for node_id in cfg.node_ids() {
let Some(block) = cfg.block(node_id) else {
continue;
};
for instr in &block.instructions {
if instr.opcode == opcodes::CALL || instr.opcode == opcodes::CALLVIRT {
if let Operand::Token(token) = &instr.operand {
if token.is_table(TableId::MethodDef) && !callees.contains(token) {
callees.push(*token);
}
}
}
}
}
callees
}
fn score_candidate(
assembly: &CilObject,
candidate: &mut ProtectionCandidate,
protection: ProtectionType,
) {
let Some(method) = assembly.method(&candidate.token) else {
return;
};
let Some(cfg) = method.cfg() else {
return;
};
let mut call_targets: Vec<String> = Vec::new();
let mut has_delegate_creation = false;
let mut has_field_access = false;
for node_id in cfg.node_ids() {
let Some(block) = cfg.block(node_id) else {
continue;
};
for instr in &block.instructions {
match instr.opcode {
opcodes::CALL | opcodes::CALLVIRT => {
if let Operand::Token(token) = &instr.operand {
if let Some(name) = assembly.resolve_method_name(*token) {
call_targets.push(name);
}
}
}
opcodes::NEWOBJ => {
if let Operand::Token(token) = &instr.operand {
if let Some(name) = assembly.resolve_method_name(*token) {
if name.contains("EventHandler") || name.contains("ResolveEventHandler")
{
has_delegate_creation = true;
}
}
}
}
0x7E | 0x80 => {
has_field_access = true;
}
_ => {}
}
}
}
match protection {
ProtectionType::AntiTamper => {
score_antitamper(candidate, &call_targets);
}
ProtectionType::Resources => {
score_resources(
candidate,
&call_targets,
has_delegate_creation,
has_field_access,
);
}
ProtectionType::Constants => {
score_constants(candidate, &call_targets, has_field_access);
}
}
}
fn score_antitamper(candidate: &mut ProtectionCandidate, call_targets: &[String]) {
for target in call_targets {
if target.contains("GetHINSTANCE") {
candidate.add_score(5, "calls GetHINSTANCE");
}
if target.contains("VirtualProtect") {
candidate.add_score(5, "calls VirtualProtect");
}
if target.contains("get_Module") || target.contains("GetModuleHandle") {
candidate.add_score(3, "accesses module handle");
}
if target.contains("GetMethod") || target.contains("MethodBase") {
candidate.add_score(2, "uses reflection on methods");
}
if target.contains("Invoke") {
candidate.add_score(2, "uses method invocation");
}
}
}
fn score_resources(
candidate: &mut ProtectionCandidate,
call_targets: &[String],
has_delegate_creation: bool,
has_field_access: bool,
) {
for target in call_targets {
if target.contains("add_AssemblyResolve") || target.contains("AssemblyResolve") {
candidate.add_score(5, "registers AssemblyResolve handler");
}
if target.contains("add_ResourceResolve") || target.contains("ResourceResolve") {
candidate.add_score(5, "registers ResourceResolve handler");
}
if target.contains("Assembly") && target.contains("Load") {
candidate.add_score(5, "calls Assembly.Load");
}
if target.contains("Decompress") || target.contains("Lzma") || target.contains("Inflate") {
candidate.add_score(3, "has decompression call");
}
if target.contains("InitializeArray") {
candidate.add_score(3, "uses InitializeArray for embedded data");
}
}
if has_delegate_creation {
candidate.add_score(2, "creates event delegate");
}
if has_field_access {
candidate.add_score(1, "accesses static fields");
}
}
fn score_constants(
candidate: &mut ProtectionCandidate,
call_targets: &[String],
has_field_access: bool,
) {
for target in call_targets {
if target.contains("BlockCopy") || target.contains("Buffer") {
candidate.add_score(3, "uses Buffer operations");
}
if target.contains("BitConverter") {
candidate.add_score(2, "uses BitConverter");
}
if target.contains("GetValue") && target.contains("Array") {
candidate.add_score(2, "uses array indexing");
}
}
if has_field_access {
candidate.add_score(2, "accesses static fields for constants");
}
}
fn global_search(assembly: &CilObject, protection: ProtectionType) -> Vec<ProtectionCandidate> {
let mut candidates = Vec::new();
for method in &assembly
.query_methods()
.has_body()
.filter(|m| !m.is_cctor())
{
let mut candidate = ProtectionCandidate::new(method.token);
score_candidate(assembly, &mut candidate, protection);
if candidate.score > 0 {
candidates.push(candidate);
}
}
candidates
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ValidationConfig;
const MAXIMUM_PATH: &str = "tests/samples/packers/confuserex/mkaring_maximum.exe";
const ORIGINAL_PATH: &str = "tests/samples/packers/confuserex/original.exe";
#[test]
fn test_module_cctor() {
let assembly =
CilObject::from_path_with_validation(MAXIMUM_PATH, ValidationConfig::analysis())
.unwrap();
let cctor = assembly.types().module_cctor();
assert!(cctor.is_some(), "Should find <Module>.cctor");
}
#[test]
fn test_find_antitamper_candidates() {
let assembly =
CilObject::from_path_with_validation(MAXIMUM_PATH, ValidationConfig::analysis())
.unwrap();
let result = find_candidates(&assembly, ProtectionType::AntiTamper);
assert!(!result.is_empty(), "Should find anti-tamper candidates");
assert!(result.cctor_token.is_some(), "Should find .cctor");
let best = result.best().expect("Should have best candidate");
assert!(best.score >= 10, "Best candidate should have score >= 10");
assert!(
best.called_from_cctor,
"Best candidate should be called from .cctor"
);
println!("Anti-tamper candidates:");
for (i, c) in result.iter().enumerate() {
println!(
" {}. 0x{:08x} score={} reasons={:?}",
i + 1,
c.token.value(),
c.score,
c.reasons
);
}
}
#[test]
fn test_find_resource_candidates() {
let assembly =
CilObject::from_path_with_validation(MAXIMUM_PATH, ValidationConfig::analysis())
.unwrap();
let result = find_candidates(&assembly, ProtectionType::Resources);
println!("Resource candidates:");
for (i, c) in result.iter().enumerate() {
println!(
" {}. 0x{:08x} score={} reasons={:?}",
i + 1,
c.token.value(),
c.score,
c.reasons
);
}
}
#[test]
fn test_no_candidates_in_original() {
let assembly = CilObject::from_path(ORIGINAL_PATH).unwrap();
let antitamper = find_candidates(&assembly, ProtectionType::AntiTamper);
let resources = find_candidates(&assembly, ProtectionType::Resources);
let best_at = antitamper.best().map(|c| c.score).unwrap_or(0);
let best_res = resources.best().map(|c| c.score).unwrap_or(0);
assert!(
best_at < 10,
"Original should not have anti-tamper candidates (score={})",
best_at
);
assert!(
best_res < 10,
"Original should not have resource candidates (score={})",
best_res
);
}
}