ᛉ Yggdrasil

Malware development

Anti-Debugging Techniques

11 Oct 2026 · 9,806 words · 45 min

Contents

Legend: [WIN] = Windows technique | [LIN] = Linux technique | [CROSS] = Cross-platform | [DETECT] = Detection vector | [BYPASS] = How to bypass as analyst | [OPSEC] = Operational security note | [TOOL] = Tool reference


1. What Is Anti-Debugging & Why It Matters

What it means: Anti-debugging is a collection of techniques used by software (mainly malware, but also DRM and game cheating protection) to detect if the program is being analyzed under a debugger. If a debugger is detected, the program can alter its behavior — terminate, crash, decrypt a fake payload, or simply silently skip the malicious code path.

Why you need to understand this as a red teamer / malware analyst:

Normal execution flow:
  Program runs → Malicious code executes → Goal achieved

Debugger attached:
  Program runs → Anti-debug check fires → Detects debugger
      ├─ Response 1: ExitProcess() — terminates cleanly
      ├─ Response 2: TerminateProcess() — hard kill
      ├─ Response 3: Enter fake code path — analyst wastes hours
      └─ Response 4: Self-destruct / overwrite evidence

As a red teamer:
  You embed anti-debug in your implant → Analyst attaches x64dbg → Implant detects → Changes behavior
  → Analyst can't figure out what it does → Your C2 survives longer

As a malware analyst / CTF player:
  You encounter anti-debug → Understand what's happening → Bypass it → Continue analysis

Two sides of the coin:

Role Anti-debug goal
Malware author / Red teamer Embed checks in implant to detect/frustrate analysts
Malware analyst / CTF reverser Identify the checks and bypass them to continue analysis

2. Windows API-Based Detection

2.1 IsDebuggerPresent

What it means: The simplest and most commonly seen anti-debug check. It queries the BeingDebugged flag in the Process Environment Block (PEB) and returns TRUE if a usermode debugger is attached. One instruction internally — just reads PEB+0x02.

Internals:

PEB layout (abbreviated):
  PEB
  ├─ [+0x000] InheritedAddressSpace     : BYTE
  ├─ [+0x001] ReadImageFileExecOptions  : BYTE
  ├─ [+0x002] BeingDebugged             : BYTE  ← IsDebuggerPresent reads this!
  ├─ [+0x003] BitField                  : BYTE
  ...

IsDebuggerPresent() internally:
  mov eax, fs:[30h]       ; Get PEB address (x86: FS, x64: GS)
  movzx eax, byte [eax+2] ; Read BeingDebugged byte
  ret                     ; Return 0 or 1

C Code:

#include <windows.h>
#include <stdio.h>

void check_debugger_api() {
    // Method 1: Standard Win32 API
    if (IsDebuggerPresent()) {
        printf("[!] Debugger detected via IsDebuggerPresent!\n");
        ExitProcess(1);
    }

    // Method 2: Direct PEB read (x64) — bypasses API hooking
    PPEB peb = (PPEB)__readgsqword(0x60);   // x64: GS:[0x60]
    // PPEB peb = (PPEB)__readfsdword(0x30); // x86: FS:[0x30]

    if (peb->BeingDebugged) {
        printf("[!] Debugger detected via direct PEB read!\n");
        ExitProcess(1);
    }

    printf("[+] No debugger detected (IsDebuggerPresent)\n");
}

Assembly (manual PEB read — avoids API detection):

; x64 MASM — read PEB.BeingDebugged without calling IsDebuggerPresent
check_peb_being_debugged PROC
    mov  rax, gs:[60h]         ; RAX = PEB address (x64)
    movzx eax, byte ptr [rax+2h] ; EAX = BeingDebugged flag
    test eax, eax
    jnz  debugger_found        ; Non-zero → debugger present
    ; ... normal code ...
    ret
debugger_found:
    ; terminate, alter behavior, etc.
    ret
check_peb_being_debugged ENDP
[DETECT] x64dbg/IDA signatures:
  - Call to "IsDebuggerPresent" in IAT (Import Address Table)
  - movzx eax, byte ptr gs:[60h+2] pattern in disassembly
  - Compare + conditional jump after the call

[BYPASS] Patching in debugger:
  1. In x64dbg → right-click after the check → Patch to NOP
  2. Or: Go to PEB in Memory Map → modify byte at offset +2 → set to 0
  3. ScyllaHide: automatically patches PEB.BeingDebugged to 0 on attach

[BYPASS] Memory patch (x64dbg command):
  mem.write(peb_address + 0x2, 0)  ; Set BeingDebugged = 0

2.2 CheckRemoteDebuggerPresent

What it means: Similar to IsDebuggerPresent but allows checking other processes. Internally calls NtQueryInformationProcess with ProcessDebugPort. Returns a Boolean via an output parameter.

#include <windows.h>
#include <stdio.h>

void check_remote_debugger() {
    BOOL isDebuggerPresent = FALSE;

    // Check if THIS process is being debugged remotely
    if (!CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebuggerPresent)) {
        printf("[-] CheckRemoteDebuggerPresent call failed\n");
        return;
    }

    if (isDebuggerPresent) {
        printf("[!] Remote debugger detected!\n");
        ExitProcess(1);
    }

    printf("[+] No remote debugger detected\n");
}

Assembly (same internal mechanism as IsDebuggerPresent):

; CheckRemoteDebuggerPresent is a thin wrapper — internally:
; 1. Calls NtQueryInformationProcess(hProcess, ProcessDebugPort, ...)
; 2. If DebugPort != 0 → debugger present
[BYPASS] Same techniques as IsDebuggerPresent:
  - Patch return value (set isDebuggerPresent to FALSE after call)
  - Hook NtQueryInformationProcess to return 0 for ProcessDebugPort
  - ScyllaHide handles this automatically

2.3 OutputDebugString

What it means: OutputDebugString() sends a string to the debugger output. If no debugger is attached, the call sets GetLastError() to a non-zero value. If a debugger IS attached, the error code is unaffected (stays at what it was). This behavior leaks debugger presence.

How the trick works:

WITHOUT debugger:
  SetLastError(0xDEAD);
  OutputDebugString("test");    → function "fails" → sets GetLastError()
  GetLastError() != 0xDEAD      → error code changed → no debugger

WITH debugger attached:
  SetLastError(0xDEAD);
  OutputDebugString("test");    → debugger receives string → no error
  GetLastError() == 0xDEAD      → error unchanged → debugger present!

C Code:

#include <windows.h>
#include <stdio.h>

void check_output_debug_string() {
    // Rombertik (real malware) called this 335,000 times to slow analysis

    // Set a known error code first
    SetLastError(0x1337);

    OutputDebugString("test_string_dbg_check");

    // If error code unchanged → debugger consumed the message → present
    if (GetLastError() == 0x1337) {
        printf("[!] Debugger detected via OutputDebugString!\n");
        ExitProcess(1);
    }

    printf("[+] No debugger (OutputDebugString check)\n");
}
[DETECT] Signatures:
  - Call to OutputDebugString followed immediately by GetLastError
  - SetLastError + OutputDebugString + compare pattern

[BYPASS] Debugger:
  - In x64dbg: step past the check and manually set EAX/RAX to 0
  - ScyllaHide hooks OutputDebugString to neutralize this check
  - Manually set LastError using x64dbg: "SetLastError 0" in command bar after the call

2.4 FindWindow (Debugger Window Check)

What it means: Searches for windows with class names or titles specific to popular debuggers (OllyDbg, x64dbg, ImmunityDebugger, WinDbg). If any such window exists on the desktop, a debugger is running.

Known debugger window class names:

OllyDbg 1.x         → "OLLYDBG"
OllyDbg 2.x         → "OllyDbg"  
x64dbg / x32dbg     → "Qt5QWindowIcon", "Qt6QWindowIcon" (Qt-based)
ImmunityDebugger    → "ID"
WinDbg              → "WinDbgFrameClass"
Cheat Engine        → "CE_MainWindow"
Visual Studio Dbg   → "wndclass_desked_gsk"
Rock Debugger       → "Rock Debugger"
Zeta Debugger       → "Zeta Debugger"

C Code:

#include <windows.h>
#include <stdio.h>

// Known debugger window class names
const char* debugger_classes[] = {
    "OLLYDBG",
    "OllyDbg",
    "ID",                   // ImmunityDebugger
    "Qt5QWindowIcon",       // x64dbg
    "Qt6QWindowIcon",       // x64dbg newer
    "WinDbgFrameClass",     // WinDbg
    "Rock Debugger",
    "Zeta Debugger",
    "ntdll.dll",            // peculiar WinDbg window class
    NULL
};

// Known debugger window titles (substrings)
const char* debugger_titles[] = {
    "x64dbg",
    "x32dbg",
    "OllyDbg",
    "Immunity Debugger",
    "WinDbg",
    "IDA ",
    "Ghidra",
    NULL
};

BOOL check_find_window() {
    // Check by class name
    for (int i = 0; debugger_classes[i] != NULL; i++) {
        HWND hwnd = FindWindowA(debugger_classes[i], NULL);
        if (hwnd != NULL) {
            printf("[!] Debugger window found — class: %s\n", debugger_classes[i]);
            return TRUE;
        }
    }

    // Check by window title (partial match would require EnumWindows)
    for (int i = 0; debugger_titles[i] != NULL; i++) {
        HWND hwnd = FindWindowA(NULL, debugger_titles[i]);
        if (hwnd != NULL) {
            printf("[!] Debugger window found — title: %s\n", debugger_titles[i]);
            return TRUE;
        }
    }

    return FALSE;
}

// More thorough: enumerate ALL windows and check titles
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
    char title[256] = {0};
    GetWindowTextA(hwnd, title, sizeof(title));

    // Check if title contains known debugger strings
    const char* dbg_names[] = {"x64dbg", "OllyDbg", "Immunity", "WinDbg", "IDA Pro", NULL};
    for (int i = 0; dbg_names[i]; i++) {
        if (strstr(title, dbg_names[i])) {
            printf("[!] Debugger window enumerated: %s\n", title);
            *(BOOL*)lParam = TRUE;
            return FALSE; // stop enumeration
        }
    }
    return TRUE; // continue
}

