Skip to content
λmaldev wiki/
pagesAnti-Debugging Techniques
T1622WindowsC / C++ASMWinAPI

Anti-Debugging Techniques

updated 2026-08-048 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

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).

Note.

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

  1. 1
    IsDebuggerPresent
    Reads the BeingDebugged byte from the PEB — the most obvious check.
  2. 2
    NtQueryInformationProcess(ProcessDebugPort)
    Kernel sets DebugPort to a non-zero value when the process is under a debugger.
  3. 3
    CheckRemoteDebuggerPresent
    Wraps NtQueryInformationProcess for cross-process checks.
  4. 4
    NtSetInformationThread(ThreadHideFromDebugger)
    Makes a thread invisible to the debug subsystem; breakpoints and single-step silently stop working.
  5. 5
    hardware breakpoint check via GetThreadContext
    Non-zero DR0–DR3 registers indicate that a debugger has set hardware breakpoints.
  6. 6
    timing check (RDTSC)
    Measure cycles between two RDTSC calls; single-stepping inflates the delta dramatically.

Reference implementation

anti_debug.cC
#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:

exception_tricks.cC
// 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.

Detection

YARA
IsDebuggerPresent / NtQueryInformationProcess with ProcessDebugPort (7) in the same function block.
BEHAVIOURAL
NtSetInformationThread with ThreadHideFromDebugger (0x11) — no legitimate use outside malware.
MEMORY SCAN
PEB.BeingDebugged is patched to 0 at runtime (self-patching to fool IsDebuggerPresent checks).
SANDBOX
Log and ignore anti-debug API returns; force IsDebuggerPresent to return 0 via API hooking.
Was this page useful?edit this page ↗