use std::{collections::HashSet, sync::Arc};
use crate::{
cilassembly::CleanupRequest,
compiler::{PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::{SentinelCondition, SentinelTaintRemovalPass},
techniques::{Detection, Evidence, Technique, TechniqueCategory},
},
metadata::token::Token,
CilObject,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CxAntiDebugMode {
Safe,
Win32,
Antinet,
Unknown,
}
#[derive(Debug)]
pub struct AntiDebugFindings {
pub method_tokens: HashSet<Token>,
pub include_module_cctor: bool,
pub mode: CxAntiDebugMode,
}
pub struct ConfuserExAntiDebug;
impl Technique for ConfuserExAntiDebug {
fn id(&self) -> &'static str {
"confuserex.debug"
}
fn name(&self) -> &'static str {
"ConfuserEx Anti-Debug Neutralisation"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Neutralization
}
fn supersedes(&self) -> &[&'static str] {
&["generic.debug"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
let mut method_tokens = HashSet::new();
let mut has_safe_mode = false;
let mut has_win32_mode = false;
for method_entry in assembly.methods() {
let method = method_entry.value();
let mut calls_is_attached = false;
let mut calls_is_logging = false;
let mut calls_failfast = false;
let mut calls_is_debugger_present = false;
let mut calls_nt_query = false;
let mut calls_set_is_background = false;
let mut calls_thread_ctor = false;
for instr in method.instructions() {
if let Some(token) = instr.get_token_operand() {
if let Some(name) = assembly.resolve_method_name(token) {
match name.as_str() {
"get_IsAttached" => calls_is_attached = true,
"IsLogging" => calls_is_logging = true,
"FailFast" => calls_failfast = true,
"IsDebuggerPresent" => calls_is_debugger_present = true,
"NtQueryInformationProcess" => calls_nt_query = true,
"set_IsBackground" => calls_set_is_background = true,
".ctor" => {
if let Some(member) = assembly.member_ref(&token) {
if let Some(type_name) = member.declaredby.fullname() {
if type_name.contains("Thread") {
calls_thread_ctor = true;
}
}
}
}
_ => {}
}
}
}
}
let creates_background_thread = calls_thread_ctor && calls_set_is_background;
let is_safe = (calls_is_attached || calls_is_logging) && calls_failfast;
let is_win32 = calls_is_debugger_present || calls_nt_query;
let has_indicator =
is_safe || is_win32 || (calls_failfast && creates_background_thread);
if has_indicator {
method_tokens.insert(method.token);
if is_win32 {
has_win32_mode = true;
}
if is_safe {
has_safe_mode = true;
}
}
}
if method_tokens.is_empty() {
return Detection::new_empty();
}
let mode = if has_win32_mode {
CxAntiDebugMode::Win32
} else if has_safe_mode {
CxAntiDebugMode::Safe
} else {
CxAntiDebugMode::Unknown
};
let include_module_cctor = !method_tokens.is_empty();
let count = method_tokens.len();
let mode_name = match mode {
CxAntiDebugMode::Safe => "Safe",
CxAntiDebugMode::Win32 => "Win32",
CxAntiDebugMode::Antinet => "Antinet",
CxAntiDebugMode::Unknown => "Unknown",
};
let mut detection = Detection::new_detected(
vec![Evidence::BytecodePattern(format!(
"{count} anti-debug methods ({mode_name} mode)",
))],
None,
);
for token in &method_tokens {
detection.cleanup_mut().add_method(*token);
}
detection.set_findings(Box::new(AntiDebugFindings {
method_tokens,
include_module_cctor,
mode,
}));
detection
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Simplify)
}
fn create_pass(
&self,
_ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<AntiDebugFindings>() else {
return Vec::new();
};
if findings.method_tokens.is_empty() {
return Vec::new();
}
let (sentinels, condition): (Vec<&'static str>, SentinelCondition) = match findings.mode {
CxAntiDebugMode::Safe => (
vec!["get_IsAttached", "IsLogging", "FailFast"],
SentinelCondition::All,
),
CxAntiDebugMode::Win32 => (
vec![
"IsDebuggerPresent",
"NtQueryInformationProcess",
"GetCurrentProcess",
"FailFast",
],
SentinelCondition::AtLeast(2),
),
_ => (
vec!["get_IsAttached", "IsLogging", "FailFast"],
SentinelCondition::All,
),
};
vec![Box::new(SentinelTaintRemovalPass::new(
"ConfuserExAntiDebug",
"Removes ConfuserEx anti-debug checks via taint analysis",
findings.method_tokens.clone(),
sentinels,
condition,
))]
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<AntiDebugFindings>()?;
if findings.method_tokens.is_empty() {
return None;
}
let mut request = CleanupRequest::new();
for token in &findings.method_tokens {
request.add_method(*token);
}
Some(request)
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
confuserex::debug::{AntiDebugFindings, ConfuserExAntiDebug},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_maximum.exe");
let technique = ConfuserExAntiDebug;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"ConfuserExAntiDebug should detect anti-debug in mkaring_maximum.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should have evidence"
);
let findings = detection
.findings::<AntiDebugFindings>()
.expect("Should have AntiDebugFindings");
assert!(
!findings.method_tokens.is_empty(),
"Should have anti-debug method tokens"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ConfuserExAntiDebug;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExAntiDebug should not detect anti-debug in original.exe"
);
}
}