BOOL check_enum_windows() {
    BOOL found = FALSE;
    EnumWindows(EnumWindowsProc, (LPARAM)&found);
    return found;
}
[BYPASS] Rename debugger window / binary:
  - In x64dbg: change process name or run without a visible window title
  - Run debugger minimized to system tray (some don't create visible windows)
  - Patch the FindWindow calls to NOP

[BYPASS] x64dbg script:
  bp FindWindowA
  run
  ; When hit: set return value to NULL (EAX = 0)
  eax = 0
  ret

3. PEB (Process Environment Block) Checks

What it means: The PEB is a per-process structure in Windows that holds metadata about the running process. When a debugger attaches, Windows automatically sets certain flags inside the PEB. Checking these flags directly (bypassing API calls) is harder to intercept.

PEB structure (key anti-debug fields):

PEB (x64 offsets):
  +0x000  InheritedAddressSpace     : BYTE
  +0x001  ReadImageFileExecOptions  : BYTE
  +0x002  BeingDebugged             : BYTE       ← §2.1
  +0x003  BitField                  : BYTE
  ...
  +0x018  ProcessHeap               : PVOID      ← §3.3
  ...
  +0x068  NtGlobalFlag              : ULONG      ← §3.2 (x86: 0x68)
  +0x0BC  NtGlobalFlag              : ULONG      ← (x64: 0xBC)
  ...

PEB address:
  x86: FS:[0x30]  (FS register segment selector)
  x64: GS:[0x60]  (GS register segment selector)

3.1 BeingDebugged Flag

Covered in §2.1 — see above.


3.2 NtGlobalFlag

What it means: When a process is created under a debugger, Windows sets three heap debugging flags in PEB.NtGlobalFlag:

Normal process (no debugger):
  NtGlobalFlag = 0x00000000

Process under debugger:
  NtGlobalFlag = 0x00000070  (three flags OR'd together)
      FLG_HEAP_ENABLE_TAIL_CHECK    (0x10)
      FLG_HEAP_ENABLE_FREE_CHECK    (0x20)
      FLG_HEAP_VALIDATE_PARAMETERS  (0x40)

C Code:

#include <windows.h>
#include <winternl.h>   // For PEB definition
#include <stdio.h>

#define FLG_HEAP_ENABLE_TAIL_CHECK    0x10
#define FLG_HEAP_ENABLE_FREE_CHECK    0x20
#define FLG_HEAP_VALIDATE_PARAMETERS  0x40
#define NT_GLOBAL_FLAG_DEBUGGED       (FLG_HEAP_ENABLE_TAIL_CHECK | \
                                       FLG_HEAP_ENABLE_FREE_CHECK  | \
                                       FLG_HEAP_VALIDATE_PARAMETERS)

void check_nt_global_flag() {
    // Get PEB address
#ifdef _WIN64
    PPEB peb = (PPEB)__readgsqword(0x60);
    DWORD nt_global_flag = *(DWORD*)((PBYTE)peb + 0xBC);  // x64 offset
#else
    PPEB peb = (PPEB)__readfsdword(0x30);
    DWORD nt_global_flag = *(DWORD*)((PBYTE)peb + 0x68);  // x86 offset
#endif

    if ((nt_global_flag & NT_GLOBAL_FLAG_DEBUGGED) == NT_GLOBAL_FLAG_DEBUGGED) {
        printf("[!] Debugger detected via NtGlobalFlag: 0x%08X\n", nt_global_flag);
        ExitProcess(1);
    }

    printf("[+] NtGlobalFlag = 0x%08X — clean\n", nt_global_flag);
}

Assembly:

; x64 ASM — check NtGlobalFlag
check_ntglobalflag PROC
    mov  rax, gs:[60h]            ; RAX = PEB (x64)
    mov  eax, dword ptr [rax+0BCh]; EAX = NtGlobalFlag (x64 offset)
    and  eax, 70h                 ; Mask the three debug flags
    cmp  eax, 70h                 ; Are all three set?
    je   debugger_found
    ret
debugger_found:
    call ExitProcess              ; Or alter behavior
check_ntglobalflag ENDP
[DETECT] Signature: Access to PEB+0x68 (x86) or PEB+0xBC (x64), mask 0x70, compare

[BYPASS] Patch NtGlobalFlag:
  x64dbg: Memory → go to PEB address → offset to NtGlobalFlag → write 0x00000000

x64dbg command:
  mov dword ptr [peb_addr + 0xBC], 0   ; Zero NtGlobalFlag

WinDbg command:
  eb @$peb+0xBC 00 00 00 00            ; Write 4 zero bytes

ScyllaHide: handles automatically

3.3 Heap Flags & ForceFlags

What it means: When a process is debugged, Windows also sets specific flags in the heap header itself (PEB.ProcessHeap). The Flags and ForceFlags fields in the heap have different values under a debugger.

Normal process (no debugger):
  ProcessHeap.Flags      = 0x00000002  (HEAP_GROWABLE)
  ProcessHeap.ForceFlags = 0x00000000

Process under debugger:
  ProcessHeap.Flags      = 0x40000062  (includes extra debug flags)
  ProcessHeap.ForceFlags = 0x40000060

Heap structure offsets (HEAP header):
  x86: Flags at +0x0C, ForceFlags at +0x10
  x64: Flags at +0x40, ForceFlags at +0x44

C Code:

#include <windows.h>
#include <stdio.h>

void check_heap_flags() {
#ifdef _WIN64
    PPEB peb = (PPEB)__readgsqword(0x60);
    PVOID heap = peb->ProcessHeap;
    DWORD flags      = *(DWORD*)((PBYTE)heap + 0x40);  // x64
    DWORD force_flags = *(DWORD*)((PBYTE)heap + 0x44);  // x64
#else
    PPEB peb = (PPEB)__readfsdword(0x30);
    PVOID heap = peb->ProcessHeap;
    DWORD flags       = *(DWORD*)((PBYTE)heap + 0x0C);  // x86
    DWORD force_flags = *(DWORD*)((PBYTE)heap + 0x10);  // x86
#endif

    printf("[*] Heap.Flags      = 0x%08X\n", flags);
    printf("[*] Heap.ForceFlags = 0x%08X\n", force_flags);

    // Normal: Flags == 2 (HEAP_GROWABLE), ForceFlags == 0
    if (flags != 0x00000002 || force_flags != 0x00000000) {
        printf("[!] Heap flags indicate debugger presence!\n");
        ExitProcess(1);
    }

    printf("[+] Heap flags are clean\n");
}
[BYPASS] Patch heap flags:
  x64dbg: Memory → ProcessHeap address → patch Flags=0x2, ForceFlags=0x0
  ScyllaHide: automatically patches both fields on attach

3.4 Low Fragmentation Heap (LFH)

What it means: When heap debugging is active (via NtGlobalFlag), the Windows low-fragmentation heap (LFH) behaves differently. Specifically, a certain internal counter (SubSegmentCode field) varies predictably when heap debugging is on, revealing debugger presence.

#include <windows.h>
#include <stdio.h>

void check_lfh() {
    // Allocate a small chunk to trigger LFH activation
    PROCESS_HEAP_ENTRY entry;
    HANDLE heap = GetProcessHeap();

    PVOID p1 = HeapAlloc(heap, 0, 0x14);
    PVOID p2 = HeapAlloc(heap, 0, 0x14);

    // Under debugger with heap checks, p2 - p1 > 0x20 (padding added)
    // Without debugger, p2 - p1 == 0x20 (packed allocations)
    if ((ULONG_PTR)p2 - (ULONG_PTR)p1 > 0x20) {
        printf("[!] LFH spacing indicates heap debugging enabled\n");
        // ExitProcess or change behavior
    }

    HeapFree(heap, 0, p1);
    HeapFree(heap, 0, p2);
}

4. NtQueryInformationProcess Checks

What it means: NtQueryInformationProcess is an undocumented NT kernel function that retrieves various information about a process. Three specific query classes expose debugger attachment that IsDebuggerPresent alone doesn't cover (and are harder to patch without hooks).

Function signature:

NTSTATUS NtQueryInformationProcess(
    HANDLE           ProcessHandle,
    PROCESSINFOCLASS ProcessInformationClass,
    PVOID            ProcessInformation,
    ULONG            ProcessInformationLength,
    PULONG           ReturnLength
);

4.1 ProcessDebugPort (Class 7)

What it means: Returns a DWORD_PTR debug port value. If a debugger is attached, returns -1 (0xFFFFFFFF / 0xFFFFFFFFFFFFFFFF). If no debugger, returns 0.

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *pNtQueryInformationProcess)(
    HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG
);

#define ProcessDebugPort    7
#define ProcessDebugFlags   31   // 0x1F
#define ProcessDebugObject  30   // 0x1E

void check_debug_port() {
    pNtQueryInformationProcess NtQIP = (pNtQueryInformationProcess)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");

    if (!NtQIP) {
        printf("[-] Failed to get NtQueryInformationProcess\n");
        return;
    }

    DWORD_PTR debug_port = 0;
    NTSTATUS status = NtQIP(
        GetCurrentProcess(),
        (PROCESSINFOCLASS)ProcessDebugPort,
        &debug_port,
        sizeof(debug_port),
        NULL
    );

    if (NT_SUCCESS(status) && debug_port != 0) {
        printf("[!] Debugger detected via ProcessDebugPort: 0x%p\n", (void*)debug_port);
        ExitProcess(1);
    }

    printf("[+] ProcessDebugPort = 0x%p — clean\n", (void*)debug_port);
}

4.2 ProcessDebugFlags (Class 31)

What it means: Returns 0 if the process is being debugged. Returns 1 (PROCESS_NO_DEBUG_INHERIT) if not. This is the inverse of ProcessDebugPort — 0 means debugger IS present.

