dotscope 0.6.0

A high-performance, cross-platform framework for analyzing and reverse engineering .NET PE executables
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
//! ConfuserEx Anti-Debug Protection Detection and Neutralization
//!
//! This module provides detection and neutralization for ConfuserEx's anti-debug
//! protection, which prevents debugging and profiling of the protected assembly.
//!
//! # Source Code Analysis
//!
//! Based on analysis of the ConfuserEx source code at:
//! - `Confuser.Protections/AntiDebugProtection.cs` - Protection entry point
//! - `Confuser.Runtime/AntiDebug.Safe.cs` - Safe mode (managed-only)
//! - `Confuser.Runtime/AntiDebug.Win32.cs` - Win32 mode (P/Invoke)
//! - `Confuser.Runtime/AntiDebug.Antinet.cs` - Antinet mode (.NET-specific)
//!
//! # Protection Preset
//!
//! Anti-debug is part of the **Minimum** preset:
//! ```csharp
//! // From AntiDebugProtection.cs line 34:
//! public override ProtectionPreset Preset {
//!     get { return ProtectionPreset.Minimum; }
//! }
//! ```
//!
//! # Protection Modes
//!
//! ## 1. Safe Mode (`AntiMode.Safe`) - Default
//!
//! **Source:** `Confuser.Runtime/AntiDebug.Safe.cs`
//!
//! Uses only managed APIs, making it cross-platform compatible:
//!
//! **Initialization checks:**
//! ```csharp
//! // Check for profiler via environment variable
//! if ("1".Equals(Environment.GetEnvironmentVariable("COR_ENABLE_PROFILING")))
//!     Environment.FailFast(null);
//! ```
//!
//! **Background thread checks (runs every 1 second):**
//! ```csharp
//! while (true) {
//!     if (Debugger.IsAttached || Debugger.IsLogging())
//!         Environment.FailFast(null);
//!     if (!th.IsAlive)  // Anti-tampering of the check thread
//!         Environment.FailFast(null);
//!     Thread.Sleep(1000);
//! }
//! ```
//!
//! **Detection signature:**
//! - Call to `Debugger.IsAttached`
//! - Call to `Debugger.IsLogging`
//! - Call to `Environment.FailFast`
//! - Call to `Environment.GetEnvironmentVariable` with "COR" prefix strings
//! - Background thread with `IsBackground = true`
//!
//! ## 2. Win32 Mode (`AntiMode.Win32`)
//!
//! **Source:** `Confuser.Runtime/AntiDebug.Win32.cs`
//!
//! Uses Windows-specific P/Invoke for stronger detection:
//!
//! **P/Invoke declarations:**
//! ```csharp
//! [DllImport("ntdll.dll")]
//! private static extern int NtQueryInformationProcess(...);
//!
//! [DllImport("kernel32.dll")]
//! static extern bool CloseHandle(IntPtr hObject);
//!
//! [DllImport("kernel32.dll")]
//! static extern bool IsDebuggerPresent();
//!
//! [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
//! static extern int OutputDebugString(string str);
//! ```
//!
//! **Anti-dnSpy check:**
//! ```csharp
//! Process here = GetParentProcess();
//! if (here != null && here.ProcessName.ToLower().Contains("dnspy"))
//!     Environment.FailFast("");
//! ```
//!
//! **Background thread checks:**
//! - `Debugger.IsAttached` and `Debugger.IsLogging()` (managed)
//! - `IsDebuggerPresent()` (native)
//! - `Process.GetCurrentProcess().Handle == IntPtr.Zero` (handle check)
//! - `OutputDebugString("") > IntPtr.Size` (debug output check)
//! - `CloseHandle(IntPtr.Zero)` throws if debugger (exception check)
//!
//! **Detection signature:**
//! - P/Invoke for `IsDebuggerPresent`, `NtQueryInformationProcess`, `CloseHandle`, `OutputDebugString`
//! - Call to `Process.GetCurrentProcess()`
//! - Parent process name check for "dnspy"
//!
//! ## 3. Antinet Mode (`AntiMode.Antinet`)
//!
//! **Source:** `Confuser.Runtime/AntiDebug.Antinet.cs`
//!
//! Uses .NET-specific techniques to detect managed debuggers:
//!
//! ```csharp
//! static void Initialize() {
//!     if (!InitializeAntiDebugger())
//!         Environment.FailFast(null);
//!     InitializeAntiProfiler();
//!     if (IsProfilerAttached) {
//!         Environment.FailFast(null);
//!         PreventActiveProfilerFromReceivingProfilingMessages();
//!     }
//! }
//! ```
//!
//! **Detection signature:**
//! - Methods named `InitializeAntiDebugger`, `InitializeAntiProfiler`
//! - Field named `IsProfilerAttached`
//! - Uses `HandleProcessCorruptedStateExceptionsAttribute`
//!
//! # Injection Point
//!
//! Anti-debug initialization is injected at the beginning of `<Module>::.cctor`:
//! ```csharp
//! // From AntiDebugProtection.cs line 94:
//! cctor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, init));
//! ```
//!
//! # Neutralization Strategy
//!
//! 1. **Debugger checks**: Replace with constant false so the check
//!    always succeeds (debugger appears not attached)
//! 2. **FailFast calls**: Replace with no-op (constant load)
//!    to prevent termination
//!
//! # Limitations
//!
//! - P/Invoke calls to `IsDebuggerPresent` require external resolution
//!   and are not handled in SSA form (handled by detection instead)
//! - Worker threads that continuously check are marked for removal
//!   via dead method elimination
//!
//! # Test Samples
//!
//! | Sample | Has Anti-Debug | Mode | Notes |
//! |--------|----------------|------|-------|
//! | `original.exe` | No | N/A | Unprotected |
//! | `mkaring_minimal.exe` | Yes | Safe | Minimum preset |
//! | `mkaring_normal.exe` | Yes | Safe | Normal preset |
//! | `mkaring_maximum.exe` | Yes | Safe | Maximum preset |

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,
};

