Every major debugger detection technique — API-based, PEB-based, timing, hardware breakpoint scanning, TLS callbacks, trap flag, and interrupt-based — with the exact bypass for each.
Sign in to mark this article as read and track your progress.
When a debugger attaches to a process, the OS modifies several internal data structures. Malware queries these structures to determine whether it is being analysed. Debugger detection is like a suspect checking the rear-view mirror for police cars. Some checks are obvious; others subtle. The analyst''s job is to be the unmarked car that does not trip any of these checks.
The simplest checks call Windows API functions that directly report debugger presence.
Reads the BeingDebugged byte at offset 0x02 in the Process Environment Block (PEB).
CALL IsDebuggerPresent ; returns 1 (debugger) or 0 (clean) in EAX
TEST EAX, EAX
JNZ exit_quietly ; jump if debugger detected
Bypass options:
CALL, set EAX = 0 in the register panel, continueIsDebuggerPresent to always return 0Calls NtQueryInformationProcess internally with class ProcessDebugPort (0x07).
LEA EAX, [EBP+bDebuggerPresent]
PUSH EAX ; output: pointer to BOOL
PUSH GetCurrentProcess()
CALL CheckRemoteDebuggerPresent
Bypass: ScyllaHide patches the output parameter to FALSE automatically.
Three information classes reveal debugger presence:
| Class | Value | What It Returns |
|---|---|---|
| ProcessDebugPort | 0x07 | Non-zero if user-mode debugger attached |
| ProcessDebugFlags | 0x1F | 0 if debugger attached (inverted logic) |
| ProcessDebugObjectHandle | 0x1E | Non-null handle if debugger attached |
Bypass: ScyllaHide hooks all three classes. Manual bypass: set breakpoint on NtQueryInformationProcess, modify return buffer to expected clean values.
The Process Environment Block contains several fields that leak debugger information without calling any API.
Set to 1 by Windows when a debugger attaches.
MOV EAX, FS:[30h] ; EAX = PEB base address
MOV AL, [EAX+2] ; AL = BeingDebugged flag
TEST AL, AL
JNZ debugger_detected
Bypass: In x64dbg, open Memory Map → find PEB → go to offset 0x02 → change byte to 00.
When a process is created by a debugger, Windows sets three heap debug flags in NtGlobalFlag:
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)FLG_HEAP_ENABLE_FREE_CHECK (0x20)FLG_HEAP_VALIDATE_PARAMETERS (0x40)Combined value: 0x70
MOV EAX, FS:[30h] ; PEB base
CMP DWORD PTR [EAX+68h], 70h ; check NtGlobalFlag
JE debugger_detected
Bypass: Set PEB+0x68 to 0x00 in the dump. Or set environment variable _NO_DEBUG_HEAP=1 before launching the sample — prevents heap flags from being set.
When created under a debugger, heap headers contain non-standard flag values. The check reads directly from the heap structure:
MOV EAX, FS:[30h] ; PEB
MOV EAX, [EAX+18h] ; ProcessHeap pointer
CMP DWORD PTR [EAX+0Ch], 2 ; Flags field (normally 2, debug = 50000062h)
JNE debugger_detected
Bypass: _NO_DEBUG_HEAP=1 environment variable, or ScyllaHide heap flag patching.
A debugged process runs slower due to single-stepping overhead. Malware measures execution time between two points and exits if the elapsed time exceeds a threshold.
| Timing Method | Resolution | Bypass |
|---|---|---|
RDTSC instruction | CPU cycles | ScyllaHide RDTSC hook; NOP the comparison |
QueryPerformanceCounter | Sub-microsecond | Modify return value; NOP check |
GetTickCount | Milliseconds | Modify return value; NOP comparison |
timeGetTime | Milliseconds | Modify return value |
RDTSC ; first timestamp in EDX:EAX
MOV [t1], EAX
; ... code being timed ...
RDTSC ; second timestamp
SUB EAX, [t1] ; elapsed cycles
CMP EAX, 0x9999999 ; threshold check
JA exit_debugger ; too slow → debugger present
NOP bypass: select the JA exit_debugger instruction in x64dbg → right-click → Binary → Fill with NOPs.
Malware scans its own code region for 0xCC bytes (INT 3 opcode — the software breakpoint):
mov ecx, function_length
lea esi, [function_start]
scan_loop:
cmp byte ptr [esi], 0CCh ; look for INT 3
je breakpoint_detected
inc esi
loop scan_loop
Bypass: Use hardware breakpoints (DR0–DR3) instead of software breakpoints — they do not write 0xCC into code.
Malware reads DR0–DR3 via GetThreadContext or a SEH exception handler. Non-zero values indicate hardware breakpoints.
Bypass: ScyllaHide clears debug registers in returned context structures automatically.
Thread Local Storage callbacks execute before the debugger-visible entry point. Anti-debug checks placed here run before the analyst has a chance to set breakpoints.
Bypass: In x64dbg → Options → Events → check "Break on TLS Callback". The debugger will pause at each TLS callback before it executes.
PUSHFD ; save EFLAGS
OR DWORD PTR [ESP], 100h ; set Trap Flag (TF)
POPFD ; restore with TF set
; next instruction causes single-step exception
; if SEH handler fires: no debugger
; if debugger intercepts: debugger detected
Bypass: Configure x64dbg to pass single-step exceptions to the program: Options → Preferences → Exceptions → add single-step to the "pass to program" list.
INT 2D is a kernel-mode debugger interrupt. Under a user-mode debugger, it causes a one-byte skip in the instruction stream — the byte following INT 2D is skipped.
INT 2Dh ; if debugger: next byte skipped
NOP ; becomes this instruction under debugger
Bypass: ScyllaHide handles INT 2D. Manual bypass: NOP out the INT 2D instruction.
Step 1: Enable ScyllaHide VMProtect profile → defeats 80% of checks
Step 2: Set _NO_DEBUG_HEAP=1 → defeats heap-based checks
Step 3: Configure x64dbg exception passthrough → defeats SEH/TF checks
Step 4: Use hardware breakpoints exclusively → defeats 0xCC scan
Step 5: Enable "Break on TLS Callback" → defeats TLS-based checks
Step 6: Manually patch remaining checks as encountered