void check_debug_flags() {
    pNtQueryInformationProcess NtQIP = (pNtQueryInformationProcess)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");

    DWORD debug_flags = 0;
    NTSTATUS status = NtQIP(
        GetCurrentProcess(),
        (PROCESSINFOCLASS)ProcessDebugFlags,  // 0x1F = 31
        &debug_flags,
        sizeof(debug_flags),
        NULL
    );

    // INVERSE: 0 means debugger present; 1 means no debugger
    if (NT_SUCCESS(status) && debug_flags == 0) {
        printf("[!] Debugger detected via ProcessDebugFlags (flags==0)\n");
        ExitProcess(1);
    }

    printf("[+] ProcessDebugFlags = 0x%08X — clean\n", debug_flags);
}

4.3 ProcessDebugObject (Class 30)

What it means: When debugging begins, Windows creates a "debug object" — a kernel object representing the debug session. ProcessDebugObject returns a handle to it. If the handle is non-null, a debugger is present. This is harder to bypass than ProcessDebugPort.

void check_debug_object() {
    pNtQueryInformationProcess NtQIP = (pNtQueryInformationProcess)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");

    HANDLE debug_object = NULL;
    NTSTATUS status = NtQIP(
        GetCurrentProcess(),
        (PROCESSINFOCLASS)ProcessDebugObject,  // 0x1E = 30
        &debug_object,
        sizeof(debug_object),
        NULL
    );

    if (NT_SUCCESS(status) && debug_object != NULL) {
        printf("[!] Debug object handle found: 0x%p\n", debug_object);
        CloseHandle(debug_object);
        ExitProcess(1);
    }

    printf("[+] No debug object — clean\n");
}
[DETECT] All three NtQueryInformationProcess checks:
  - Dynamic resolution of NtQueryInformationProcess via GetProcAddress
  - ProcessInformationClass values 7, 30, 31 in the call arguments
  - NTSTATUS + conditional branch pattern

[BYPASS] Hook NtQueryInformationProcess:
  Method 1: ScyllaHide — hooks at ntdll level and spoofs return values
  Method 2: x64dbg — set breakpoint on ntdll!NtQueryInformationProcess
             → when ProcessDebugPort query: set output buffer to 0 before return
             → when ProcessDebugFlags query: set output buffer to 1 before return
             → when ProcessDebugObject query: set output buffer to NULL before return

x64dbg plugin command:
  // ScyllaHide → select "NtQueryInformationProcess" → all three options
  // or manually using the scripting engine

IDAPython / frida hook:
  interceptor.attach(Module.findExportByName("ntdll", "NtQueryInformationProcess"), {
    onLeave(retval) {
      // spoof the return values
    }
  });

5. Timing-Based Detection

What it means: Debuggers slow down execution dramatically — each step, each breakpoint causes delays measured in milliseconds or longer. Timing checks measure the execution time between two points. If execution is too slow, a debugger or sandbox is assumed.


5.1 RDTSC Instruction

What it means: RDTSC (Read Time-Stamp Counter) is an x86 instruction that reads the CPU's cycle counter into EDX:EAX. Between two RDTSC reads, normally only a few hundred cycles pass. Under a debugger (especially with breakpoints), hundreds of thousands of cycles can pass.

How it works:

Normal execution:
  RDTSC → t1 = 1,000,000 cycles
  [100 instructions execute]
  RDTSC → t2 = 1,000,350 cycles
  delta = 350 cycles → NORMAL

With debugger (user stepped through each instruction):
  RDTSC → t1 = 1,000,000 cycles
  [user takes 3 seconds to press F8 × 100 times]
  RDTSC → t2 = 9,000,000,000 cycles
  delta = 8,999,000,000 cycles → DEBUGGER DETECTED!

With sandbox (may speed up time):
  RDTSC → t1 = 1,000,000 cycles
  RDTSC → t2 = 1,000,001 cycles (suspicious: too fast)
  delta = 1 cycle → SANDBOX ACCELERATION DETECTED!

C Code:

#include <windows.h>
#include <intrin.h>
#include <stdio.h>

#define RDTSC_THRESHOLD  1000000ULL   // 1 million cycles max

void check_rdtsc() {
    UINT64 t1, t2, delta;

    // Force VM exit if in hypervisor (harder to fake in VMs):
    int cpu_info[4];
    __cpuid(cpu_info, 0);   // CPUID forces VM exit, resetting TSC timing

    t1 = __rdtsc();         // Read TSC before

    // Inline some work between the two readings
    __cpuid(cpu_info, 0);   // Another CPUID to force VM exit

    t2 = __rdtsc();         // Read TSC after

    delta = t2 - t1;
    printf("[*] RDTSC delta: %llu cycles\n", delta);

    if (delta > RDTSC_THRESHOLD) {
        printf("[!] Timing anomaly — debugger or sandbox detected! (delta=%llu)\n", delta);
        ExitProcess(1);
    }

    printf("[+] RDTSC timing OK\n");
}

Pure assembly version (no intrinsics):

; x64 MASM
check_rdtsc_asm PROC
    cpuid                   ; Force VM exit (serializing instruction)
    rdtsc                   ; t1 → EDX:EAX
    mov  r8, rax            ; Save low 32 bits
    shl  rdx, 32
    or   r8, rdx            ; r8 = full 64-bit t1

    cpuid                   ; Another serializing instruction

    rdtsc                   ; t2 → EDX:EAX
    shl  rdx, 32
    or   rax, rdx           ; RAX = full 64-bit t2

    sub  rax, r8            ; delta = t2 - t1
    cmp  rax, 100000h       ; Compare to threshold
    ja   timing_fail        ; Jump if above (too slow)

    xor  eax, eax           ; Return 0 = no debugger
    ret
timing_fail:
    mov  eax, 1             ; Return 1 = debugger detected
    ret
check_rdtsc_asm ENDP
[BYPASS] RDTSC:
  - x64dbg: Disable RDTSC user-mode access (in OllyDbg: Options → Debugging → Forbid RDTSC)
  - Frida: Hook __rdtsc to return consistent values
  - x64dbg plugin: "fake RDTSC" — some plugins patch RDTSC handler to return
    predictable low-delta values
  - WinDbg: use "!runaway" to identify delay, then NOP out the timing check

5.2 GetTickCount / QueryPerformanceCounter

What it means: Higher-level timing APIs. Easier to hook but less precise than RDTSC. Still effective against slow human-operated debuggers. QueryPerformanceCounter is resolution ~100ns.

#include <windows.h>
#include <stdio.h>

void check_timing_api() {
    // Method 1: GetTickCount (milliseconds)
    DWORD t1 = GetTickCount();

    // Do something that should take microseconds max
    volatile int x = 0;
    for (int i = 0; i < 10000; i++) x += i;

    DWORD t2 = GetTickCount();
    DWORD delta_ms = t2 - t1;

    printf("[*] GetTickCount delta: %lu ms\n", delta_ms);
    if (delta_ms > 500) {  // More than 500ms → being debugged
        printf("[!] Timing check failed (GetTickCount) — debugger detected\n");
        ExitProcess(1);
    }

    // Method 2: QueryPerformanceCounter (100ns resolution)
    LARGE_INTEGER freq, start, end;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&start);

    Sleep(1);   // 1ms sleep

    QueryPerformanceCounter(&end);
    double elapsed_ms = (double)(end.QuadPart - start.QuadPart) / freq.QuadPart * 1000.0;

    printf("[*] QPC elapsed: %.3f ms\n", elapsed_ms);
    if (elapsed_ms > 1000.0) {
        printf("[!] QPC timing anomaly — debugger or sandbox\n");
        ExitProcess(1);
    }
}
[BYPASS] Timing APIs:
  - Hook GetTickCount/QueryPerformanceCounter to return consistent values
  - Frida: Interceptor.attach on GetTickCount → return canned values
  - Sandbox: configure "clock acceleration" to normalized speed
  - x64dbg: Set breakpoint before check → manually advance EIP past it

5.3 Time Delay Traps (Sandbox-Specific)

What it means: Sandboxes speed up Sleep() calls to process samples faster. By sleeping for a known time and then checking whether real time passed, you can detect sandbox time acceleration.

#include <windows.h>
#include <stdio.h>

void check_sleep_acceleration() {
    DWORD before = GetTickCount();
    Sleep(2000);    // Request 2-second sleep
    DWORD after  = GetTickCount();
    DWORD elapsed = after - before;

    printf("[*] Sleep(2000) actual elapsed: %lu ms\n", elapsed);

    // If less than 1800ms elapsed → sandbox sped up the sleep
    if (elapsed < 1800) {
        printf("[!] Sleep acceleration detected — sandbox environment!\n");
        ExitProcess(0);   // Silently exit — appear benign
    }

    printf("[+] Real time passed — likely not a sandbox\n");
}

6. Exception-Based Detection

What it means: Debuggers intercept exceptions differently than the OS. When running normally, exceptions are passed to the program's own handlers (SEH/VEH). When a debugger is attached, it intercepts the exception first (first-chance exception). Programs can detect this difference.


6.1 INT3 / Software Breakpoint Detection

What it means: A software breakpoint is an INT3 instruction (opcode 0xCC). When the debugger sets a breakpoint, it replaces the original byte with 0xCC. A program can scan its own code section for 0xCC bytes to detect tampered instructions.

#include <windows.h>
#include <stdio.h>

// Check if function has been breakpointed (contains 0xCC)
BOOL check_int3_in_function(PVOID func_addr, SIZE_T func_size) {
    PBYTE code = (PBYTE)func_addr;

    for (SIZE_T i = 0; i < func_size; i++) {
        if (code[i] == 0xCC) {  // INT3 opcode
            printf("[!] INT3 (0xCC) found at offset +%zu — breakpoint detected!\n", i);
            return TRUE;
        }
    }
    return FALSE;
}

// Self-modifying check: place INT3, catch exception, verify handler was called
EXCEPTION_DISPOSITION __cdecl int3_handler(
    struct _EXCEPTION_RECORD* ExceptionRecord,
    PVOID EstablisherFrame,
    struct _CONTEXT* ContextRecord,
    PVOID DispatcherContext
) {
    if (ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) {
        // We caught our own INT3 — no debugger consumed it
        ContextRecord->Rip++;  // Skip past INT3 instruction
        return ExceptionContinueExecution;
    }
    return ExceptionContinueSearch;
}

