use std::any::Any;
use crate::{
deobfuscation::techniques::{
netreactor::helpers, Detection, Evidence, Technique, TechniqueCategory,
},
metadata::token::Token,
CilObject,
};
#[derive(Debug)]
pub struct LicenseCheckFindings {
pub method_tokens: Vec<Token>,
}
pub struct NetReactorLicenseCheck;
impl Technique for NetReactorLicenseCheck {
fn id(&self) -> &'static str {
"netreactor.licensecheck"
}
fn name(&self) -> &'static str {
".NET Reactor License Check Removal"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Neutralization
}
fn requires(&self) -> &[&'static str] {
&["netreactor.antitrial"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
let trials = helpers::find_trial_checks(assembly);
let has_module_trial = trials.iter().any(|t| t.is_on_module_type);
if !has_module_trial {
return Detection::new_empty();
}
let method_tokens: Vec<Token> = trials
.into_iter()
.filter(|t| !t.is_on_module_type)
.map(|t| t.method_token)
.filter(|token| helpers::has_single_shot_bool_guard(assembly, *token))
.collect();
if method_tokens.is_empty() {
return Detection::new_empty();
}
let count = method_tokens.len();
let mut detection = Detection::new_detected(
vec![Evidence::BytecodePattern(format!(
"{count} NR license-check method(s) (trial pattern + single-shot bool guard)"
))],
Some(Box::new(LicenseCheckFindings {
method_tokens: method_tokens.clone(),
}) as Box<dyn Any + Send + Sync>),
);
for token in &method_tokens {
detection.cleanup_mut().add_method(*token);
}
detection
}
}
#[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_obfuscation() {
let Some(assembly) = try_load_sample("reactor_obfuscation.exe") else {
return;
};
let detection = NetReactorLicenseCheck.detect(&assembly);
assert!(
detection.is_detected(),
"Should detect NR license check in reactor_obfuscation.exe"
);
let findings = detection
.findings::<LicenseCheckFindings>()
.expect("Should attach findings");
assert!(
!findings.method_tokens.is_empty(),
"Should record at least one license-check method"
);
}
#[test]
fn test_detect_negative_baseline() {
let Some(assembly) = try_load_sample("original.exe") else {
return;
};
let detection = NetReactorLicenseCheck.detect(&assembly);
assert!(
!detection.is_detected(),
"Should not detect a license check in unprotected original.exe"
);
}
}