Every serious anti-cheat ships a debugger-detection layer. It is one of the earliest and cheapest wins an AC vendor has: a would-be reverser attaches WinDbg or x64dbg, the AC notices, and the game refuses to run or terminates within seconds. The bar for "notice" is low, because a debugger leaves fingerprints in half a dozen places the OS itself exposes to any curious process, and the AC only needs one hit.
This post walks through the full menu of debugger-detection techniques a 2026 anti-cheat driver will actually check. It is not a bypass guide, because bypasses are per-check and per-anti-cheat and change monthly. It is the checklist a defender uses and a reverser has to internalize before opening a game binary.
The trivial layer: PEB flags and IsDebuggerPresent
The Process Environment Block has a single-byte BeingDebugged flag at offset 0x02 that every user-mode debugger flips to 1 when it attaches. IsDebuggerPresent in kernel32 is literally a one-line function that reads that byte:
BOOL IsDebuggerPresent(void)
{
return NtCurrentTeb()->ProcessEnvironmentBlock->BeingDebugged;
}
Any anti-cheat that ships without an inline copy of that check has bigger problems than debuggers. The same PEB carries NtGlobalFlag at offset 0xBC, which the loader sets to FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS (0x70) when a process is created under a debugger. Reading these two fields is one memory access each and catches every naive attach.
The kernel-side twin lives in EPROCESS. PsGetProcessDebugPort returns non-null when a kernel-visible debug port is attached, and NtQueryInformationProcess with ProcessDebugPort (class 7), ProcessDebugObjectHandle (class 30), or ProcessDebugFlags (class 31) each reveal the same fact from user mode. A driver that walks EPROCESS.DebugPort directly bypasses any user-mode hook that might patch NtQueryInformationProcess.
Handle-based checks
When a kernel debugger attaches, the kernel creates a DebugObject and attaches it to the target. The object is enumerable through NtQuerySystemInformation with SystemHandleInformation, and any process holding a debug-object handle to the game is a debugger. The check is expensive (walk every handle on the system, filter by object type, correlate to PID), which is why it usually runs on a slow timer rather than every syscall.
The user-mode equivalent is checking whether CheckRemoteDebuggerPresent returns TRUE for the AC's own process, which catches cases where an attacker attached to the AC service itself rather than the game.
Hardware breakpoint detection
Software breakpoints (0xCC bytes patched into code) are trivially detected by CRC-scanning the game's own text section. Hardware breakpoints, set through DR0-DR3 with control bits in DR7, are harder because they are per-thread CPU state and do not modify code. Any thread that has non-zero DR7 breakpoint enable bits is being debugged with a hardware breakpoint.
The check is:
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3 || (ctx.Dr7 & 0xFF))
ReportDebugger();
An anti-cheat driver running in kernel mode can walk every thread of the game with PsGetNextProcessThread, capture context for each, and flag any non-zero debug register. It can also periodically clear the debug registers itself as a countermeasure. Riot Vanguard has done this for years.
Timing checks
Debuggers slow execution. A reverser stepping through code introduces microsecond to millisecond gaps between two adjacent instructions that would normally execute in nanoseconds. The classic timing check pairs a RDTSC (or QueryPerformanceCounter) before and after a tight code sequence and flags any delta above a threshold:
uint64_t t0 = __rdtsc();
// short instruction sequence, e.g. 50-100 cycles
uint64_t t1 = __rdtsc();
if (t1 - t0 > 1000000ULL) ReportDebugger();
The check is noisy on virtualized hosts (VM exits inflate the delta), so real-world thresholds are calibrated per-platform. Bypasses either patch RDTSC to return incremented fake values or step over the timing block without single-stepping the intermediate instructions.
The choice between RDTSC and QueryPerformanceCounter matters. RDTSC reads the CPU timestamp counter directly and is the cheapest source available, but it is also the easiest to spoof at the hypervisor layer: any Type-1 hypervisor can enable the RDTSC-exit VMCS control, trap every RDTSC the guest issues, and hand back a fabricated counter derived from a slower clock plus a fudge factor. Intel VT-x even exposes a TSC-offset field so the delta can be applied without touching the underlying hardware counter. QueryPerformanceCounter bottoms out in KeQueryPerformanceCounter and eventually the HPET, ACPI PM timer, or a synthetic Hyper-V reference-TSC page; a cheat platform hosted in its own hypervisor rewrites the reference page too. The consequence is that any timing check written against a single source can be flattened by a hypervisor-based cheat that traps that source and returns a delta well inside the AC's threshold. Serious ACs cross-check two independent time sources (RDTSC against KeQueryUnbiasedInterruptTime, or CPU counter against GPU present-time) because trapping both consistently is much harder than trapping one, and the divergence itself is a stronger signal than any single delta.
Exception-based tricks
INT 3 (0xCC) fires a breakpoint exception the OS delivers to the exception dispatcher. If a debugger is attached, the debugger catches the exception first; if not, the process's own SEH handler catches it. A process can install a handler, execute INT 3, and check whether the handler ran. If the handler did not run, a debugger swallowed the exception.
The trick generalizes to other exceptions: SetUnhandledExceptionFilter, AddVectoredExceptionHandler, or the traditional __try/__except all provide a way to distinguish "my handler saw the exception" from "someone else saw it first." Every serious AC uses at least two exception-based checks because they defeat different classes of bypass.
Anti-attach: locking out debuggers preemptively
A more aggressive posture is to prevent a debugger from attaching at all. Three techniques matter:
NtSetInformationThreadwithThreadHideFromDebugger(class 17). A thread with this flag set does not deliver debug events to any attached debugger. The debugger sees the thread run but cannot single-step it. Nearly every AC hides its own critical threads this way.- Self-debug. A process can call
DebugActiveProcesson itself, or spawn a helper that debugs it. Windows allows only one debugger per process, so a second attach fails withSTATUS_PORT_ALREADY_SET. Every mature AC uses this on its critical protection service. - Patching
DbgUiRemoteBreakin. When a debugger attaches viaCreateRemoteThreadintoDbgUiRemoteBreakin, that function raises the breakpoint that starts the debug loop. Patching it toExitProcess(or to a no-op that swallows the attach) kills the standard attach path. The technique is well-documented; every reverser knows about it, and every AC uses it anyway because the alternative is worse.
Object-name checks
Debuggers create named kernel objects a process can enumerate. \BaseNamedObjects\DBWinMutex is the mutex used by OutputDebugString monitors. \Sessions\1\BaseNamedObjects\... contains debugger-specific mutexes for x64dbg, WinDbg, and OllyDbg descendants. NtOpenMutant on a known name that succeeds means the corresponding debugger tool is running on the system, which is enough for many ACs to refuse to launch even without a specific attach.
Kernel debuggers register the special \Callback\KiDebugRoutineHash and set KdDebuggerEnabled / KdDebuggerNotPresent globals that a kernel driver can read directly.
Comparison table
| Technique | Layer | Cost to check | Cost to bypass | Reliability |
|---|---|---|---|---|
PEB BeingDebugged |
User | 1 read | Trivial (patch byte) | Low |
NtGlobalFlag |
User | 1 read | Trivial (patch byte) | Low |
NtQueryInformationProcess ProcessDebugPort |
User | 1 syscall | Hook syscall | Medium |
EPROCESS.DebugPort (kernel) |
Kernel | 1 read | Kernel patch | High |
| Debug object handle enum | Kernel | Slow scan | Hide handle | Medium |
| Hardware breakpoint scan | Kernel | Per-thread walk | Save/restore DRs | High |
| RDTSC timing | User | Cheap | RDTSC hook | Medium |
| INT 3 exception | User | Cheap | Handler swap | Medium |
ThreadHideFromDebugger (defensive) |
User | 1 syscall | ScyllaHide-class tools | High |
| Self-debug | User | 1 syscall | Preempt with own debugger | Very high |
| DbgUiRemoteBreakin patch | User | 1 patch | Attach via alternate path | Medium |
What actually catches modern reversers
The trivial layer catches nothing serious. Any reverser who cannot handle IsDebuggerPresent is not reversing anti-cheat drivers in 2026. The interesting fight is between the kernel-side checks (which see through user-mode hooks) and kernel-mode tooling like the checked kernel plus WinDbg over a serial or 1394 link, which sees through kernel-side checks.
The realistic ladder:
- Attach x64dbg with ScyllaHide's full anti-anti-debug plugin set. Defeats every user-mode check listed above.
- Attach WinDbg to a live kernel via KDNET. Defeats every kernel-side check that runs in the same VTL as the debugger.
- Use a hypervisor-level debugger (HyperDbg, LiveCloudKd on a snapshot). Defeats every check that assumes the guest OS is the top of the stack.
- Physical hardware attach with an SMM debugger. Defeats everything, at the cost of a lab bench and hardware most reversers do not have.
ScyllaHide is worth naming specifically because it is the public reference for user-mode anti-anti-debug and every AC vendor's baseline threat model. Its default profile hooks NtQueryInformationProcess, NtQuerySystemInformation, NtSetInformationThread, NtClose, NtGetContextThread, NtSetContextThread, NtQueryObject, OutputDebugStringA, and the process instrumentation callback, then patches the PEB BeingDebugged byte, NtGlobalFlag, and the heap flags in place on process start. That covers roughly the top half of the checklist in this post and nothing below it. What ScyllaHide does not touch is anything the AC reads directly from EPROCESS or KTHREAD in kernel mode, which is why the fight moved into ring 0 years ago and why bringing a scratch kernel driver has become a prerequisite for serious research. Mapper drivers such as the manual-mapping path described in our manual-mapping kernel drivers post exist precisely so the researcher can load an unsigned test driver, attach WinDbg to its own address space, and exercise the AC's kernel-side detection from the same ring without leaving a signed image on disk.
Real published behavior of the two dominant ACs decides whether a slip during development is recoverable. Easy Anti-Cheat's response to a detected debugger during a live match is to disconnect the player and to append a hardware-ID flag that feeds into a later ban wave; the ban is not immediate, and the account often survives the session while the flag propagates. BattlEye is less forgiving: a debugger hit typically results in a same-wave ban within the twice-monthly cadence, with the offending machine's SMBIOS strings and disk serials added to the shared banlist. KyTech's kernel driver clears the debug registers on entry to every published callback and refuses to load if the loader detects a debugger in its own process, precisely because either outcome in production means the customer's account is already flagged before we get a chance to fail closed.
Anti-cheat vendors know this ladder. They do not expect to defeat rung 3 or 4. They expect to slow down rungs 1 and 2 enough that reversing is expensive, and to catch anyone who tries to attach in production because production reversers are much less common than lab reversers.
How KyTech handles this
KyTech was founded in 2025 by two engineers with backgrounds in Windows internals and reverse engineering. Our own driver ships with the full anti-debug menu documented in this post: PEB and NtGlobalFlag checks in every user-mode helper, EPROCESS.DebugPort reads from the kernel side, hardware breakpoint sweeps on the driver's own threads, ThreadHideFromDebugger on every critical thread, and self-debug on the loader service.
KyTech ships this hardening because our customers deserve a driver that does not immediately hand its internals over to a competitor with x64dbg. The same techniques the AC vendor uses to catch us are the ones we use to protect the customer-facing surface of our driver. The technical symmetry is genuine: whoever ships better anti-debug wins the reversing timeline, and every one of the six titles we support (Apex Legends, CS2, Overwatch 2, Black Ops 7, Forza Horizon 6, Roblox) benefits from a driver that is expensive to disassemble live. The current lineup and beta status of the Apex HWID spoofer live on the purchase page. See how this ships in KyTech Apex.
Enough theory. get in.
The loader is one click away. Ring-0 kernel driver, polymorphic per download, memory-only injection. Six games across VAC, EAC, Ricochet, Byfron, and Warden.
Get in ›