void check_int3_exception() {
    BOOL debugger_consumed = TRUE;  // Assume debugger consumed it

    __try {
        __asm volatile ("int3");   // Raise INT3
        // If we reach here without the handler: debugger DID NOT give it to us
        debugger_consumed = FALSE;
    } __except(EXCEPTION_EXECUTE_HANDLER) {
        // We caught it ourselves → no debugger consumed it
        debugger_consumed = FALSE;
        printf("[+] INT3 exception handled internally — no debugger\n");
    }

    if (debugger_consumed) {
        printf("[!] Debugger intercepted INT3 — debugger present!\n");
        ExitProcess(1);
    }
}

6.2 CloseHandle / NtClose with Invalid Handle

What it means: Calling CloseHandle() or NtClose() with an invalid handle raises EXCEPTION_INVALID_HANDLE (0xC0000008) — but only if a debugger is attached. Without a debugger, the call silently fails. A program can detect this difference using SEH.

#include <windows.h>
#include <stdio.h>

void check_close_handle_exception() {
    BOOL debugger_detected = FALSE;

    __try {
        // Pass an obviously invalid handle (e.g., 0xDEADBEEF or (HANDLE)0x1)
        CloseHandle((HANDLE)0xDEADBEEF);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DWORD code = GetExceptionCode();
        if (code == 0xC0000008) {  // EXCEPTION_INVALID_HANDLE
            // Exception was raised AND our handler caught it.
            // Under a debugger, the debugger would have swallowed this exception FIRST.
            // If we caught it → debugger forwarded it to us → but that itself is suspicious!
            // 
            // The real check: without debugger → no exception (CloseHandle fails silently)
            // With debugger → exception raised → debugger catches it (unless set to "pass to program")
            printf("[!] EXCEPTION_INVALID_HANDLE raised — possible debugger present\n");
            debugger_detected = TRUE;
        }
    }

    // More precise version using NtClose directly:
    // Without debugger: NtClose(invalid) returns error silently
    // With debugger: raises exception (caught by debugger first)

    if (debugger_detected) {
        ExitProcess(1);
    }
}

WinDbg/x64dbg perspective:

Without debugger:
  CloseHandle(invalid) → kernel returns error → no exception → program continues

With user-mode debugger (default settings):
  CloseHandle(invalid) → kernel raises exception → DEBUGGER catches it first
  → Unless debugger configured to "pass to program"
  → Program's __except handler may OR may not run depending on debugger settings

Detection logic:
  Set a flag before CloseHandle
  Clear it inside the exception handler
  If flag never cleared → debugger swallowed the exception → debugger present

[BYPASS] x64dbg:
  Options → Exceptions → add 0xC0000008 to "Ignore list"
  This causes x64dbg to forward EXCEPTION_INVALID_HANDLE to the program
  → exception handler runs → flag cleared → no detection

6.3 Trap Flag (Single-Step) Detection

What it means: The x86 CPU has a Trap Flag (TF) in the EFLAGS register. When set, it raises a EXCEPTION_SINGLE_STEP exception after every instruction. Under a debugger doing single-step execution, TF is set by the debugger. A program can detect if TF causes an exception when it shouldn't.

#include <windows.h>
#include <stdio.h>

BOOL g_trap_detected = FALSE;

LONG WINAPI TrapFlagVEH(PEXCEPTION_POINTERS pExceptionInfo) {
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
        // We received SINGLE_STEP exception — was TF set by a debugger?
        g_trap_detected = TRUE;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

void check_trap_flag() {
    PVOID handler = AddVectoredExceptionHandler(1, TrapFlagVEH);

    // Manually set the Trap Flag
    __asm {
        pushfd
        or dword ptr [esp], 0x100   // Set TF bit
        popfd
        nop                          // TF fires here → SINGLE_STEP exception
    }

    RemoveVectoredExceptionHandler(handler);

    // Under a debugger doing single-step: the debugger intercepts SINGLE_STEP
    // Our VEH never gets called → g_trap_detected remains FALSE
    // Without debugger: exception goes to VEH → g_trap_detected = TRUE

    if (!g_trap_detected) {
        printf("[!] Trap flag exception intercepted by debugger!\n");
        ExitProcess(1);
    }
    printf("[+] Trap flag check: no debugger\n");
}

6.4 INT 2D Detection

What it means: INT 2D is a kernel-mode interrupt used for kernel debugging breakpoints. In user mode, it causes EXCEPTION_BREAKPOINT (same as INT3). However, INT 2D has a quirk: under a debugger, it skips one byte when continuing execution. This behavioral difference can be detected.

#include <windows.h>
#include <stdio.h>

BOOL g_int2d_detected = FALSE;

LONG WINAPI INT2D_VEH(PEXCEPTION_POINTERS pExceptionInfo) {
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) {
        g_int2d_detected = FALSE;  // Exception was caught by us (no debugger)
        // Under debugger: RIP is advanced by 1 extra byte after INT2D
        // We can detect this by checking the instruction pointer value
        pExceptionInfo->ContextRecord->Rip++;  // Skip
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

void check_int2d() {
    g_int2d_detected = TRUE;   // Assume debugger until proven otherwise

    PVOID handler = AddVectoredExceptionHandler(1, INT2D_VEH);

    __asm volatile (
        ".byte 0xCD, 0x2D\n\t"  // INT 2D
        "nop\n\t"               // debugger skips this nop (byte-skip behavior)
    );

    RemoveVectoredExceptionHandler(handler);

    if (g_int2d_detected) {
        printf("[!] INT 2D byte-skip behavior detected — kernel debugger!\n");
        ExitProcess(1);
    }
}

7. Breakpoint Detection

7.1 Software Breakpoint Scanning (0xCC)

What it means: Scan the memory of critical functions for 0xCC bytes. A debugger sets software breakpoints by replacing the first byte of an instruction with 0xCC (INT3). Scanning your own code detects this.

#include <windows.h>
#include <stdio.h>

// Compute CRC32 of a buffer
DWORD crc32(const PBYTE data, SIZE_T len) {
    DWORD crc = 0xFFFFFFFF;
    for (SIZE_T i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++)
            crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
    }
    return ~crc;
}

// Scan a function for INT3 bytes
BOOL scan_for_int3(PVOID func_start, SIZE_T func_size) {
    PBYTE code = (PBYTE)func_start;
    for (SIZE_T i = 0; i < func_size; i++) {
        if (code[i] == 0xCC) {
            printf("[!] INT3 (0xCC) found at function+%zu\n", i);
            return TRUE;
        }
    }
    return FALSE;
}

// Checksum-based: precompute expected CRC, compare at runtime
DWORD g_expected_crc = 0;  // Must be precomputed during build

void set_expected_crc(PVOID func, SIZE_T size) {
    g_expected_crc = crc32((PBYTE)func, size);
}

BOOL verify_code_integrity(PVOID func, SIZE_T size) {
    DWORD current_crc = crc32((PBYTE)func, size);
    if (current_crc != g_expected_crc) {
        printf("[!] Code CRC mismatch — breakpoint or hook detected!\n");
        printf("    Expected: 0x%08X  Got: 0x%08X\n", g_expected_crc, current_crc);
        return TRUE;   // Tampering detected
    }
    printf("[+] Code CRC OK\n");
    return FALSE;
}

// Usage example (scanning main function)
void check_software_bp() {
    // Approximate function size (use IDA/Ghidra to get exact size)
    SIZE_T func_size = 256;
    PVOID func_addr  = (PVOID)check_software_bp;

    if (scan_for_int3(func_addr, func_size)) {
        printf("[!] Software breakpoint found in function!\n");
        ExitProcess(1);
    }
}
[BYPASS] Software breakpoint detection:
  - Use hardware breakpoints instead of software breakpoints in x64dbg
  - In x64dbg: Right-click → Breakpoints → Set as HW breakpoint
    → No 0xCC written to code → scan finds nothing
  - Alternatively: patch the scan loop to skip the check

7.2 Hardware Breakpoint Detection (DR0–DR7)

What it means: Hardware breakpoints use CPU debug registers (DR0–DR3 for addresses, DR7 for control). A program can read these registers via GetThreadContext() and check if any debug registers are set.

CPU Debug Registers:
  DR0 — Breakpoint address 0
  DR1 — Breakpoint address 1
  DR2 — Breakpoint address 2
  DR3 — Breakpoint address 3
  DR4 — Reserved (alias for DR6 with debug extensions)
  DR5 — Reserved (alias for DR7 with debug extensions)
  DR6 — Debug status (which breakpoint triggered)
  DR7 — Debug control (enable/disable, type, size of each breakpoint)

When a hardware breakpoint is set in x64dbg:
  DR0/DR1/DR2/DR3 = address of the breakpoint
  DR7 = control word with enable bits set

Clean system (no HW breakpoints):
  DR0 = DR1 = DR2 = DR3 = 0
  DR7 = 0x00000400 (default)

C Code:

#include <windows.h>
#include <stdio.h>

int check_hardware_breakpoints() {
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

    HANDLE hThread = GetCurrentThread();

    if (!GetThreadContext(hThread, &ctx)) {
        printf("[-] GetThreadContext failed: %lu\n", GetLastError());
        return -1;
    }

    int count = 0;

    printf("[*] Debug Registers:\n");
    printf("    DR0 = 0x%p\n", (void*)ctx.Dr0);
    printf("    DR1 = 0x%p\n", (void*)ctx.Dr1);
    printf("    DR2 = 0x%p\n", (void*)ctx.Dr2);
    printf("    DR3 = 0x%p\n", (void*)ctx.Dr3);
    printf("    DR6 = 0x%08lX\n", (unsigned long)ctx.Dr6);
    printf("    DR7 = 0x%08lX\n", (unsigned long)ctx.Dr7);

    if (ctx.Dr0 != 0) { printf("[!] HW breakpoint at DR0: 0x%p\n", (void*)ctx.Dr0); count++; }
    if (ctx.Dr1 != 0) { printf("[!] HW breakpoint at DR1: 0x%p\n", (void*)ctx.Dr1); count++; }
    if (ctx.Dr2 != 0) { printf("[!] HW breakpoint at DR2: 0x%p\n", (void*)ctx.Dr2); count++; }
    if (ctx.Dr3 != 0) { printf("[!] HW breakpoint at DR3: 0x%p\n", (void*)ctx.Dr3); count++; }

    if (count > 0) {
        printf("[!] %d hardware breakpoint(s) detected!\n", count);
        ExitProcess(1);
    }

    printf("[+] No hardware breakpoints\n");
    return 0;
}

// More aggressive: also clear hardware breakpoints
void clear_hardware_breakpoints() {
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    GetCurrentThread();
    GetThreadContext(GetCurrentThread(), &ctx);

    ctx.Dr0 = 0;
    ctx.Dr1 = 0;
    ctx.Dr2 = 0;
    ctx.Dr3 = 0;
    ctx.Dr6 = 0;
    ctx.Dr7 = 0;

    SetThreadContext(GetCurrentThread(), &ctx);
    printf("[*] Hardware breakpoints cleared!\n");
}

Via SEH (no GetThreadContext needed):

// Detect hardware breakpoints using exception-based approach
BOOL g_hw_bp_cleared = FALSE;

LONG WINAPI HWBPHandler(PEXCEPTION_POINTERS pExInfo) {
    if (pExInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
        // A hardware breakpoint or trace flag fired — clear it
        pExInfo->ContextRecord->Dr0 = 0;
        pExInfo->ContextRecord->Dr1 = 0;
        pExInfo->ContextRecord->Dr2 = 0;
        pExInfo->ContextRecord->Dr3 = 0;
        pExInfo->ContextRecord->Dr6 = 0;
        pExInfo->ContextRecord->Dr7 = 0;
        g_hw_bp_cleared = TRUE;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}
[BYPASS] Hardware breakpoint detection:
  - ScyllaHide: hooks GetThreadContext to return zeroed debug registers
  - x64dbg plugin: manipulate CONTEXT structure returned by GetThreadContext

x64dbg — manually spoof GetThreadContext:
  bp GetThreadContext
  ; On return from GetThreadContext:
  ; Go to the CONTEXT structure in memory
  ; Zero out Dr0-Dr7 fields
  ; (ScyllaHide does this automatically)

7.3 Memory (Guard Page) Breakpoints

What it means: Memory breakpoints use PAGE_GUARD protection flag on a memory page. When the page is accessed, STATUS_GUARD_PAGE_VIOLATION (0x80000001) exception fires. Debuggers like OllyDbg use this internally. Detection involves checking page protection flags.

#include <windows.h>
#include <stdio.h>

void check_guard_page_breakpoints() {
    // Allocate test page
    PVOID test_page = VirtualAlloc(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!test_page) return;

    // Mark as guard page
    DWORD old_protect;
    VirtualProtect(test_page, 4096, PAGE_READWRITE | PAGE_GUARD, &old_protect);

    __try {
        // Access the guard page — this SHOULD raise STATUS_GUARD_PAGE_VIOLATION
        volatile BYTE b = *(PBYTE)test_page;
        (void)b;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DWORD code = GetExceptionCode();
        if (code == STATUS_GUARD_PAGE_VIOLATION) {
            // Normal — guard page fired and our handler caught it
            // Under OllyDbg memory BP: debugger intercepts before us
        }
    }

    VirtualFree(test_page, 0, MEM_RELEASE);
}

8. Thread-Based Detection

8.1 NtSetInformationThread — HideThreadFromDebugger

What it means: Calling NtSetInformationThread with ThreadHideFromDebugger (0x11) hides a thread from the debugger. The debugger can no longer receive debug events (exceptions, DLL loads) for this thread. If you put your important code in a hidden thread, the debugger can't follow it.

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI* pNtSetInformationThread)(
    HANDLE ThreadHandle,
    ULONG  ThreadInformationClass,
    PVOID  ThreadInformation,
    ULONG  ThreadInformationLength
);

#define ThreadHideFromDebugger 0x11

BOOL hide_thread_from_debugger(HANDLE hThread) {
    pNtSetInformationThread NtSIT = (pNtSetInformationThread)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");

    if (!NtSIT) {
        printf("[-] NtSetInformationThread not found\n");
        return FALSE;
    }

    // Hide current thread from any attached debugger
    NTSTATUS status = NtSIT(
        hThread,                  // Thread to hide
        ThreadHideFromDebugger,   // 0x11
        NULL,                     // No info needed
        0
    );

    if (NT_SUCCESS(status)) {
        printf("[*] Thread 0x%p hidden from debugger\n", hThread);
        return TRUE;
    }

    printf("[-] NtSetInformationThread failed: 0x%08X\n", status);
    return FALSE;
}

// Hide current thread and do sensitive work
DWORD WINAPI SensitiveThread(LPVOID param) {
    // Hide this thread immediately upon start
    hide_thread_from_debugger(GetCurrentThread());

    // Now: debugger cannot trace, set breakpoints, or receive events for this thread
    printf("[*] Running in hidden thread — debugger blind here\n");

    // Do sensitive operations here...

    return 0;
}
[DETECT] Signatures:
  - GetProcAddress("ntdll.dll", "NtSetInformationThread")
  - NtSetInformationThread call with ThreadInformationClass == 0x11

[BYPASS] Anti-debug bypass:
  Method 1: Hook NtSetInformationThread — intercept calls where Class == 0x11 and NOP them
  Method 2: ScyllaHide has built-in "NtSetInformationThread" bypass
  Method 3: Frida:
    Interceptor.attach(Module.findExportByName("ntdll", "NtSetInformationThread"), {
      onEnter(args) {
        if (args[1].toInt32() === 0x11) {
          args[1] = ptr(0);  // Change class → NOP the hide
        }
      }
    });

8.2 TLS (Thread Local Storage) Callbacks

What it means: TLS callbacks are functions registered in the PE header's .tls section. Windows calls them before main() or WinMain() runs — even before many debugger hooks are set up. This makes them ideal for anti-debug checks that run before the analyst expects anything to happen.

How TLS callbacks are registered (PE structure):

PE file structure:
  IMAGE_NT_HEADERS
    IMAGE_OPTIONAL_HEADER
      DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]  ← pointer to TLS directory

