Pavel Yosifovich, co-author of “Windows Internals 7th Edition, Part 1” and author of “Windows Native API Programming”, takes apart a structure that anyone working below the Windows API meets sooner or later: OBJECT_ATTRIBUTES. Unlike LIST_ENTRY, which lives inside many kernel/native data structures, OBJECT_ATTRIBUTES is never stored anywhere. It only exists in code, as the argument you hand to a function that creates or opens a kernel object. That makes it easy to overlook and impossible to avoid. This post is for native API programmers, driver developers, and reverse engineers, and by the end you will know what every member does, how to fill it in, and how to use one of its least understood members, the root directory, to open objects by relative name.
Why Don’t You See OBJECT_ATTRIBUTES in the Windows API?
Look at the parameters of CreateFile, CreateMutex, or OpenProcess and there is nothing that looks like it. The Windows API hides it. But every one of these create and open functions, when it calls its native equivalent in NTDLL, builds an OBJECT_ATTRIBUTES and passes it along. OpenProcess calls NtOpenProcess, and NtOpenProcess requires one. The same is true of every native and kernel API that creates or opens a kernel object. So if you ever step into NTDLL in a debugger, or reverse a binary that calls the native API directly, this is the structure you will be looking at.
To see it in action, the video uses NtOpenFile. It is not officially documented as a user mode API, but it is documented indirectly through the Windows Driver Kit, and a declaration is available in winternl.h. NtOpenFile works only with file and device objects that already exist. Its big brother, NtCreateFile, can also create new files, but it is a more complex function, so for this demonstration the simpler one is enough.
What Can the Native API Open That CreateFile Cannot?
The job of OBJECT_ATTRIBUTES is, first and foremost, to carry the path to the thing you want to open or create. That path lives in the object manager namespace, the one you browse with WinObj. The directories there are kernel objects, held in memory by the object manager. They are not file system directories.
Here is the key difference. CreateFile can only reach what is exposed through the \?? directory, which is full of symbolic links. NtOpenFile can technically reach any device in the hierarchy. It may still fail on security grounds, but at least the path is reachable. The demo target is \Device\Beep, the device that, well, makes a beep. Search WinObj for a symbolic link that points to it and you will find none, because Microsoft decided not to create one. You could create a local symbolic link yourself, but the more direct route is to open \Device\Beep straight through the native API. If you have followed the Process Memory Map series, you have already seen how device paths and DOS paths relate through exactly these symbolic links.
What Is Inside OBJECT_ATTRIBUTES?
The structure has six members:
Length: the size of the structure, a common versioning pattern that has to be set correctlyRootDirectory: an optional handle that makes the object name relative (more on this below)ObjectName: a pointer to aUNICODE_STRINGwith the name or pathAttributes: a set ofOBJ_*flagsSecurityDescriptor: the same kind of security descriptor that sits insideSECURITY_ATTRIBUTESin the Windows API, controlling who can do what with a new objectSecurityQualityOfService: rarely used, and skipped in the video
The security descriptor deserves a note. It only applies when an object is created. When you open an existing object, it has no meaning at all.
The most common way to fill all this in is the InitializeObjectAttributes macro. It takes a pointer to the structure, the name, the attributes, the root directory, and the security descriptor. It sets Length properly, assigns the four remaining members, and sets SecurityQualityOfService to NULL. If you need quality of service, you set it manually afterward. Having Length done for you is, as Pavel puts it, kind of nice.
How Do You Build the Object Name?
The name is not a plain null terminated string the way the Windows API expects. It is a UNICODE_STRING, the string descriptor the native API and the kernel use everywhere. It points to a buffer and carries a Length and a MaximumLength, which means the string does not have to be NULL-terminated. MaximumLength tells you how much room the buffer has. Both lengths are in bytes, not characters, which is a classic source of off by two bugs.
The simplest initializer is RtlInitUnicodeString, which takes a null terminated string, computes its length, and fills in the descriptor. Initialize it with L"\\Device\\Beep" and the debugger shows a length of 0x18 bytes: twelve characters, two bytes each.
Which Attribute Flags Matter?
The Attributes member is a set of flags that start with OBJ_. A few worth knowing:
OBJ_CASE_INSENSITIVE: the name lookup ignores case, so the string does not have to match exactlyOBJ_KERNEL_HANDLE: the returned handle is a kernel handle that does not leak to user mode. Useful in a driver, pointless and non functional from user modeOBJ_OPENIF: changes how a create call behaves when an object with that name already exists, opening the existing object instead of failing
There are more, documented in the WDK. For the demo, OBJ_CASE_INSENSITIVE is enough. The root directory is NULL for now, and the security descriptor is NULL too, because nothing is being created.
Does It Actually Open the Device?
With the structure ready, the remaining NtOpenFile parameters are straightforward. The output handle, an access mask of GENERIC_WRITE, the attributes, an IO_STATUS_BLOCK that is output only (and not very interesting here), a share access that only matters for file system files, and open options set to zero. Like most native APIs, it returns an NTSTATUS, where zero means success. The call links against ntdll.lib, which you have to add or the linker complains. When you are done, NtClose closes the handle, which is basically CloseHandle if you want to feel native about it.
The call succeeds, and Process Explorer confirms it: the process holds a handle to \Device\Beep, with GENERIC_WRITE mapped to its file specific meaning, which includes SYNCHRONIZE and READ_CONTROL. No sound needs to be played. The point was to prove the open works.
What Is the RootDirectory Member For?
So far the name was a full path starting at the root of the namespace. That is one way to go. In some cases it is more convenient to hold an open handle to a directory object, say \Device, and open objects beneath it using relative names, just like relative paths in the file system.
Try it without a root first. Change the name to just Beep with RootDirectory still NULL, and the call fails with 0xC000003B, STATUS_OBJECT_PATH_SYNTAX_BAD: “Object path component was not a directory object.” A relative name with nothing to be relative to cannot work.
To make it work, you need a handle to the \Device directory object, and that requires NtOpenDirectoryObject. It is not declared in winternl.h, so the video switches to the phnt headers, which declare most of the native API with full structures. Unsurprisingly, NtOpenDirectoryObject takes an OBJECT_ATTRIBUTES too. Notice that there is no parameter for the directory name. It hides inside the object attributes, and that is always the pattern with native and kernel APIs.
So there are now two OBJECT_ATTRIBUTES. The first, for the directory, has the name \Device, OBJ_CASE_INSENSITIVE, no root, and no security descriptor, and the call asks for DIRECTORY_QUERY access. Pavel’s advice here applies broadly: ask for the minimum access you need, because you might not get more without admin rights. The second, for the device, has the relative name Beep and the directory handle as its RootDirectory.
One practical snag appears on camera: phnt had not caught up with the latest Windows SDK and failed to compile, so the project switches to an earlier SDK version. After that, both calls succeed. Process Explorer shows a handle to a directory object named \Device with query access (a directory object, not a file system directory, which would be a file object), and a second handle to \Device\Beep, opened by relative name this time.
Directories in the object manager namespace are one use case. The same mechanism applies to any hierarchy. Registry keys are a natural fit: open a key with something like NtOpenKey, then open several subkeys relative to it. Files in a file system work the same way. Full names that start with a backslash always work regardless, but relative names can make code that walks a hierarchy much cleaner.
What This Means Practically
- Expect
OBJECT_ATTRIBUTESin every native or kernel API that creates or opens a kernel object, even when the Windows API wrapper you call never shows it - Use
InitializeObjectAttributessoLengthis set correctly, and setSecurityQualityOfServiceby hand only if you need it - Build the name as a
UNICODE_STRING, and remember its lengths are in bytes, not characters - Pass NULL for the security descriptor when opening existing objects, since it only applies on creation
- Reach objects with no symbolic link, like
\Device\Beep, through native calls such asNtOpenFile, sinceCreateFileonly sees the\??directory - Use
RootDirectorywith a relative name to open multiple objects under one directory or registry key, and expectSTATUS_OBJECT_PATH_SYNTAX_BADif you pass a relative name without a root - When reversing, recognize the six member layout on the stack before a native call: it tells you exactly what object is being opened or created
Keep Learning
If you want to work comfortably at this level, these TrainSec courses cover it in depth:
- Windows Native API Programming: the native API surface,
OBJECT_ATTRIBUTES,UNICODE_STRING, and the NTDLL functions the Windows API is built on - Windows Kernel Programming 1: where
OBJ_KERNEL_HANDLE,Zwfunctions, and object attributes become everyday driver code
Related reading in the knowledge library: Process Memory Map in Code: Walking VirtualQueryEx and Finding the PEB (Part 1), which also leans on NTDLL and the native API, and Process Memory Map in Code: Thread Stacks and TEBs (Part 3), which uses the same phnt headers to reach native definitions the SDK does not provide.