Problem
The default anti-debug mode (Safe) does not prevent dnSpy from running or debugging protected assemblies. A user can open an obfuscated exe in dnSpy, hit "Start" or even F5 (Debug), and the app runs normally.
Root Cause Analysis
Three modes exist
| Mode |
Checks |
Strength |
Default? |
| Safe |
`Debugger.IsAttached`, `Debugger.IsLogging()`, `COR_ENABLE_PROFILING` env var |
Weak |
Yes |
| Win32 |
All Safe checks + native `IsDebuggerPresent()`, parent process name "dnspy", `OutputDebugString`, `CloseHandle` trap |
Medium |
No |
| Antinet |
Kills the CLR debugger thread by manipulating `DebuggerRCThread` memory + profiler detection |
Strong |
No |
Why Safe mode fails
- dnSpy "Start without debugging": `Debugger.IsAttached` is `false` — the app runs freely
- dnSpy "Debug" (F5): The check runs on a background thread with a 500ms initial delay + 1s polling interval. dnSpy attaches before the first check, and there is a race window. Even when detected, the app has already started
- No startup-time check: `Initialize()` spawns a thread and returns immediately — the anti-debug check is async, not blocking
Why Win32 mode is better but still incomplete
- Checks `IsDebuggerPresent()` (native API, harder to bypass than managed)
- Checks parent process name for "dnspy" — but trivially bypassed by renaming dnspy.exe
- Same async background thread model with the same race window
- P/Invoke to kernel32.dll — incompatible with .NET Core/5+/6/7/8/10 on non-Windows
Why Antinet mode is strongest but most limited
- Kills the CLR debugger thread at the runtime level (de4dot's technique)
- Hard-coded CLR 2.0 and 4.0 memory offsets — will not work on .NET Core/5+
- Uses `HandleProcessCorruptedStateExceptions` which is ignored on .NET Core
- Memory scanning approach is fragile across CLR versions
Weaknesses Summary
| Weakness |
Affects |
Severity |
| Safe mode is the default, trivially bypassed |
All users |
High |
| All modes use async polling (500ms delay) |
All modes |
Medium |
| Parent process check is name-based (rename bypass) |
Win32 |
Medium |
| Win32 P/Invoke is Windows-only |
.NET Core cross-platform |
Medium |
| Antinet hard-codes CLR offsets |
.NET Core/5+ |
High |
| No anti-attach prevention (only detection) |
All modes |
Medium |
| `Environment.FailFast(null)` is the only response |
All modes |
Low |
Proposed Improvements
Phase 1 — Quick wins (low risk)
- Change default mode from Safe to Win32 — catches dnSpy parent process and native debugger
- Move initial check to startup (blocking) — check before the background thread, inside `Initialize()` directly, so the app never starts if a debugger is attached
- Reduce polling interval — 1000ms is too slow, 100-200ms is more appropriate
- Remove the 500ms initial sleep in the Worker thread — check immediately
Phase 2 — Hardening (medium risk)
- Hash-based process detection instead of name-based — hash the parent process exe bytes, not just the name
- Check debug port via `NtQueryInformationProcess(ProcessDebugPort)` — returns non-zero if any debugger is attached, regardless of type
- Timing checks — measure execution time of known operations; debugger stepping causes measurable slowdowns
- Hardware breakpoint detection — read DR0-DR7 debug registers via `GetThreadContext`
- PEB.BeingDebugged — read directly from PEB structure, harder to hook than `IsDebuggerPresent`
Phase 3 — Modern .NET support (high value)
- Create a .NET Core-compatible anti-debug mode — can't use Antinet's CLR internals approach. Use:
- `Debugger.IsAttached` (still works on .NET Core)
- `EventPipe` / diagnostics port detection
- `DOTNET_EnableDiagnostics` environment variable check
- Native `IsDebuggerPresent` via `[DllImport]` (Windows) or `ptrace(PTRACE_TRACEME)` (Linux)
- Auto-select mode based on target framework — detect if the target assembly is .NET Framework or .NET Core and choose the appropriate anti-debug implementation
Phase 4 — Response diversity (low risk, high impact)
- Vary the response — instead of always calling `Environment.FailFast()`, randomly choose between: silent exit, corrupted output, delayed crash, infinite loop, throw misleading exception
- Obfuscate the anti-debug code itself — the Initialize() call in .cctor is a known pattern that deobfuscators strip automatically
Compatibility Considerations
- Safe mode must remain available for users who need maximum compatibility (some legitimate tools trigger debugger-like behavior)
- Win32 mode breaks .NET Core on Linux/macOS — needs conditional compilation or runtime OS detection
- Antinet mode is .NET Framework 2.0-4.x only — document this clearly
- Any mode change should be opt-in or behind a version bump — changing the default is a breaking behavioral change
Relevant Code
- `Confuser.Runtime/AntiDebug.Safe.cs` — Safe mode runtime stub
- `Confuser.Runtime/AntiDebug.Win32.cs` — Win32 mode runtime stub
- `Confuser.Runtime/AntiDebug.Antinet.cs` — Antinet mode entry point
- `Confuser.Runtime/antinet/AntiManagedDebugger.cs` — CLR debugger thread killer
- `Confuser.Runtime/antinet/AntiManagedProfiler.cs` — Profiler detection
- `Confuser.Protections/AntiDebugProtection.cs:63` — Mode selection (defaults to Safe)
- `Confuser.Protections/AntiDebugProtection.cs:33` — Preset is Minimum (included in all presets >= Minimum)
Problem
The default anti-debug mode (
Safe) does not prevent dnSpy from running or debugging protected assemblies. A user can open an obfuscated exe in dnSpy, hit "Start" or even F5 (Debug), and the app runs normally.Root Cause Analysis
Three modes exist
Why Safe mode fails
Why Win32 mode is better but still incomplete
Why Antinet mode is strongest but most limited
Weaknesses Summary
Proposed Improvements
Phase 1 — Quick wins (low risk)
Phase 2 — Hardening (medium risk)
Phase 3 — Modern .NET support (high value)
Phase 4 — Response diversity (low risk, high impact)
Compatibility Considerations
Relevant Code