/// Detected anti-debug mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntiDebugMode {
    /// Safe mode: managed-only APIs (Debugger.IsAttached, IsLogging, FailFast)
    Safe,

    /// Win32 mode: P/Invoke (IsDebuggerPresent, NtQueryInformationProcess, etc.)
    Win32,

    /// Antinet mode: .NET-specific anti-debug and anti-profiler
    Antinet,

    /// Unknown mode: anti-debug detected but mode couldn't be determined
    Unknown,
}

/// Result of anti-debug detection for a single method.
#[derive(Debug, Clone)]
pub struct AntiDebugMethodInfo {
    /// The method token.
    pub token: Token,
    /// Detected anti-debug mode.
    pub mode: AntiDebugMode,
    /// Calls Debugger.IsAttached
    pub calls_is_attached: bool,
    /// Calls Debugger.IsLogging
    pub calls_is_logging: bool,
    /// Calls Environment.FailFast
    pub calls_failfast: bool,
    /// Calls IsDebuggerPresent (Win32)
    pub calls_is_debugger_present: bool,
    /// Calls NtQueryInformationProcess (Win32)
    pub calls_nt_query: bool,
    /// Calls Process.GetCurrentProcess
    pub calls_get_current_process: bool,
    /// Creates background thread (Thread constructor + IsBackground)
    pub creates_background_thread: bool,
}

/// Result of anti-debug detection.
#[derive(Debug, Default)]
pub struct AntiDebugDetectionResult {
    /// Methods detected as anti-debug.
    pub methods: Vec<AntiDebugMethodInfo>,
    /// Detected mode (most confident).
    pub detected_mode: Option<AntiDebugMode>,
    /// P/Invoke methods found for anti-debug APIs.
    pub pinvoke_methods: Vec<Token>,
}

impl AntiDebugDetectionResult {
    /// Returns true if any anti-debug protection was detected.
    pub fn is_detected(&self) -> bool {
        !self.methods.is_empty()
    }
}

/// Detects anti-debug protection and returns detailed results.
///
/// This performs comprehensive detection of ConfuserEx anti-debug protection
/// and returns structured results with mode information.
pub fn detect_antidebug(assembly: &CilObject) -> AntiDebugDetectionResult {
    let mut result = AntiDebugDetectionResult::default();

    // Detect P/Invoke methods for anti-debug APIs
    result.pinvoke_methods = find_antidebug_pinvokes(assembly);

    // Detect anti-debug methods
    result.methods = find_antidebug_methods(assembly);

    // Determine mode
    result.detected_mode = determine_mode(&result);

    result
}

/// Adds detection evidence to the score from detailed results.
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,
        });
    }
}

/// Finds P/Invoke methods for anti-debug APIs.
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
}

/// Finds methods that appear to be anti-debug methods using CFG analysis.
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" => {
                                    // Check if it's Thread constructor
                                    if token.is_table(TableId::MemberRef) {
                                        // 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;

        // Determine if this looks like an anti-debug method
        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
}

/// Determines the most likely anti-debug mode.
fn determine_mode(result: &AntiDebugDetectionResult) -> Option<AntiDebugMode> {
    if result.methods.is_empty() {
        return None;
    }

    // Win32 mode has the strongest indicators
    if result
        .methods
        .iter()
        .any(|m| m.mode == AntiDebugMode::Win32)
        || !result.pinvoke_methods.is_empty()
    {
        return Some(AntiDebugMode::Win32);
    }

    // Safe mode is the default
    if result.methods.iter().any(|m| m.mode == AntiDebugMode::Safe) {
        return Some(AntiDebugMode::Safe);
    }

    Some(AntiDebugMode::Unknown)
}

/// Detects anti-debug protection patterns and populates findings.
///
/// This is called by the orchestrator in detection.rs to detect
/// ConfuserEx anti-debug protection.
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
    let result = detect_antidebug(assembly);

    // Populate findings
    for method_info in &result.methods {
        findings.anti_debug_methods.push(method_info.token);
    }

    // Add detection evidence
    add_antidebug_evidence(&result, score);
}