IMAGE_TLS_DIRECTORY:
  StartAddressOfRawData  : VA of TLS data start
  EndAddressOfRawData    : VA of TLS data end
  AddressOfIndex         : VA of TLS index variable
  AddressOfCallBacks     : VA of array of TLS callbacks ← KEY FIELD
  SizeOfZeroFill         : size of zero-initialized data
  Characteristics        : reserved

TLS callback execution order:
  1. OS Loader maps DLLs
  2. ALL TLS callbacks execute (for each DLL, then for main EXE)
  3. main() / WinMain() / DllMain runs
  ← Debugger has very limited visibility into step 2!

C Code (declaring a TLS callback):

#include <windows.h>
#include <stdio.h>

// Forward declaration
void NTAPI AntiDebugTLSCallback(PVOID DllHandle, DWORD dwReason, PVOID Reserved);

// Register the callback in the TLS section
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma comment(linker, "/INCLUDE:__tls_callback")

// Declare the TLS callback array
#pragma data_seg(".CRT$XLB")
    PIMAGE_TLS_CALLBACK __tls_callback = AntiDebugTLSCallback;
#pragma data_seg()

// The actual TLS callback function
void NTAPI AntiDebugTLSCallback(PVOID DllHandle, DWORD dwReason, PVOID Reserved) {
    if (dwReason == DLL_PROCESS_ATTACH) {
        // This runs BEFORE main() — debugger may not have set up yet

        // Check 1: IsDebuggerPresent
        if (IsDebuggerPresent()) {
            TerminateProcess(GetCurrentProcess(), 0xDEAD);
        }

        // Check 2: PEB.NtGlobalFlag
        PPEB peb = (PPEB)__readgsqword(0x60);
        DWORD ntgf = *(DWORD*)((PBYTE)peb + 0xBC);
        if ((ntgf & 0x70) == 0x70) {
            TerminateProcess(GetCurrentProcess(), 0xDEAD);
        }

        // Check 3: Process heap flags
        PVOID heap = peb->ProcessHeap;
        DWORD flags = *(DWORD*)((PBYTE)heap + 0x40);
        if (flags != 0x00000002) {
            TerminateProcess(GetCurrentProcess(), 0xDEAD);
        }

        printf("[*] TLS callback: no debugger detected\n");
    }
}

int main() {
    printf("[*] main() reached — TLS checks passed\n");
    return 0;
}
[DETECT] Finding TLS callbacks:
  x64dbg: View → PE Header → TLS Directory → AddressOfCallBacks
  IDA Pro: Segments → look for .tls section; xrefs on TLS directory
  CFF Explorer: TLS Directory → Callbacks

[BYPASS] TLS callbacks:
  x64dbg: Options → Events → "Break on TLS Callbacks" → enabled
  → Debugger will break at each TLS callback before it runs
  → You can patch/skip checks before they execute

  Manual: In x64dbg, before launch:
    → Plugin → ScyllaHide → all anti-debug options checked
    → Covers PEB patches that TLS callbacks check

9. Process & Parent Process Checks

9.1 Parent Process Name / PPID Check

What it means: Normally, a process is launched by explorer.exe (double-clicking). If a process is launched by a debugger, its parent is the debugger process (x64dbg, WinDbg, etc.). The program can check its own parent process name.

#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <stdio.h>
#pragma comment(lib, "psapi.lib")

// Get parent process ID
DWORD get_parent_pid() {
    DWORD current_pid = GetCurrentProcessId();
    HANDLE snapshot   = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (snapshot == INVALID_HANDLE_VALUE) return 0;

    PROCESSENTRY32W pe32 = {0};
    pe32.dwSize = sizeof(pe32);

    if (!Process32FirstW(snapshot, &pe32)) {
        CloseHandle(snapshot);
        return 0;
    }

    do {
        if (pe32.th32ProcessID == current_pid) {
            CloseHandle(snapshot);
            return pe32.th32ParentProcessID;
        }
    } while (Process32NextW(snapshot, &pe32));

    CloseHandle(snapshot);
    return 0;
}

