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)