NtCreateDebugObject
Erzeugt ein Kernel-DebugObject — den Debugger-spezifischen Port für Debug-Events der angehängten Prozesse.
Prototyp
NTSTATUS NtCreateDebugObject( PHANDLE DebugObjectHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, ULONG Flags );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| DebugObjectHandle | PHANDLE | out | Empfängt ein Handle auf das neu erzeugte DebugObject. An NtDebugActiveProcess weitergeben. |
| DesiredAccess | ACCESS_MASK | in | Zugriffsmaske. DEBUG_ALL_ACCESS (0x1F000F) ist für Debugger üblich. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Objekt-Attribute. Üblicherweise nullinitialisiert — DebugObjects sind selten benannt. |
| Flags | ULONG | in | DEBUG_KILL_ON_CLOSE (1) beendet den Debuggee, wenn das letzte Handle des DebugObject geschlossen wird. |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | 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 |
Kernel-Modul
Verwandte APIs
Syscall-Stub
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
Undokumentierte Hinweise
DebugObject ist die Kernel-Primitive hinter der Win32-Debug-API: kernel32!DebugActiveProcess, WaitForDebugEvent und ContinueDebugEvent arbeiten alle über ein solches Objekt. Ein Prozess kann immer nur an genau einen DebugObject angehängt sein — diese Exklusivität ist die tragende Eigenschaft der gesamten Self-Debug-Anti-Attach-Familie. Die SSN wandert mit fast jedem Feature-Update (0x9A → 0xAB von 1507 bis 24H2); statische Tabellen sind unsicher, dynamisch auflösen. Nach der Erzeugung folgt typischerweise NtCreateDebugObject → NtDebugActiveProcess(Ziel, dbgObj) → NtWaitForDebugEvent-Schleife, optional NtRemoveProcessDebug zum Lösen.
Häufige Malware-Nutzung
Self-Debug-Anti-Attach: Die Malware (oder ein mit DEBUG_PROCESS gestarteter Child-Prozess) hängt sich selbst oder ihr geschütztes Kind an einen eigenen DebugObject. Da Windows nur einen Debug-Port pro Prozess erlaubt, scheitert jeder spätere Attach-Versuch von WinDbg, x64dbg oder dem Attach-on-Crash eines EDRs via DebugActiveProcess mit STATUS_PORT_ALREADY_SET. Kommerzielle Protectoren (Themida, VMProtect, Enigma) bringen dieses Muster mit; ebenso die meisten Crypter, GuLoader-artige Loader und von Donut erzeugte Stager. Der Preis: Die Malware muss Debug-Events nun selbst bedienen, und unbehandelte Exceptions des Debuggees landen in dieser Schleife statt bei WER.
Erkennungsmöglichkeiten
Die Erzeugung eines DebugObject durch einen Nicht-Debugger-Prozess ist auf Workstations ungewöhnlich. Der ETW-Provider Microsoft-Windows-Kernel-Object emittiert Object-Create-Events; mit ImageFileName != bekannten Debuggern (windbg.exe, vsjitdebugger.exe, devenv.exe, ProcMon.exe) korrelieren. Hochwertiges Signal ist das Paar NtCreateDebugObject unmittelbar gefolgt von NtDebugActiveProcess auf Self oder ein frisch gespawntes Kind (besonders mit DEBUG_KILL_ON_CLOSE — das Killen des Debugger-Threads tötet den Loader, klar feindselig). Defender for Endpoint zeigt DebugActiveProcess über den ETW-Threat-Intelligence-Kanal.
Direkte Syscall-Beispiele
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 }
}MITRE ATT&CK-Mappings
Last verified: 2026-05-20