// Get process name from PID
BOOL get_process_name(DWORD pid, char* name, SIZE_T size) {
    HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
    if (!hProc) return FALSE;

    DWORD len = (DWORD)size;
    QueryFullProcessImageNameA(hProc, 0, name, &len);
    CloseHandle(hProc);
    return TRUE;
}

void check_parent_process() {
    DWORD parent_pid = get_parent_pid();
    if (parent_pid == 0) return;

    char parent_name[MAX_PATH] = {0};
    if (!get_process_name(parent_pid, parent_name, sizeof(parent_name))) {
        printf("[-] Could not get parent process name (PID: %lu)\n", parent_pid);
        return;
    }

    printf("[*] Parent process: %s (PID: %lu)\n", parent_name, parent_pid);

    // Suspicious parent process names
    const char* suspicious_parents[] = {
        "x64dbg.exe",
        "x32dbg.exe",
        "ollydbg.exe",
        "windbg.exe",
        "idag.exe",
        "idag64.exe",
        "ida64.exe",
        "idaw.exe",
        "ghidra",
        "devenv.exe",     // Visual Studio debugger
        "msvsmon.exe",    // VS remote debugger
        "dnspy.exe",      // .NET debugger
        NULL
    };

    // Convert to lowercase for comparison
    CharLowerA(parent_name);

    for (int i = 0; suspicious_parents[i]; i++) {
        if (strstr(parent_name, suspicious_parents[i])) {
            printf("[!] Suspicious parent process: %s — debugger detected!\n", parent_name);
            ExitProcess(1);
        }
    }

    printf("[+] Parent process appears clean: %s\n", parent_name);
}
[BYPASS] Parent process check:
  - Run the binary from a batch file / cmd.exe launched by explorer.exe
  - Use PPID spoofing when launching: CreateProcess with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
    (attaches to a different parent process — makes parent appear as explorer.exe)
  - Rename debugger binary (x64dbg → notepad_helper.exe)
  - Patch the comparison: find "x64dbg" string in memory, NOP the strstr/strcmp call

9.2 Self-Debugging Technique

What it means: Only one debugger can be attached to a process at a time. The target process spawns a child copy of itself that tries to attach as a debugger to the parent. If the attach fails — another debugger is already attached.

#include <windows.h>
#include <stdio.h>

// Self-debugging anti-debug: spawn a child that tries to debug us
void check_self_debug() {
    char self_path[MAX_PATH] = {0};
    GetModuleFileNameA(NULL, self_path, sizeof(self_path));

    // Spawn child with "--debug-parent <current_pid>" argument
    char cmd[512] = {0};
    DWORD current_pid = GetCurrentProcessId();
    snprintf(cmd, sizeof(cmd), "\"%s\" --internal-debug-check %lu", self_path, current_pid);

    STARTUPINFOA si = {sizeof(si)};
    PROCESS_INFORMATION pi = {0};

    if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        return;
    }

    // Wait for child to check
    WaitForSingleObject(pi.hProcess, 3000);

    DWORD exit_code = 0;
    GetExitCodeProcess(pi.hProcess, &exit_code);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    // Child exits with 1 if another debugger is present
    if (exit_code == 1) {
        printf("[!] Self-debug check: another debugger detected!\n");
        ExitProcess(0xBAD);
    }
}

// Child process: attempt to attach as debugger to parent PID
// Called when "--internal-debug-check <pid>" args are present
void try_attach_to_parent(DWORD parent_pid) {
    if (DebugActiveProcess(parent_pid)) {
        // Successfully attached → no other debugger
        DebugActiveProcessStop(parent_pid);
        ExitProcess(0);   // Exit cleanly — no debugger
    } else {
        // Failed → another debugger already attached
        ExitProcess(1);   // Signal: debugger present
    }
}

10. Object Handle Tricks

10.1 CreateFile Exclusive Access Check

What it means: When a debugger opens a process's executable file (to read its symbols or sections), it may hold a file handle. Attempting to open the same file with GENERIC_WRITE and FILE_SHARE_READ will fail if the debugger holds it open exclusively.

#include <windows.h>
#include <stdio.h>

void check_exclusive_file_access() {
    // Get path to our own executable
    char self_path[MAX_PATH] = {0};
    GetModuleFileNameA(NULL, self_path, sizeof(self_path));

    // Try to open our own EXE exclusively
    HANDLE hFile = CreateFileA(
        self_path,
        GENERIC_READ,
        0,              // No sharing — exclusive access
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        // Failed to open exclusively → someone else holds the file open
        DWORD err = GetLastError();
        if (err == ERROR_SHARING_VIOLATION) {
            printf("[!] Exclusive file access failed — debugger may be holding file open\n");
            // Note: This is circumstantial evidence, not definitive
        }
    } else {
        CloseHandle(hFile);
        printf("[+] Exclusive file access succeeded — no file-holding debugger\n");
    }
}

10.2 Debug Object Handle Detection

What it means: When a debugging session starts, Windows creates a kernel DEBUG_OBJECT. Using NtQueryInformationProcess(ProcessDebugObject) returns a handle to this object. The existence of this handle proves a debugger is attached. See §4.3 above.


11. Linux Anti-Debugging Techniques

11.1 ptrace(PTRACE_TRACEME)

What it means: On Linux, ptrace(PTRACE_TRACEME, ...) allows a process to tell the kernel "let my parent trace me." If a process calls this and it fails (returns -1), it means another tracer is already attached (the kernel only allows one tracer at a time). This is the standard Linux anti-debug check.

#include <sys/ptrace.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

void check_ptrace_traceme() {
    // Attempt to self-trace
    if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
        // ptrace failed — another tracer (gdb, strace) is already attached
        if (errno == EPERM) {
            fprintf(stderr, "[!] ptrace(TRACEME) failed: already being traced!\n");
            fprintf(stderr, "    errno = EPERM (Operation not permitted)\n");
            exit(1);
        }
    }
    printf("[+] ptrace(TRACEME) succeeded — no tracer detected\n");
}

// Fork variant: more reliable
void check_ptrace_fork() {
    pid_t child = fork();

    if (child == 0) {
        // Child: try to ptrace parent
        pid_t parent = getppid();
        long ret = ptrace(PTRACE_ATTACH, parent, NULL, NULL);
        if (ret == 0) {
            // Success → parent not being debugged
            ptrace(PTRACE_DETACH, parent, NULL, NULL);
            exit(0);   // Not being debugged
        } else {
            exit(1);   // Already being debugged
        }
    } else {
        // Parent: wait for child result
        int status;
        waitpid(child, &status, 0);
        if (WIFEXITED(status) && WEXITSTATUS(status) == 1) {
            fprintf(stderr, "[!] Debugger detected (fork+ptrace check)\n");
            exit(1);
        }
    }
}
[BYPASS] ptrace(TRACEME) check:
  Method 1: LD_PRELOAD hook
    // bypass_ptrace.c
    #include <sys/ptrace.h>
    #define _GNU_SOURCE
    #include <dlfcn.h>

    long ptrace(enum __ptrace_request req, ...) {
        // Hook: always return 0 for PTRACE_TRACEME
        if (req == PTRACE_TRACEME) return 0;
        // Forward others to real ptrace
        return 0;
    }

    Compile: gcc -shared -fPIC bypass_ptrace.c -o bypass_ptrace.so -ldl
    Use:     LD_PRELOAD=./bypass_ptrace.so gdb ./target

  Method 2: GDB script (catch and forward PTRACE_TRACEME):
    (gdb) catch syscall ptrace
    (gdb) commands
    > if ($rdi == 0)
    >   set $rax = 0
    >   continue
    > end
    > end
    (gdb) run

  Method 3: Kernel module (most powerful — hide all tracer info)

11.2 /proc/self/status — TracerPid

What it means: The Linux kernel exposes per-process status in /proc/<pid>/status. The TracerPid field is non-zero if a debugger (gdb, strace) is attached to the process. Checking this file is a reliable way to detect a tracer.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int check_tracerpid() {
    FILE *f = fopen("/proc/self/status", "r");
    if (!f) {
        fprintf(stderr, "[-] Could not open /proc/self/status\n");
        return -1;
    }

    char line[256];
    int tracer_pid = 0;

    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "TracerPid:", 10) == 0) {
            tracer_pid = atoi(line + 10);
            break;
        }
    }
    fclose(f);

    printf("[*] TracerPid: %d\n", tracer_pid);

    if (tracer_pid != 0) {
        fprintf(stderr, "[!] Tracer detected: PID %d is attached!\n", tracer_pid);
        return 1;   // Debugger detected
    }

    printf("[+] TracerPid is 0 — no tracer attached\n");
    return 0;
}

int main() {
    if (check_tracerpid()) {
        fprintf(stderr, "[!] Exiting — debugger detected\n");
        exit(1);
    }
    return 0;
}

Bypass via LD_PRELOAD:

// bypass_proc.c — spoof /proc/self/status TracerPid to 0
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>

typedef FILE* (*orig_fopen_t)(const char*, const char*);
typedef char* (*orig_fgets_t)(char*, int, FILE*);

static FILE *g_fake_file = NULL;

FILE *fopen(const char *path, const char *mode) {
    orig_fopen_t orig_fopen = dlsym(RTLD_NEXT, "fopen");

    if (strcmp(path, "/proc/self/status") == 0) {
        FILE *real = orig_fopen(path, mode);
        // Store and return it, but we'll filter fgets
        g_fake_file = real;
        return real;
    }
    return orig_fopen(path, mode);
}

char *fgets(char *s, int size, FILE *stream) {
    orig_fgets_t orig_fgets = dlsym(RTLD_NEXT, "fgets");
    char *ret = orig_fgets(s, size, stream);

    if (ret && stream == g_fake_file) {
        if (strncmp(s, "TracerPid:", 10) == 0) {
            strcpy(s, "TracerPid:\t0\n");   // Spoof to 0
        }
    }
    return ret;
}
# Compile and use:
gcc -shared -fPIC bypass_proc.c -o bypass_proc.so -ldl
LD_PRELOAD=./bypass_proc.so gdb ./anti_debug_binary

11.3 /proc/self/maps Inspection

