> Windows Syscalls
ntoskrnl.exeT1622T1106

NtCreateDebugObject

Crée un DebugObject noyau — le port par débogueur qui reçoit les événements des processus attachés.

Prototype

NTSTATUS NtCreateDebugObject(
  PHANDLE            DebugObjectHandle,
  ACCESS_MASK        DesiredAccess,
  POBJECT_ATTRIBUTES ObjectAttributes,
  ULONG              Flags
);

Arguments

NameTypeDirDescription
DebugObjectHandlePHANDLEoutReçoit un handle vers le DebugObject nouvellement créé. À passer à NtDebugActiveProcess.
DesiredAccessACCESS_MASKinMasque d'accès. DEBUG_ALL_ACCESS (0x1F000F) est typique pour un débogueur.
ObjectAttributesPOBJECT_ATTRIBUTESinAttributs d'objet. En général à zéro — les DebugObjects sont rarement nommés.
FlagsULONGinDEBUG_KILL_ON_CLOSE (1) termine le débuggé quand le dernier handle du DebugObject se ferme.

IDs de syscalls par version de Windows

Version de WindowsID de syscallBuild
Win10 15070x9Awin10-1507
Win10 16070x9Bwin10-1607
Win10 17030x9Ewin10-1703
Win10 17090x9Fwin10-1709
Win10 18030xA0win10-1803
Win10 18090xA0win10-1809
Win10 19030xA1win10-1903
Win10 19090xA1win10-1909
Win10 20040xA5win10-2004
Win10 20H20xA5win10-20h2
Win10 21H10xA5win10-21h1
Win10 21H20xA6win10-21h2
Win10 22H20xA6win10-22h2
Win11 21H20xA8win11-21h2
Win11 22H20xA9win11-22h2
Win11 23H20xA9win11-23h2
Win11 24H20xABwin11-24h2
Server 20160x9Bwinserver-2016
Server 20190xA0winserver-2019
Server 20220xA8winserver-2022
Server 20250xABwinserver-2025

Module noyau

ntoskrnl.exeNtCreateDebugObject

APIs liées

DebugActiveProcessDebugActiveProcessStopWaitForDebugEventExContinueDebugEventNtDebugActiveProcessNtRemoveProcessDebugNtDebugContinueNtWaitForDebugEvent

Stub du syscall

4C 8B D1            mov r10, rcx
B8 AB 00 00 00      mov eax, 0xAB
F6 04 25 08 03 FE 7F 01   test byte ptr [0x7FFE0308], 1
75 03               jne short +3
0F 05               syscall
C3                  ret
CD 2E               int 2Eh
C3                  ret

Notes non documentées

Le DebugObject est la primitive noyau qui sous-tend l'API de debug Win32 : kernel32!DebugActiveProcess, WaitForDebugEvent, ContinueDebugEvent passent tous par un de ces objets. Un processus ne peut être attaché qu'à un seul DebugObject à la fois — ce caractère exclusif est le pivot de toute la famille des trucs anti-attach par self-debug. La SSN dérive à presque chaque mise à jour majeure (0x9A → 0xAB de 1507 à 24H2), les tables figées sont donc dangereuses ; résolvez dynamiquement. La séquence usuelle après création est NtCreateDebugObject → NtDebugActiveProcess(cible, dbgObj) → boucle NtWaitForDebugEvent, éventuellement NtRemoveProcessDebug pour détacher.

Usage courant par les malwares

Anti-attach par self-debug : le malware (ou un enfant lancé avec DEBUG_PROCESS) s'attache lui-même ou son enfant protégé à un DebugObject qu'il détient. Windows n'autorisant qu'un seul port de debug par processus, toute tentative ultérieure de WinDbg, x64dbg ou d'un attach-on-crash d'EDR via DebugActiveProcess échoue avec STATUS_PORT_ALREADY_SET. Les protecteurs commerciaux (Themida, VMProtect, Enigma) embarquent ce motif ; idem la plupart des crypters, loaders type GuLoader et stagers générés par Donut. Le coût existe : le malware doit alors servir les événements de debug sur son propre thread, et les exceptions non gérées du débuggé remontent dans cette boucle au lieu d'aller à WER.

Opportunités de détection

La création d'un DebugObject par un processus non débogueur est inhabituelle sur poste. Le provider ETW Microsoft-Windows-Kernel-Object émet des événements de création d'objet ; à corréler avec ImageFileName différent des débogueurs connus (windbg.exe, vsjitdebugger.exe, devenv.exe, ProcMon.exe). Le signal le plus fiable est le couple NtCreateDebugObject suivi immédiatement de NtDebugActiveProcess visant soi-même ou un enfant fraîchement spawné (surtout avec DEBUG_KILL_ON_CLOSE — tuer le thread débogueur tue alors le loader, intention hostile). Defender for Endpoint remonte DebugActiveProcess via le canal ETW Threat Intelligence.

Exemples de syscalls directs

cSelf-debug anti-attach setup

// Phase 1: create a DebugObject owned by us, then attach to our own PID.
// Any later DebugActiveProcess() against us will return STATUS_PORT_ALREADY_SET.
typedef NTSTATUS(NTAPI* fnNtCreateDebugObject)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, ULONG);
typedef NTSTATUS(NTAPI* fnNtDebugActiveProcess)(HANDLE, HANDLE);

HANDLE g_dbg = NULL;
void InstallSelfDebug(void) {
    HMODULE n = GetModuleHandleA("ntdll.dll");
    fnNtCreateDebugObject pCreate = (fnNtCreateDebugObject)GetProcAddress(n, "NtCreateDebugObject");
    fnNtDebugActiveProcess pAttach = (fnNtDebugActiveProcess)GetProcAddress(n, "NtDebugActiveProcess");
    OBJECT_ATTRIBUTES oa = { sizeof(oa) };
    pCreate(&g_dbg, 0x1F000F /* DEBUG_ALL_ACCESS */, &oa, 0 /* no kill-on-close */);
    pAttach((HANDLE)-1 /* self */, g_dbg);
    // Servicing the debug-event loop on a worker thread is still required so
    // the parent does not deadlock on its own exceptions.
}

asmx64 direct stub (Win11 24H2 SSN)

; SSN 0xAB on win11-24h2 / winserver-2025. Resolve dynamically across builds.
NtCreateDebugObject PROC
    mov  r10, rcx
    mov  eax, 0ABh
    syscall
    ret
NtCreateDebugObject ENDP

rustwindows-sys + ntdll lookup

// Cargo: windows-sys = "0.59"
use std::ffi::c_void;
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};

type NtCreateDebugObject = unsafe extern "system" fn(
    out_handle: *mut isize,
    desired_access: u32,
    object_attributes: *mut c_void,
    flags: u32,
) -> i32;

pub unsafe fn create_debug_object() -> Option<isize> {
    let ntdll = GetModuleHandleA(b"ntdll.dll\0".as_ptr());
    if ntdll.is_null() { return None; }
    let addr = GetProcAddress(ntdll, b"NtCreateDebugObject\0".as_ptr())?;
    let f: NtCreateDebugObject = std::mem::transmute(addr);
    let mut h: isize = 0;
    let mut oa = [0u8; 48];
    if f(&mut h, 0x1F000F, oa.as_mut_ptr().cast(), 0) == 0 { Some(h) } else { None }
}

Mappings MITRE ATT&CK

Last verified: 2026-05-20