/// Anti-debug neutralization pass for ConfuserEx.
///
/// This pass identifies and neutralizes anti-debug checks in SSA form.
/// It should run during the cleanup phase after SSA normalization.
///
/// The pass also handles `.cctor` cleanup for anti-debug patterns that
/// are injected into the module static constructor.
pub struct ConfuserExAntiDebugPass {
    /// Tokens of anti-debug methods (from detection findings).
    anti_debug_method_tokens: HashSet<Token>,
    /// Whether to also process module .cctor for anti-debug patterns.
    include_module_cctor: bool,
    /// Cached token for the module .cctor (populated on first use).
    module_cctor_token: OnceLock<Option<Token>>,
}

impl Default for ConfuserExAntiDebugPass {
    fn default() -> Self {
        Self::new()
    }
}

impl ConfuserExAntiDebugPass {
    /// Creates a new anti-debug pass.
    #[must_use]
    pub fn new() -> Self {
        Self {
            anti_debug_method_tokens: HashSet::new(),
            include_module_cctor: false,
            module_cctor_token: OnceLock::new(),
        }
    }

    /// Creates a new pass with known anti-debug method tokens.
    ///
    /// These tokens come from the detection phase and help the pass
    /// focus on methods that actually contain anti-debug code.
    #[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(); // Include .cctor if any anti-debug detected
        Self {
            anti_debug_method_tokens: tokens,
            include_module_cctor: include_cctor,
            module_cctor_token: OnceLock::new(),
        }
    }

    /// Gets the module .cctor token, lazily initializing if needed.
    fn get_module_cctor(&self, assembly: &CilObject) -> Option<Token> {
        *self
            .module_cctor_token
            .get_or_init(|| assembly.types().module_cctor())
    }

    /// Checks if a method call is a debugger check.
    fn is_debugger_check(method_name: &str) -> bool {
        method_name.contains("Debugger")
            && (method_name.contains("IsAttached") || method_name.contains("IsLogging"))
    }

    /// Checks if a method call is Environment.FailFast.
    fn is_fail_fast(method_name: &str) -> bool {
        method_name.contains("Environment") && method_name.contains("FailFast")
    }

    /// Checks if a method call is Environment.GetEnvironmentVariable.
    fn is_env_var_check(method_name: &str) -> bool {
        method_name.contains("Environment") && method_name.contains("GetEnvironmentVariable")
    }

    /// Checks if a method call is Type.GetMethod (reflection pattern used in anti-debug).
    fn is_reflection_get_method(method_name: &str) -> bool {
        method_name.contains("Type") && method_name.contains("GetMethod")
    }

    /// Checks if a method call is MethodBase.Invoke (reflection pattern used in anti-debug).
    fn is_reflection_invoke(method_name: &str) -> bool {
        (method_name.contains("MethodBase") || method_name.contains("MethodInfo"))
            && method_name.contains("Invoke")
    }

    /// Neutralizes anti-debug operations in a single SSA function.
    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() {
                    // Check for Call operations
                    SsaOp::Call { dest, method, .. } | SsaOp::CallVirt { dest, method, .. } => {
                        // Resolve method token to full name for pattern matching
                        let method_name = utils::get_type_name_from_token(assembly, method.token())
                            .unwrap_or_else(|| format!("{method}"));
                        let dest = *dest;
                        let method_ref = *method;

                        // Replace debugger checks with constant false
                        if Self::is_debugger_check(&method_name) {
                            if let Some(dest_var) = dest {
                                // Replace with: dest = const false
                                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"
                                    ));
                            }
                        }
                        // Replace FailFast with no-op (load null)
                        else if Self::is_fail_fast(&method_name) {
                            // For FailFast, we need to replace it with something harmless.
                            // Use a dummy variable if no dest, otherwise use the existing dest.
                            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}"));
                        }
                        // Replace Environment.GetEnvironmentVariable with null
                        // This neutralizes COR_ENABLE_PROFILING checks
                        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"
                                    ));
                            }
                        }
                        // Replace reflection GetMethod with null to break reflection-based checks
                        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"
                                    ));
                            }
                        }
                        // Replace reflection Invoke with null to break reflection-based execution
                        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 {
        // Run on known anti-debug methods
        if self.anti_debug_method_tokens.contains(&method_token) {
            return true;
        }

        // If we're including module .cctor, we need to run on all methods
        // (we'll filter in run_on_method since we don't have assembly here)
        if self.include_module_cctor {
            return true;
        }

        // If no specific tokens, run on all methods
        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> {
        // Check if we should process this method
        let is_target_method = self.anti_debug_method_tokens.contains(&method_token);

        // Also check if this is the module .cctor when include_module_cctor is set
        let is_module_cctor = if self.include_module_cctor && !is_target_method {
            // Use OnceLock to lazily find and cache the .cctor token
            let cctor_token = self.get_module_cctor(assembly);
            cctor_token == Some(method_token)
        } else {
            false
        };

        // Skip if not a target method and not the module .cctor
        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(())
    }
}