Anti-Debug Technique Recognition

30 minIn Progress

Anti-Debug Technique Recognition

Overview and Learning Objectives

Anti-debugging is one of the most common self-defense mechanisms found in modern malware. By the end of this lesson, you will be able to:

  • Identify all major categories of anti-debug techniques in disassembly
  • Understand why each technique works at the OS level
  • Recognize the assembly patterns in both Ghidra and x64dbg
  • Select the correct bypass strategy for each technique category
  • Map anti-debug findings to MITRE ATT&CK T1622 (Debugger Evasion)

ATT&CK Reference: T1622 - Debugger Evasion. Adversaries may employ various means to detect and avoid debuggers. Used by APT groups including APT28 (Fancy Bear), Lazarus Group, and APT41.


Why Malware Detects Debuggers

Debuggers let analysts step through code instruction-by-instruction, inspect memory, and modify program state. Malware authors embed checks that detect debugger presence and alter behavior -- typically by exiting cleanly, crashing, executing decoy code, or corrupting analysis results.

Common malware responses to debugger detection:

ResponseDescriptionExample
Silent exitCalls ExitProcess(0) cleanlyMost commodity malware
CrashTriggers unhandled exceptionPacked/protected samples
Decoy pathExecutes benign-looking codeSophisticated APT malware
Data corruptionDestroys or garbles payloadRansomware families
Delayed executionSleeps for extended periodSandbox-aware samples
Self-deletionRemoves itself from diskAnti-forensics aware

Your job as an analyst is to recognize these checks and bypass them.


Category 1: Windows API-Based Detection

IsDebuggerPresent

The simplest and most common check. Internally, this API simply reads the BeingDebugged byte from the Process Environment Block (PEB):

// What the API does internally:
// return NtCurrentPeb()->BeingDebugged;

if (IsDebuggerPresent()) {
    ExitProcess(0);  // Bail out
}

What it looks like in a disassembler (x64):

call    qword ptr ds:[<&IsDebuggerPresent>]
test    eax, eax
jne     getdown.140001216    ; Jump to exit if debugger detected

In Ghidra, look for:

  • Import of IsDebuggerPresent from kernel32.dll
  • A TEST EAX, EAX immediately after the CALL
  • A conditional jump (JNE/JNZ) to an exit or cleanup block

Bypass in x64dbg:

  1. Set breakpoint on the TEST EAX, EAX instruction after the call
  2. When hit, double-click the RAX/EAX register and set it to 0
  3. Or more permanently: select the JNE instruction, right-click > Assemble, type NOP, enable "Fill with NOPs"

Pro Tip: If the malware calls IsDebuggerPresent frequently (e.g., in a loop or timer callback), manual register patching becomes impractical. Use ScyllaHide or NOP the entire check instead.

CheckRemoteDebuggerPresent

Detects debuggers attached to the process, including remote debuggers:

BOOL isDebugged = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebugged);
if (isDebugged) ExitProcess(0);

Bypass: Hook the API to always write FALSE to the output parameter, or NOP the conditional jump.

NtQueryInformationProcess -- ProcessDebugPort (Class 0x07)

More advanced -- queries the kernel for the debug port associated with the process. A non-zero return value indicates a debugger is attached:

DWORD debugPort = 0;
NtQueryInformationProcess(
    GetCurrentProcess(),
    ProcessDebugPort,     // Information class 7
    &debugPort,
    sizeof(debugPort),
    NULL
);
if (debugPort != 0) ExitProcess(0);

NtQueryInformationProcess -- ProcessDebugObjectHandle (Class 0x1E)

HANDLE debugObject;
NTSTATUS status = NtQueryInformationProcess(
    GetCurrentProcess(), 0x1E,
    &debugObject, sizeof(debugObject), NULL
);
if (status == STATUS_SUCCESS) ExitProcess(0);  // Debug object exists

NtQueryInformationProcess -- ProcessDebugFlags (Class 0x1F)

DWORD noDebugInherit;
NtQueryInformationProcess(
    GetCurrentProcess(), 0x1F,
    &noDebugInherit, sizeof(noDebugInherit), NULL
);
if (noDebugInherit == 0) ExitProcess(0);  // 0 means debugger present

Complete NtQueryInformationProcess reference:

Info ClassValueWhat It ChecksDebugged Result
ProcessDebugPort0x07Debug port numberNon-zero
ProcessDebugObjectHandle0x1EDebug object existenceSTATUS_SUCCESS
ProcessDebugFlags0x1FNoDebugInherit flag0

