use std::{any::Any, sync::Arc};
use crate::{
compiler::{PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::netreactor::TokenResolverPass,
techniques::{netreactor::helpers, Detection, Evidence, Technique, TechniqueCategory},
},
metadata::token::Token,
CilObject,
};
#[derive(Debug)]
pub struct AntiTampFindings {
pub init_method_token: Token,
pub runtime_type_token: Option<Token>,
pub purely_injected_cctors: Vec<Token>,
pub modified_cctors: Vec<Token>,
pub guid_module_type_tokens: Vec<Token>,
pub token_resolver_accessor_tokens: Vec<Token>,
pub token_resolver_type_token: Option<Token>,
pub runtime_resource_tokens: Vec<Token>,
}
pub struct NetReactorAntiTamp;
impl Technique for NetReactorAntiTamp {
fn id(&self) -> &'static str {
"netreactor.antitamp"
}
fn name(&self) -> &'static str {
".NET Reactor Anti-Tamper Init Removal"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Neutralization
}
fn requires(&self) -> &[&'static str] {
&["netreactor.antitrial"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
let has_module_trial = helpers::find_trial_checks(assembly)
.iter()
.any(|t| t.is_on_module_type);
if !has_module_trial {
return Detection::new_empty();
}
let Some(fan_in) = helpers::find_cctor_fan_in_target(assembly) else {
return Detection::new_empty();
};
let guid_module_type_tokens = helpers::find_nr_guid_module_containers(assembly);
let private_impl_containers = helpers::find_nr_private_impl_containers(assembly);
if guid_module_type_tokens.is_empty() && private_impl_containers.is_empty() {
return Detection::new_empty();
}
let init_method_token = fan_in.target_token;
let runtime_type_token = assembly
.method(&init_method_token)
.and_then(|m| m.declaring_type_rc())
.map(|t| t.token);
let classification =
helpers::classify_injected_cctors(assembly, init_method_token, &fan_in.calling_cctors);
let resolver = helpers::find_nr_token_resolver(assembly);
let mut accessor_tokens: Vec<Token> = Vec::new();
let resolver_type_token = resolver.as_ref().map(|r| {
accessor_tokens.extend(r.type_handle_accessors.iter().copied());
accessor_tokens.extend(r.field_handle_accessors.iter().copied());
accessor_tokens.extend(r.method_handle_accessors.iter().copied());
r.type_token
});
let runtime_method_tokens = collect_runtime_method_tokens(assembly, runtime_type_token);
let runtime_resource_tokens = if runtime_method_tokens.is_empty() {
Vec::new()
} else {
helpers::find_resources_referenced_by_methods(assembly, &runtime_method_tokens)
};
let mut evidence = vec![
Evidence::Structural(format!(
"Init method 0x{:08X} called by {} type .cctor(s) ({} purely-injected, {} modified)",
init_method_token.value(),
fan_in.calling_cctors.len(),
classification.purely_injected.len(),
classification.modified.len(),
)),
Evidence::Structural(format!(
"{} <Module>{{GUID}} marker type(s), {} <PrivateImplementationDetails>{{GUID}} container(s)",
guid_module_type_tokens.len(),
private_impl_containers.len(),
)),
];
if let Some(ref r) = resolver {
evidence.push(Evidence::Structural(format!(
"Metadata-token resolver type 0x{:08X}: {} type-handle, {} field-handle, {} method-handle accessor(s)",
r.type_token.value(),
r.type_handle_accessors.len(),
r.field_handle_accessors.len(),
r.method_handle_accessors.len(),
)));
}
if !runtime_resource_tokens.is_empty() {
evidence.push(Evidence::Structural(format!(
"{} manifest resource(s) referenced only from anti-tamper runtime",
runtime_resource_tokens.len(),
)));
}
let findings = AntiTampFindings {
init_method_token,
runtime_type_token,
purely_injected_cctors: classification.purely_injected.clone(),
modified_cctors: classification.modified,
guid_module_type_tokens: guid_module_type_tokens.clone(),
token_resolver_accessor_tokens: accessor_tokens,
token_resolver_type_token: resolver_type_token,
runtime_resource_tokens: runtime_resource_tokens.clone(),
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
detection.cleanup_mut().add_method(init_method_token);
if let Some(rt) = runtime_type_token {
detection.cleanup_mut().add_type(rt);
}
for &cctor in &classification.purely_injected {
detection.cleanup_mut().add_method(cctor);
}
for &t in &guid_module_type_tokens {
detection.cleanup_mut().add_type(t);
}
if let Some(rt) = resolver_type_token {
detection.cleanup_mut().add_type(rt);
}
for &res in &runtime_resource_tokens {
detection.cleanup_mut().add_manifest_resource(res);
}
detection
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Value)
}
fn create_pass(
&self,
_ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<AntiTampFindings>() else {
return Vec::new();
};
if findings.token_resolver_accessor_tokens.is_empty() {
return Vec::new();
}
vec![Box::new(TokenResolverPass::new(
findings.token_resolver_accessor_tokens.iter().copied(),
))]
}
}
fn collect_runtime_method_tokens(assembly: &CilObject, runtime_type: Option<Token>) -> Vec<Token> {
let Some(root_token) = runtime_type else {
return Vec::new();
};
let Some(root_type) = assembly.types().get(&root_token) else {
return Vec::new();
};
let mut stack = vec![root_type];
let mut out = Vec::new();
while let Some(cil_type) = stack.pop() {
for method in cil_type.methods() {
out.push(method.token);
}
for (_, nested_ref) in cil_type.nested_types.iter() {
if let Some(nested) = nested_ref.upgrade() {
stack.push(nested);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::validation::ValidationConfig;
fn try_load_sample(name: &str) -> Option<CilObject> {
let path = format!("tests/samples/packers/netreactor/7.5.0/{name}");
if !std::path::Path::new(&path).exists() {
eprintln!("Skipping test: sample not found at {path}");
return None;
}
Some(
CilObject::from_path_with_validation(&path, ValidationConfig::analysis())
.unwrap_or_else(|e| panic!("Failed to load {name}: {e}")),
)
}
#[test]
fn test_detect_positive_antitamp() {
let Some(assembly) = try_load_sample("reactor_antitamp.exe") else {
return;
};
let detection = NetReactorAntiTamp.detect(&assembly);
assert!(
detection.is_detected(),
"Should detect anti-tamper init in reactor_antitamp.exe"
);
let findings = detection
.findings::<AntiTampFindings>()
.expect("Should attach findings");
assert!(
!findings.purely_injected_cctors.is_empty(),
"Should classify at least one purely-injected .cctor"
);
assert!(
!findings.guid_module_type_tokens.is_empty(),
"Should find a <Module>{{GUID}} marker type"
);
}
#[test]
fn test_detect_negative_baseline() {
let Some(assembly) = try_load_sample("original.exe") else {
return;
};
let detection = NetReactorAntiTamp.detect(&assembly);
assert!(
!detection.is_detected(),
"Should not detect anti-tamper in unprotected original.exe"
);
}
#[test]
fn test_detect_negative_no_antitamp_nr() {
let Some(assembly) = try_load_sample("reactor_obfuscation.exe") else {
return;
};
let detection = NetReactorAntiTamp.detect(&assembly);
assert!(
!detection.is_detected(),
"Should not fire on reactor_obfuscation.exe (only 3 .cctors, no fan-in)"
);
}
}