use std::{any::Any, collections::HashSet, sync::Arc};
use crate::{
assembly::Operand,
cilassembly::GeneratorConfig,
compiler::{EventKind, EventLog},
deobfuscation::techniques::{
netreactor::{helpers::find_resources_referenced_by_methods, hooks},
Detection, Detections, Evidence, Technique, TechniqueCategory, WorkingAssembly,
},
emulation::{EmulationOutcome, ProcessBuilder},
error::Error,
metadata::{
signatures::TypeSignature,
tables::{ManifestResourceBuilder, TableId, TypeRefRaw},
token::Token,
typesystem::{wellknown, CilTypeRc, CilTypeReference},
validation::ValidationConfig,
},
CilObject, Result,
};
const MIN_DISPATCHER_CASES: usize = 200;
#[derive(Debug)]
pub struct ResourceFindings {
pub resolver_type_token: Token,
pub lazy_init_token: Token,
pub resolver_ctor_token: Token,
pub handler_method_token: Token,
pub decrypter_method_token: Token,
pub encrypted_resource_tokens: Vec<Token>,
pub lazy_init_call_sites: Vec<Token>,
pub purely_injected_cctors: Vec<Token>,
pub assembly_load_shim_tokens: Vec<Token>,
pub get_manifest_resource_names_shim_tokens: Vec<Token>,
pub bcl_get_manifest_resource_names: Token,
}
pub struct NetReactorResources;
impl Technique for NetReactorResources {
fn id(&self) -> &'static str {
"netreactor.resources"
}
fn name(&self) -> &'static str {
".NET Reactor Resource Decryption"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn detect(&self, assembly: &CilObject) -> Detection {
let Some(candidate) = find_resolver_candidate(assembly) else {
return Detection::new_empty();
};
let runtime_method_tokens: Vec<Token> =
candidate.resolver_type.methods().map(|m| m.token).collect();
let encrypted_resource_tokens =
find_resources_referenced_by_methods(assembly, &runtime_method_tokens);
if encrypted_resource_tokens.is_empty() {
return Detection::new_empty();
}
let lazy_init_call_sites = find_lazy_init_call_sites(assembly, candidate.lazy_init_token);
let purely_injected_cctors =
classify_purely_injected_cctors(assembly, candidate.lazy_init_token);
let assembly_load_shim_tokens =
find_assembly_load_shim_methods(assembly, &candidate.resolver_type);
let get_manifest_resource_names_shim_tokens =
find_get_manifest_resource_names_shims(assembly, &candidate.resolver_type);
let bcl_get_manifest_resource_names = find_bcl_member_ref(
assembly,
"System.Reflection",
"Assembly",
"GetManifestResourceNames",
)
.unwrap_or_else(|| Token::new(0));
let evidence = vec![
Evidence::Structural(format!(
"Resolver type 0x{:08X} with handler 0x{:08X} + dispatcher 0x{:08X} \
({} switch cases)",
candidate.resolver_type.token.value(),
candidate.handler_method_token.value(),
candidate.decrypter_method_token.value(),
candidate.dispatcher_case_count,
)),
Evidence::Structural(format!(
"Lazy init 0x{:08X} called from {} site(s); {} purely-injected .cctor(s); \
{} encrypted resource(s); {} Assembly.Load reflection shim(s); \
{} GetManifestResourceNames shim(s); \
BCL GetManifestResourceNames MemberRef = 0x{:08X}",
candidate.lazy_init_token.value(),
lazy_init_call_sites.len(),
purely_injected_cctors.len(),
encrypted_resource_tokens.len(),
assembly_load_shim_tokens.len(),
get_manifest_resource_names_shim_tokens.len(),
bcl_get_manifest_resource_names.value(),
)),
];
let findings = ResourceFindings {
resolver_type_token: candidate.resolver_type.token,
lazy_init_token: candidate.lazy_init_token,
resolver_ctor_token: candidate.resolver_ctor_token,
handler_method_token: candidate.handler_method_token,
decrypter_method_token: candidate.decrypter_method_token,
encrypted_resource_tokens: encrypted_resource_tokens.clone(),
lazy_init_call_sites,
purely_injected_cctors: purely_injected_cctors.clone(),
assembly_load_shim_tokens,
get_manifest_resource_names_shim_tokens,
bcl_get_manifest_resource_names,
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
for &res in &encrypted_resource_tokens {
detection.cleanup_mut().add_manifest_resource(res);
}
for &cctor in &purely_injected_cctors {
detection.cleanup_mut().add_method(cctor);
}
detection
}
fn create_pass(
&self,
_ctx: &crate::deobfuscation::context::AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn crate::compiler::SsaPass>> {
let Some(findings) = detection.findings::<ResourceFindings>() else {
return Vec::new();
};
vec![Box::new(
crate::deobfuscation::passes::netreactor::ResourceShimRewritePass::new(
findings
.get_manifest_resource_names_shim_tokens
.iter()
.copied(),
findings.lazy_init_token,
findings.bcl_get_manifest_resource_names,
),
)]
}
fn ssa_phase(&self) -> Option<crate::compiler::PassPhase> {
Some(crate::compiler::PassPhase::Value)
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<ResourceFindings>() else {
return Some(Ok(events));
};
let co = match assembly.cilobject() {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let bytes = co.file().data().to_vec();
let cilobject =
match CilObject::from_mem_with_validation(bytes, ValidationConfig::analysis()) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let assembly_typeref = match find_assembly_typeref(&cilobject) {
Some(t) => t,
None => {
return Some(Err(Error::Deobfuscation(
"NR resources: assembly does not import \
System.Reflection.Assembly — cannot install Load shim hook"
.to_string(),
)));
}
};
let cilobject_arc = Arc::new(cilobject);
let shim_set: HashSet<Token> = findings.assembly_load_shim_tokens.iter().copied().collect();
let mut builder = ProcessBuilder::new()
.assembly_arc(Arc::clone(&cilobject_arc))
.name("netreactor-resources")
.with_max_instructions(50_000_000)
.with_max_call_depth(200)
.with_timeout_ms(120_000)
.capture_assemblies();
if !shim_set.is_empty() {
builder = builder.hook(hooks::create_resources_load_shim_hook(
shim_set,
assembly_typeref,
));
}
let process = match builder.build() {
Ok(p) => p,
Err(e) => return Some(Err(e)),
};
let outcome = match process.execute_method(findings.decrypter_method_token, vec![]) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let instructions_executed = match &outcome {
EmulationOutcome::Completed { instructions, .. }
| EmulationOutcome::Breakpoint { instructions, .. } => *instructions,
EmulationOutcome::UnhandledException {
instructions,
exception,
..
} => {
log::info!(
"NR resources: decrypter raised after {instructions} instructions: \
{exception:?} — extracting any captured assemblies"
);
*instructions
}
EmulationOutcome::LimitReached { limit, .. } => {
log::warn!(
"NR resources: decrypter exceeded limit ({limit:?}) — \
attempting capture extraction from partial state"
);
0
}
EmulationOutcome::Stopped { reason, .. } => {
return Some(Err(Error::Deobfuscation(format!(
"NR resources: decrypter stopped: {reason}"
))));
}
EmulationOutcome::RequiresSymbolic { reason, .. } => {
return Some(Err(Error::Deobfuscation(format!(
"NR resources: decrypter requires symbolic execution: {reason}"
))));
}
};
let captured = process.capture().assemblies();
drop(process);
if captured.is_empty() {
log::debug!(
"NR resources: decrypter completed ({instructions_executed} instructions) \
but no Assembly bytes were captured — leaving encrypted blob in place"
);
return Some(Ok(events));
}
log::info!(
"NR resources: decrypter ran in {instructions_executed} instructions, \
captured {} Assembly.Load(byte[]) call(s)",
captured.len()
);
let decrypted_resources = harvest_resources(&captured);
if decrypted_resources.is_empty() {
return Some(Err(Error::Deobfuscation(
"NR resources: captured assemblies contained no extractable resources".to_string(),
)));
}
log::info!(
"NR resources: recovered {} embedded resource(s) from captured assemblies",
decrypted_resources.len()
);
for (name, data) in &decrypted_resources {
log::debug!("NR resources: recovered {:?} ({} bytes)", name, data.len());
}
let cilobject = match Arc::try_unwrap(cilobject_arc).map_err(|_| {
Error::Deobfuscation(
"NR resources: emulation assembly still shared after process drop".to_string(),
)
}) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut cil_assembly = cilobject.into_assembly();
let mut injected = 0usize;
for (name, data) in &decrypted_resources {
match ManifestResourceBuilder::new()
.name(name.clone())
.public()
.resource_data(data)
.build(&mut cil_assembly)
{
Ok(_) => {
injected += 1;
events.record(EventKind::ResourceDecrypted).message(format!(
"Injected NR-decrypted resource {:?} ({} bytes)",
name,
data.len()
));
}
Err(e) => {
log::warn!(
"NR resources: failed to inject {:?} ({} bytes): {e}",
name,
data.len()
);
}
}
}
if injected == 0 {
return Some(Err(Error::Deobfuscation(
"NR resources: failed to inject any decrypted resource".to_string(),
)));
}
events.record(EventKind::ResourceDecrypted).message(format!(
"NR resources: {injected} resource(s) restored from {} captured assembly bytes \
({instructions_executed} instructions emulated)",
captured.iter().map(|c| c.data.len()).sum::<usize>(),
));
let new_assembly = match cil_assembly
.into_cilobject_with(ValidationConfig::analysis(), GeneratorConfig::default())
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
assembly.replace_assembly(new_assembly);
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
}
fn harvest_resources(captured: &[crate::emulation::CapturedAssembly]) -> Vec<(String, Vec<u8>)> {
let mut out = Vec::new();
for cap in captured {
let parsed = match CilObject::from_mem_with_validation(
cap.data.clone(),
ValidationConfig::analysis(),
) {
Ok(p) => p,
Err(e) => {
log::warn!(
"NR resources: failed to parse captured assembly ({} bytes): {e}",
cap.data.len()
);
continue;
}
};
let res_table = parsed.resources();
for entry in res_table.iter() {
let res = entry.value();
let Some(bytes) = res_table.get_data(res) else {
log::debug!(
"NR resources: captured assembly resource {:?} has no accessible data",
res.name
);
continue;
};
if bytes.is_empty() {
log::debug!(
"NR resources: captured assembly has empty body for {:?}",
res.name
);
continue;
}
out.push((res.name.clone(), bytes.to_vec()));
}
}
out
}
struct ResolverCandidate {
resolver_type: CilTypeRc,
lazy_init_token: Token,
resolver_ctor_token: Token,
handler_method_token: Token,
decrypter_method_token: Token,
dispatcher_case_count: usize,
}
fn find_resolver_candidate(assembly: &CilObject) -> Option<ResolverCandidate> {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value().clone();
if let Some(c) = match_resolver_type(assembly, &cil_type) {
return Some(c);
}
}
None
}
fn match_resolver_type(assembly: &CilObject, cil_type: &CilTypeRc) -> Option<ResolverCandidate> {
let (resolver_ctor_token, handler_method_token) =
find_resolve_handler_registration(assembly, cil_type)?;
let lazy_init_token = find_lazy_init(assembly, cil_type)?;
let (decrypter_method_token, dispatcher_case_count) =
find_dispatcher_method(assembly, cil_type)?;
Some(ResolverCandidate {
resolver_type: cil_type.clone(),
lazy_init_token,
resolver_ctor_token,
handler_method_token,
decrypter_method_token,
dispatcher_case_count,
})
}
fn find_resolve_handler_registration(
assembly: &CilObject,
cil_type: &CilTypeRc,
) -> Option<(Token, Token)> {
for method in cil_type.methods() {
if method.name != wellknown::members::CTOR {
continue;
}
if !method.has_body() {
continue;
}
let instructions: Vec<_> = method.instructions().collect();
let mut last_ldftn: Option<Token> = None;
for instr in &instructions {
match instr.mnemonic {
"ldftn" => {
if let Operand::Token(t) = &instr.operand {
last_ldftn = Some(*t);
}
}
"callvirt" | "call" => {
if let Operand::Token(t) = &instr.operand {
let target_name = assembly.resolve_method_name(*t);
let is_resolve_register = target_name.as_deref().is_some_and(|n| {
matches!(n, "add_ResourceResolve" | "add_AssemblyResolve")
});
if is_resolve_register {
if let Some(handler_token) = last_ldftn {
if handler_lives_on_type(assembly, handler_token, cil_type) {
return Some((method.token, handler_token));
}
}
}
}
}
_ => {}
}
}
}
None
}
fn handler_lives_on_type(assembly: &CilObject, handler_token: Token, cil_type: &CilTypeRc) -> bool {
let Some(handler) = assembly.method(&handler_token) else {
return false;
};
let Some(declaring) = handler.declaring_type_rc() else {
return false;
};
declaring.token == cil_type.token
}
fn find_lazy_init(assembly: &CilObject, cil_type: &CilTypeRc) -> Option<Token> {
for method in cil_type.methods() {
if !method.is_static() {
continue;
}
if !matches!(method.signature.return_type.base, TypeSignature::Void) {
continue;
}
if !method.signature.params.is_empty() {
continue;
}
if !method.has_body() {
continue;
}
if !is_lazy_init_body(assembly, &method, cil_type) {
continue;
}
return Some(method.token);
}
None
}
fn is_lazy_init_body(
assembly: &CilObject,
method: &crate::metadata::method::MethodRc,
cil_type: &CilTypeRc,
) -> bool {
let instrs: Vec<_> = method
.instructions()
.filter(|i| !matches!(i.mnemonic, "nop" | "br" | "br.s"))
.collect();
if instrs.len() < 7 {
return false;
}
let Operand::Token(flag_load) = &instrs[0].operand else {
return false;
};
if instrs[0].mnemonic != "ldsfld" {
return false;
}
if !matches!(instrs[1].mnemonic, "brtrue" | "brtrue.s") {
return false;
}
if !matches!(instrs[2].mnemonic, "ldc.i4.1") {
return false;
}
if instrs[3].mnemonic != "stsfld" {
return false;
}
let Operand::Token(flag_store) = &instrs[3].operand else {
return false;
};
if flag_load != flag_store {
return false;
}
if instrs[4].mnemonic != "newobj" {
return false;
}
let Operand::Token(ctor_token) = &instrs[4].operand else {
return false;
};
let Some(ctor) = assembly.method(ctor_token) else {
return false;
};
let Some(declaring) = ctor.declaring_type_rc() else {
return false;
};
if declaring.token != cil_type.token {
return false;
}
if instrs[5].mnemonic != "pop" {
return false;
}
if instrs[6].mnemonic != "ret" {
return false;
}
true
}
fn find_dispatcher_method(assembly: &CilObject, cil_type: &CilTypeRc) -> Option<(Token, usize)> {
let mut best: Option<(Token, usize)> = None;
for method in cil_type.methods() {
if !method.has_body() {
continue;
}
for instr in method.instructions() {
if instr.mnemonic != "switch" {
continue;
}
let Operand::Switch(targets) = &instr.operand else {
continue;
};
let n = targets.len();
if n >= MIN_DISPATCHER_CASES && best.as_ref().is_none_or(|(_, prev)| n > *prev) {
best = Some((method.token, n));
}
}
}
best.filter(|(_, n)| *n >= MIN_DISPATCHER_CASES)
.or_else(|| {
let _ = assembly;
None
})
}
fn find_lazy_init_call_sites(assembly: &CilObject, lazy_init_token: Token) -> Vec<Token> {
let mut out = Vec::new();
for entry in assembly.methods() {
let method = entry.value();
if method.token == lazy_init_token {
continue;
}
if !method.has_body() {
continue;
}
let mut iter = method.instructions();
let Some(first) = iter.next() else { continue };
if first.mnemonic != "call" {
continue;
}
let Operand::Token(t) = &first.operand else {
continue;
};
if *t == lazy_init_token {
out.push(method.token);
}
}
out
}
fn classify_purely_injected_cctors(assembly: &CilObject, lazy_init_token: Token) -> Vec<Token> {
let mut out = Vec::new();
for entry in assembly.methods() {
let method = entry.value();
if method.name != wellknown::members::CCTOR {
continue;
}
if !method.has_body() {
continue;
}
let instrs: Vec<_> = method
.instructions()
.filter(|i| !matches!(i.mnemonic, "nop" | "br" | "br.s"))
.collect();
if instrs.len() != 2 {
continue;
}
if instrs[0].mnemonic != "call" {
continue;
}
let Operand::Token(t) = &instrs[0].operand else {
continue;
};
if *t != lazy_init_token {
continue;
}
if instrs[1].mnemonic != "ret" {
continue;
}
out.push(method.token);
}
out
}
fn find_get_manifest_resource_names_shims(
assembly: &CilObject,
cil_type: &CilTypeRc,
) -> Vec<Token> {
let mut out = Vec::new();
let mut stack = vec![cil_type.clone()];
let mut visited: HashSet<Token> = std::iter::once(cil_type.token).collect();
while let Some(ty) = stack.pop() {
for method in ty.methods() {
if !method.is_static() {
continue;
}
let returns_string_array = match &method.signature.return_type.base {
TypeSignature::SzArray(inner) => matches!(*inner.base, TypeSignature::String),
_ => false,
};
if !returns_string_array {
continue;
}
if method.signature.params.len() != 1 {
continue;
}
if !matches!(method.signature.params[0].base, TypeSignature::Class(_)) {
continue;
}
if !method.has_body() {
continue;
}
let mut calls_bcl = false;
for instr in method.instructions() {
if !matches!(instr.mnemonic, "callvirt" | "call") {
continue;
}
if let Some(t) = instr.get_token_operand() {
if let Some(name) = assembly.resolve_method_name(t) {
if name == "GetManifestResourceNames" {
calls_bcl = true;
break;
}
}
}
}
if calls_bcl {
out.push(method.token);
}
}
for (_, nested_ref) in ty.nested_types.iter() {
if let Some(nested) = nested_ref.upgrade() {
if visited.insert(nested.token) {
stack.push(nested);
}
}
}
}
out.sort_by_key(|t| t.value());
out
}
fn find_bcl_member_ref(
assembly: &CilObject,
namespace: &str,
type_name: &str,
method_name: &str,
) -> Option<Token> {
for entry in assembly.refs_members().iter() {
let mref = entry.value();
if mref.name != method_name {
continue;
}
let parent = match &mref.declaredby {
CilTypeReference::TypeRef(r) | CilTypeReference::TypeDef(r) => r.upgrade()?,
_ => continue,
};
if parent.name == type_name && parent.namespace == namespace {
return Some(mref.token);
}
}
None
}
pub fn find_assembly_typeref(assembly: &CilObject) -> Option<Token> {
let tables = assembly.tables()?;
let table = tables.table::<TypeRefRaw>()?;
let strings = assembly.strings()?;
for row in table {
let Ok(name) = strings.get(row.type_name as usize) else {
continue;
};
let Ok(ns) = strings.get(row.type_namespace as usize) else {
continue;
};
if name == "Assembly" && ns == "System.Reflection" {
return Some(Token::new((TableId::TypeRef as u32) << 24 | row.rid));
}
}
None
}
fn find_assembly_load_shim_methods(assembly: &CilObject, cil_type: &CilTypeRc) -> Vec<Token> {
let mut out = Vec::new();
let mut stack = vec![cil_type.clone()];
let mut visited: HashSet<Token> = std::iter::once(cil_type.token).collect();
while let Some(ty) = stack.pop() {
for method in ty.methods() {
if !method.is_static() {
continue;
}
if !matches!(
method.signature.return_type.base,
TypeSignature::Object | TypeSignature::Class(_)
) {
continue;
}
if method.signature.params.len() != 1 {
continue;
}
let param = &method.signature.params[0];
let is_byte_array = match ¶m.base {
TypeSignature::SzArray(inner) => matches!(*inner.base, TypeSignature::U1),
_ => false,
};
if !is_byte_array {
continue;
}
if !method.has_body() {
continue;
}
if !is_assembly_load_reflection_shim(&method, assembly) {
continue;
}
out.push(method.token);
}
for (_, nested_ref) in ty.nested_types.iter() {
if let Some(nested) = nested_ref.upgrade() {
if visited.insert(nested.token) {
stack.push(nested);
}
}
}
}
out.sort_by_key(|t| t.value());
let _ = assembly;
out
}
fn is_assembly_load_reflection_shim(
method: &crate::metadata::method::MethodRc,
assembly: &CilObject,
) -> bool {
let mut saw_assembly_token = false;
let mut saw_invoke_call = false;
for instr in method.instructions() {
if instr.mnemonic == "ldtoken" {
if let Operand::Token(_) = &instr.operand {
saw_assembly_token = true;
}
}
if matches!(instr.mnemonic, "callvirt" | "call") {
if let Operand::Token(t) = &instr.operand {
if let Some(name) = assembly.resolve_method_name(*t) {
if name == "Invoke" {
saw_invoke_call = true;
}
}
}
}
if saw_assembly_token && saw_invoke_call {
return true;
}
}
false
}
#[allow(dead_code)]
fn encrypted_resource_names(assembly: &CilObject, findings: &ResourceFindings) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut out = Vec::new();
for &token in &findings.encrypted_resource_tokens {
if let Some(res) = assembly.resources().iter().find(|r| r.token == token) {
let name = res.name.clone();
if seen.insert(name.clone()) {
out.push(name);
}
}
}
out
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::{
emulation::{EmulationOutcome, ProcessBuilder, TracingConfig},
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_reactor_resources() {
let Some(assembly) = try_load_sample("reactor_resources.exe") else {
return;
};
let detection = NetReactorResources.detect(&assembly);
assert!(
detection.is_detected(),
"Should detect resource encryption in reactor_resources.exe"
);
let findings = detection
.findings::<ResourceFindings>()
.expect("Should attach findings");
assert!(
!findings.encrypted_resource_tokens.is_empty(),
"Should mark at least one encrypted resource"
);
assert!(
!findings.lazy_init_call_sites.is_empty(),
"Should locate at least one lazy-init injection site"
);
assert!(
!findings.purely_injected_cctors.is_empty(),
"Should classify at least one purely-injected .cctor"
);
assert!(
!findings.assembly_load_shim_tokens.is_empty(),
"Should locate at least one Assembly.Load reflection shim ({} found)",
findings.assembly_load_shim_tokens.len()
);
assert!(
!findings.get_manifest_resource_names_shim_tokens.is_empty(),
"Should locate at least one GetManifestResourceNames shim ({} found)",
findings.get_manifest_resource_names_shim_tokens.len()
);
eprintln!(
"Resolver type 0x{:08X}, decrypter 0x{:08X}, lazy init 0x{:08X}, \
ctor 0x{:08X}, handler 0x{:08X}, {} load shims, {} GMRN shims, \
{} encrypted resources, BCL GMRN MemberRef = 0x{:08X}",
findings.resolver_type_token.value(),
findings.decrypter_method_token.value(),
findings.lazy_init_token.value(),
findings.resolver_ctor_token.value(),
findings.handler_method_token.value(),
findings.assembly_load_shim_tokens.len(),
findings.get_manifest_resource_names_shim_tokens.len(),
findings.encrypted_resource_tokens.len(),
findings.bcl_get_manifest_resource_names.value(),
);
for t in &findings.assembly_load_shim_tokens {
eprintln!(" shim: 0x{:08X}", t.value());
}
}
#[test]
fn test_detect_negative_reactor_full_variants() {
for name in &["reactor_full.exe", "reactor_virtualization_full.exe"] {
let Some(assembly) = try_load_sample(name) else {
continue;
};
let detection = NetReactorResources.detect(&assembly);
assert!(
!detection.is_detected(),
"{name} uses an alternate resource lookup; detection should not fire here yet"
);
}
}
#[test]
fn test_detect_negative_baseline() {
let Some(assembly) = try_load_sample("original.exe") else {
return;
};
let detection = NetReactorResources.detect(&assembly);
assert!(
!detection.is_detected(),
"Should not detect in unprotected original.exe"
);
}
#[test]
#[ignore]
fn test_emulate_resource_decryption() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
let Some(assembly) = try_load_sample("reactor_resources.exe") else {
return;
};
let detection = NetReactorResources.detect(&assembly);
assert!(detection.is_detected(), "detection should fire");
let findings = detection.findings::<ResourceFindings>().unwrap();
eprintln!(
"Detected: decrypter=0x{:08X}, lazy_init=0x{:08X}, handler=0x{:08X}, \
ctor=0x{:08X}, shims={:?}",
findings.decrypter_method_token.value(),
findings.lazy_init_token.value(),
findings.handler_method_token.value(),
findings.resolver_ctor_token.value(),
findings
.assembly_load_shim_tokens
.iter()
.map(|t| format!("0x{:08X}", t.value()))
.collect::<Vec<_>>()
);
let cilobject = Arc::new(assembly);
let trace_path = std::env::var("NR_TRACE").ok().map(std::path::PathBuf::from);
let mut builder = ProcessBuilder::new()
.assembly_arc(Arc::clone(&cilobject))
.name("nr-resources-probe")
.with_max_instructions(80_000_000)
.with_max_call_depth(300)
.with_timeout_ms(180_000)
.capture_assemblies();
if !findings.assembly_load_shim_tokens.is_empty() {
let shim_set: HashSet<Token> =
findings.assembly_load_shim_tokens.iter().copied().collect();
let asm_typeref =
find_assembly_typeref(&cilobject).expect("Assembly TypeRef must exist");
builder = builder.hook(hooks::create_resources_load_shim_hook(
shim_set,
asm_typeref,
));
}
if let Some(path) = trace_path {
eprintln!("Tracing calls to {}", path.display());
let tracing = TracingConfig {
trace_calls: true,
trace_exceptions: true,
output_path: Some(path),
context_prefix: Some("nr-resources-probe".to_string()),
..TracingConfig::default()
};
builder = builder.with_tracing(tracing);
}
let process = builder.build().expect("process build");
let target = findings.decrypter_method_token;
eprintln!("=== Executing decrypter 0x{:08X} ===", target.value());
let outcome = process.execute_method(target, vec![]);
match outcome {
Ok(EmulationOutcome::Completed { instructions, .. }) => {
eprintln!(
" Completed in {} instructions; captured assemblies = {}",
instructions,
process.capture().assembly_count()
);
}
Ok(EmulationOutcome::UnhandledException {
instructions,
exception,
..
}) => {
eprintln!(
" Threw after {instructions}: {exception:?}; captured = {}",
process.capture().assembly_count()
);
}
Ok(other) => eprintln!(" Other outcome: {other}"),
Err(e) => eprintln!(" Error: {e}"),
}
for (i, asm) in process.capture().assemblies().iter().enumerate() {
eprintln!(
" captured[{i}] = {} bytes, name={:?}",
asm.data.len(),
asm.name
);
}
}
#[test]
fn test_detect_negative_no_resources_nr() {
for name in &[
"reactor_obfuscation.exe",
"reactor_strings.exe",
"reactor_necrobit.exe",
"reactor_antitamp.exe",
] {
let Some(assembly) = try_load_sample(name) else {
continue;
};
let detection = NetReactorResources.detect(&assembly);
assert!(
!detection.is_detected(),
"Should not fire on {name} (no resource resolver)"
);
}
}
}