Bypass: Hook NtQueryInformationProcess via ScyllaHide, or patch the conditional jumps after each call.

NtSetInformationThread -- ThreadHideFromDebugger

NtSetInformationThread(
    GetCurrentThread(),
    ThreadHideFromDebugger,  // Class 0x11
    NULL, 0
);
// After this call, breakpoints in this thread cause crashes
// instead of debug breaks -- the thread becomes invisible to the debugger

Bypass: Hook NtSetInformationThread to ignore class 0x11 (ScyllaHide handles this automatically).


Category 2: PEB Field Checks

The Process Environment Block is a user-mode structure accessible without any API calls, making these checks harder to hook.

PEB anti-debug field reference:

PEB FieldOffset (x86)Offset (x64)Normal ValueDebugged Value
BeingDebugged0x020x0201
NtGlobalFlag0x680xBC00x70
Heap.Flagsvariesvaries0x020x50000062
Heap.ForceFlagsvariesvaries0x000x40000060

BeingDebugged Flag (PEB+0x02)

; x86 - Access PEB via FS segment
mov  eax, dword ptr fs:[0x30]   ; PEB address
movzx eax, byte ptr [eax+0x02]  ; BeingDebugged flag
test eax, eax
jnz  debugger_detected

; x64 - Access PEB via GS segment
mov  rax, qword ptr gs:[0x60]   ; PEB address
movzx eax, byte ptr [rax+0x02]  ; BeingDebugged flag
test eax, eax
jnz  debugger_detected

Key Insight: If the program directly checks the BeingDebugged bit without calling IsDebuggerPresent, then setting a breakpoint on IsDebuggerPresent will not help you. Instead, you need to look for FS:[0x30] or GS:[0x60] access patterns and patch the PEB directly.

NtGlobalFlag (PEB+0x68 / PEB+0xBC)

When a debugger launches a process, it sets three heap debugging flags that combine to 0x70:

  • FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
  • FLG_HEAP_ENABLE_FREE_CHECK (0x20)
  • FLG_HEAP_VALIDATE_PARAMETERS (0x40)
mov  eax, dword ptr fs:[0x30]
test byte ptr [eax+0x68], 0x70
jnz  debugger_detected

Bypass: Zero out the NtGlobalFlag field in x64dbg's dump window, or use ScyllaHide which patches it automatically.

Heap Flags

The process heap has flags that differ under a debugger. Malware reads the heap base and checks:

// Get the process heap
PVOID heap = (PVOID)(*(PDWORD)((PBYTE)peb + 0x18)); // PEB->ProcessHeap
DWORD flags = *(PDWORD)((PBYTE)heap + 0x0C);        // Heap.Flags
DWORD forceFlags = *(PDWORD)((PBYTE)heap + 0x10);   // Heap.ForceFlags

if (flags != 0x02 || forceFlags != 0x00) {
    // Debugger detected
}

Category 3: Timing-Based Detection

Single-stepping through code in a debugger dramatically slows execution. Malware exploits this by measuring elapsed time around code blocks.

RDTSC (Read Time-Stamp Counter)

RDTSC                    ; Read TSC into EDX:EAX
MOV  ECX, EAX            ; Save low 32 bits of first timestamp
; ... some code block ...
RDTSC                    ; Read TSC again
SUB  EAX, ECX            ; Compute elapsed cycles
CMP  EAX, 0x1000         ; If too many cycles elapsed
JA   debugger_detected   ; ... someone is stepping through

GetTickCount / QueryPerformanceCounter

DWORD t1 = GetTickCount();
// ... suspicious code ...
DWORD t2 = GetTickCount();
if ((t2 - t1) > 5000) {
    // More than 5 seconds = probably being single-stepped
    ExitProcess(0);
}

Other timing APIs: GetLocalTime, GetSystemTime, NtQuerySystemTime, QueryPerformanceCounter, timeGetTime

Bypass strategies:

  1. Hook timing functions via ScyllaHide to return normalized values
  2. Patch the comparison: change the threshold to a very large value
  3. NOP the conditional jump: skip the check entirely
  4. Use hardware breakpoints: avoid the slowdown of software breakpoints in timing-sensitive code

Category 4: Exception-Based Detection

INT 2D Technique

INT 2D raises a breakpoint exception. Under a debugger, the debugger typically catches it and the instruction after INT 2D executes. Without a debugger, the SEH handler runs:

PUSH  handler               ; Install SEH handler
PUSH  DWORD PTR FS:[0]
MOV   FS:[0], ESP
INT   2D                    ; Trigger exception
; If we reach here, debugger swallowed the exception
JMP   debugger_detected

