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
| Name | Type | Dir | Description |
|---|---|---|---|
| DebugObjectHandle | PHANDLE | out | Reçoit un handle vers le DebugObject nouvellement créé. À passer à NtDebugActiveProcess. |
| DesiredAccess | ACCESS_MASK | in | Masque d'accès. DEBUG_ALL_ACCESS (0x1F000F) est typique pour un débogueur. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Attributs d'objet. En général à zéro — les DebugObjects sont rarement nommés. |
| Flags | ULONG | in | DEBUG_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 Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x9A | win10-1507 |
| Win10 1607 | 0x9B | win10-1607 |
| Win10 1703 | 0x9E | win10-1703 |
| Win10 1709 | 0x9F | win10-1709 |
| Win10 1803 | 0xA0 | win10-1803 |
| Win10 1809 | 0xA0 | win10-1809 |
| Win10 1903 | 0xA1 | win10-1903 |
| Win10 1909 | 0xA1 | win10-1909 |
| Win10 2004 | 0xA5 | win10-2004 |
| Win10 20H2 | 0xA5 | win10-20h2 |
| Win10 21H1 | 0xA5 | win10-21h1 |
| Win10 21H2 | 0xA6 | win10-21h2 |
| Win10 22H2 | 0xA6 | win10-22h2 |
| Win11 21H2 | 0xA8 | win11-21h2 |
| Win11 22H2 | 0xA9 | win11-22h2 |
| Win11 23H2 | 0xA9 | win11-23h2 |
| Win11 24H2 | 0xAB | win11-24h2 |
| Server 2016 | 0x9B | winserver-2016 |
| Server 2019 | 0xA0 | winserver-2019 |
| Server 2022 | 0xA8 | winserver-2022 |
| Server 2025 | 0xAB | winserver-2025 |
Module noyau
APIs liées
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 ENDPrustwindows-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