NtOpenDirectoryObject
Opens an existing directory object in the Windows object manager namespace.
Prototype
NTSTATUS NtOpenDirectoryObject( PHANDLE DirectoryHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| DirectoryHandle | PHANDLE | out | Receives the handle to the opened directory object. |
| DesiredAccess | ACCESS_MASK | in | DIRECTORY_QUERY for enumeration, DIRECTORY_TRAVERSE for path resolution, etc. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Name and optional RootDirectory identifying the target directory (e.g. L"\\BaseNamedObjects"). |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x58 | win10-1507 |
| Win10 1607 | 0x58 | win10-1607 |
| Win10 1703 | 0x58 | win10-1703 |
| Win10 1709 | 0x58 | win10-1709 |
| Win10 1803 | 0x58 | win10-1803 |
| Win10 1809 | 0x58 | win10-1809 |
| Win10 1903 | 0x58 | win10-1903 |
| Win10 1909 | 0x58 | win10-1909 |
| Win10 2004 | 0x58 | win10-2004 |
| Win10 20H2 | 0x58 | win10-20h2 |
| Win10 21H1 | 0x58 | win10-21h1 |
| Win10 21H2 | 0x58 | win10-21h2 |
| Win10 22H2 | 0x58 | win10-22h2 |
| Win11 21H2 | 0x58 | win11-21h2 |
| Win11 22H2 | 0x58 | win11-22h2 |
| Win11 23H2 | 0x58 | win11-23h2 |
| Win11 24H2 | 0x58 | win11-24h2 |
| Server 2016 | 0x58 | winserver-2016 |
| Server 2019 | 0x58 | winserver-2019 |
| Server 2022 | 0x58 | winserver-2022 |
| Server 2025 | 0x58 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 58 00 00 00 mov eax, 0x58 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
Undocumented notes
SSN `0x58` has remained constant from 1507 through 24H2, which is unusual for an object-manager routine — this is a side effect of the open-style syscalls living in the front half of the table where Microsoft has been more careful about churn. Opening a Directory object is the first step in any namespace exploration: with `DIRECTORY_QUERY` access you can call NtQueryDirectoryObject to enumerate children; with `DIRECTORY_TRAVERSE` you can use the returned handle as a `RootDirectory` in subsequent OBJECT_ATTRIBUTES to shortcut name resolution. The standard prefixes are `\\` (global root), `\\??\\` (DOS device link directory for the current login session — `\\GLOBAL??` is the system-wide variant), `\\BaseNamedObjects`, `\\Sessions\\<n>\\BaseNamedObjects`, `\\Device`, `\\Driver`, `\\KnownDlls`, `\\KernelObjects`.
Common malware usage
Most often used as a setup step for **namespace enumeration** — implants open `\\Sessions\\<n>\\BaseNamedObjects` with DIRECTORY_QUERY and then call NtQueryDirectoryObject to look for known mutex/event names belonging to AVs and EDRs (Defender's `Global\\SQMMUTEX_TIMER`, several Symantec/CrowdStrike named events, sandbox mutants from Cuckoo and Triage), banking malware named pipes used by competitors, and existing C2-implant rendezvous directories for survivor checks. Also used as the **RootDirectory shortcut** when opening many objects beneath a known parent — opening the parent once and reusing it avoids re-parsing the full prefix path and stays slightly cleaner in traces. **Sandbox/container survey**: an AppContainer-confined process can open its own per-AppContainer BNO root to inventory what objects its broker exposes.
Detection opportunities
Almost no legitimate code outside of csrss.exe, services.exe and Process Explorer enumerates the object-manager namespace at scale, but **single opens of common directories** (`\\BaseNamedObjects`, `\\KnownDlls`) are noisy and present in many legitimate apps. The high-value pivot is **NtOpenDirectoryObject followed by a tight loop of NtQueryDirectoryObject** — the canonical enumeration fingerprint. Detection lives in kernel-mode ObRegisterCallbacks on `IoDirectoryObjectType`, ETW Microsoft-Windows-Kernel-Audit-API-Calls, and — surprisingly — Microsoft Defender for Endpoint emits a synthetic alert for non-system processes opening `\\GLOBAL??` with DIRECTORY_QUERY. Direct syscalls bypass ntdll hooks; kernel callbacks still fire.
Direct syscall examples
asmx64 direct stub
; Direct syscall stub for NtOpenDirectoryObject (SSN 0x58, all builds)
NtOpenDirectoryObject PROC
mov r10, rcx ; syscall convention
mov eax, 58h ; SSN
syscall
ret
NtOpenDirectoryObject ENDPcOpen per-session BNO for AV/EDR mutex sweep
// Open the current session's BaseNamedObjects directory with QUERY access
// so we can later NtQueryDirectoryObject() it and look for known
// AV/EDR mutex names (Defender, CrowdStrike, Cuckoo, Triage, ...).
#include <windows.h>
#include <winternl.h>
#define DIRECTORY_QUERY 0x0001
#define DIRECTORY_TRAVERSE 0x0002
#define OBJ_CASE_INSENSITIVE 0x00000040
typedef NTSTATUS (NTAPI *pNtOpenDirectoryObject)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES);
HANDLE OpenSessionBNO(ULONG sessionId) {
WCHAR path[64];
swprintf_s(path, 64, L"\\Sessions\\%lu\\BaseNamedObjects", sessionId);
UNICODE_STRING us; RtlInitUnicodeString(&us, path);
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, &us, OBJ_CASE_INSENSITIVE, NULL, NULL);
pNtOpenDirectoryObject NtOpenDirectoryObject = (pNtOpenDirectoryObject)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenDirectoryObject");
HANDLE h = NULL;
if (NT_SUCCESS(NtOpenDirectoryObject(&h, DIRECTORY_QUERY | DIRECTORY_TRAVERSE, &oa)))
return h;
return NULL;
}rustOpen \GLOBAL?? as DOS-device root
// Cargo: ntapi = "0.4", widestring = "1"
// Open the system-wide DOS device-link directory; subsequent
// NtCreateSymbolicLinkObject calls with this as RootDirectory plant
// links visible to every session (requires SeCreateSymbolicLinkPrivilege).
use ntapi::ntobapi::NtOpenDirectoryObject;
use ntapi::ntrtl::RtlInitUnicodeString;
use ntapi::ntobapi::OBJ_CASE_INSENSITIVE;
use winapi::shared::ntdef::{OBJECT_ATTRIBUTES, UNICODE_STRING};
use widestring::U16CString;
pub unsafe fn open_global_dosdev() -> Option<isize> {
let w = U16CString::from_str("\\GLOBAL??").ok()?;
let mut us: UNICODE_STRING = std::mem::zeroed();
RtlInitUnicodeString(&mut us, w.as_ptr());
let mut oa: OBJECT_ATTRIBUTES = std::mem::zeroed();
oa.Length = std::mem::size_of::<OBJECT_ATTRIBUTES>() as u32;
oa.ObjectName = &mut us;
oa.Attributes = OBJ_CASE_INSENSITIVE;
let mut h: isize = 0;
let s = NtOpenDirectoryObject(&mut h as *mut _ as _, 0x3, &mut oa);
if s >= 0 { Some(h) } else { None }
}MITRE ATT&CK mappings
Last verified: 2026-05-20