handler:
    ; Normal execution continues here (no debugger)

Single-Step (Trap Flag) Detection

Setting the trap flag (TF) in EFLAGS causes a single-step exception after the next instruction:

PUSHF
OR   DWORD PTR [ESP], 0x100  ; Set TF (bit 8)
POPF
NOP                            ; Should trigger SINGLE_STEP exception here
; If we reach here without exception, debugger consumed it

OutputDebugString Technique

SetLastError(0);
OutputDebugStringA("test");
if (GetLastError() == 0) {
    // Debugger is present (it consumed the debug string without error)
}

Category 5: Breakpoint Detection

Software Breakpoint Scanning (0xCC / INT 3)

Debuggers insert 0xCC (INT 3) bytes for software breakpoints. Malware can scan its own code:

unsigned char *code = (unsigned char *)FunctionAddress;
for (int i = 0; i < 100; i++) {
    if (code[i] == 0xCC) {
        ExitProcess(0);  // Software breakpoint detected
    }
}

Bypass: Use hardware breakpoints instead (limited to 4 via DR0-DR3 registers).

Hardware Breakpoint Detection

CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) {
    ExitProcess(0);  // Hardware breakpoints are set
}

Bypass: Hook GetThreadContext to zero out debug registers (ScyllaHide DRx Protection option).


Recognizing Anti-Debug in Practice

Quick Scan Workflow

  1. Run CAPA first -- it automatically identifies anti-debug techniques:
capa sample.exe | grep -i "debug"
  1. Check imports in PEStudio or Ghidra for: IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess, GetTickCount, QueryPerformanceCounter
  2. Search for PEB access patterns: FS:[0x30] (x86) or GS:[0x60] (x64)
  3. Search for exception instructions: INT 2D, INT 3, INT 1
  4. Look for timing pairs: two calls to the same timing API with subtraction between them

Using x64dbg Intermodular Calls

In x64dbg, right-click in the Disassembler area of the CPU tab, select Search for > Current Region > Intermodular calls. This lists all external API calls referenced by the specimen, making it easy to spot anti-debug APIs.

Anti-Debug Technique Summary Table

CategoryTechniqueDetection PatternBypass
APIIsDebuggerPresentCALL + TEST + JNZNOP jump or set EAX=0
APINtQueryInformationProcessPUSH 7/0x1E/0x1F + CALLScyllaHide hook
APINtSetInformationThreadPUSH 0x11 + CALLScyllaHide hook
PEBBeingDebuggedFS:[30]+2 readPatch PEB byte to 0
PEBNtGlobalFlagFS:[30]+68 readPatch PEB field to 0
PEBHeap FlagsHeap base + offset readPatch heap flags
TimingRDTSC pairRDTSC + code + RDTSC + CMPPatch threshold or NOP
TimingGetTickCount pairTwo calls + SUB + CMPScyllaHide timing hooks
ExceptionINT 2DSEH install + INT 2DPass exception to program
ExceptionTrap FlagPUSHF + OR 0x100 + POPFConfigure exception handling
Breakpoint0xCC scanLoop reading own code bytesUse hardware breakpoints
BreakpointDR register checkGetThreadContext + DR checkScyllaHide DRx protection

Practical Exercise

  1. Load a malware sample (e.g., getdown.exe) in x64dbg
  2. Use Search for > Intermodular calls to list API references
  3. Identify all anti-debug checks in the code and classify by category
  4. For each check found:
    • Document the address and technique type
    • Determine what happens on detection (exit, crash, decoy)
    • Apply the appropriate bypass (NOP, register edit, or ScyllaHide)
  5. Verify the bypass worked by running the patched sample
  6. Map all findings to ATT&CK T1622 (Debugger Evasion)
  7. Save your patches via File > Patch file > Patch File for reproducibility
Try it in the shell
Practise this lesson's tooling on its sample in an emulated analyst shell. Output is pre-recorded — nothing executes.

Suggested triage steps

  1. 1

    Find the debugger checks

    IsDebuggerPresent is the first anti-debug call anyone learns to recognise.

  2. 2

    See them in the import table

    Reading them here tells you they are statically imported, so a breakpoint on each one will hold.

  3. 3

    Confirm the evasion techniques

    T1622 Debugger Evasion, plus the timing check that usually accompanies it.

analyst@lab:~emulated · nothing executes

MAA analyst shell — emulated. Nothing executes.

Type 'help', or click a step on the left.

$