use std::{
collections::{HashMap, HashSet, VecDeque},
sync::Mutex,
};
use crate::deobfuscation::techniques::{
bitmono::{
BitMonoAntiDebug, BitMonoCalli, BitMonoHooks, BitMonoJunk, BitMonoNops, BitMonoPeRepair,
BitMonoRenamer, BitMonoStrings, BitMonoUnmanaged,
},
confuserex::{
ConfuserExAntiDebug, ConfuserExAntiDump, ConfuserExAntiTamper, ConfuserExConstants,
ConfuserExMarker, ConfuserExMetadata, ConfuserExNativeHelpers, ConfuserExReferenceProxy,
ConfuserExResources,
},
generic::{
GenericAntiDebug, GenericAntiDump, GenericConstants, GenericDecompiler,
GenericDelegateProxy, GenericFlattening, GenericHandlers, GenericIldasm, GenericMetadata,
GenericOpaquePredicates, GenericStrings,
},
jiejienet::{
JiejieNetArrays, JiejieNetConstants, JiejieNetResources, JiejieNetStrings, JiejieNetTypeOf,
},
netreactor::{
NetReactorAntiTamp, NetReactorAntiTrial, NetReactorLicenseCheck, NetReactorNecroBit,
NetReactorPrivateImpl, NetReactorResources,
},
obfuscar::ObfuscarStrings,
AttributionResult, Detections, Technique,
};
pub struct ObfuscatorSignature {
pub name: &'static str,
pub required: Vec<&'static str>,
pub supporting: Vec<&'static str>,
}
pub struct TechniqueRegistry {
techniques: Vec<Box<dyn Technique>>,
sorted_cache: Mutex<(u64, Vec<usize>)>,
}
impl TechniqueRegistry {
#[must_use]
pub fn new() -> Self {
Self {
techniques: Vec::new(),
sorted_cache: Mutex::new((u64::MAX, Vec::new())),
}
}
#[must_use]
pub fn with_defaults() -> Self {
Self::with_config(50_000)
}
#[must_use]
pub fn with_config(nop_threshold: usize) -> Self {
let mut registry = Self::new();
registry.register(Box::new(GenericMetadata));
registry.register(Box::new(GenericIldasm));
registry.register(Box::new(GenericHandlers));
registry.register(Box::new(GenericDecompiler));
registry.register(Box::new(GenericFlattening));
registry.register(Box::new(GenericStrings));
registry.register(Box::new(GenericConstants));
registry.register(Box::new(GenericAntiDebug));
registry.register(Box::new(GenericAntiDump));
registry.register(Box::new(GenericOpaquePredicates));
registry.register(Box::new(GenericDelegateProxy));
registry.register(Box::new(ConfuserExMarker));
registry.register(Box::new(ConfuserExMetadata));
registry.register(Box::new(ConfuserExAntiTamper));
registry.register(Box::new(ConfuserExResources));
registry.register(Box::new(ConfuserExNativeHelpers));
registry.register(Box::new(ConfuserExConstants));
registry.register(Box::new(ConfuserExReferenceProxy));
registry.register(Box::new(ConfuserExAntiDebug));
registry.register(Box::new(ConfuserExAntiDump));
registry.register(Box::new(BitMonoPeRepair));
registry.register(Box::new(BitMonoHooks));
registry.register(Box::new(BitMonoJunk));
registry.register(Box::new(BitMonoCalli));
registry.register(Box::new(BitMonoStrings));
registry.register(Box::new(BitMonoUnmanaged));
registry.register(Box::new(BitMonoAntiDebug));
registry.register(Box::new(BitMonoNops::new(nop_threshold)));
registry.register(Box::new(BitMonoRenamer));
registry.register(Box::new(JiejieNetConstants));
registry.register(Box::new(JiejieNetStrings));
registry.register(Box::new(JiejieNetTypeOf));
registry.register(Box::new(JiejieNetArrays));
registry.register(Box::new(JiejieNetResources));
registry.register(Box::new(ObfuscarStrings));
registry.register(Box::new(NetReactorNecroBit));
registry.register(Box::new(NetReactorAntiTrial));
registry.register(Box::new(NetReactorAntiTamp));
registry.register(Box::new(NetReactorLicenseCheck));
registry.register(Box::new(NetReactorPrivateImpl));
registry.register(Box::new(NetReactorResources));
registry
}
pub fn register(&mut self, technique: Box<dyn Technique>) {
self.techniques.push(technique);
}
#[must_use]
pub fn techniques(&self) -> &[Box<dyn Technique>] {
&self.techniques
}
#[must_use]
pub fn has_techniques(&self) -> bool {
!self.techniques.is_empty()
}
#[must_use]
pub fn sorted_techniques(&self, detections: &Detections) -> Vec<&dyn Technique> {
let gen = detections.generation();
if let Ok(cache) = self.sorted_cache.lock() {
if cache.0 == gen {
return cache.1.iter().map(|&i| &*self.techniques[i]).collect();
}
}
let sorted_indices = self.compute_sorted_indices(detections);
let result: Vec<&dyn Technique> = sorted_indices
.iter()
.map(|&i| &*self.techniques[i])
.collect();
if let Ok(mut cache) = self.sorted_cache.lock() {
*cache = (gen, sorted_indices);
}
result
}
fn compute_sorted_indices(&self, detections: &Detections) -> Vec<usize> {
let mut superseded: HashSet<&str> = HashSet::new();
for tech in &self.techniques {
if detections.is_detected(tech.id()) {
for s in tech.supersedes() {
superseded.insert(s);
}
}
}
let eligible: Vec<(usize, &dyn Technique)> = self
.techniques
.iter()
.enumerate()
.filter(|(_, t)| !superseded.contains(t.id()))
.map(|(i, t)| (i, &**t))
.collect();
let id_to_idx: HashMap<&str, usize> = eligible
.iter()
.enumerate()
.map(|(i, (_, t))| (t.id(), i))
.collect();
let n = eligible.len();
let mut in_degree = vec![0usize; n];
let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
for (idx, (_, tech)) in eligible.iter().enumerate() {
for &req_id in tech.requires() {
if let Some(&req_idx) = id_to_idx.get(req_id) {
dependents[req_idx].push(idx);
in_degree[idx] += 1;
}
}
}
let mut queue: VecDeque<usize> = VecDeque::new();
for (idx, °) in in_degree.iter().enumerate() {
if deg == 0 {
queue.push_back(idx);
}
}
let mut sorted: Vec<usize> = Vec::with_capacity(n);
while let Some(idx) = queue.pop_front() {
sorted.push(eligible[idx].0);
for &dep_idx in &dependents[idx] {
in_degree[dep_idx] -= 1;
if in_degree[dep_idx] == 0 {
queue.push_back(dep_idx);
}
}
}
if sorted.len() < n {
log::warn!(
"Technique dependency cycle detected: {} techniques could not be topologically sorted",
n - sorted.len()
);
let sorted_set: HashSet<usize> = sorted.iter().copied().collect();
for &(orig_idx, _) in &eligible {
if !sorted_set.contains(&orig_idx) {
sorted.push(orig_idx);
}
}
}
sorted
}
}
impl Default for TechniqueRegistry {
fn default() -> Self {
Self::with_defaults()
}
}
pub struct ObfuscatorMatcher {
signatures: Vec<ObfuscatorSignature>,
}
impl ObfuscatorMatcher {
#[must_use]
pub fn new() -> Self {
Self {
signatures: Vec::new(),
}
}
#[must_use]
pub fn with_defaults() -> Self {
let mut matcher = Self::new();
matcher.add_signature(ObfuscatorSignature {
name: "ConfuserEx",
required: vec!["confuserex.marker"],
supporting: vec![
"confuserex.constants",
"confuserex.proxy",
"confuserex.debug",
"confuserex.dump",
"confuserex.tamper",
"confuserex.resources",
"confuserex.metadata",
"confuserex.natives",
],
});
matcher.add_signature(ObfuscatorSignature {
name: "BitMono",
required: vec!["bitmono.renamer", "bitmono.junk"],
supporting: vec![
"bitmono.strings",
"bitmono.calli",
"bitmono.debug",
"bitmono.hooks",
"bitmono.nops",
"bitmono.unmanaged",
"bitmono.pe",
],
});
let bitmono_ids: &[&str] = &[
"bitmono.strings",
"bitmono.calli",
"bitmono.hooks",
"bitmono.unmanaged",
"bitmono.debug",
"bitmono.junk",
"bitmono.nops",
"bitmono.pe",
];
for sig in single_technique_signatures("BitMono", bitmono_ids, &[]) {
matcher.add_signature(sig);
}
let jiejie_ids: &[&str] = &[
"jiejienet.constants",
"jiejienet.strings",
"jiejienet.typeof",
"jiejienet.arrays",
"jiejienet.resources",
];
for sig in single_technique_signatures("JIEJIE.NET", jiejie_ids, &["generic.flattening"]) {
matcher.add_signature(sig);
}
matcher.add_signature(ObfuscatorSignature {
name: "Obfuscar",
required: vec!["obfuscar.strings"],
supporting: vec![],
});
let netreactor_ids: &[&str] = &[
"netreactor.necrobit",
"netreactor.antitrial",
"netreactor.antitamp",
"netreactor.licensecheck",
"netreactor.privateimpl",
"netreactor.resources",
];
for sig in single_technique_signatures(".NET Reactor", netreactor_ids, &[]) {
matcher.add_signature(sig);
}
matcher
}
pub fn add_signature(&mut self, sig: ObfuscatorSignature) {
self.signatures.push(sig);
}
#[must_use]
pub fn compute_attribution(&self, detections: &Detections) -> Option<AttributionResult> {
let mut candidates: Vec<AttributionResult> = self
.signatures
.iter()
.filter(|sig| sig.required.iter().all(|id| detections.is_detected(id)))
.map(|sig| {
let supporting_matched = sig
.supporting
.iter()
.filter(|id| detections.is_detected(id))
.count();
let mut technique_ids: Vec<String> =
sig.required.iter().map(|s| s.to_string()).collect();
for id in &sig.supporting {
if detections.is_detected(id) {
technique_ids.push(id.to_string());
}
}
AttributionResult {
obfuscator_name: sig.name.to_string(),
technique_ids,
supporting_matched,
}
})
.collect();
candidates.sort_by_key(|c| std::cmp::Reverse(c.supporting_matched));
candidates.into_iter().next()
}
#[must_use]
pub fn compute_attributions_all(&self, detections: &Detections) -> Vec<AttributionResult> {
let mut best_by_name: HashMap<&str, AttributionResult> = HashMap::new();
for sig in &self.signatures {
if !sig.required.iter().all(|id| detections.is_detected(id)) {
continue;
}
let supporting_matched = sig
.supporting
.iter()
.filter(|id| detections.is_detected(id))
.count();
let mut technique_ids: Vec<String> =
sig.required.iter().map(|s| s.to_string()).collect();
for id in &sig.supporting {
if detections.is_detected(id) {
technique_ids.push(id.to_string());
}
}
let candidate = AttributionResult {
obfuscator_name: sig.name.to_string(),
technique_ids,
supporting_matched,
};
match best_by_name.get(sig.name) {
Some(existing) if existing.supporting_matched >= supporting_matched => {}
_ => {
best_by_name.insert(sig.name, candidate);
}
}
}
let mut result: Vec<AttributionResult> = best_by_name.into_values().collect();
result.sort_by_key(|r| std::cmp::Reverse(r.supporting_matched));
result
}
}
impl Default for ObfuscatorMatcher {
fn default() -> Self {
Self::with_defaults()
}
}
fn single_technique_signatures(
name: &'static str,
all_ids: &[&'static str],
extra_supporting: &[&'static str],
) -> Vec<ObfuscatorSignature> {
all_ids
.iter()
.map(|&required_id| {
let mut supporting: Vec<&'static str> = all_ids
.iter()
.filter(|&&id| id != required_id)
.copied()
.collect();
supporting.extend_from_slice(extra_supporting);
ObfuscatorSignature {
name,
required: vec![required_id],
supporting,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::deobfuscation::techniques::{
Detection, Detections, ObfuscatorMatcher, ObfuscatorSignature, Technique,
TechniqueCategory, TechniqueRegistry,
};
struct MockTechnique {
id: &'static str,
requires: &'static [&'static str],
supersedes: &'static [&'static str],
}
impl MockTechnique {
fn new(id: &'static str) -> Self {
Self {
id,
requires: &[],
supersedes: &[],
}
}
fn with_requires(id: &'static str, requires: &'static [&'static str]) -> Self {
Self {
id,
requires,
supersedes: &[],
}
}
fn with_supersedes(id: &'static str, supersedes: &'static [&'static str]) -> Self {
Self {
id,
requires: &[],
supersedes,
}
}
}
impl Technique for MockTechnique {
fn id(&self) -> &'static str {
self.id
}
fn name(&self) -> &'static str {
self.id
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Metadata
}
fn detect(&self, _assembly: &crate::CilObject) -> Detection {
Detection::new_empty()
}
fn requires(&self) -> &[&'static str] {
self.requires
}
fn supersedes(&self) -> &[&'static str] {
self.supersedes
}
}
#[test]
fn test_registry_new_is_empty() {
let r = TechniqueRegistry::new();
assert!(!r.has_techniques());
assert!(r.techniques().is_empty());
}
#[test]
fn test_registry_register_and_retrieve() {
let mut r = TechniqueRegistry::new();
r.register(Box::new(MockTechnique::new("test.a")));
r.register(Box::new(MockTechnique::new("test.b")));
assert!(r.has_techniques());
assert_eq!(r.techniques().len(), 2);
assert_eq!(r.techniques()[0].id(), "test.a");
assert_eq!(r.techniques()[1].id(), "test.b");
}
#[test]
fn test_with_defaults_has_techniques() {
let r = TechniqueRegistry::with_defaults();
assert!(r.has_techniques());
assert!(r.techniques().len() >= 30);
}
#[test]
fn test_sorted_techniques_dependency_order() {
let mut r = TechniqueRegistry::new();
r.register(Box::new(MockTechnique::with_requires("b", &["a"])));
r.register(Box::new(MockTechnique::new("a")));
let ds = Detections::new();
let sorted = r.sorted_techniques(&ds);
let ids: Vec<&str> = sorted.iter().map(|t| t.id()).collect();
assert_eq!(ids, vec!["a", "b"]);
}
#[test]
fn test_sorted_techniques_topological_diamond() {
let mut r = TechniqueRegistry::new();
r.register(Box::new(MockTechnique::with_requires("d", &["b", "c"])));
r.register(Box::new(MockTechnique::with_requires("b", &["a"])));
r.register(Box::new(MockTechnique::with_requires("c", &["a"])));
r.register(Box::new(MockTechnique::new("a")));
let ds = Detections::new();
let sorted = r.sorted_techniques(&ds);
let ids: Vec<&str> = sorted.iter().map(|t| t.id()).collect();
let pos = |id: &str| ids.iter().position(|&x| x == id).unwrap();
assert!(pos("a") < pos("b"));
assert!(pos("a") < pos("c"));
assert!(pos("b") < pos("d"));
assert!(pos("c") < pos("d"));
}
#[test]
fn test_sorted_techniques_missing_dependency() {
let mut r = TechniqueRegistry::new();
r.register(Box::new(MockTechnique::new("a")));
r.register(Box::new(MockTechnique::with_requires(
"b",
&["nonexistent"],
)));
let ds = Detections::new();
let sorted = r.sorted_techniques(&ds);
let ids: Vec<&str> = sorted.iter().map(|t| t.id()).collect();
assert!(ids.contains(&"a"));
assert!(ids.contains(&"b"));
}
#[test]
fn test_sorted_techniques_supersedes_filtering() {
let mut r = TechniqueRegistry::new();
r.register(Box::new(MockTechnique::new("old")));
r.register(Box::new(MockTechnique::with_supersedes("new", &["old"])));
let mut ds = Detections::new();
ds.insert("new", Detection::new_detected(vec![], None));
let sorted = r.sorted_techniques(&ds);
let ids: Vec<&str> = sorted.iter().map(|t| t.id()).collect();
assert!(!ids.contains(&"old"));
assert!(ids.contains(&"new"));
}
#[test]
fn test_sorted_techniques_supersedes_not_detected() {
let mut r = TechniqueRegistry::new();
r.register(Box::new(MockTechnique::new("old")));
r.register(Box::new(MockTechnique::with_supersedes("new", &["old"])));
let ds = Detections::new(); let sorted = r.sorted_techniques(&ds);
let ids: Vec<&str> = sorted.iter().map(|t| t.id()).collect();
assert!(ids.contains(&"old"));
assert!(ids.contains(&"new"));
}
#[test]
fn test_registry_default() {
let r = TechniqueRegistry::default();
assert!(r.has_techniques());
assert!(r.techniques().len() >= 30);
}
#[test]
fn test_compute_attribution_no_match() {
let mut m = ObfuscatorMatcher::new();
m.add_signature(ObfuscatorSignature {
name: "TestObfuscator",
required: vec!["test.a"],
supporting: vec![],
});
let ds = Detections::new();
assert!(m.compute_attribution(&ds).is_none());
}
#[test]
fn test_compute_attribution_required_match() {
let mut m = ObfuscatorMatcher::new();
m.add_signature(ObfuscatorSignature {
name: "TestObfuscator",
required: vec!["test.a"],
supporting: vec!["test.b"],
});
let mut ds = Detections::new();
ds.insert("test.a", Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap();
assert_eq!(attr.obfuscator_name, "TestObfuscator");
assert_eq!(attr.supporting_matched, 0);
}
#[test]
fn test_compute_attribution_with_supporting() {
let mut m = ObfuscatorMatcher::new();
m.add_signature(ObfuscatorSignature {
name: "TestObfuscator",
required: vec!["test.a"],
supporting: vec!["test.b", "test.c"],
});
let mut ds = Detections::new();
ds.insert("test.a", Detection::new_detected(vec![], None));
ds.insert("test.b", Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap();
assert_eq!(attr.supporting_matched, 1);
assert!(attr.technique_ids.contains(&"test.a".to_string()));
assert!(attr.technique_ids.contains(&"test.b".to_string()));
}
#[test]
fn test_compute_attribution_best_match_wins() {
let mut m = ObfuscatorMatcher::new();
m.add_signature(ObfuscatorSignature {
name: "Weak",
required: vec!["weak.a"],
supporting: vec![],
});
m.add_signature(ObfuscatorSignature {
name: "Strong",
required: vec!["strong.a"],
supporting: vec!["strong.b", "strong.c"],
});
let mut ds = Detections::new();
ds.insert("weak.a", Detection::new_detected(vec![], None));
ds.insert("strong.a", Detection::new_detected(vec![], None));
ds.insert("strong.b", Detection::new_detected(vec![], None));
ds.insert("strong.c", Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap();
assert_eq!(attr.obfuscator_name, "Strong");
assert_eq!(attr.supporting_matched, 2);
}
#[test]
fn test_compute_attributions_all() {
let mut m = ObfuscatorMatcher::new();
m.add_signature(ObfuscatorSignature {
name: "Alpha",
required: vec!["alpha.a"],
supporting: vec![],
});
m.add_signature(ObfuscatorSignature {
name: "Beta",
required: vec!["beta.a"],
supporting: vec!["beta.b"],
});
let mut ds = Detections::new();
ds.insert("alpha.a", Detection::new_detected(vec![], None));
ds.insert("beta.a", Detection::new_detected(vec![], None));
ds.insert("beta.b", Detection::new_detected(vec![], None));
let attrs = m.compute_attributions_all(&ds);
assert_eq!(attrs.len(), 2);
assert_eq!(attrs[0].obfuscator_name, "Beta");
assert_eq!(attrs[1].obfuscator_name, "Alpha");
}
#[test]
fn test_matcher_default() {
let m = ObfuscatorMatcher::default();
assert!(m.compute_attribution(&Detections::new()).is_none());
}
#[test]
fn test_matcher_with_defaults_has_signatures() {
let m = ObfuscatorMatcher::with_defaults();
let mut ds = Detections::new();
ds.insert("confuserex.marker", Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap();
assert_eq!(attr.obfuscator_name, "ConfuserEx");
let mut ds = Detections::new();
ds.insert("bitmono.strings", Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap();
assert_eq!(attr.obfuscator_name, "BitMono");
let mut ds = Detections::new();
ds.insert("jiejienet.constants", Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap();
assert_eq!(attr.obfuscator_name, "JIEJIE.NET");
for nr_id in &[
"netreactor.necrobit",
"netreactor.antitrial",
"netreactor.antitamp",
"netreactor.licensecheck",
"netreactor.privateimpl",
"netreactor.resources",
] {
let mut ds = Detections::new();
ds.insert(*nr_id, Detection::new_detected(vec![], None));
let attr = m.compute_attribution(&ds).unwrap_or_else(|| {
panic!("Single NR technique {nr_id} should attribute to .NET Reactor")
});
assert_eq!(
attr.obfuscator_name, ".NET Reactor",
"{nr_id} should attribute to .NET Reactor"
);
}
}
}