Anti-Debugging Techniques
Overview
Anti-debugging is a cat-and-mouse game between a malware author and an analyst’s debugger. The goal is not to be impossible to debug — given enough time and a hypervisor, everything can be analysed — but to raise the cost enough that automated detonation fails and interactive analysis requires deliberate counter-counter-measures.
Detection checks fall into two categories: passive checks that query state a debugger
changes (the PEB, DebugPort, CPU context registers), and active disruptions that
alter the debugging environment (ThreadHideFromDebugger, exception-based anti-debug tricks).
NtSetInformationThread(ThreadHideFromDebugger) has no legitimate use in production code.
Any binary that calls it is either a debugger itself, a DRM system, or hostile. It is one of
the clearest single-API heuristics in the catalogue.
The call chain
- 1IsDebuggerPresentReads the BeingDebugged byte from the PEB — the most obvious check.
- 2NtQueryInformationProcess(ProcessDebugPort)Kernel sets DebugPort to a non-zero value when the process is under a debugger.
- 3CheckRemoteDebuggerPresentWraps NtQueryInformationProcess for cross-process checks.
- 4NtSetInformationThread(ThreadHideFromDebugger)Makes a thread invisible to the debug subsystem; breakpoints and single-step silently stop working.
- 5hardware breakpoint check via GetThreadContextNon-zero DR0–DR3 registers indicate that a debugger has set hardware breakpoints.
- 6timing check (RDTSC)Measure cycles between two RDTSC calls; single-stepping inflates the delta dramatically.
Reference implementation
#include <windows.h>
#include <intrin.h>
// forward-declare NtSetInformationThread (not in the standard headers)
typedef NTSTATUS (NTAPI *NtSIT_t)(HANDLE, ULONG, PVOID, ULONG);
#define ThreadHideFromDebugger 0x11
// 1. PEB flag — trivially patched by a debugger plugin
static BOOL peb_check(void) {
return IsDebuggerPresent();
}
// 2. DebugPort via NtQueryInformationProcess
static BOOL debug_port_check(void) {
HANDLE port = NULL;
NtQueryInformationProcess(GetCurrentProcess(),
7 /* ProcessDebugPort */, &port, sizeof port, NULL);
return port != NULL;
}
// 3. Hardware breakpoints in DR registers
static BOOL hwbp_check(void) {
CONTEXT ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext(GetCurrentThread(), &ctx);
return ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3;
}
// 4. RDTSC timing — single-stepping bloats the delta to millions of cycles
static BOOL timing_check(void) {
UINT64 t1 = __rdtsc();
__cpuid((int[4]){0}, 0); // serialising instruction
UINT64 t2 = __rdtsc();
return (t2 - t1) > 1000; // normal: ~40 cycles; debugger: >> 10k
}
// 5. Hide the current thread from the debug subsystem (no patch survives this)
static void hide_thread(void) {
NtSIT_t fn = (NtSIT_t)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");
if (fn) fn(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
}
BOOL debugger_present(void) {
return peb_check() || debug_port_check() ||
hwbp_check() || timing_check();
}
// call early in the loader, before the first beacon
void anti_debug_init(void) {
hide_thread();
if (debugger_present()) ExitProcess(0);
}#include <windows.h>
#include <intrin.h>
// forward-declare NtSetInformationThread (not in the standard headers)
typedef NTSTATUS (NTAPI *NtSIT_t)(HANDLE, ULONG, PVOID, ULONG);
#define ThreadHideFromDebugger 0x11
// 1. PEB flag — trivially patched by a debugger plugin
static BOOL peb_check(void) {
return IsDebuggerPresent();
}
// 2. DebugPort via NtQueryInformationProcess
static BOOL debug_port_check(void) {
HANDLE port = NULL;
NtQueryInformationProcess(GetCurrentProcess(),
7 /* ProcessDebugPort */, &port, sizeof port, NULL);
return port != NULL;
}
// 3. Hardware breakpoints in DR registers
static BOOL hwbp_check(void) {
CONTEXT ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext(GetCurrentThread(), &ctx);
return ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3;
}
// 4. RDTSC timing — single-stepping bloats the delta to millions of cycles
static BOOL timing_check(void) {
UINT64 t1 = __rdtsc();
__cpuid((int[4]){0}, 0); // serialising instruction
UINT64 t2 = __rdtsc();
return (t2 - t1) > 1000; // normal: ~40 cycles; debugger: >> 10k
}
// 5. Hide the current thread from the debug subsystem (no patch survives this)
static void hide_thread(void) {
NtSIT_t fn = (NtSIT_t)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");
if (fn) fn(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
}
BOOL debugger_present(void) {
return peb_check() || debug_port_check() ||
hwbp_check() || timing_check();
}
// call early in the loader, before the first beacon
void anti_debug_init(void) {
hide_thread();
if (debugger_present()) ExitProcess(0);
}Exception-based tricks
Beyond state polling, some samples use the CPU’s own exception machinery:
// OutputDebugStringA poisoning: a debugger consumes the string;
// the error code differs between debugged and undebuggged processes.
static BOOL output_debug_check(void) {
SetLastError(0xDEAD);
OutputDebugStringA("x");
return GetLastError() == 0xDEAD; // no debugger consumed it
}
// Single-step trap: set TF, install a VEH, verify the exception fires.
// Under some debuggers the exception is swallowed and the VEH never runs.
static volatile BOOL seh_fired = FALSE;
static LONG WINAPI seh_handler(EXCEPTION_POINTERS *ep) {
if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
seh_fired = TRUE;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
static BOOL seh_check(void) {
HANDLE h = AddVectoredExceptionHandler(1, seh_handler);
__asm__ volatile ("pushfq; orq $0x100,(%%rsp); popfq" ::: "cc");
__asm__ volatile ("nop");
RemoveVectoredExceptionHandler(h);
return !seh_fired; // TRUE → debugger swallowed the exception
}// OutputDebugStringA poisoning: a debugger consumes the string;
// the error code differs between debugged and undebuggged processes.
static BOOL output_debug_check(void) {
SetLastError(0xDEAD);
OutputDebugStringA("x");
return GetLastError() == 0xDEAD; // no debugger consumed it
}
// Single-step trap: set TF, install a VEH, verify the exception fires.
// Under some debuggers the exception is swallowed and the VEH never runs.
static volatile BOOL seh_fired = FALSE;
static LONG WINAPI seh_handler(EXCEPTION_POINTERS *ep) {
if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
seh_fired = TRUE;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
static BOOL seh_check(void) {
HANDLE h = AddVectoredExceptionHandler(1, seh_handler);
__asm__ volatile ("pushfq; orq $0x100,(%%rsp); popfq" ::: "cc");
__asm__ volatile ("nop");
RemoveVectoredExceptionHandler(h);
return !seh_fired; // TRUE → debugger swallowed the exception
}Bypassing as an analyst
| Check | Bypass |
|---|---|
IsDebuggerPresent |
ScyllaHide plugin; patch PEB.BeingDebugged to 0 |
NtQueryInformationProcess(DebugPort) |
Hook the syscall or use ScyllaHide |
| Hardware breakpoints | Use software breakpoints (int3) instead |
ThreadHideFromDebugger |
Kernel debugger (WinDbg kernel attach) is not affected |
| RDTSC timing | VMware option “Disable time-stamp counter” in .vmx |
| Sleep acceleration | Some sandboxes: NtDelayExecution hook that reports real elapsed time |
The ThreadHideFromDebugger call is the one check that a user-mode debugger cannot transparently
bypass without kernel support — the thread genuinely becomes invisible to the debug port’s event
dispatch.