mod antitamp;
mod antitrial;
mod helpers;
mod hooks;
mod licensecheck;
mod necrobit;
mod privateimpl;
mod resources;
pub(crate) use antitamp::NetReactorAntiTamp;
pub(crate) use antitrial::NetReactorAntiTrial;
pub(crate) use helpers::find_resources_referenced_by_methods;
pub(crate) use licensecheck::NetReactorLicenseCheck;
pub(crate) use necrobit::NetReactorNecroBit;
pub(crate) use privateimpl::NetReactorPrivateImpl;
pub(crate) use resources::NetReactorResources;
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{
assembly::Operand,
deobfuscation::{engine::DeobfuscationEngine, EngineConfig},
metadata::validation::ValidationConfig,
CilObject,
};
fn load_nr_sample(name: &str) -> CilObject {
let path = format!("tests/samples/packers/netreactor/7.5.0/{name}");
CilObject::from_path_with_validation(&path, ValidationConfig::analysis())
.unwrap_or_else(|e| panic!("Failed to load {name}: {e}"))
}
fn sample_exists(name: &str) -> bool {
let path = format!("tests/samples/packers/netreactor/7.5.0/{name}");
std::path::Path::new(&path).exists()
}
fn collect_ldstr_strings(asm: &CilObject) -> HashSet<String> {
let mut strings = HashSet::new();
let us = match asm.userstrings() {
Some(us) => us,
None => return strings,
};
for method_entry in asm.methods() {
let method = method_entry.value();
for instr in method.instructions() {
if instr.mnemonic == "ldstr" {
if let Operand::Token(token) = &instr.operand {
if token.table() == 0x70 {
if let Ok(s) = us.get(token.row() as usize) {
strings.insert(s.to_string_lossy());
}
}
}
}
}
}
strings
}
#[test]
#[ignore]
fn test_nr_string_decryption() {
if !sample_exists("reactor_strings.exe") {
eprintln!("Skipping: reactor_strings.exe not found");
return;
}
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
let original = load_nr_sample("original.exe");
let original_strings = collect_ldstr_strings(&original);
eprintln!(
"Original has {} unique string literals",
original_strings.len()
);
for s in &original_strings {
eprintln!(" original: {:?}", s);
}
let assembly = load_nr_sample("reactor_strings.exe");
let engine = DeobfuscationEngine::new(EngineConfig::default());
let (output, result) = engine
.process_assembly(assembly)
.expect("Deobfuscation should succeed");
let stats = result.stats();
eprintln!("\nDeobfuscation stats:");
eprintln!(" strings_decrypted: {}", stats.strings_decrypted);
eprintln!(" constants_folded: {}", stats.constants_folded);
eprintln!(" methods_transformed: {}", stats.methods_transformed);
eprintln!(" methods_regenerated: {}", stats.methods_regenerated);
eprintln!(" iterations: {}", result.iterations);
for t in &result.techniques {
eprintln!(
" technique: {} detected={} transformed={}",
t.id, t.detected, t.transformed
);
}
let strings_technique = result.techniques.iter().find(|t| t.id == "generic.strings");
assert!(
strings_technique.is_some_and(|t| t.detected),
"generic.strings should be detected"
);
assert!(
stats.strings_decrypted >= 80,
"Expected at least 80 strings decrypted, got {}",
stats.strings_decrypted
);
let deobf_strings = collect_ldstr_strings(&output);
eprintln!(
"\nDeobfuscated has {} unique string literals",
deobf_strings.len()
);
for s in &deobf_strings {
eprintln!(" deobf: {:?}", s);
}
let mut missing = Vec::new();
let all_deobf: Vec<&String> = deobf_strings.iter().collect();
for s in &original_strings {
let exact_match = deobf_strings.contains(s);
let substring_match = all_deobf
.iter()
.any(|ds| ds.contains(s.as_str()) && *ds != s);
if !exact_match && !substring_match {
missing.push(s.clone());
}
}
if !missing.is_empty() {
eprintln!("\nMISSING strings (not found even as substring):");
for s in &missing {
eprintln!(" MISSING: {:?}", s);
}
}
assert!(
missing.len() <= 5,
"{} original strings completely missing from deobfuscated output: {:?}",
missing.len(),
missing
);
}
#[test]
#[ignore]
fn test_nr_necrobit_strings_decryption() {
if !sample_exists("reactor_necrobit_strings.exe") {
eprintln!("Skipping: reactor_necrobit_strings.exe not found");
return;
}
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
let assembly = load_nr_sample("reactor_necrobit_strings.exe");
let engine = DeobfuscationEngine::new(EngineConfig::default());
let (_output, result) = engine
.process_assembly(assembly)
.expect("Deobfuscation should succeed");
let stats = result.stats();
eprintln!("\nNecroBit + Strings stats:");
eprintln!(" strings_decrypted: {}", stats.strings_decrypted);
eprintln!(" methods_transformed: {}", stats.methods_transformed);
for t in &result.techniques {
eprintln!(
" technique: {} detected={} transformed={}",
t.id, t.detected, t.transformed
);
}
assert!(
stats.strings_decrypted >= 80,
"Expected at least 80 strings decrypted for necrobit+strings, got {}",
stats.strings_decrypted
);
}
}