What it means: /proc/self/maps shows the memory layout of the process. Under a debugger, the memory map often contains extra mapped regions (debugger injected code, debug libraries). Additionally, checking for the presence of known debugger-injected libraries is a detection method.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void check_proc_maps() {
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) return;

    const char *suspicious_libs[] = {
        "libasan",      // AddressSanitizer
        "libpthread",   // Could be normal, but check for GDB injection
        "linux-vdso",   // Normal
        "gdb_server",   // GDB server
        NULL
    };

    char line[512];
    while (fgets(line, sizeof(line), f)) {
        // Look for unusual mappings
        if (strstr(line, "[stack]")) continue;
        if (strstr(line, "[heap]"))  continue;
        if (strstr(line, "[vvar]"))  continue;
        if (strstr(line, "[vsyscall]")) continue;

        // Check for LD_PRELOAD-injected libraries
        for (int i = 0; suspicious_libs[i]; i++) {
            if (strstr(line, suspicious_libs[i])) {
                printf("[?] Possibly suspicious mapping: %s", line);
            }
        }
    }
    fclose(f);
}

11.4 prctl(PR_SET_DUMPABLE)

What it means: prctl(PR_SET_DUMPABLE, 0) marks the process as non-dumpable. This prevents ptrace from attaching and prevents core dumps. An offensive use: call this early to block gdb or strace from ever attaching.

#include <sys/prctl.h>
#include <stdio.h>
#include <stdlib.h>

void harden_against_debugger() {
    // Set process as non-dumpable → ptrace will fail with EPERM
    if (prctl(PR_SET_DUMPABLE, 0) != 0) {
        perror("prctl PR_SET_DUMPABLE failed");
    } else {
        printf("[*] Process set as non-dumpable — ptrace attach blocked\n");
    }

    // macOS equivalent:
    // ptrace(PT_DENY_ATTACH, 0, NULL, 0);
}

11.5 Signal-Based Detection (SIGTRAP)

What it means: On Linux, when a breakpoint (INT3) fires, the process receives SIGTRAP. If the process has its own signal handler for SIGTRAP, and a debugger is attached, the debugger intercepts the signal first. The program can detect whether its handler actually ran.

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static volatile int g_sigtrap_received = 0;

void sigtrap_handler(int sig) {
    g_sigtrap_received = 1;
}

void check_sigtrap() {
    // Register our own SIGTRAP handler
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigtrap_handler;
    sigaction(SIGTRAP, &sa, NULL);

    g_sigtrap_received = 0;

    // Raise SIGTRAP manually
    __asm__("int3");    // or: raise(SIGTRAP);

    // If debugger is NOT attached: our handler ran → g_sigtrap_received = 1
    // If debugger IS attached: debugger intercepted SIGTRAP first → handler may not run

    if (!g_sigtrap_received) {
        fprintf(stderr, "[!] SIGTRAP was not delivered to our handler — debugger intercepted!\n");
        exit(1);
    }

    printf("[+] SIGTRAP handler ran — no debugger\n");

    // Restore default handler
    signal(SIGTRAP, SIG_DFL);
}
[BYPASS] Signal-based detection:
  GDB: handle SIGTRAP nostop noprint pass
  → Tell GDB to forward SIGTRAP to the process, not stop on it
  → Our signal handler will run → no detection

strace: strace -e signal=!SIGTRAP ./binary
  → Suppress SIGTRAP reporting

12. Anti-Disassembly Tricks (paired with anti-debug)

What it means: Alongside anti-debugging, these tricks confuse disassemblers. Combined, they frustrate both static (IDA/Ghidra) and dynamic (x64dbg) analysis.

12.1 Overlapping Instructions

; The disassembler thinks one thing, CPU executes another
jmp short +1    ; Jump 1 byte forward — skips the junk byte
db 0xE8         ; "Junk" byte that disassembler interprets as CALL
; Real code continues here (disassembler misaligned)
mov eax, 1

12.2 Junk Code Insertion

// Insert junk that confuses static analysis but is harmless at runtime
#define JUNK_CODE do { \
    __asm volatile ("nop;nop;xor eax,eax;nop" :::); \
} while(0)

void sensitive_function() {
    JUNK_CODE;
    IsDebuggerPresent();   // Actual check buried in junk
    JUNK_CODE;
}

12.3 Self-Modifying Code

// Code modifies its own bytes at runtime — no static analysis possible
void self_mod_example() {
    // Placeholder NOP that will become a conditional jump
    unsigned char *code = (unsigned char*)&&check_label;

    DWORD old_protect;
    VirtualProtect(code, 10, PAGE_EXECUTE_READWRITE, &old_protect);

    // Overwrite NOP with JNZ: debugger detection → conditional skip
    code[0] = 0x75;   // JNZ
    code[1] = 0x05;   // +5 bytes

    VirtualProtect(code, 10, PAGE_EXECUTE_READ, &old_protect);

check_label:
    __asm volatile("nop");   // Will be JNZ at runtime
}

13. Bypass Techniques & Tools

13.1 ScyllaHide — x64dbg / IDA Plugin

The most comprehensive anti-anti-debug plugin for Windows.

# Installation (x64dbg):
# 1. Download ScyllaHide from: https://github.com/x64dbg/ScyllaHide
# 2. Copy plugin DLLs to x64dbg\x64\plugins\ (or x32\plugins\)
# 3. Restart x64dbg

# What ScyllaHide patches automatically:
#   ✅ PEB.BeingDebugged → 0
#   ✅ PEB.NtGlobalFlag → 0
#   ✅ ProcessHeap.Flags → 0x02
#   ✅ ProcessHeap.ForceFlags → 0
#   ✅ NtQueryInformationProcess (ProcessDebugPort) → return 0
#   ✅ NtQueryInformationProcess (ProcessDebugFlags) → return 1
#   ✅ NtQueryInformationProcess (ProcessDebugObject) → return NULL
#   ✅ NtSetInformationThread (ThreadHideFromDebugger) → NOP
#   ✅ GetThreadContext DR0-DR7 → zeroed
#   ✅ OutputDebugString → normalized behavior
#   ✅ FindWindow → can block specific class names
ScyllaHide options (enable all for maximum bypass):
  [x] PEB (BeingDebugged, NtGlobalFlag, HeapFlags)
  [x] NtQueryInformationProcess (all three classes)
  [x] NtSetInformationThread
  [x] GetTickCount / QueryPerformanceCounter
  [x] OutputDebugString
  [x] Hardware Breakpoints (GetThreadContext)
  [x] Timing checks

13.2 x64dbg Manual Patches

# x64dbg command line — patch PEB directly
mov byte ptr [peb_address + 0x2], 0     ; Clear BeingDebugged
mov dword ptr [peb_address + 0xBC], 0   ; Clear NtGlobalFlag (x64)

# Find PEB address: View → CPU → Memory Map → look for [PEB] tag
# Or: run "peb" in x64dbg command bar → jumps to PEB in dump

# Bypass NtQueryInformationProcess manually:
bp ntdll:NtQueryInformationProcess
# When hit: check if [rsp+10h] (ProcessInformationClass) == 7 or 30 or 31
# If so: set return buffer to appropriate value before ret

13.3 Frida — Dynamic Instrumentation Bypass

// frida_antidebug_bypass.js
// Run: frida -l frida_antidebug_bypass.js -f target.exe --no-pause

// ── Windows API hooks ──
const IsDebuggerPresent = Module.findExportByName("kernel32.dll", "IsDebuggerPresent");
Interceptor.attach(IsDebuggerPresent, {
    onLeave(retval) {
        retval.replace(0);  // Always return FALSE
        console.log("[*] IsDebuggerPresent → spoofed to 0");
    }
});

const CheckRemoteDebuggerPresent = Module.findExportByName("kernel32.dll", "CheckRemoteDebuggerPresent");
Interceptor.attach(CheckRemoteDebuggerPresent, {
    onLeave(retval) {
        // The output parameter is in args[1] (PBOOL pbDebuggerPresent)
        this.pbDebuggerPresent.writeU32(0);
        console.log("[*] CheckRemoteDebuggerPresent → spoofed to FALSE");
    },
    onEnter(args) {
        this.pbDebuggerPresent = args[1];
    }
});

// NtQueryInformationProcess hook
const NtQIP = Module.findExportByName("ntdll.dll", "NtQueryInformationProcess");
Interceptor.attach(NtQIP, {
    onEnter(args) {
        this.infoClass = args[1].toInt32();
        this.infoPtr   = args[2];
    },
    onLeave(retval) {
        if (this.infoClass === 7) {   // ProcessDebugPort
            this.infoPtr.writePointer(ptr(0));
            console.log("[*] NtQIP(ProcessDebugPort) → spoofed to 0");
        }
        if (this.infoClass === 31) {  // ProcessDebugFlags
            this.infoPtr.writeU32(1);
            console.log("[*] NtQIP(ProcessDebugFlags) → spoofed to 1");
        }
        if (this.infoClass === 30) {  // ProcessDebugObject
            this.infoPtr.writePointer(ptr(0));
            console.log("[*] NtQIP(ProcessDebugObject) → spoofed to NULL");
        }
    }
});

// NtSetInformationThread — block ThreadHideFromDebugger
const NtSIT = Module.findExportByName("ntdll.dll", "NtSetInformationThread");
Interceptor.attach(NtSIT, {
    onEnter(args) {
        if (args[1].toInt32() === 0x11) {  // ThreadHideFromDebugger
            args[1] = ptr(0);  // Change the class → NOP it
            console.log("[*] NtSetInformationThread(ThreadHideFromDebugger) → neutralized");
        }
    }
});

// GetTickCount spoof for timing checks
const GetTickCount = Module.findExportByName("kernel32.dll", "GetTickCount");
let g_tick_base = Date.now();
Interceptor.replace(GetTickCount, new NativeCallback(function() {
    return (Date.now() - g_tick_base);  // Normalized time
}, 'uint32', []));

console.log("[*] Anti-debug bypass hooks installed");
# Run Frida bypass:
frida -l frida_antidebug_bypass.js -f target.exe --no-pause
frida -l frida_antidebug_bypass.js -p <pid>          # Attach to running process

