use std::{
collections::HashSet,
sync::{Arc, OnceLock},
};
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp, SsaVarId},
assembly::{opcodes, Operand},
compiler::{CompilerContext, EventKind, EventLog, SsaPass},
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
obfuscators::confuserex::utils,
},
metadata::{tables::TableId, token::Token, typesystem::CilTypeReference},
CilObject, Result,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntiDebugMode {
Safe,
Win32,
Antinet,
Unknown,
}
#[derive(Debug, Clone)]
pub struct AntiDebugMethodInfo {
pub token: Token,
pub mode: AntiDebugMode,
pub calls_is_attached: bool,
pub calls_is_logging: bool,
pub calls_failfast: bool,
pub calls_is_debugger_present: bool,
pub calls_nt_query: bool,
pub calls_get_current_process: bool,
pub creates_background_thread: bool,
}
#[derive(Debug, Default)]
pub struct AntiDebugDetectionResult {
pub methods: Vec<AntiDebugMethodInfo>,
pub detected_mode: Option<AntiDebugMode>,
pub pinvoke_methods: Vec<Token>,
}
impl AntiDebugDetectionResult {
pub fn is_detected(&self) -> bool {
!self.methods.is_empty()
}
}
pub fn detect_antidebug(assembly: &CilObject) -> AntiDebugDetectionResult {
let mut result = AntiDebugDetectionResult::default();
result.pinvoke_methods = find_antidebug_pinvokes(assembly);
result.methods = find_antidebug_methods(assembly);
result.detected_mode = determine_mode(&result);
result
}
pub fn add_antidebug_evidence(result: &AntiDebugDetectionResult, score: &DetectionScore) {
if !result.methods.is_empty() {
let locations: boxcar::Vec<Token> = boxcar::Vec::new();
for m in &result.methods {
locations.push(m.token);
}
let mode_name = match result.detected_mode {
Some(AntiDebugMode::Safe) => "Safe",
Some(AntiDebugMode::Win32) => "Win32",
Some(AntiDebugMode::Antinet) => "Antinet",
Some(AntiDebugMode::Unknown) | None => "Unknown",
};
let confidence = (result.methods.len() * 20).min(40);
score.add(DetectionEvidence::BytecodePattern {
name: format!(
"ConfuserEx anti-debug ({} mode, {} methods)",
mode_name,
result.methods.len()
),
locations,
confidence,
});
}
}
fn find_antidebug_pinvokes(assembly: &CilObject) -> Vec<Token> {
let antidebug_apis = [
"IsDebuggerPresent",
"NtQueryInformationProcess",
"CloseHandle",
"OutputDebugString",
"CheckRemoteDebuggerPresent",
];
let pinvokes = assembly
.query_methods()
.native()
.filter(|m| antidebug_apis.iter().any(|api| m.name == *api))
.tokens();
pinvokes
}
fn find_antidebug_methods(assembly: &CilObject) -> Vec<AntiDebugMethodInfo> {
let mut found = Vec::new();
for method in &assembly.query_methods().has_body() {
let Some(cfg) = method.cfg() else {
continue;
};
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_get_current_process = false;
let mut calls_set_is_background = false;
let mut calls_thread_ctor = false;
for node_id in cfg.node_ids() {
let Some(block) = cfg.block(node_id) else {
continue;
};
for instr in &block.instructions {
if instr.opcode == opcodes::CALL || instr.opcode == opcodes::CALLVIRT {
if let Operand::Token(token) = &instr.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,
"GetCurrentProcess" => calls_get_current_process = true,
"set_IsBackground" => calls_set_is_background = true,
".ctor" => {
if token.is_table(TableId::MemberRef) {
if let Some(member) = assembly.member_ref(token) {
if let CilTypeReference::TypeRef(type_ref) =
&member.declaredby
{
if let Some(name) = type_ref.name() {
if name.contains("Thread") {
calls_thread_ctor = true;
}
}
}
}
}
}
_ => {}
}
}
}
}
}
}
let creates_background_thread = calls_thread_ctor && calls_set_is_background;
let is_safe_mode = (calls_is_attached || calls_is_logging) && calls_failfast;
let is_win32_mode = calls_is_debugger_present || calls_nt_query;
let has_any_indicator =
is_safe_mode || is_win32_mode || (calls_failfast && creates_background_thread);
if has_any_indicator {
let mode = if is_win32_mode {
AntiDebugMode::Win32
} else if is_safe_mode {
AntiDebugMode::Safe
} else {
AntiDebugMode::Unknown
};
found.push(AntiDebugMethodInfo {
token: method.token,
mode,
calls_is_attached,
calls_is_logging,
calls_failfast,
calls_is_debugger_present,
calls_nt_query,
calls_get_current_process,
creates_background_thread,
});
}
}
found
}
fn determine_mode(result: &AntiDebugDetectionResult) -> Option<AntiDebugMode> {
if result.methods.is_empty() {
return None;
}
if result
.methods
.iter()
.any(|m| m.mode == AntiDebugMode::Win32)
|| !result.pinvoke_methods.is_empty()
{
return Some(AntiDebugMode::Win32);
}
if result.methods.iter().any(|m| m.mode == AntiDebugMode::Safe) {
return Some(AntiDebugMode::Safe);
}
Some(AntiDebugMode::Unknown)
}
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
let result = detect_antidebug(assembly);
for method_info in &result.methods {
findings.anti_debug_methods.push(method_info.token);
}
add_antidebug_evidence(&result, score);
}
pub struct ConfuserExAntiDebugPass {
anti_debug_method_tokens: HashSet<Token>,
include_module_cctor: bool,
module_cctor_token: OnceLock<Option<Token>>,
}
impl Default for ConfuserExAntiDebugPass {
fn default() -> Self {
Self::new()
}
}
impl ConfuserExAntiDebugPass {
#[must_use]
pub fn new() -> Self {
Self {
anti_debug_method_tokens: HashSet::new(),
include_module_cctor: false,
module_cctor_token: OnceLock::new(),
}
}
#[must_use]
pub fn with_methods(tokens: impl IntoIterator<Item = Token>) -> Self {
let tokens: HashSet<_> = tokens.into_iter().collect();
let include_cctor = !tokens.is_empty(); Self {
anti_debug_method_tokens: tokens,
include_module_cctor: include_cctor,
module_cctor_token: OnceLock::new(),
}
}
fn get_module_cctor(&self, assembly: &CilObject) -> Option<Token> {
*self
.module_cctor_token
.get_or_init(|| assembly.types().module_cctor())
}
fn is_debugger_check(method_name: &str) -> bool {
method_name.contains("Debugger")
&& (method_name.contains("IsAttached") || method_name.contains("IsLogging"))
}
fn is_fail_fast(method_name: &str) -> bool {
method_name.contains("Environment") && method_name.contains("FailFast")
}
fn is_env_var_check(method_name: &str) -> bool {
method_name.contains("Environment") && method_name.contains("GetEnvironmentVariable")
}
fn is_reflection_get_method(method_name: &str) -> bool {
method_name.contains("Type") && method_name.contains("GetMethod")
}
fn is_reflection_invoke(method_name: &str) -> bool {
(method_name.contains("MethodBase") || method_name.contains("MethodInfo"))
&& method_name.contains("Invoke")
}
fn neutralize_antidebug(
ssa: &mut SsaFunction,
method_token: Token,
assembly: &CilObject,
changeset: &mut EventLog,
) {
for block in ssa.blocks_mut() {
for instr in block.instructions_mut() {
match instr.op() {
SsaOp::Call { dest, method, .. } | SsaOp::CallVirt { dest, method, .. } => {
let method_name = utils::get_type_name_from_token(assembly, method.token())
.unwrap_or_else(|| format!("{method}"));
let dest = *dest;
let method_ref = *method;
if Self::is_debugger_check(&method_name) {
if let Some(dest_var) = dest {
instr.set_op(SsaOp::Const {
dest: dest_var,
value: ConstValue::from_bool(false),
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!(
"Neutralized debugger check: {method_ref} -> false"
));
}
}
else if Self::is_fail_fast(&method_name) {
let dummy_dest = dest.unwrap_or_else(SsaVarId::new);
instr.set_op(SsaOp::Const {
dest: dummy_dest,
value: ConstValue::Null,
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!("Neutralized FailFast call: {method_ref}"));
}
else if Self::is_env_var_check(&method_name) {
if let Some(dest_var) = dest {
instr.set_op(SsaOp::Const {
dest: dest_var,
value: ConstValue::Null,
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!(
"Neutralized environment variable check: {method_ref} -> null"
));
}
}
else if Self::is_reflection_get_method(&method_name) {
if let Some(dest_var) = dest {
instr.set_op(SsaOp::Const {
dest: dest_var,
value: ConstValue::Null,
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!(
"Neutralized reflection GetMethod: {method_ref} -> null"
));
}
}
else if Self::is_reflection_invoke(&method_name) {
let dummy_dest = dest.unwrap_or_else(SsaVarId::new);
instr.set_op(SsaOp::Const {
dest: dummy_dest,
value: ConstValue::Null,
});
changeset
.record(EventKind::InstructionRemoved)
.method(method_token)
.message(format!("Neutralized reflection Invoke: {method_ref}"));
}
}
_ => {}
}
}
}
}
}
impl SsaPass for ConfuserExAntiDebugPass {
fn name(&self) -> &'static str {
"ConfuserExAntiDebug"
}
fn should_run(&self, method_token: Token, _ctx: &CompilerContext) -> bool {
if self.anti_debug_method_tokens.contains(&method_token) {
return true;
}
if self.include_module_cctor {
return true;
}
self.anti_debug_method_tokens.is_empty()
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
assembly: &Arc<CilObject>,
) -> Result<bool> {
let is_target_method = self.anti_debug_method_tokens.contains(&method_token);
let is_module_cctor = if self.include_module_cctor && !is_target_method {
let cctor_token = self.get_module_cctor(assembly);
cctor_token == Some(method_token)
} else {
false
};
if !is_target_method && !is_module_cctor && !self.anti_debug_method_tokens.is_empty() {
return Ok(false);
}
let mut changes = EventLog::new();
Self::neutralize_antidebug(ssa, method_token, assembly, &mut changes);
let changed = !changes.is_empty();
if changed {
ctx.events.merge(&changes);
}
Ok(changed)
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::obfuscators::confuserex::antidebug::{
detect_antidebug, ConfuserExAntiDebugPass,
},
CilObject, ValidationConfig,
};
const SAMPLES_DIR: &str = "tests/samples/packers/confuserex";
#[test]
fn test_is_debugger_check() {
assert!(ConfuserExAntiDebugPass::is_debugger_check(
"System.Diagnostics.Debugger::get_IsAttached"
));
assert!(ConfuserExAntiDebugPass::is_debugger_check(
"System.Diagnostics.Debugger::IsLogging"
));
assert!(!ConfuserExAntiDebugPass::is_debugger_check(
"System.Console::WriteLine"
));
}
#[test]
fn test_is_fail_fast() {
assert!(ConfuserExAntiDebugPass::is_fail_fast(
"System.Environment::FailFast"
));
assert!(!ConfuserExAntiDebugPass::is_fail_fast(
"System.Environment::Exit"
));
}
#[test]
fn test_original_no_antidebug() -> crate::Result<()> {
let path = format!("{}/original.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antidebug(&assembly);
assert!(!result.is_detected(), "Original should have no anti-debug");
Ok(())
}
#[test]
fn test_normal_has_antidebug() -> crate::Result<()> {
let path = format!("{}/mkaring_normal.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antidebug(&assembly);
eprintln!("Anti-debug methods found: {}", result.methods.len());
for m in &result.methods {
eprintln!(
" 0x{:08X}: mode={:?}, IsAttached={}, IsLogging={}, FailFast={}, BGThread={}",
m.token.value(),
m.mode,
m.calls_is_attached,
m.calls_is_logging,
m.calls_failfast,
m.creates_background_thread
);
}
eprintln!("Detected mode: {:?}", result.detected_mode);
assert!(result.is_detected(), "Normal preset should have anti-debug");
Ok(())
}
}