NtOpenDirectoryObject
Ouvre un objet directory existant dans l'espace de noms de l'object manager Windows.
Prototype
NTSTATUS NtOpenDirectoryObject( PHANDLE DirectoryHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| DirectoryHandle | PHANDLE | out | Reçoit le handle vers l'objet directory ouvert. |
| DesiredAccess | ACCESS_MASK | in | DIRECTORY_QUERY pour énumération, DIRECTORY_TRAVERSE pour résolution de chemin, etc. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Nom et RootDirectory optionnel identifiant le directory cible (ex. L"\\BaseNamedObjects"). |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | 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 |
Module noyau
APIs liées
Stub du syscall
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
Notes non documentées
Le SSN `0x58` est resté constant de 1507 à 24H2, ce qui est rare pour une routine d'object manager — effet du fait que les syscalls d'ouverture vivent dans la première moitié de la table, là où Microsoft fait plus attention à la stabilité. Ouvrir un Directory est la première étape de toute exploration de namespace : avec `DIRECTORY_QUERY` on peut appeler NtQueryDirectoryObject pour énumérer les enfants ; avec `DIRECTORY_TRAVERSE` le handle peut servir de `RootDirectory` dans des OBJECT_ATTRIBUTES ultérieurs pour court-circuiter la résolution de nom. Préfixes standards : `\\` (racine globale), `\\??\\` (répertoire des liens DOS pour la session courante — `\\GLOBAL??` est la variante système), `\\BaseNamedObjects`, `\\Sessions\\<n>\\BaseNamedObjects`, `\\Device`, `\\Driver`, `\\KnownDlls`, `\\KernelObjects`.
Usage courant par les malwares
Le plus souvent comme étape de préparation à une **énumération du namespace** — les implants ouvrent `\\Sessions\\<n>\\BaseNamedObjects` avec DIRECTORY_QUERY puis appellent NtQueryDirectoryObject pour chercher des noms de mutex/event connus appartenant aux AV et EDR (le `Global\\SQMMUTEX_TIMER` de Defender, plusieurs events nommés Symantec/CrowdStrike, mutants sandbox de Cuckoo et Triage), des named pipes de banking malware concurrents, et des répertoires de rendez-vous d'implants C2 existants pour vérifier les survivants. Aussi comme **raccourci RootDirectory** quand on ouvre de nombreux objets sous un parent connu — ouvrir le parent une fois et le réutiliser évite de re-parser le chemin et reste un peu plus propre dans les traces. **Inventaire sandbox/container** : un processus enfermé en AppContainer peut ouvrir son propre BNO racine pour inventorier ce que son broker expose.
Opportunités de détection
Quasi aucun code légitime hors csrss.exe, services.exe et Process Explorer n'énumère le namespace de l'object manager à grande échelle, mais **les ouvertures uniques de directories communs** (`\\BaseNamedObjects`, `\\KnownDlls`) sont bruyantes et présentes dans beaucoup d'apps légitimes. Pivot à haute valeur : **NtOpenDirectoryObject suivi d'une boucle serrée de NtQueryDirectoryObject** — empreinte canonique de l'énumération. La détection vit dans ObRegisterCallbacks côté noyau sur `IoDirectoryObjectType`, ETW Microsoft-Windows-Kernel-Audit-API-Calls, et — étonnamment — Microsoft Defender for Endpoint émet une alerte synthétique sur les processus non système ouvrant `\\GLOBAL??` avec DIRECTORY_QUERY. Les syscalls directs contournent les hooks ntdll ; les callbacks noyau se déclenchent quand même.
Exemples de syscalls directs
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 }
}Mappings MITRE ATT&CK
Last verified: 2026-05-20