13.4 Linux Bypass Tooling

# ── LD_PRELOAD ptrace bypass ──
gcc -shared -fPIC -o bypass_ptrace.so bypass_ptrace.c -ldl
LD_PRELOAD=./bypass_ptrace.so gdb ./target

# ── GDB built-in bypasses ──
# Bypass ptrace(TRACEME):
(gdb) catch syscall ptrace
(gdb) commands
>   if ($rdi == 0)
>     set $rax = 0
>     continue
>   end
> end

# Bypass /proc/self/status TracerPid check:
# Set follow-fork-mode so GDB follows the process reading its own status
(gdb) set follow-fork-mode parent

# Bypass SIGTRAP interception:
(gdb) handle SIGTRAP nostop noprint pass

# Bypass SIGILL (some programs use illegal instructions as anti-debug):
(gdb) handle SIGILL nostop noprint pass

# ── strace bypass ──
# Don't stop on SIGTRAP:
strace -e signal=!SIGTRAP -e trace=\!ptrace ./target

# ── Bypass parent process check (rename binary) ──
cp $(which gdb) /tmp/explorer
/tmp/explorer ./target   # GDB named "explorer" → parent check passes

13.5 TitanHide — Kernel-Level Anti-Anti-Debug

[TOOL] TitanHide — Windows kernel driver for transparent anti-debug bypass
GitHub: https://github.com/mrexodia/TitanHide
Supported checks it hides:
  - IOCTL_HIDE_DEBUG_PORT      (NtQueryInformationProcess)
  - IOCTL_HIDE_DEBUG_OBJECT    (NtQueryInformationProcess)
  - IOCTL_HIDE_NtQuerySysInfo  (hides debugger in system info)
  - IOCTL_HIDE_KernelDebugger  (hides kernel debugger presence)

Usage:
  sc create TitanHide binPath= "path\to\TitanHide.sys" type= kernel
  sc start TitanHide
  ; Attach debugger — all kernel-level debug flags hidden

14. Detection Comparison Matrix

Technique Complexity Evasion Strength Bypass Difficulty OS Bypassed by ScyllaHide
IsDebuggerPresent Trivial Very Low Trivial WIN ✅
CheckRemoteDebuggerPresent Very Low Low Easy WIN ✅
OutputDebugString Very Low Low Easy WIN ✅
FindWindow Low Low Easy (rename window) WIN Partial
PEB.BeingDebugged (direct) Low Low Easy WIN ✅
PEB.NtGlobalFlag Low Medium Easy WIN ✅
Heap Flags Low Medium Easy WIN ✅
NtQIP ProcessDebugPort Medium Medium-High Medium WIN ✅
NtQIP ProcessDebugFlags Medium Medium-High Medium WIN ✅
NtQIP ProcessDebugObject Medium High Medium WIN ✅
RDTSC timing Medium High Hard WIN/LIN Partial
Sleep acceleration Low Medium Medium WIN No
INT3 code scan Medium High Hard WIN No
HW Breakpoints (DR0-DR7) Medium High Medium WIN ✅
Guard page detection Medium Medium Medium WIN No
ThreadHideFromDebugger Medium Very High Hard WIN ✅
TLS Callbacks High High Hard WIN ✅ (with option set)
Parent process check Low Medium Easy WIN/LIN No (rename)
Self-debugging High High Hard WIN/LIN No
ptrace(TRACEME) Very Low Medium Easy LIN LD_PRELOAD
TracerPid check Very Low Medium Easy LIN LD_PRELOAD
CloseHandle exception Low Medium Easy WIN Partial
RDTSC (CPUID variant) Medium Very High Hard WIN/LIN No

15. Cheat Sheet

Quick Anti-Debug Implementation Order

For your implant / loader (add in this order):

Priority 1 — Run at startup, before any real code:
  [TLS Callback] → runs before main()
    ├─ IsDebuggerPresent()
    ├─ PEB.NtGlobalFlag check
    └─ Heap flags check

Priority 2 — At start of main/entry point:
  ├─ CheckRemoteDebuggerPresent
  ├─ NtQIP ProcessDebugPort
  ├─ NtQIP ProcessDebugFlags
  └─ Parent process name check

Priority 3 — Before sensitive operations:
  ├─ RDTSC timing check
  ├─ Sleep acceleration check
  ├─ HW breakpoint scan (DR0-DR7)
  └─ NtSetInformationThread(ThreadHideFromDebugger) on main thread

Priority 4 — Advanced / optional:
  ├─ Self-debugging child spawn
  ├─ INT3 code integrity scan
  └─ FindWindow for known debugger class names

Key Offsets Reference

PEB Offsets:
  Field                 x86 offset   x64 offset
  ──────────────────────────────────────────────
  BeingDebugged         +0x02        +0x02
  ProcessHeap           +0x18        +0x30
  NtGlobalFlag          +0x68        +0xBC

Heap Header Offsets:
  Field                 x86          x64
  ─────────────────────────────────────────
  Flags                 +0x0C        +0x40
  ForceFlags            +0x10        +0x44

PEB Access:
  x86: mov eax, fs:[30h]
  x64: mov rax, gs:[60h]

x64dbg Quick Bypass Commands

# Find PEB address
peb                      # Jump to PEB in dump view

# Patch BeingDebugged
eb peb_addr+2 0          # Write 0 to BeingDebugged (x64dbg command bar)

# Patch NtGlobalFlag (x64)
ed peb_addr+BC 0         # Write DWORD 0 to NtGlobalFlag

# Patch Heap Flags
ed processheap_addr+40 2  # Set Flags to HEAP_GROWABLE
ed processheap_addr+44 0  # Set ForceFlags to 0

# Run with ScyllaHide:
# Plugin → ScyllaHide → enable all PEB / NtQuery options → attach normally

Linux Quick Bypass Commands

# LD_PRELOAD ptrace bypass
export LD_PRELOAD=./bypass_ptrace.so
gdb ./target

# GDB handle signals
(gdb) handle SIGTRAP nostop noprint pass
(gdb) handle SIGILL  nostop noprint pass
(gdb) set follow-fork-mode parent

# Rename gdb to avoid parent process check
cp $(which gdb) /tmp/code
/tmp/code ./target

# Read TracerPid manually
cat /proc/<pid>/status | grep TracerPid

Python Script — Scan Binary for Anti-Debug Patterns

#!/usr/bin/env python3
# scan_antidebug.py — scan PE binary for known anti-debug API imports and byte patterns

import pefile
import sys

SUSPICIOUS_IMPORTS = [
    "IsDebuggerPresent",
    "CheckRemoteDebuggerPresent",
    "OutputDebugString",
    "FindWindow",
    "FindWindowA",
    "FindWindowW",
    "NtQueryInformationProcess",
    "ZwQueryInformationProcess",
    "NtSetInformationThread",
    "GetThreadContext",
]

SUSPICIOUS_BYTE_PATTERNS = [
    (b"\xcd\x2d",           "INT 2D instruction"),
    (b"\x64\xa1\x30\x00",   "FS:[30h] PEB access (x86)"),
    (b"\x65\x48\x8b\x04",   "GS:[60h] PEB access (x64)"),
    (b"\x0f\x31",           "RDTSC instruction"),
    (b"\x0f\xa2",           "CPUID instruction"),
    (b"\xcc",               "INT3 / breakpoint (single byte)"),
]

def scan_file(path: str):
    print(f"[*] Scanning: {path}")
    print()

    try:
        pe = pefile.PE(path)
    except Exception as e:
        print(f"[-] PE parse error: {e}")
        return

    # ── Check imports ──
    print("=== Suspicious API Imports ===")
    found_imports = []
    if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
        for entry in pe.DIRECTORY_ENTRY_IMPORT:
            dll = entry.dll.decode(errors='replace')
            for imp in entry.imports:
                if imp.name:
                    name = imp.name.decode(errors='replace')
                    if name in SUSPICIOUS_IMPORTS:
                        print(f"  [!] {dll} → {name}")
                        found_imports.append(name)

    if not found_imports:
        print("  [+] No suspicious imports found")

    # ── Check for RDTSC / PEB access patterns in code ──
    print()
    print("=== Suspicious Byte Patterns ===")
    raw = open(path, "rb").read()
    for pattern, desc in SUSPICIOUS_BYTE_PATTERNS:
        if pattern == b"\xcc":
            continue   # Too common, skip single-byte INT3
        positions = []
        start = 0
        while True:
            pos = raw.find(pattern, start)
            if pos == -1:
                break
            positions.append(pos)
            start = pos + 1
        if positions:
            print(f"  [!] {desc}: found at {len(positions)} location(s) — first: 0x{positions[0]:08X}")

    print()
    print("[*] Scan complete. Verify findings in IDA/Ghidra/x64dbg for context.")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <PE_file>")
        sys.exit(1)
    scan_file(sys.argv[1])
# Usage:
pip install pefile
python3 scan_antidebug.py target.exe

References & Tools

Tools: - ScyllaHide — https://github.com/x64dbg/ScyllaHide (x64dbg/IDA anti-anti-debug plugin) - TitanHide — https://github.com/mrexodia/TitanHide (kernel-level bypass driver) - al-khaser — https://github.com/ayoubfaouzi/al-khaser (anti-analysis PoC test suite) - Frida — https://frida.re (dynamic instrumentation framework) - x64dbg — https://x64dbg.com (primary Windows debugger for malware analysis) - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck (find AV/EDR trigger bytes)

References: - Checkpoint Anti-Debug Reference — https://anti-debug.checkpoint.com - Peter Ferrie — "The Ultimate Anti-Debugging Reference" (PDF) - Checkpoint Evasion Reference — https://evasions.checkpoint.com - MBC Malware Behavior Catalog (Debugger Detection B0001) — https://github.com/MBCProject/mbc-markdown - ired.team red team notes — https://www.ired.team

CTF / Practice: - al-khaser binary (benign malware test suite) — run it in a VM, analyze in x64dbg - HackTheBox / CTFtime reverse challenges with anti-debug - Crackmes.one — https://crackmes.one (search "anti-debug" tag)


← Malware development top ↑