Windows Native API Programming
Windows Native API Programming
Pavel Yosifovich
This book is for sale at http://leanpub.com/windowsnativeapiprogramming
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean
Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get
reader feedback, pivot until you have the right book and build traction once you do.
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1: Who should read the book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2: Disclaimer and Caution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3: Sample Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4: Feedback and Error Reporting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.5: What’s Next? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Chapter 5: Processes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.1: Creating Processes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.2: Process Information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.3: The Process Environment Block (PEB) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
6.4: Suspending and Resuming Processes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.5: Enumerating Processes (Take 2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.6: Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
The other DLL used to invoke system calls is Win32u.dll, used for user interface services and graphics through
the Graphics Device Interface (GDI). Calls from User32.dll and Gdi32.Dll go through Win32u.dll to reach the
kernel. These APIs are not discussed in this book.
Windows. The native API is undocumented for a reason - Microsoft makes no guarantees as to the stability
of the API, meaning it can change at any time and no one can complain. That said, in practice, the API is
mostly stable, as some Microsoft tools use the API extensively, such as Task Manager and the Sysinternals
tools.
Pavel Yosifovich
June, 2024
Chapter 1: Introduction to Native API
Development
In this first chapter, we’ll describe the Windows system architecture as it pertains to application development,
and then see where the native API enters the picture. Finally, we’ll see how to add the required headers to a
Visual Studio C++ application to be able to use the native API.
In this chapter:
A typical application calls a documented Windows API, implemented in one of a set of Subsystem DLLs. For
example, the CreateFile API is implemented in kernel32.dll (its actual implementation is in kernelbase.dll
- kernel32.dll jumps there, but this detail is unimportant in practice).
Chapter 1: Introduction to Native API Development 5
The term “Subsystem” refers to the original subsystems concept introduced in Windows NT. For a full
description, consult the book *Windows Internals“, 7th edition, part 1. Suffice it to say that the only remaining
subsystem (from the original OS2, POSIX, and Windows subsystems) is the Windows subsystem. This
subsystem provides the APIs system developers are familiar with, such as CreateFile. Also note that the
term “subsystem” as used here has nothing to do with the “Windows Subsystem for Linux”; refer to the same
book for more information.
The CreateFile API will (after some parameter checks) invoke the NtCreateFile native API, part of
NtDll.Dll, which is the lowest-layer DLL that is still in user-mode. This is where the native API is
implemented. Its purpose is to make the transition to kernel-mode, so that the real NtCreateFile function
will be invoked. This is accomplished (on x64 processors) by loading a value into the EAX register, and then
invoking the syscall machine instruction. The value placed in the EAX register is the one indicating the kind
of “service” required from the kernel. Here is a disassembly of the NtCreateFile function from NtDll.dll on
some Windows 10 machine:
ntdll!NtCreateFile:
00007ffa`d138db40 mov r10,rcx
00007ffa`d138db43 mov eax,55h
00007ffa`d138db48 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`d138db50 jne ntdll!NtCreateFile+0x15 (00007ffa`d138db55)
00007ffa`d138db52 syscall
00007ffa`d138db54 ret
00007ffa`d138db55 int 2Eh
00007ffa`d138db57 ret
In the above code snippet, the value 0x55 represents the NtCreateFile system call. Every system call
has its own system call number. Behind the scenes, on the kernel side of things, this value is used as an
index to a table known as System Service Dispatch Table (SSDT), that stores the actual system call address
implementations.
Technically, on 64-bit Windows systems, the stored values in the SSDT are not absolute addresses.
Rather, a 32-bit offset from the beginning of the SSDT is stored (in the 28 most significant bits). The
lower 4 bits store the number of arguments passed on the stack to the system call, so that the System
Service Dispatcher (the common function that invokes system calls) knows how many arguments it
needs to clean from the stack after the call returns.
The jne branch instruction just before the syscall is normally not taken, so that syscall is invoked. For
compatibility reasons (at least), using int 0x2E still works if the tested bit happens to be clear, or is invoked
directly.
All system call invocations inside NtDll.Dll look exactly the same, except for the number stored in EAX. It’s
important to note that the “real” system call in the kernel has exactly the same prototype as it does in user-
Chapter 1: Introduction to Native API Development 6
mode (NtDll.Dll). NtDll.Dll is used as a “trampoline” of sorts to make the quantum jump to the kernel; on
the kernel side, the real implementation is invoked.
Once the system call executes within the kernel, which could mean contacting a device driver, or not,
depending on the system call, the call returns back to user-mode. Note that it’s the same thread that makes
the call - it starts in user-mode, transitions to the kernel when syscall is executed, and finally transitions
back to user-mode when the opposite instruction (sysret) is executed.
As shown in figure 1-1, there is yet another NtDll-like DLL - Win32u.Dll. This one serves the same purpose
as NtDll.Dll for USER and GDI (Graphics Device Interface) APIs. A function such as CreateWindowEx
implemented in User32.dll invokes NtUserCreateWindowEx implemented in Win32u.dll. Here is the system
call invocation for that:
win32u!NtUserCreateWindowEx:
00007ffa`cefd1eb0 mov r10,rcx
00007ffa`cefd1eb3 mov eax,1074h
00007ffa`cefd1eb8 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`cefd1ec0 jne win32u!NtUserCreateWindowEx+0x15 (00007ffa`cefd1ec5)
00007ffa`cefd1ec2 syscall
00007ffa`cefd1ec4 ret
00007ffa`cefd1ec5 int 2Eh
00007ffa`cefd1ec7 ret
Not surprisingly, perhaps, the code is identical to the NtCreateFile code except for the system service
number stashed in EAX - 0x1074 for NtUserCreateWindowEx. USER and GDI system call numbers start with
0x1000 (bit 12 set); this is intentional. On the kernel side, Win32k.sys is the target of these system calls.
Win32k.sys is referred to as the “Kernel component of the Windows subsystem”, essentially implementing
the windowing of the OS and the calls to use the classic GDI.
The Native API is mostly undocumented. Some of of it is documented indirectly in the Windows Driver
Kit (WDK), to be used by driver developers. NtCreateFile, for example, happens to be documented in the
WDK. Most of the native API is undocumented, however; even the prototypes are not officially provided.
This begs the question: how can we use something that is not documented?
Before we tackle that question, let’s consider another, perhaps more obvious question: why would we want
to use the Native API in the first place? Is there something wrong with the standard, documented, Windows
API?
Using the native APIs can have the following benefits:
• Performance - using the native API bypasses the standard Windows API, thus removing a software
layer, speeding things up.
Chapter 1: Introduction to Native API Development 8
• Power - some capabilities are not provided by the standard Windows API, but are available with the
native API.
• Dependencies - using the native API removes dependencies on subsystem DLLs, creating potentially
smaller, leaner executables.
• Flexibility - in the early stages of Windows boot, native applications (those dependent on NtDll.dll only)
can execute, while others cannot.
That said, there are potential disadvantages to using the native API:
• It’s mostly undocumented, making it more difficult to use from the get go. This is where this book
comes in!
• Being undocumented also means that Microsoft can make changes to the native API without warning,
and no one can complain. That said, removal of capabilities from the native API or modification of
existing functionality are rare. This is because some of Microsoft’s own tools and applications leverage
the native API.
Back to the first question: how can we get the prototypes of the native API functions? One source is the
WDK, which provides a good start, although it’s hardly complete. Reverse engineering can help as well, if
you’re so inclined. Fortunately, it’s not necessary. The winsiderss/phnt (used to be processhacker) project on
Github has most of the native API prototypes along with constants, enumerations, and structures - ready to
go.
The NTAPI macro is expanded to __stdcall, the standard calling convention used by most of the Windows
API and the native API. It only means something in 32-bit processes, as in 64-bit there is just one calling
convention.
We’ll discuss the details of the NtQuerySystemInformation API in the chapter 3. For now, let’s just utilize
it to get some basic system information with a certain SYSTEM_INFORMATION_CLASS value and its associated
structure. Here is the one enumeration value we’ll use along with the expected structure:
Chapter 1: Introduction to Native API Development 9
We’ll add these definitions manually to a new C++ console application project created in Visual Studio. Then
we’ll make the call, after which we’ll display some of the returned information. Here is the full main function:
int main() {
SYSTEM_BASIC_INFORMATION sysInfo;
NTSTATUS status = NtQuerySystemInformation(SystemBasicInformation,
&sysInfo, sizeof(sysInfo), nullptr);
if (status == 0) {
//
// call succeeded
//
printf("Page size: %u bytes\n", sysInfo.PageSize);
printf("Processors: %u\n", (ULONG)sysInfo.NumberOfProcessors);
printf("Physical pages: %u\n", sysInfo.NumberOfPhysicalPages);
printf("Lowest Physical page: %u\n", sysInfo.LowestPhysicalPageNumber);
printf("Highest Physical page: %u\n", sysInfo.HighestPhysicalPageNumber);
}
else {
printf("Error calling NtQuerySystemInformation (0x%X)\n", status);
}
return 0;
}
Compiling this code produces a linker error indicating that the implementation of NtQuerySystemInforma-
tion is nowhere to be found:
Chapter 1: Introduction to Native API Development 10
Even though we get one error, we really have two issues here. The first is the fact that the linker has no idea
where NtQuerySystemInformation is implemented. We know it’s inside NtDll.dll, but the linker doesn’t, so
we have to tell it.
One way of doing that is by adding the import library, ntdll.lib, that is provided with a Visual Studio
installation, to the linker’s Input node where additional import libraries can be added (figure 1-3).
Note that it’s best to select All Configurations and All Platforms at the top of the dialog in figure 1-3, so we
don’t have to repeat this setting in every configuration and platform.
The second way to achieve the same thing is to use a #pragma to add the same import library in source code:
Even with that change (utilizing either option), we still get the same linker error. The problem is that
NtQuerySystemInformation is defined in a C++ file, that the C++ compiler processes. The function is
named based on the assumption that multiple functions with the same name can exist (function overloading)
with different arguments, so the linker does not connect the function name to its “true” function in NtDll.dll,
which is a C function. You can see the “true” name of the function is mangled by the compiler with weird
symbols to capture the uniqueness of its arguments.
Fortunately, the solution is simple: decorate the function with the extern "C" modifier to force the compiler
to treat the function as a C function (not C++):
Chapter 1: Introduction to Native API Development 11
Now everything should link successfully, and we can run the application that produces output similar to the
following:
#include <Windows.h>
#include <stdio.h>
int main() {
SYSTEM_BASIC_INFORMATION sysInfo;
NTSTATUS status = NtQuerySystemInformation(SystemBasicInformation,
&sysInfo, sizeof(sysInfo), nullptr);
if (status == 0) {
//
// call succeeded
//
printf("Page size: %u bytes\n", sysInfo.PageSize);
printf("Processors: %u\n", (ULONG)sysInfo.NumberOfProcessors);
printf("Physical pages: %u\n", sysInfo.NumberOfPhysicalPages);
printf("Lowest Physical page: %u\n", sysInfo.LowestPhysicalPageNumber);
printf("Highest Physical page: %u\n", sysInfo.HighestPhysicalPageNumber);
}
else {
printf("Error calling NtQuerySystemInformation (0x%X)\n", status);
}
return 0;
}
The #include of <Windows.h> is necessary (at least for now), as it provides basic definitions like ULONG,
PVOID, and other standard Windows types.
The $(CoreLibraryDependencies) Visual Studio variable visible in figure 1-3 includes many of the standard
subsystem DLLs, but it does not include NtDll.lib, which is why the project failed to link. You can verify this
by clicking the rightmost down arrow (figure 1-3) and selecting Edit… and then click the Macros button. You
can type at the top edit box of the expanded window to filter the variable list and get to the default list of
import libraries hiding under this specific variable (figure 1-4).
Chapter 1: Introduction to Native API Development 13
auto pNtQuerySystemInformation =
(decltype(NtQuerySystemInformation)*)GetProcAddress(
GetModuleHandle(L"ntdll"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation) {
SYSTEM_BASIC_INFORMATION sysInfo;
NTSTATUS status = pNtQuerySystemInformation(SystemBasicInformation,
&sysInfo, sizeof(sysInfo), nullptr);
//...
Using the auto and decltype C++11 keywords simplifies the code, as it does not require to define a function
pointer. But that can be done if needed:
int main() {
auto NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(
GetModuleHandle(L"ntdll"), "NtQuerySystemInformation");
if (NtQuerySystemInformation) {
SYSTEM_BASIC_INFORMATION sysInfo;
NTSTATUS status = NtQuerySystemInformation(SystemBasicInformation,
&sysInfo, sizeof(sysInfo), nullptr);
//...
In this case we can define the function pointer type with a “P” prefix, so we can use the “real” function name
as the name of the variable, making the code feel perhaps slightly more “natural”. Notice that extern "C"
is no longer needed, as GetProcAddress always assumes an unmangled function name.
GetModuleHandle is used to retrieve the instance handle (just the address of) the NtDll.Dll DLL in the process,
that is fed to GetProcAddress. This will always succeed because NtDll.Dll is mapped to every user-mode
process by the kernel, and so is available at a very early stage of a process’ lifetime.
Astute readers may be wondering why we’re using GetProcAddress and GetModuleHandle, as one of our
goals is to use native APIs instead of standard Windows APIs; we’ll do that in the next chapter, because the
Chapter 1: Introduction to Native API Development 15
corresponding native APIs require some more background, which is provided in the next chapter. In any
case, the goal is not necessarily to remove all usage of the Windows API in all cases, but to use the native
API to our advantage where it makes sense.
Ultimately, the way code binds to native APIs is up to the developer. It’s also possible to combine both ways -
use static binding (import library) for APIs that exist in all Windows versions being targeted, and use dynamic
binding for functions that may be unavailable.
If you feel rusty as to how to bind to DLLs (statically and dynamically), look it up online, or read
chapter 15 in my book “Windows 10 System Programming, Part 2”.
00000000 characteristics
6349A4F2 time date stamp
0.00 version
8 ordinal base
2435 number of functions
2434 number of names
9 0 00040280 A_SHAFinal
10 1 000410B0 A_SHAInit
11 2 000410F0 A_SHAUpdate
12 3 000E09C0 AlpcAdjustCompletionListConcurrencyCount
13 4 00070780 AlpcFreeCompletionListMessage
14 5 000E09F0 AlpcGetCompletionListLastMessageInformation
Chapter 1: Introduction to Native API Development 16
15 6 000E0A10 AlpcGetCompletionListMessageAttributes
16 7 000704B0 AlpcGetHeaderSize
17 8 00070470 AlpcGetMessageAttribute
18 9 00010A60 AlpcGetMessageFromCompletionList
19 A 00085E00 AlpcGetOutstandingCompletionListMessageCount
...
2439 97E 00097FF0 wcstok_s
2440 97F 00092410 wcstol
2441 980 000924B0 wcstombs
2442 981 00092470 wcstoul
...
You can also see these with any graphical Portable Executable (PE) viewer, like my own TotalPE, by loading
NtDll.Dll and examining the Exports data directory (figure 1-5). Notice there are 2435 exported functions on
the Windows version where this screenshot was taken.
One way to get the prototypes of functions, structures, and enumerations, is to copy what we need from
the headers provided by the winsiderss/phnt1 project. There is one catch, however: certain definitions are
common to many functions, and you would have to hunt down each and every definition and copy them as
1 https://github.com/winsiderss/phnt
Chapter 1: Introduction to Native API Development 17
#include <phnt_windows.h>
#include <phnt.h>
In addition, there are a few macros you can use to let the phnt headers include APIs from required Windows
versions. By default, only Windows 7 functions are included. Set the PHNT_VERSION macro to the required
version selected from the following, before including the above headers:
The names used above are the internal names used by Microsoft to represent various Windows versions. The
list is likely to be extended in the future.
There is yet another way to get the phnt headers, by installing the phnt package using vcpkg2 . Vcpkg is a
package manager for C++. Follow the instructions on the above link to install Vcpkg (if you haven’t done so
already). Then you can install phnt by typing:
Assuming Vcpkg is integrated with Visual Studio (vcpkg integrate install), you’ll be able to use the phnt
headers without adding anything special to a project.
The following shows the code that performs the same operations as HelloNative, but using the phnt headers,
assuming it has been installed with Vcpkg, copied to the project manually, or added as a submodule to a Git
repository (HelloNative3 project in the samples):
2 https://github.com/microsoft/vcpkg
Chapter 1: Introduction to Native API Development 18
#include <phnt_windows.h>
#include <phnt.h>
#include <stdio.h>
int main() {
SYSTEM_BASIC_INFORMATION sysInfo;
NTSTATUS status = NtQuerySystemInformation(SystemBasicInformation,
&sysInfo, sizeof(sysInfo), nullptr);
if (status == STATUS_SUCCESS) {
printf("Page size: %u bytes\n", sysInfo.PageSize);
printf("Processors: %u\n", (ULONG)sysInfo.NumberOfProcessors);
printf("Physical pages: %u\n", sysInfo.NumberOfPhysicalPages);
printf("Lowest Physical page: %u\n", sysInfo.LowestPhysicalPageNumber);
printf("Highest Physical page: %u\n", sysInfo.HighestPhysicalPageNumber);
}
else {
printf("Error calling NtQuerySystemInformation (0x%X)\n", status);
}
return 0;
}
Notice that the code can use STATUS_SUCCESS (0) to compare against, as it’s properly defined. Also note that
linking is still the responsibility of the developer. The above example uses the #pragma approach to add the
NtDll.lib dependency to the project.
Install phnt with Vcpkg. Create a new C++ project, add the above code and make sure everything
compiles, links, and executes successfully.
There is a header provided in the Windows SDK named <Winternl.h>. This header contains a small subset of
native APIs and definitions. It’s tempting to use it, and for very simple requirements, may be good enough.
However, the definitions that exist in that file are not always complete. For example, here is how the SYSTEM_-
BASIC_INFORMATION structure is defined:
Chapter 1: Introduction to Native API Development 19
This definition only provides the number of processors, and nothing else. If you try to use this header with
phnt, many definitions will clash. Bottom line - don’t use the <Winternl.h> header for any serious work with
the native API.
2.6: Summary
In this chapter we looked at how system calls are invoked, provided by the native API. We added support
for using native API definitions from the phnt project into a Visual Studio project. In the next chapter, we’ll
examine the foundational structures, enumerations, and constants, used with the native API, and write native
applications.
Chapter 2: Native API Fundamentals
The native API uses common patterns and types throughout. This chapter will focus on these commonalities.
In this chapter:
• Function Prefixes
• Errors
• Strings
• Linked Lists
• Object Attributes
• Client ID
• Time and Time Span
• Bitmaps
• Sample: Terminating a Process
Which prefix you use doesn’t matter (if both exist), but I recommend you stick with the Nt prefix.
Why are there two prefixes? It turns out that within the kernel, these are not identical functions
- the Zw variant changes the previous thread access mode to kernel-mode before invoking the real
system call (the Nt variant). This allows kernel code to invoke system calls directly without being
considered as coming from user-mode. In user-mode, there is no distinction between these pairs of
functions.
What does Zw stand for? Microsoft’s official documentation states that it means absolutely nothing,
and this is why it was chosen. Folklore suggests it’s the initials of a developer within Microsoft.
There is another common prefix within NtDll.Dll’s exports: Rtl, which stands for Runtime Library. There
are two categories these functions are split into:
• Helper routines that do not perform any call into the kernel. Examples include working with
strings, numbers, memory, data structures (bit maps, hash tables, trees). Examples: RtlClearBits,
RtlCompareMemory, RtlComputeCrc32, RtlCreateHashTable.
• Convenient wrappers around Nt functions that make it easier to invoke certain system calls. For
example, RtlCreateUserProcessEx does some work and then delegates to NtCreateUserProcess,
which is the system call itself.
Chapter 2: Native API Fundamentals 22
Finally, there are other, more specific prefixes, such as the following. Some of these are wrappers around
system calls, some provide higher level functionality - it depends on the specific function.
There are a few more specialized prefixes (MD4, MD5, Sb, Ship, Rtlx, Ki, Exp, Nls, Etwp, Evt) that we may
encounter later in this book.
3.2: Errors
Most native API functions return NTSTATUS as a direct result. This is a 32-bit signed integer, where zero
indicates success (STATUS_SUCCESS), while negative values indicate some kind of failure.
The usual way to check for success or failure is to use the NT_SUCCESS macro, that returns true if the given
status is zero (or positive).
While debugging, you may encounter an error, and would want to get a textual description of the error
without searching in the headers for the particular error. Fortunately, the Visual Studio debugger supports
a suffix (,hr) that can be appended to an error value in the Watch window to get a textual description. See
some examples in figure 2-2.
3.3: Strings
The native API uses strings in many scenarios as needed. In some cases, these strings are simple Unicode
pointers (wchar_t* or one of their typedefs such as WCHAR*), but most functions dealing with strings expect
a structure of type UNICODE_STRING.
Chapter 2: Native API Fundamentals 23
The term Unicode as used in this book is roughly equivalent to UTF-16, which means 2 bytes per character.
This is how strings are stored internally within kernel components. Unicode in general is a set of standards
related to character encoding. You can find more information at https://unicode.org.
The UNICODE_STRING structure is a string descriptor - it describes a string, but does not necessarily own it.
Here is a simplified definition of the structure:
The Length member stores the string length in bytes (not characters) and does not include a Unicode- NULL
terminator, if one exists (the string does not have to be NULL-terminated). The MaximumLength member is
the number of bytes the string can grow to without requiring a memory reallocation.
Manipulating UNICODE_STRING structures is typically done with a set of Rtl functions that deal specifically
with strings. Table 2-1 lists some of the common functions for string manipulation provided by the Rtl
functions.
Function Description
RtlInitUnicodeString Initializes a UNICODE_STRING based on an existing C-string pointer. It sets
Buffer, then calculates the Length and sets MaximumLength to the
Length+2 to accommodate the NULL-terminator. Note that this function
does not allocate any memory - it just initializes the internal members.
RtlCopyUnicodeString Copies one UNICODE_STRING to another. The destination string pointer
(Buffer) must be allocated before the copy and MaximumLength set
appropriately.
RtlCompareUnicodeString Compares two UNICODE_STRINGs (equal, less, greater), specifying whether
to do a case sensitive comparison.
RtlCompareUnicodeStrings Compares two NULL-terminated (C-style) Unicode strings, specifying
whether to do a case sensitive comparison.
RtlEqualUnicodeString Compares two UNICODE_STRINGs for equality, with case sensitivity
specification.
RtlAppendUnicodeStringToString Appends one UNICODE_STRING to another.
RtlAppendUnicodeToString Appends a C-style string to a UNICODE_STRING.
RtlPrefixUnicodeString Checks if the first string is a prefix of the second string, with optional case
sensitivity.
Chapter 2: Native API Fundamentals 24
In addition to the above functions, there are functions that work on C-string pointers. Moreover, some of
the well-known string functions from the C Runtime Library are implemented within NtDll.Dll as well for
convenience: wcscpy_s, wcscat_s, wcslen, wcschr, strcpy, strcpy_s and others. The reason for this
implementation (compared to using the Visual C++ Runtime Library) will be clear in the next chapter where
we discuss native applications.
The wcs prefix works with C Unicode strings, while the str prefix works with C Ansi strings. The
suffix _s in some functions indicates a safe function, where an additional argument indicating the
maximum length of the string must be provided so the function would not transfer more data than
the string buffer can accommodate.
Don’t use the non-safe functions. You can include <dontuse.h> to get errors for deprecated functions
if you do use these in code.
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"SomeString");
This works, but is slightly inefficient, since calculation of the string length is performed at runtime even
though the string’s length can be computed at compile time. To get over this slight inefficiency, the RTL_-
CONSTANT_STRING macro is provided by phnt, and can be used like so:
This, however, at the time of writing, fails in a C++ compilation because of a constness issue that the macro
does not take care of. The definition of this macro in the WDK headers is more complex, and caters for
constness properly, as well as for Ansi string initialization.
Here is one possible definition for this macro that forcefully casts away the constness for a Unicode string
initialization:
#ifdef RTL_CONSTANT_STRING
#undef RTL_CONSTANT_STRING
#endif
#define RTL_CONSTANT_STRING(s) { sizeof(s) - sizeof((s)[0]), sizeof(s), (PWSTR)s }
We can add a similar macro for Ansi literal strings initialization (needed in a few cases):
Chapter 2: Native API Fundamentals 25
You can technically copy the definition of this macro from the WDK, but the above definitions do
the work just fine and are simple to understand and use.
Figure 2-3 depicts an example of such a list containing a head and three instances.
One such structure is embedded inside the real structure of interest. For example, in the EPROCESS structure,
the member ActiveProcessLinks is of type LIST_ENTRY, pointing to the next and previous LIST_ENTRY
objects of other EPROCESS structures. The head of a list is stored separately; in the case of the process,
that’s PsActiveProcessHead. To get the pointer to the actual structure of interest given the address of a
LIST_ENTRY can be obtained with the CONTAINING_RECORD macro.
For example, suppose you want to manage a list of structures of type MyDataItem defined like so:
Chapter 2: Native API Fundamentals 26
struct MyDataItem {
// some data members
LIST_ENTRY Link;
// more data members
};
When working with these linked lists, we have a head for the list, stored in a variable. This means that natural
traversal is done by using the Flink member of the list to point to the next LIST_ENTRY in the list. Given a
pointer to the LIST_ENTRY, what we’re really after is the MyDataItem that contains this list entry member.
This is where the CONTAINING_RECORD macro comes in:
The macro does the proper offset calculation and does the casting to the actual data type (MyDataItem in the
example).
Table 2-2 shows the common functions (implemented inline in the header) for working with these linked
lists. All operations use constant time.
Function Description
InitializeListHead Initializes a list head to make an empty list. The forward and back pointers point to the
forward pointer.
InsertHeadList Insert an item to the head of the list.
AppendTailList Appends one list to another.
InsertTailList Insert an item to the tail of the list.
IsListEmpty Check if the list is empty.
RemoveHeadList Remove the item at the head of the list.
RemoveTailList Remove the item at the tail of the list.
RemoveEntryList Remove a specific item from the list.
The following code example shows the list of modules loaded in the current process (see chapter 5 for more
details):
Chapter 2: Native API Fundamentals 27
#include <phnt_windows.h>
#include <phnt.h>
#include <stdio.h>
int main() {
PPEB peb = NtCurrentPeb();
auto& head = peb->Ldr->InLoadOrderModuleList;
NtCurrentPeb is a macro that returns the pointer to the current process’ PEB. Loaded modules are stored
in three separate linked lists - the example uses one of them, that stored the modules in load order (we’ll
examine the other lists in chapter 5). The head of the in-load-order list is stored within a member variable
called Ldr (of type PPEB_LDR_DATA) in its InLoadOrderModuleList member (a LIST_ENTRY).
Since the list is circular, iterating by looking for a NULL pointer is futile - that will never happen. Instead the
pointer next is initialized with the first element, moving through the loop until next becomes the same as
the address of peb->Ldr->InLoadOrderModuleList - this indicates the end of iteration.
Each next pointer points to a LIST_ENTRY within the structure representing a module (LDR_DATA_TABLE_-
ENTRY) in the InLoadOrderLinks member. This is where the CONTAINING_RECORD macro comes in - it moves
the pointer back to the beginning of the structure and performs the cast to the provided type. Using the C++
auto keyword is particularly nice here, since the type (LDR_DATA_TABLE_ENTRY*) needs no repetition.
Running this example in Debug x64 build shows something like this (ModList project in the source code for
this chapter):
0x00007FF6B0B40000: ModList.exe
0x00007FF937EF0000: ntdll.dll
0x00007FF937510000: KERNEL32.DLL
0x00007FF935C50000: KERNELBASE.dll
0x00007FF8D5D50000: VCRUNTIME140D.dll
0x00007FF847280000: ucrtbased.dll
Chapter 2: Native API Fundamentals 28
The structure is typically initialized with the InitializeObjectAttributes macro, that allows specifying
all the structure members except Length (set automatically by the macro), and
SecurityQualityOfService, which is not normally needed. Here is the description of the members:
• ObjectName is the name of the object to be created or opened, provided as a pointer to a UNICODE_-
STRING. In some cases it may be ok to set it to NULL, for objects that don’t have names, such as processes.
• RootDirectory is an optional directory pointer in the object manager namespace if the name of the
object is a relative one. If ObjectName specifies a fully-qualified name, RootDirectory should be set
to NULL.
• Attributes allows specifying a set of flags that have effect on the operation in question. Table 2-3
shows the defined flags and their meaning.
• SecurityDescriptor is an optional security descriptor (SECURITY_DESCRIPTOR) to set on the newly
created object. NULL indicates the new object gets a default security descriptor, based on the caller’s
token.
• SecurityQualityOfService is an optional set of attributes related to the new object’s impersonation
level and context tracking mode. It has no meaning for most object types. Consult the documentation
for more information.
3.6: Client ID
The CLIENT_ID structure is a fairly simple one, but deserves attention since it can be somewhat confusing:
Its purpose is to specify a process and/or thread ID. The confusing part is that these IDs are typed as HANDLEs,
rather than simple integers. The reason has to do with the fact that the kernel generates unique process and
thread IDs by using a private handle table. This means the type internally is HANDLE, but it should be treated
as an ID.
Chapter 2: Native API Fundamentals 30
Process (and thread IDs) are limited to about 26 bits, which means a 64-value is not needed to represent them.
If you have a 32-bit value that needs to be placed into a HANDLE, some casting are required for the compiler
to be happy. Here is one way of doing it:
The double cast is necessary for 64-bit processes - first extending the value to 64-bit and then turning it into
a HANDLE (all handles are typed as void pointers).
If you’re strict about “proper” c++ usage, you can use more specific casts:
cid.UniqueProcess = reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(pid));
Feel free to use such forms, I will stick with C-style casts for simplicity and for catering for C developers and
beginning C++ developers.
There is yet another way to do the same thing - using simple functions/macros provided by Windows headers:
cid.UniqueProcess = ULongToHandle(pid);
You may notice there is a macro, UlongToHandle (notice the lowercase l), that just calls the inline
function. Either option works.
I recommend using the inline functions/macros for these kind of conversions - it’s clearer and less
cluttered.
It’s a glorified 64-bit integer, typically accessed directly with the QuadPart member. dates and times are
conveyed in 100 nano-second units, measured from January 1, 1601 at midnight GMT. For example, the
number 10000000 (10 million) represents one second after midnight on that date.
The fact that 100 nano-second units are used does not mean that Windows is capable of such resolution,
although it may be capable of that in the future. The units of measurement, however, apply.
For time spans, the same units are used, but they are relative to the time of invocation. Some APIs allow
specifying either absolute time or relative time. In such a case, negative values are interpreted as relative
time, while positive values are interpreted as absolute time.
An canonical example is the NtDelayExecution API (which is a rough equivalent of the Sleep(Ex) Windows
API, where the time to sleep can be specified as relative or absolute. Note that Sleep(Ex) only support relative
times in units of milliseconds.
Here is an example that sets a sleep of 100 milliseconds:
LARGE_INTEGER interval;
interval.QuadPart = -100 * 10000; // 100 msec
NtDelayExecution(FALSE, &interval);
The following is an example that causes a sleep until March 12, 2026 at noon GMT:
TIME_FIELDS tf{};
tf.Year = 2026;
tf.Month = 3;
tf.Day = 12;
tf.Hour = 12;
LARGE_INTEGER interval;
RtlTimeFieldsToTime(&tf, &interval);
NtDelayExecution(FALSE, &interval);
This example uses the helper TIME_FIELDS structure, that is easier for human consumption:
Chapter 2: Native API Fundamentals 32
LARGE_INTEGER RtlGetSystemTimePrecise();
RtlGetSystemTimePrecise is currently not in the phnt headers. Add it manually if you need it.
There are other Rtl functions for working with times, such as RtlCutoverTimeToSystemTime, RtlSystem-
TimeToLocalTime, RtlLocalTimeToSystemTime, RtlTimeToElapsedTimeFields, and others.
3.8: Bitmaps
A bitmap, represented by the RTL_BITMAP type, provides an efficient way to store the existence or absence
of something, each occupying a single bit. Functions exit to set, clear, and otherwise manipulate the bitmap.
Here is RTL_BITMAP:
A bitmap stores its size (in bits) in the SizeOfBitMap member, and a pointer to the buffer, which must be 4-
byte aligned where the actual bits are stored. Initializing such a bitmap is done with RtlInitializeBitMap:
Chapter 2: Native API Fundamentals 33
VOID RtlInitializeBitMap(
_Out_ PRTL_BITMAP BitMapHeader,
_In_ PULONG BitMapBuffer,
_In_ ULONG SizeOfBitMap);
BitMapBuffer is a pointer where the bits are stored, allocated by the client. SizeOfBitMap is the number of
bits to manage. RtlInitializeBitMap copies the pointer to the bits and the number of bits to the provided
structure. It does not initialize the bits, a job left to the caller.
The following simple APIs are used with bitmaps:
These APIs are self-explanatory. The more interesting APIs relate to finding a set of one or more bits with a
given value (set or clear):
Chapter 2: Native API Fundamentals 34
ULONG RtlFindClearBits(
_In_ PRTL_BITMAP BitMapHeader,
_In_ ULONG NumberToFind,
_In_ ULONG HintIndex);
ULONG RtlFindSetBits(
_In_ PRTL_BITMAP BitMapHeader,
_In_ ULONG NumberToFind,
_In_ ULONG HintIndex);
ULONG RtlFindClearBitsAndSet(
_In_ PRTL_BITMAP BitMapHeader,
_In_ ULONG NumberToFind,
_In_ ULONG HintIndex);
ULONG RtlFindSetBitsAndClear(
_In_ PRTL_BITMAP BitMapHeader,
_In_ ULONG NumberToFind,
_In_ ULONG HintIndex);
All the above functions search for a set of bits given a hint index to begin with ( HintIndex) and return the bit
index for the beginning of the range. If no such range could be found, these functions return -1 ( 0xFFFFFFFF).
The latter two functions also flip the state of the bits. Note that the hint is just that, and if the searched range
cannot be found from the hinted index, the search continues from index zero.
More search APIs are available, including RtlFindFirstRunClear, RtlFindNextForwardRunClear,
RtlFindLastBackwardRunClear, and others. They are documented in the Windows Driver Kit.
All the mentioned functions are not thread-safe. There are, however, some that are thread-safe that use
Interlocked instructions for thread and CPU safety:
VOID RtlInterlockedClearBitRun(
_In_ PRTL_BITMAP BitMapHeader,
_In_range_(0, BitMapHeader->SizeOfBitMap - NumberToClear) ULONG StartingIndex,
_In_range_(0, BitMapHeader->SizeOfBitMap - StartingIndex) ULONG NumberToClear);
VOID RtlInterlockedSetBitRun(
_In_ PRTL_BITMAP BitMapHeader,
_In_range_(0, BitMapHeader->SizeOfBitMap - NumberToSet) ULONG StartingIndex,
_In_range_(0, BitMapHeader->SizeOfBitMap - StartingIndex) ULONG NumberToSet);
NTSTATUS NtOpenProcess(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId);
The function returns the handle to the process in the first parameter (if the call succeeds). DesiredAccess
is the access mask requested (should be PROCESS_TERMINATE in our case). We must provide an OBJECT_-
ATTRIBUTES structure, which may seem a bit odd.
Processes have no names - they have IDs. The ID is clearly provided by the last parameter. Why is OBJECT_-
ATTRIBUTES needed? The attributes flags may make a difference, and that’s why its needed; the name can
be set to NULL.
Given the above information, we can implement a native version of terminating a process:
return status;
}
Notice that it’s important to zero out the CLIENT_ID. Otherwise, a garbage thread ID is provided to the API,
which causes it to fail, even though it’s not interested in the thread ID at all. Here is the rest of the code:
#include <phnt_windows.h>
#include <phnt.h>
#include <stdio.h>
#include <stdlib.h>
Notice the use of the strtoul C function (defined in <stdlib.h>), that allows receiving numeric values in
hexadecimal if these are prefixed with 0x. The last parameter to that function is a radix (base), where zero
indicates the function should figure it out on its own.
Similar functions exist for other integer sizes and types: strtol, strtoll, etc.
3.10: Summary
This chapter focused on common types and patterns of the native API. In the next chapter, we’ll examine
what native applications are, and how (and why) to write them.
Chapter 3: Native Applications
The sample applications we looked at in the previous chapters were “standard” Windows applications, that
happened to use the native API. They had a dependency on NtDll.dll, but not just NtDll.Dll. An executable
that depends on NtDll.dll only is a Native Application.
In this chapter:
Although the two numbers are different, they represent the same thing: the Windows Subsystem. These
are equivalent because a console application can create a graphical user interface, and a GUI application can
create a console (AllocConsole Windows API).
If you examine the dependencies of such executables, you may or may not see NtDll.dll in the Imports PE
directory, but that dependency always exists (albeit indirectly). Looking at the Imports directory for Kill.exe
from chapter 2 (Release build) shows it has dependencies on Kernel32.dll, NtDll.Dll, and VCRuntime140.Dll.
Selecting a specific DLL shows the imported functions at the bottom (figure 3-3).
Chapter 3: Native Applications 39
A native application is one that has a sole dependency on NtDll.Dll and nothing else. A canonical example is
Smss.exe (the session manager), which is the first user-mode process created in the system. Figure 3-4 shows
it’s part of the Native subsystem (value of 1), which can be described as no subsystem at all. Looking at its
imports, NtDll.Dll is the only dependency (figure 3-5).
Chapter 3: Native Applications 40
Another example of a native application is AutoChk.exe, found in the System32 directory. This is a native
application that does some work when Windows does not shutdown cleanly, and there is a chance that hard
disk integrity has been affected. It offers to check the disk(s) when Windows boots.
There is, however, another version of AutoChk.exe called ChkDsk.exe, which is a normal console Windows
application, that has dependencies on the msvcrt.dll and other DLLs (figure 3-6). Why have two versions that
Chapter 3: Native Applications 41
First, native applications cannot be run directly by normal means such as double-clicking in Explorer or
running from a command window. Here is what you get if you try to run AuthoChk.exe:
In other words, the CreateProcess Windows API is unable to launch native applications. Second,
only native applications can run early in Windows boot process. In fact, one of Smss.exe’s jobs
is to run native applications based on a multi-string value named BootExecute in the Registry key
HKLM\System\CurrentControlSet\Control\Session Manager. Figure 3-7 shows what that value looks like in
RegEdit.
Chapter 3: Native Applications 42
Each line in the multi-string value consists of an executable name and command line arguments. Autochk gets
special treatment, and has a friendly name to identify it as such. As can be seen in figure 3-7, “autocheck” is
the friendly name, and “autochk *” is the executable name and command line arguments. The executable must
be in the System32 directory - full paths are not supported. This is intentional, because writing to System32
requires administrator privileges by default. The Registry key itself allows write access to administrator
accounts only, to further limit arbitrary users from making changes to this key.
You can also see the BootExecute information in a somewhat friendlier manner with the Sysinternals Autoruns
utility (figure 3-8).
Chapter 3: Native Applications 43
Any string added to BootExecute will be run by Smss.exe at system startup. Such applications, however, must
be native, as the Windows subsystem process (Csrss.exe) has not been loaded yet.
The Session Manager is responsible for loading Csrss.exe for each session. At the time BootExecute
executables run, the Session Manager is the only user-mode process alive.
Building native applications requires removing all the “standard” dependencies, such as Kernel32.dll, the
Visual C++ runtime - everything. Only NtDll.dll can remain. Here are the steps required after creating a
standard console C++ application:
• Open the project’s properties, select “All Platforms” and “All Configurations” in the top comboboxes,
so that you don’t have to repeat these procedures for every combination of platform/configuration
separately.
• Navigate to the Linker / Input node, and set “Ignore all Default Libraries” to Yes. While you’re at it,
add NtDll.lib as a dependency (figure 3-9).
Chapter 3: Native Applications 44
• Navigate to the Linker / System node, and change the Subsystem to “Native” (figure 3-10).
• Navigate to the C/C++ / Code Generation node. Change “Basic Runtime Checks” to “Default”, which
effectively turns off runtime checks. This is required, because these checks are provided by the CRT.
Chapter 3: Native Applications 45
• Navigate to the C/C++ / General node. Change “SDL Checks” to “No”. Change “Debug Information
Format” to “Program Database”. See figure 3-12.
Chapter 3: Native Applications 46
This should be it. At this point, assuming the project has just an empty main function, it will compile but
fail to link, complaining about an unresolved external called NtProcessStartup. As they say - we’re not in
Kansas anymore.
A natural question to ask is who calls these functions with the correct arguments? It is the CRT. This is one
reason the CRT provides the startup code for the process, with function names like mainCRTStartup; you
Chapter 3: Native Applications 47
can see that clearly when looking the the call stack if you set a breakpoint in the main function. In fact, the
CRT source code is provided with Visual Studio, which means you can trace (and debug) these calls.
The CRT makes the call to the provided main/wmain with the expected semantics to adhere to the C/C++
standards. The CRT has other duties - for example, it’s responsible for invoking constructors for global
objects (before main is even invoked). This is necessary to comply with the C++ standard rules.
Now that we removed any dependency on the CRT (which we have to for native applications), there is no
one that can call a standard main function, nor invoke constructors for global objects. The basic “main” for
a Windows user-mode process looks like this:
The name of the function itself can be changed with a linker option, but the parameter and return type are
as shown. The return value is conceptually the same as with the normal main function - the exit code of
the process. The input, however, is fundamentally different - it’s a pointer to the Process Environment Block
(PEB).
We expect that information such as command line arguments to be present in the PEB, and they are.
Specifically, they can be found in the ProcessParameters member, which is a large structure of type
RTL_USER_PROCESS_PARAMETERS.
Once we have the information, we need to build a string that uses some of the returned members. We can use
the swprintf_s function that is exported from NtDll.dll, and thus available. Unfortunately, the phnt headers
don’t provide the prototypes for the CRT-like functions, so we need to define it ourselves. Fortunately, this
is easy since the definition is known and documented for usage with the standard CRT:
You may be temped to simply #include <stdio.h> to get the definition. Unfortunately, this won’t compile,
because the header has other C++ dependencies that get rejected by the compiler when there is no CRT. You
can, however, copy the definition from the official headers.
Now we can build a string, and pass it to NtDrawText to show on the boot screen:
Chapter 3: Native Applications 49
WCHAR text[256];
swprintf_s(text, ARRAYSIZE(text), L"Windows version: %d.%d.%d\n",
osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber);
UNICODE_STRING str;
RtlInitUnicodeString(&str, text);
NtDrawText(&str);
The string uses the major and minor versions of the OS, as well as the build number. Feel free to use other
structure members of interest.
NtDrawText requires a UNICODE_STRING, so we initialize such a string with RtlInitUnicodeString, as what
we have is a NULL-terminated string to begin with.
The last thing to do is to delay the thread for a few seconds before the process shuts down:
LARGE_INTEGER li;
li.QuadPart = -10000000 * 10;
NtDelayExecution(FALSE, &li);
NtDelayExeution is a rough native equivalent to the SleepEx Windows API. We’ll discuss its full semantics
in a later chapter, but for now the above code causes a 10 second sleep. Here is the full code for this sample:
#include <phnt_windows.h>
#include <phnt.h>
UNICODE_STRING str;
RtlInitUnicodeString(&str, text);
NtDrawText(&str);
LARGE_INTEGER li;
li.QuadPart = -10000000 * 10;
Chapter 3: Native Applications 50
NtDelayExecution(FALSE, &li);
return STATUS_SUCCESS;
}
Unfortunately, building the project fails with the linker complaining that swprintf_s is not found (“unre-
solved external”). How could this be?
As it turns out, the import library NtDll.Lib that is provided with the Windows SDK does not list the CRT-like
functions that are implemented in NtDll.Dll, so the linker complains.
One way to resolve this is to bind to the function dynamically as discussed in chapter 2. This is certainly
possible, but obviously we cannot use GetModuleHandle and GetProcAddress, as these are unavailable. The
functions we need are LdrGetDllHandle and LdrGetProcedureAddress. We’ll look at this in chapter 5. For
now, there is another thing we can try.
We could build our own NtDll.lib. Remember, that an import library consists of two pieces of information:
the DLL name (without any path information), and a list of exported symbols. There is no need to really
implement anything, because it’s implemented by the DLL itself. It’s just about making the linker happy, so
the binding to functions is deferred to runtime.
As it turns out, someone already did that. A Github project at https://github.com/Fyyre/ntdll provides such
an “extended” NtDll.lib. To get the extended library, clone the repository (or download the code to some
folder), open a Visual Studio command window, navigate to the project directory on your system, and type
the following:
C:\Github\ntdll>nmake msvc
The result is two files, NtDll64.lib (for 64-bit) and NtDll86.lib (for 32-bit). Just copy the correct file (NtDll64.lib
in our case, as only 64-bit executables work as native applications on 64-bit Windows), and use that file instead
of the “standard” NtDll.lib. I have included this LIB file in the SimpleNative sample project.
Now the executable should compile and link successfully.
The next step, if we wish to run the executable on startup, is to copy the SimpleNative.exe file to the System32
directory, and then to modify the BootExecute Registry value to include our executable. Figure 3-13 shows
the change to the Registry value.
Chapter 3: Native Applications 51
Now reboot the machine (you can test on a Virtual Machine if you prefer), and you should see the Windows
version string showing up for a few seconds (figure 3-14).
similar means fails. In all these cases, the CreateProcess (or one of its variants like CreateProcessAsUser)
is utilized, but these APIs cannot run native applications.
We’ll create a standard Windows application that can run a native application, given its executable path and
optional command line arguments.
Create a new C++ console application named nativerun. We’ll add the usual phnt headers, since we’ll have
to use native APIs to accomplish launching native applications!
Let’s start with a standard wmain function to get the native application path and any optional arguments to
be passed to the executable:
Next, we need to build the command line arguments to the executable as a UNICODE_STRING. We’ll use the
std::wstring C++ class from the standard library for easy concatenation:
std::wstring args;
for (int i = 2; i < argc; i++) {
args += argv[i];
args += L" ";
}
UNICODE_STRING cmdline;
RtlInitUnicodeString(&cmdline, args.c_str());
UNICODE_STRING name;
RtlInitUnicodeString(&name, argv[1]);
Creating a process requires two steps: the first is creating a helper structure called RTL_USER_PROCESS_-
PARAMETERS by calling RtlCreateProcessParameters like so:
PRTL_USER_PROCESS_PARAMETERS params;
auto status = RtlCreateProcessParameters(¶ms, &name,
nullptr, nullptr, &cmdline,
nullptr, nullptr, nullptr, nullptr, nullptr);
There are quite a few parameters to this function, and we’ll deal with these in more detail in chapter 5. For
now, just note the name of the executable and the command line arguments passed to this function.
Next, we create the process by calling RtlCreateUserProcess with the parameters structure and other
details like so:
Chapter 3: Native Applications 53
RTL_USER_PROCESS_INFORMATION info;
status = RtlCreateUserProcess(&name, 0, params,
nullptr, nullptr, nullptr, 0, nullptr, nullptr, &info);
RtlDestroyProcessParameters(params);
Detailed discussion of this API is deferred to chapter 5. The last parameter is an output structure that contains
some details of the created process if successful. Part of that is provided in a CreateProcess call in the
PROCESS_INFORMATION structure.
The created process has the first thread ready to go, but it’s suspended initially. We need to resume the thread
to kick the process into action:
ResumeThread(info.ThreadHandle);
We can finally close the two handles we received back, even though in this case it’s not a big deal, as our
NativeRun process will soon exit:
CloseHandle(info.ThreadHandle);
CloseHandle(info.ProcessHandle);
#include <phnt_windows.h>
#include <phnt.h>
#include <stdio.h>
#include <string>
//
// build command line arguments
//
std::wstring args;
for (int i = 2; i < argc; i++) {
args += argv[i];
args += L" ";
}
UNICODE_STRING cmdline;
RtlInitUnicodeString(&cmdline, args.c_str());
UNICODE_STRING name;
RtlInitUnicodeString(&name, argv[1]);
PRTL_USER_PROCESS_PARAMETERS params;
auto status = RtlCreateProcessParameters(¶ms, &name,
nullptr, nullptr, &cmdline,
nullptr, nullptr, nullptr, nullptr, nullptr);
if (!NT_SUCCESS(status))
return Error(status);
RTL_USER_PROCESS_INFORMATION info;
status = RtlCreateUserProcess(&name, 0, params, nullptr,
nullptr, nullptr, 0, nullptr, nullptr, &info);
if (!NT_SUCCESS(status))
return Error(status);
RtlDestroyProcessParameters(params);
ResumeThread(info.ThreadHandle);
CloseHandle(info.ThreadHandle);
CloseHandle(info.ProcessHandle);
return 0;
}
Let’s test the application by invoking a native application like SimpleNative from the previous section. Open
Chapter 3: Native Applications 55
a command window, navigate to the location of the resulting NativeRun.exe file, and type the following:
C:\Chapter03\x64\Debug>nativerun.exe SimpleNative.exe
Error (status=0xC000003B)
We get an error, whose value means “Object Path Component was not a directory object”. Well, maybe we
need a full path:
C:\Chapter03\x64\Debug>nativerun.exe C:\Chapter03\x64\Debug\SimpleNative.exe
Error (status=0xC000003B)
Same result. The problem is that the native API expects paths to be in “NT” style rather than Win32 style.
This means paths must be based on the Object Manager’s namespace. It may seem that something like “C:”
is very fundamental, but it’s not. Drive letters get special treatment by standard Windows APIs.
One way to fix this is to prepend “??” before continuing with the normal path like so:
C:\Chapter03\x64\Debug>nativerun.exe \??\C:\Chapter03\x64\Debug\SimpleNative.exe
Process 0xAC48 (44104) created successfully.
What does this strange “\??\” mean? It’s a directory in the Object Manager’s namespace where symbolic links
are stored, one of which is “C:”. You can view all this with the Sysinternals WinObj tool, or my own Object
Explorer. Figure 3-15 shows WinObj displaying the “\Global??\” directory (another name for “\??\”). It also
shows the target of this symbolic link: “Device\Harddiskvolume3” (on your system it may be different).
You can replace “\??\c:” with “\Device\Harddiskvolume3” and it would work just as well:
Chapter 3: Native Applications 56
C:\Chapter03\x64\Debug>nativerun.exe \Device\harddiskvolume3\Chapter03\x64\Debug\Sim\
pleNative.exe
Process 0x93B0 (37808) created successfully.
We can do better, however. When creating a native application process, the main thread does not start
running, so we can take advantage of this and attach a debugger at this point, and then let the thread resume
execution. It would be simple enough to add support for that in NativeRun like so:
int start = 1;
bool debug = _wcsicmp(argv[1], L"-d") == 0;
if (debug)
start = 2;
std::wstring args;
for (int i = start + 1; i < argc; i++) {
args += argv[i];
args += L" ";
}
UNICODE_STRING cmdline;
RtlInitUnicodeString(&cmdline, args.c_str());
UNICODE_STRING name;
RtlInitUnicodeString(&name, argv[start]);
PRTL_USER_PROCESS_PARAMETERS params;
auto status = RtlCreateProcessParameters(¶ms, &name,
Chapter 3: Native Applications 57
RTL_USER_PROCESS_INFORMATION info;
status = RtlCreateUserProcess(&name, 0, params, nullptr,
nullptr, nullptr, 0, nullptr, nullptr, &info);
if (!NT_SUCCESS(status))
return Error(status);
RtlDestroyProcessParameters(params);
if (debug) {
printf("Attach with a debugger. Press ENTER to resume thread...\n");
char dummy[3];
gets_s(dummy);
}
ResumeThread(info.ThreadHandle);
CloseHandle(info.ThreadHandle);
CloseHandle(info.ProcessHandle);
return 0;
}
If you are not familiar with kernel-mode debugging, consult the WinDbg documentation or look online. Also,
chapter 5 in my book “Windows Kernel Programming” has a good introduction to WinDbg for user-mode
and kernel-mode debugging.
As a reminder, copy the executable’s PDB (symbols) file to the target’s System32 directory (where the
executable is) to make it easy for the debugger to locate the symbols.
Chapter 3: Native Applications 58
Once the target restarts and the initial debugger breakpoint is hit, you can set an event to break when the
native application executable is loaded:
!gflag +ksl
sxe ld:simplenative.exe
The first command instructs the debugger to load kernel symbols automatically rather than be lazy about
it. It improves its ability to satisfy the second command which indicates that the first time the image name
indicated is loaded, the debugger should break.
At the breakpoint, the process is (barely) created. In fact, the command !process 0 0 will not show it.
However, you can view the basics with !process @$proc or !process -1, because it’s the current process:
No active threads
Unfortunately, the debugger is not happy enough with the process, so any breakpoints set will fail or be
ignored, for example:
bp simplenative!NtProcessStartup
bp /p ffff820384609140 simplenative!NtProcessStartup
Chapter 3: Native Applications 59
Setting breakpoints in NtDll.dll similarly fails. The best way I have found to make it work is to artificially add
NtDelayExecution code to the beginning of NtProcessStartup and then forcefully break into the debugger
while the main thread is sleeping. Then we can set a breakpoint normally using commands or by opening
the source file and pressing F9 where a breakpoint should hit.
4.7: Summary
In this chapter we looked at native applications that depend on NtDll.Dll only. These are mostly useful for
running at system boot, by Smss.exe. Once we learn more about the native API you may come up with ideas
of things to do at this early time in the Windows boot process.
In the next chapter, we’ll look at system information that can be obtained and/or changed with the native
API.
Chapter 4: System Information
In this chapter we’ll focus on getting and setting various system-level details. Some of the native API
capabilities have Windows API counterparts, but some do not. The chapter is not exhaustive by any means
- the sheer number of information classes used with NtQueryInformationClass is staggering. The chapter
covers some of the more “interesting” or non-trivial system information details, that can be difficult to
intuitively use.
In this chapter:
NTSTATUS NtQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength);
NTSTATUS NtSetSystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_In_reads_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
_In_ ULONG SystemInformationLength);
Chapter 4: System Information 61
Both functions are generic, based on a SYSTEM_INFORMATION_CLASS enumeration and some associated
expected buffer. For query, there are cases where the expected buffer size is fixed, but in others the
information retrieved is dynamic, so the size may not be known in advance. A typical pattern is to call
NtQuerySystemInformation once with a NULL buffer and a size of zero to get back the required size (in
the last argument) (the return value in this case is STATUS_INFO_LENGTH_MISMATCH). Next, the buffer is
allocated, and the call is reissued, but this time with the real buffer and the allocated size. Care must be taken
in some cases to allocate a bit more than the returned size indicates in case the information size has increased
between getting the required size and issuing the second call. For example, if a list of processes is requested,
new processes may have been added just after the required size is returned.
The SYSTEM_INFORMATION_CLASS enumeration is a large one, and keeps growing with newer versions of
Windows. Since these APIs are generic, their prototypes don’t need to change. We’ll spend some time to
look at some enumeration values and how to use them.
The phnt definition of SYSTEM_INFORMATION_CLASS provides comments for most values and the
expected structure, and whether the enumeration supports querying, setting, or both.
Another query variant exists which takes an input buffer (not just an output buffer):
NTSTATUS NtQuerySystemInformationEx(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_In_reads_bytes_(InputBufferLength) PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength);
The input buffer serves as an extra detail that affects the output. Only a subset of system information classes
support this API. A NULL input buffer or an input length of zero are invalid - the function fails rather than
call NtQuerySystemInformation.
In this section, we’ll examine some system information classes that provide various general details for the
system as a whole.
Some of the above information is available with the Windows API functions GetSystemInfo and
GetNativeSystemInfo.
Most members are self-explanatory, or are explained in the documentation of the SYSTEM_INFO structure. A
few that may not be:
• TimerResolution - number of 100 nsec units used for the default timer ticks, used with thread
scheduling. This value should be 156250 (15.625 msec) on most systems. This value is the maximum
number, where the current timer resolution and the minimum one is available with another native API,
NtQueryTimerResolution, discussed later in this chapter.
• LowestPhysicalPageNumber, HighestPhysicalPageNumber - lowest and highest physical page num-
bers supported on the system. The lowest is typically 1, meaning the first page in physical memory is
not used.
• NumberOfPhysicalPages - total number of physical pages, indicating the amount of RAM supported
on the system (multiply by PAGE_SIZE (4 KB) to get the number of bytes.
• NumberOfProcessors - this might seem obvious, but notice the type (CCHAR) - it’s between 0 and 255.
Windows supports more than 255 logical processors. This number is actually the number of processors
in the current processor group, which can be between 1 and 64. The total number of processors is given
by a different system information class (SystemProcessorInformation), discussed next.
The total number of logical processors in the system is returned in MaximumProcessors (not just the
current processor group). The other members are documented as part of the SYSTEM_INFO structure, except
ProcessorFeatureBits, which provides CPU-specific feature bits.
The returned structure is of type SYSTEM_PERFORMANCE_INFORMATION, and it’s a huge one. Most members
are self-explanatory.
• BootTime is the local time the system booted (in the standard 100 nsec units since January 1, 1601).
• CurrentTime is the current local time.
• TimeZoneBias is the difference (in the usual 100 nsec units) from Universal Time.
• BootBiasTime and SleepBiasTime are typically zero.
• TimeZoneId is the index of the time zone configured.
The returned names may not be normal strings, but rather have a Multiple User Interface (MUI) format
involving a DLL and an ID. For example: “@tzres.dll,-112”. This means the string is a resource ID 112 in
a DLL named tzres.dll (in one of the system directories, typically System32) or one based on the current
locale. For example, if the locale is “en-US”, then the file where the string is actually located is *System32\en-
US\tzres.dll.mui“.
Reaching into these DLLs to locate the actual string is tedious and error prone. Fortunately, the Shell provides
an API that does the work:
HRESULT SHLoadIndirectString(
_In_ PCWSTR pszSource,
_Out_writes_(cchOutBuf) PWSTR pszOutBuf,
_In_ UINT cchOutBuf,
_Reserved_ void **ppvReserved);
Here is an example usage to bring the standard name of the current time zone:
RTL_TIME_ZONE_INFORMATION tzinfo;
RtlQueryTimeZoneInformation(&tzinfo);
WCHAR text[64];
SHLoadIndirectString(tzinfo.StandardName, text, _countof(text), nullptr);
printf("Time zone: %ws\n", text);
The full example is part of the SysInfo sample project for this chapter.
NTSTATUS NtQueryTimerResolution(
_Out_ PULONG MaximumTime,
_Out_ PULONG MinimumTime,
_Out_ PULONG CurrentTime);
NTSTATUS NtSetTimerResolution(
_In_ ULONG DesiredTime,
_In_ BOOLEAN SetResolution,
_Out_ PULONG ActualTime);
NtQueryTimerResolution returns the maximum, minimum and current timer tick interval, in the usual
100n sec units.
Since multiple processes may want to affect the timer resolution, the kernel keeps track of each change done
by which process. The highest resolution requested is the one used, but once a process exits, or removes its
resolution effect (calling NtSetTimerResolution with SetResolution being FALSE), the kernel will reduce
the resolution (increment the tick count) if other processes did not request that small tick.
The last argument to NtSetTimerResolution returns the actual value set, based on the hardware’s
capabilities and other process requests, in 100 nsec unites (the same units used for DesiredTime).
Several information classes provide dynamic information on each processor in the current processor group
or in a specific processor group if calling NtQuerySystemInformationEx. Table 4-1 lists the information
classes, and their corresponding structures. The processor group number can be specified as the input buffer
(USHORT), or the current processor group is used if NtQuerySystemInformation is called. Note that the
special ALL_PROCESSOR_GROUPS value is invalid for these calls.
The expected output buffer must be a multiple of a single structure, one instance for each processor
information required. If zero size is provided for the output buffer, the API returns the needed size for
all processors in the requested group.
The following example shows one way to get one of these processor details:
ULONG len;
USHORT group = 0; // group 0
NtQuerySystemInformationEx(SystemProcessorPerformanceInformation,
&group, sizeof(group), nullptr, 0, &len);
ULONG cpuCount = len / sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION);
auto pi = std::make_unique<SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION[]>(cpuCount);
NtQuerySystemInformationEx(SystemProcessorPerformanceInformation,
&group, sizeof(group), pi.get(), len, nullptr);
for(ULONG i = 0; i < cpuCount; i++) {
// use pi[i] to access information for CPU i
}
Here is the breakdown of the members (all times are in 100 nsec units):
The loaded kernel modules (the kernel itself, the HAL, and all drivers) can be obtained with two information
classes, SystemModuleInformation and SystemModuleInformationEx. Here are the associated structures:
Although the name “process” is part of these structure names, they just describe modules - kernel or user. In
the context of the above information classes, it’s the former.
Chapter 4: System Information 68
The details of these members will be discussed in chapter 5. For kernel modules (as used here), the section
handle and mapped base are always zero.
The following shows how to get module information using the extended information class:
ULONG size;
NtQuerySystemInformation(SystemModuleInformationEx, nullptr, 0, &size);
std::unique_ptr<BYTE[]> buffer;
for (;;) {
buffer = std::make_unique<BYTE[]>(size);
auto status = NtQuerySystemInformation(SystemModuleInformationEx,
buffer.get(), size, &size);
if (NT_SUCCESS(status))
break;
}
if (mod->NextOffset == 0)
break;
Task Manager uses this API to enumerate processes, and Process Explorer uses these APIs to
enumerate processes and threads.
Most of its members are self-explanatory. Here are a few that may be not obvious:
• ImageName is the executable path in NT device form (“\Device\HarddiskVolume…”) rather than Win32
path. For kernel processes, the special name is provided: “System”, “Secure System”, “Registry”,
“Memory Compression”. The Idle process (PID 0) is provided with no name.
• InheritedFromUniqueProcessId is the process parent ID.
• NextEntryOffset is the key to moving to the next process. This is the byte offset of the next process
in the list. The enumeration should terminate when this value is zero.
• NumberOfThreadsHighWatermark is the highest number of threads that ever existed in this process.
• UniqueProcessKey is the same as the process ID, since the process uniqueness is defined by its ID for
as long as the object is alive.
• VirtualSize describes the total usage of address space in the process (committed and reserved
memory). All other memory counters don’t consider reserved memory. More details on memory
counters are provided in chapter 7.
The *Pool members represent kernel memory that is attributes to this process. For example, handle entries
are allocated from the paged memory pool in the kernel.
Chapter 4: System Information 71
The following example shows how to obtain process information and iterate through the process list:
ULONG size = 0;
auto status = NtQuerySystemInformation(SystemProcessInformation, nullptr, 0, &size);
std::unique_ptr<BYTE[]> buffer;
while(status == STATUS_INFO_LENGTH_MISMATCH) {
size += 1024; // in case some processes get created
buffer = std::make_unique<BYTE[]>(size);
status = NtQuerySystemInformation(SystemProcessInformation,
buffer.get(), size, &size);
}
auto p = (SYSTEM_PROCESS_INFORMATION*)buffer.get();
for(;;) {
//
// use p...
//
if (p->NextEntryOffset == 0)
break;
//
// move to the next process
//
p = (SYSTEM_PROCESS_INFORMATION*)((PBYTE)p + p->NextEntryOffset);
}
Moving to the next process is accomplished by adding NextEntryOffset to the current SYSTEM_PROCESS_-
INFORMATION pointer, just note the casting to a BYTE pointer so that the addition is done in units of bytes.
A list of threads for each process is provided in the Threads array. The basic structure follows:
KWAIT_REASON WaitReason;
} SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
The thread description structures have fixed sizes, which means iteration is generally simpler. Given a pointer
to a SYSTEM_PROCESS_INFORMATION, thread iteration could take this form (assuming the system info class is
SystemProcessInformation):
void EnumThreads(SYSTEM_PROCESS_INFORMATION* p) {
auto t = p->Threads;
for (ULONG i = 0; i < p->NumberOfThreads; i++) {
// do something with t
t++;
}
}
structure to where the SID of the user running this process is stored in its binary format. To convert to
a string, you can call RtlConvertSidToUnicodeString like so:
Another option for converting a SID to a string is using the Windows API ConvertSidToStringSid.
To get a unique process ID on a system for any time, combine the process ID and the process creation
time. Together, these are guaranteed to be unique at any time on the same machine.
Accessing the extended process information is a bit tricky if the SYSTEM_PROCESS_INFORMATION is declared
as shown earlier. This is because the Threads first array index is included in the declaration. It would
be somewhat easier to remove the Threads item from the declaration, and you can certainly do that. The
declaration provided by the phnt header makes it easy to use SystemProcessInformation, but makes it
clunkier to use SystemFullProcessInformation.
Nevertheless, this is possible as shown in the following code snippet:
Chapter 4: System Information 77
for (;;) {
auto px = (SYSTEM_PROCESS_INFORMATION_EXTENSION*)((PBYTE)p
+ sizeof(*p) - sizeof(SYSTEM_THREAD_INFORMATION)
+ p->NumberOfThreads * sizeof(SYSTEM_EXTENDED_THREAD_INFORMATION));
//
// use p and px as needed...
//
if (p->NextEntryOffset == 0)
break;
p = (SYSTEM_PROCESS_INFORMATION*)((PBYTE)p + p->NextEntryOffset);
}
As can be seen from the code above, with SystemFullProcessInformation (and SystemExtendedPro-
cessInformation), each thread information is now an SYSTEM_EXTENDED_THREAD_INFORMATION:
The basic structure is the first member, thus an “extended” structure, rather than “extension” in the process
case which is distinct from the original structure.
Here is a quick rundown of the extra members:
• StackBase and StackLimit provide the beginning and end of the current thread stack. For user-mode
threads, the addresses are in user-space, whereas for kernel-mode threads the addresses are in kernel
space. StackBase is greater than StackLimit on Intel/AMD platforms, as the stack grows down in
memory.
Chapter 4: System Information 78
• Win32StartAddress is the address provided to a user-mode thread creation function, as opposed to the
RtlUserThreadStart address seen in the SYSTEM_THREAD_INFORMATION structure. For kernel threads,
this value is zero.
• TebBase is the Thread Environment Block (TEB) address for this thread (zero for kernel threads). See
chapter 6 for more on the TEB.
Full discussion of handles and objects is beyond the scope of this book.
Briefly, a handle entry has the following data associated with it:
5.3.1: Handles
NtQuerySystemInformation has information classes to retrieve the list of handles in the system: Sys-
temHandleInformation (16) and SystemExtendedHandleInformation (64). The former should be con-
sidered deprecated, as the information it provides is more limited than the latter, but also because it uses
USHORT as a data type for process IDs and handle values, which is too small. Process IDs and handle values
are limited to 26 bits (and not just 16); therefore, we’ll be using SystemExtendedHandleInformation only.
The structures associated with this information class are as follows:
Chapter 4: System Information 79
Object is the object’s address in kernel space. UniqueProcessId is the process ID handle table where
this handle is part of, while HandleValue is the handle’s numeric value. Handle values are multiples of
4, where the first valid handle is 4. GrantedAccess is the access mask this handle has towards the object.
ObjectTypeIndex is the type index referring to the object type (e.g. process, thread, file, etc.). We’ll see how
to retrieve the type name based on that index in the next section. Finally, HandleAttributes consists of the
optional handle flags mentioned earlier. The possible values are OBJ_PROTECT_CLOSE (1), OBJ_INHERIT (2),
and OBJ_AUDIT_OBJECT_CLOSE (4). CreateorBackTraceIndex is always zero as far as I can tell.
The usual technique of calling NtQuerySystemInformation with zero size and getting back the required size
does not work with SystemExtendedHandleInformation; more precisely, it will return the size of SYSTEM_-
HANDLE_INFORMATION_EX as the minimum required, which just provides the handle count.
One approach here is to allocate memory, check if it’s large enough, and if not - double the allocation, and
so on, until the allocation is large enough:
std::unique_ptr<BYTE[]> buffer;
ULONG size = 1 << 20; // start with 1MB
NTSTATUS status;
do {
buffer = std::make_unique<BYTE[]>(size);
status = NtQuerySystemInformation(SystemExtendedHandleInformation,
buffer.get(), size, nullptr);
if (NT_SUCCESS(status))
break;
size *= 2;
} while (status == STATUS_INFO_LENGTH_MISMATCH);
Chapter 4: System Information 80
if(!NT_SUCCESS(status)) {
// some error
}
auto p = (SYSTEM_HANDLE_INFORMATION_EX*)buffer.get();
for (ULONG_PTR i = 0; i < p->NumberOfHandles; i++) {
auto& h = p->Handles[i];
// do something with h
}
Windows supports many object types; different Windows versions may support a slightly different set of
object types. You can see details of object types in my tool Object Explorer (figure 4-4). How can we get to
this information?
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_writes_bytes_opt_(ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength);
This API normally requires a handle to an object in order to provide the requested detail. However, to get
information about all existing object types, a NULL handle can be provided along with ObjectTypesInfor-
mation. This case also requires “guessing” the size of the required buffer, rather than getting the needed size.
The returned information size is fixed, however, as it’s about types and not any specific object.
The returned pointer is of type OBJECT_TYPES_INFORMATION:
The following shows how to get object types information assuming some fixed allocation that is large enough
(error handling omitted):
auto p = (OBJECT_TYPES_INFORMATION*)buffer.get();
auto type = (OBJECT_TYPE_INFORMATION*)((PBYTE)p + sizeof(ULONG_PTR));
for (ULONG i = 0; i < p->NumberOfTypes; i++) {
// do something with type...
The code is complicated by the fact that every type object starts on an aligned address (4 bytes in 32-bit
processes, or 8 bytes in 64-bit processes). The offset to the next type must take into account the type’s name
and add padding if the final address is not a multiple of the alignment size.
For every type we get the following information:
The following members are used (all others are always zero):
• HighWatermarkNumberOfHandles is the peak number of handles to objects of this type since the most
recent boot.
• InvalidAttributes is the set of attribute flags (OBJ_xxx) that are not valid for handles of this type.
• GenericMapping is the mappings of standard right (GENERIC_READ, GENERIC_WRITE, GENERIC_EXE-
CUTE and GENERIC_ALL) to specific rights for this type of object. See the GENERIC_MAPPING structure
SDK documentation.
• ValidAccessMask is the set of valid access bits for this type.
• SecurityRequired indicates whether objects of this type must be created with a Security Descriptor.
• MaintainHandleCount indicates whether a list of handles to objects of this type is maintained within
the type object. This flag is set mostly for UI-related kernel objects (Window Stations, Desktops,
Composition, and a few others).
• TypeIndex is the index of this type. This allows correlation with the ObjectTypeIndex member in
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.
• PoolType is the type of pool that should be used to allocate objects of this type. The following
enumeration (not present in phnt) provides the values:
The ObjectTypes sample provides a full example of showing the set of types with some of their members.
The bulk of the code is as follows:
printf("%2d %-34wZ H: %6u O: %6u PH: %6u PO: %6u Pool: %s\n",
type->TypeIndex, &type->TypeName,
type->TotalNumberOfHandles, type->TotalNumberOfObjects,
type->HighWaterNumberOfHandles, type->HighWaterNumberOfObjects,
PoolTypeToString(type->PoolType));
}
int main() {
ULONG size = 1 << 14;
auto buffer = std::make_unique<BYTE[]>(size);
if (!NT_SUCCESS(NtQueryObject(nullptr, ObjectTypesInformation,
buffer.get(), size, nullptr))) {
return 1;
}
auto p = (OBJECT_TYPES_INFORMATION*)buffer.get();
auto type = (OBJECT_TYPE_INFORMATION*)((PBYTE)p + sizeof(ULONG_PTR));
long long handles = 0, objects = 0;
for (ULONG i = 0; i < p->NumberOfTypes; i++) {
DisplayType(type);
handles += type->TotalNumberOfHandles;
objects += type->TotalNumberOfObjects;
Check this on a Windows 11 machine. You’ll discover some new and removed object types.
Some kernel objects have string-based names associated with them. Using QuerySystemInformation with
SystemExtendedHandleInformation does not provide the object’s name each handle points to (if any). One
reason is that it may be expensive and not strictly necessary. Furthermore, multiple handles to the same object
might allocate the same name multiple times, or else it would be costly to maintain a list of objects whose
names was already retrieved.
To get an object’s name (pointed to with a handle), the native API offers NtQueryObject with the
ObjectNameInformation information class. One issue that may be evident when enumerating handles
is that handle values returned from the enumeration only have meaning in their respective process. This
means that to get an object’s name we first have to duplicate the handle into the calling process before using
NtQueryObject.
Here is an initial version to return an object’s name (if any) as a std::wstring given a process ID and a
handle that is valid in that process. The first step is to open a handle to the provided process for handling
duplication purposes:
Chapter 4: System Information 86
The next step is to invoke NtDuplicateObject, which (despite the name) is the equivalent of the Windows
API DuplicateHandle. Full discussion of NtDuplicateObject is saved for chapter 7:
HANDLE hDup;
status = NtDuplicateObject(hProcess, h, NtCurrentProcess(),
&hDup, READ_CONTROL, 0, 0);
We duplicate the handle to our process, so that hDup now has meaning. Assuming the call succeeds, we can
continue like so:
This works for most object types, except for many file objects - the call to NtQueryObject hangs. This
is because many file objects cannot be accessed for name query without grabbing locks that are usually
maintained by the file system in question.
The only reasonable way I have found to skip “problematic” file objects is to perform the query on a separate
thread, and if it blocks for too long, terminate it, and move on.
The code becomes more complicated, but cannot be avoided. The following shows the full GetObjectName
implementation, where a Boolean flag indicates the handle in question is for a file. Discussion of
NtCreateThreadEx, NtWaitForSingleObject, NtTerminateThread, and NtQueryInformationThread is
deferred to chapters 6 (“Threads”) and 7 (“Objects and Handles”).
Chapter 4: System Information 87
HANDLE hDup;
status = NtDuplicateObject(hProcess, h, NtCurrentProcess(),
&hDup, READ_CONTROL, 0, 0);
if (NT_SUCCESS(status)) {
static BYTE buffer[1024]; // hopefully large enough :)
auto sname = (UNICODE_STRING*)buffer;
if (file) {
//
// special case for files
// get name on a separate thread
// kill thread if not available after some waiting
//
HANDLE hThread;
//
// create the thread
//
status = NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS,
&procAttr, NtCurrentProcess(),
(PTHREAD_START_ROUTINE)[](auto param) -> DWORD {
auto h = (HANDLE)param;
auto status = NtQueryObject(h, ObjectNameInformation,
buffer, sizeof(buffer), nullptr);
//
// status becomes the exit code of the thread
//
return status;
}, hDup, 0, 0, 0, 0, nullptr);
if (NT_SUCCESS(status)) {
LARGE_INTEGER interval;
interval.QuadPart = -50 * 10000; // 50 msec
status = NtWaitForSingleObject(hThread, FALSE, &interval);
if (status == STATUS_TIMEOUT) {
Chapter 4: System Information 88
NtTerminateThread(hThread, 1);
}
else {
//
// read the thread's exit code
//
THREAD_BASIC_INFORMATION tbi;
NtQueryInformationThread(hThread, ThreadBasicInformation,
&tbi, sizeof(tbi), nullptr);
status = tbi.ExitStatus;
}
NtClose(hThread);
}
}
else {
//
// not a File object, perform normal query
//
status = NtQueryObject(hDup, ObjectNameInformation,
sname, sizeof(buffer), nullptr);
}
//
// assign the result to std::wstring
//
if (NT_SUCCESS(status))
name.assign(sname->Buffer, sname->Length / sizeof(WCHAR));
NtClose(hDup);
}
NtClose(hProcess);
return status;
}
The Handles sample contains the full code. More information on objects and handles is provided in chapter
7.
Some of the details in that structure are provided by NtQuerySystemInformation. Regardless, it can be
accessed directly. For example, the Windows version can be displayed like so:
The phnt headers provide the macro USER_SHARED_DATA to access the structure without casting.
5.5: Summary
A lot of system information is available with just one function: NtQuerySystemInformation. I recommend
an exploration mindset when looking at the various information classes, including trial and error. That said,
this chapter is likely to be expanded in future versions of this book.
Chapter 5: Processes
Processes are probably the most recognizable of all kernel object types. In this chapter, we’ll examine the
native APIs related to process creation, enumeration, and manipulation.
In this chapter:
• Creating Processes
• Process Information
• The Process Environment Block
• Suspending and Resuming Processes
• Enumerating Processes (Take 2)
• Jobs
There have been several attempts in the Infosec community to utilize RtlCreateUserProcess and/or
NtCreateUserProcess to allow running Windows subsystem applications, with varying degrees of success.
Part of the problem is the need to communicate with the Windows Subsystem process (csrss.exe) to notify it of
the new process and thread. This turns out to be fragile, as different Windows versions may have somewhat
different expectations. This may still be an interesting research project, but is out of scope for this book.
Here are the RtlCreateUserProcess(Ex) functions with the returned structure that contains information on
the created process if successful:
Chapter 5: Processes 91
NTSTATUS RtlCreateUserProcess(
_In_ PUNICODE_STRING NtImagePathName,
_In_ ULONG AttributesDeprecated,
_In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
_In_opt_ PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
_In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
_In_opt_ HANDLE ParentProcess,
_In_ BOOLEAN InheritHandles,
_In_opt_ HANDLE DebugPort,
_In_opt_ HANDLE TokenHandle, // used to be ExceptionPort
_Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation);
NTSTATUS RtlCreateUserProcessEx(
_In_ PUNICODE_STRING NtImagePathName,
_In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
_In_ BOOLEAN InheritHandles,
_In_opt_ PRTL_USER_PROCESS_EXTENDED_PARAMETERS ExtendedParameters,
_Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation);
It may be surprising that the extended function has less parameters than the original function. In this case,
some of the less common parameters from the original function are bundled in an extended parameters
structure defined like so:
At the time of this writing, the above structure is not defined by the phnt headers.
The non-optional process parameters structure is a big one, defined likes so:
ULONG Flags;
ULONG DebugFlags;
HANDLE ConsoleHandle;
ULONG ConsoleFlags;
HANDLE StandardInput; // STARTUPINFO.hStdInput
HANDLE StandardOutput; // STARTUPINFO.hStdOutput
HANDLE StandardError; // STARTUPINFO.hStdError
CURDIR CurrentDirectory;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PVOID Environment;
ULONG_PTR EnvironmentSize;
Chapter 5: Processes 93
ULONG_PTR EnvironmentVersion;
PVOID PackageDependencyData;
ULONG ProcessGroupId;
ULONG LoaderThreads;
Some of the structure members correspond directly to the STARTUPINFO structure defined in the Windows API
and used with CreateProcess. Other members are more obscure. After a process is created, this structure
can be found in the PEB.ProcessParamaters member.
This is a variable-length structure, as there are some strings inside. The native API provides two functions
to create and partly initialize an RTL_USER_PROCESS_PARAMETERS, and another one to free the structure:
NTSTATUS RtlCreateProcessParameters(
_Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters,
_In_ PUNICODE_STRING ImagePathName,
_In_opt_ PUNICODE_STRING DllPath,
_In_opt_ PUNICODE_STRING CurrentDirectory,
_In_opt_ PUNICODE_STRING CommandLine,
_In_opt_ PVOID Environment,
_In_opt_ PUNICODE_STRING WindowTitle,
_In_opt_ PUNICODE_STRING DesktopInfo,
_In_opt_ PUNICODE_STRING ShellInfo,
_In_opt_ PUNICODE_STRING RuntimeData);
NTSTATUS RtlCreateProcessParametersEx(
_Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters,
_In_ PUNICODE_STRING ImagePathName,
_In_opt_ PUNICODE_STRING DllPath,
_In_opt_ PUNICODE_STRING CurrentDirectory,
_In_opt_ PUNICODE_STRING CommandLine,
_In_opt_ PVOID Environment,
_In_opt_ PUNICODE_STRING WindowTitle,
_In_opt_ PUNICODE_STRING DesktopInfo,
_In_opt_ PUNICODE_STRING ShellInfo,
_In_opt_ PUNICODE_STRING RuntimeData,
_In_ ULONG Flags);
Chapter 5: Processes 94
NTSTATUS RtlDestroyProcessParameters(
_In_ _Post_invalid_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters);
The two creation functions are practically identical except for the extra Flags parameter. The most
common flag is RTL_CREATE_PROC_PARAMS_NORMALIZED (1) that indicates the structure should be initialized
as “normalized”. Normalized means that the string pointers within are true pointers and can be readily used
in subsequent calls that require this structure. The downside is that copying the structure for later use is
not possible, as the pointers are absolute. Creating the structure as de-normalized stores offsets only in the
various UNICODE_STRING.Buffer members so that copying the structure can be done by a simple memcpy-
like call. The non-Ex function initializes the structure as de-normalized. RtlCreateUserProcess normalizes
the structure if needed before using it.
The native API provides two functions to perform the normalization or de-normalization:
PRTL_USER_PROCESS_PARAMETERS RtlNormalizeProcessParams(
_Inout_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters);
PRTL_USER_PROCESS_PARAMETERS RtlDeNormalizeProcessParams(
_Inout_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters);
The Flags member of RTL_USER_PROCESS_PARAMETERS keeps track of the normalization state so that the
operation is not performed if not required.
Here is more information on members of RTL_USER_PROCESS_PARAMETERS:
• ShellInfo - arbitrary string or buffer that is passed as is to the new process. Some console application
use this internally.
• DesktopInfo is an optional string that points to another Window Station and/or Desktop in the format
“winstaname\desktopname”. This is the same parameter passed in STARTUPINFO.lpDesktop.
• RuntimeData - arbitrary strings or buffers that are passed as is to the new process. Remember that it
does not have to be a string, since UNICODE_STRING has a length, meaning the buffer can point to any
arbitrary data.
• DllPath - optional NT path that serves as another directory to look for DLLs by the loader for the new
process.
• CommandLine - optional command line arguments to pass to the new process.
• Environment - optional environment block created by calling RtlCreateEnvironment. If NULL, copies
the environment from the parent process. See the the documentation for the lpEnvironment argument
to CreateProcess.
• RedirectionDllName - optional NT path DLL that instructs the loader to perform function redirection.
If specified, the DLL must export a global variable named __RedirectionInformation__ that is of
type REDIRECTION_DESCRIPTOR defined like so:
Chapter 5: Processes 95
Two optional functions can be further exported from that DLL named __ShouldApplyRedirection__ and
__ShouldApplyRedirectionToFunction__ whose expected prototypes are the following:
NTSTATUS NtOpenProcess(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId);
DesiredAccess is the access mask required, either one of the generic ones (e.g. GENERIC_READ) or better yet,
a combination of specific ones for processes (e.g. PROCESS_TERMINATE, PROCESS_VM_OPERATION) - all which
are officially documented.
ObjectAttributes does not need a name
(as processes don’t have names), so can be initialized very simply
by using RTL_CONST_OBJECT_ATTRIBUTES like so:
The most important piece is ClientId, where the process ID is specified. Note that the thread ID must be
zero, or the call fails. Here is an example that returns a handle to a given process ID and access mask (if
successful):
Chapter 5: Processes 96
With a process handle in hand, the primary APIs to get and set information are the following:
NTSTATUS NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_writes_bytes_(ProcessInformationLength) PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength);
NTSTATUS NtSetInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_In_reads_bytes_(ProcessInformationLength) PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength);
The pattern should look familiar - NtQuerySystemInformation and NtSetSystemInformation use the same
pattern.
The PROCESSINFOCLASS enumeration provides the various pieces of data that can be retrieved and modified.
It’s not shown here for brevity, but the following subsections list some of the more useful ones.
Unless otherwise specified, the required access mask for the process handle is PROCESS_QUERY_LIMITED_-
INFORMATION or PROCESS_QUERY_INFORMATION.
This information class provides some basic details about the process. One of the following structures are
accepted for this query:
Chapter 5: Processes 97
• ExitStatus is the exit code of the process (if exited). Otherwise, STATUS_PENDING is returned (0x103);
this is called STILL_ACTIVE in Windows API headers.
• PebBaseAddress is the address of the Process Environment Block (PEB). See the next section for more
details on the PEB.
• AffinityMask is a processor bitmask, indicating which processors can be used by threads in this process
(for the current process group). A usable processor is represented with a 1 bit.
• UniqueProcessId is the process ID.
• InheritedFromUniqueProcessId is the parent process ID, from which certain properties are inherited
(if not explicitly specified), such as current directory, environment variables, and more.
The extended structure has some additional flags for the process:
Chapter 5: Processes 98
There is no need to set the Size member; it’s filled in automatically by the API based on the passed
in buffer size.
if (!flags.empty())
return flags.substr(0, flags.length() - 2);
return "";
}
The various terms used for the memory counters are officially documented, so will not be detailed here.
This information class provides the process creation time, exit time (if exited), user-time and kernel-time
execution using the following structure:
Chapter 5: Processes 101
The Windows API provides the GetProcessTimes function to retrieved this same data.
CreateTime and ExitTime are in the usual 100 nsec units from January 1, 1601. KernelTime and UserTime
are in the usual 100 nsec units.
#define PROCESS_PRIORITY_CLASS_UNKNOWN 0
#define PROCESS_PRIORITY_CLASS_IDLE 1
#define PROCESS_PRIORITY_CLASS_NORMAL 2
#define PROCESS_PRIORITY_CLASS_HIGH 3
#define PROCESS_PRIORITY_CLASS_REALTIME 4
#define PROCESS_PRIORITY_CLASS_BELOW_NORMAL 5
#define PROCESS_PRIORITY_CLASS_ABOVE_NORMAL 6
The Foreground member indicates the process should be considered a foreground process, which means
that on client machines the quantum of threads in this process are triple length (around ~90 msec by default
instead of the standard ~30 msec). See chapter 6 for more on thread quantums.
SystemBasePriority provides a more flexible means to set a base priority that does not have to be one of
the above values. The value provided is a KPRIORITY, which is just a LONG. The number translates directly
to a base priority. The high (31) bit can be set to specify the Foreground bit.
Chapter 5: Processes 102
KPRIORITY priority = 7;
NtSetInformationProcess(NtCurrentProcess(), ProcessBasePriority,
&priority, sizeof(priority));
Curiously enough, increasing a process base priority (beyond its current level) with ProcessBasePriority
requires the SeIncreaseBasePriorityPrivilege privilege to be in the target’s process token (by default
granted to administrators and not standard users), but using ProcessPriorityClass is allowed with all
priority classes except Realtime without any special privileges.
You can supply a pointer to a single ULONG, in which case the result would be the current handle count only.
BYTE buffer[512];
NtQueryInformationProcess(hProcess, ProcessImageFileName,
buffer, sizeof(buffer), nullptr);
auto exePath = (UNICODE_STRING*)buffer;
printf("NT: %wZ\n", exePath);
NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32,
buffer, sizeof(buffer), nullptr);
exePath = (UNICODE_STRING*)buffer;
printf("Win32: %wZ\n", exePath);
Chapter 5: Processes 103
This information class returns a fixed structure of type SECTION_IMAGE_INFORMATION that provides some of
the details part of the Portable Executable (PE):
ULONG ImageFileSize;
ULONG CheckSum;
} SECTION_IMAGE_INFORMATION, *PSECTION_IMAGE_INFORMATION;
The various members correspond to various parts of a PE header. Look online for a full description.
The term “COMPlus” used in the above structure means “.NET”. COM+ has a different meaning
today, but the initial name for .NET was “COM+”.
// private
typedef struct _PROCESS_HANDLE_SNAPSHOT_INFORMATION {
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
PROCESS_HANDLE_TABLE_ENTRY_INFO Handles[1];
} PROCESS_HANDLE_SNAPSHOT_INFORMATION, *PPROCESS_HANDLE_SNAPSHOT_INFORMATION;
The information is similar to the system-wide handles returned by NtQuerySystemInformation we’ve seen
in chapter 4. Most notable is the missing object address.
The longest possible command line can have 32767 characters (the longest string UNICODE_STRING
can describe).
The current TEB (NtCurrentTeb) is available by accessing the GS register (x64) or FS register (x86) (Similarly,
it’s taken from certain registers on ARM/ARM64). Both NtCurrentPeb and NtCurrentTeb are defined in
WinNt.h
Getting access to the PEB of another process requires calling NtQueryInformationProcess with ProcessBa-
sicInformation. Of course, the retrieved PEB pointer is only valid in the target process. Getting to that data
requires calling ReadProcessMemory / WriteProcessMemory (Windows API), or NtReadVirtualMemory /
NtWriteVirtualMemory (native API). The latter functions are described in chapter 8.
The PEB is a large structure - displaying it here would take too many pages. I recommend you look at the
structure in the phnt headers (it’s also available with the public Microsoft symbols) while various members
are described.
Not all members of the PEB are described as some are no longer used, and the list of members is quite long
as it is.
The main ingredient is a set of three linked list that store information about the loaded modules in the process
in three different ways:
Each module entry is of type LDR_DATA_TABLE_ENTRY, containing quite a few details about a module:
Chapter 5: Processes 107
ULONG Redirected : 1;
ULONG ReservedFlags6 : 2;
ULONG CompatDatabaseProcessed : 1;
};
};
USHORT ObsoleteLoadCount;
USHORT TlsIndex;
LIST_ENTRY HashLinks;
ULONG TimeDateStamp;
struct _ACTIVATION_CONTEXT *EntryPointActivationContext;
PVOID Lock; // SRW Lock
PLDR_DDAG_NODE DdagNode;
LIST_ENTRY NodeModuleLink;
struct _LDRP_LOAD_CONTEXT *LoadContext;
PVOID ParentDllBase;
PVOID SwitchBackContext;
RTL_BALANCED_NODE BaseAddressIndexNode;
RTL_BALANCED_NODE MappingInfoIndexNode;
ULONG_PTR OriginalBase;
LARGE_INTEGER LoadTime;
ULONG BaseNameHashValue;
LDR_DLL_LOAD_REASON LoadReason;
ULONG ImplicitPathOptions;
ULONG ReferenceCount; // since WIN10
ULONG DependentLoadFlags;
UCHAR SigningLevel; // since REDSTONE2
ULONG CheckSum; // since 22H1
PVOID ActivePatchImageBase;
LDR_HOT_PATCH_STATE HotPatchState;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
printf("Load Order\n");
ListModules(&peb->Ldr->InLoadOrderModuleList,
offsetof(LDR_DATA_TABLE_ENTRY, InLoadOrderLinks));
printf("\nMemory Order\n");
ListModules(&peb->Ldr->InMemoryOrderModuleList,
offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
printf("\nInitialization Order\n");
ListModules(&peb->Ldr->InInitializationOrderModuleList,
offsetof(LDR_DATA_TABLE_ENTRY, InInitializationOrderLinks));
return 0;
}
The trick is to pass the offset of the corresponding LIST_ENTRY embedded in the LDR_DATA_TABLE_ENTRY
structure, so that the correct pointer is obtained. This code does the traversal on the current process, yielding
output such as the following (Debug build):
Load Order
0x00007FF7696F0000: ModList.exe
0x00007FFC832D0000: ntdll.dll
0x00007FFC82590000: KERNEL32.DLL
0x00007FFC81030000: KERNELBASE.dll
0x00007FFC69000000: VCRUNTIME140D.dll
0x00007FFBD9C20000: ucrtbased.dll
Memory Order
0x00007FF7696F0000: ModList.exe
0x00007FFC832D0000: ntdll.dll
0x00007FFC82590000: KERNEL32.DLL
0x00007FFC81030000: KERNELBASE.dll
0x00007FFC69000000: VCRUNTIME140D.dll
0x00007FFBD9C20000: ucrtbased.dll
Initialization Order
0x00007FFC832D0000: ntdll.dll
0x00007FFC81030000: KERNELBASE.dll
0x00007FFC82590000: KERNEL32.DLL
0x00007FFBD9C20000: ucrtbased.dll
0x00007FFC69000000: VCRUNTIME140D.dll
Doing the same traversal in another process is trickier, since we would have to read memory that belongs to
Chapter 5: Processes 110
another process. This is left as an exercise to the interested reader. We’ll see full code for that in chapter 8.
The Windows API provides the EnumProcessModules API to traverse the modules loaded in any process
with a powerful-enough handle, which it does by reading the relevant details from the process’ PEB. This
function only returns the base addresses of the modules, which forces the caller to call more APIs such as
GetModuleBaseName and GetModuleInformation to get more details, but the returned details are not as rich
as the full LDR_DATA_TABLE_ENTRY.
The full member of list of LDR_DATA_TABLE_ENTRY will not be described here in the interest of time.
Back to the PEB structure members:
• ProcessHeap is the default process heap handle, available with the macro RtlProcessHeap or the
Windows API GetProcessHeap. See chapter 8 for more in heaps.
• FastPebLock is a pointer to a critical section (RTL_CRITICAL_SECTION) that is used to synchronize
access to various PEB members. The native APIs RtlAcquirePebLock and RtlReleasePebLock can
be used to acquire and release it.
Next are several flags under stored in a union where CreateProcessFlags is available as the overarching
member:
• KernelCallbackTable is a pointer to an array of callbacks used by User32.Dll that occur after a relevant
system call is invoked. You can try the following in WinDbg attached to a process that uses User32.Dll
(e.g. Notepad.exe):
NTSTATUS KernelToUserCallback(
PVOID InputBuffer,
ULONG InputLength);
• ApiSetMap is a pointer to the API Set mappings, pointing API Set names to their implementation on
the current system. Full discussion of API Sets is beyond the scope of this book, please refer to the book
“Windows Internals, part 1”. Here is the gist of it: API Sets allow separating sets of functions (think of
each set as an interface) from the actual implementation binary.
The following example lists that mapping (see the ApiSets sample):
#define API_SET_SCHEMA_ENTRY_FLAGS_SEALED 1
if ((j + 1) != nsEntry->ValueCount)
printf(", ");
//
// If there's an alias
//
if (valueEntry->NameLength != 0) {
name.assign(PWCHAR(apiSetMapAsNumber + valueEntry->NameOffset),
valueEntry->NameLength / sizeof(WCHAR));
printf(" [%ws]", name.c_str());
}
valueEntry++;
}
printf("}\n");
nsEntry++;
}
• TlsBitmap is a pointer to RTL_BITMAP (see chapter 2 for more on bitmaps) that stores information about
indices used with Thread Local Storage (TLS). A process guarantees at least 64 TLS indices, stored
in the next member, TlsBitmapBits (an array of 2 32-bit values). If more than 64 TLS indices are
needed, the TlsExpansionBitmap member provides that service, with its own bitmap in the following
member (TlsExpansionBitmapBits), which is an array of 32 ULONG values, supporting 1024 (32 * 8)
bits (indices).
Detailed description of TLS is beyond the scope of this book, but it’s use is fully documented in the Windows
API. Look for the functions TlsAlloc, TlsFree, TlsSetValue, and TlsGetValue.
Chapter 5: Processes 113
• NtGlobalFlags stores the Image File Execution Options flags read from the Registry that apply
to this process based on the executable name. The key is HKLM\Software\Microsoft\Windows
NT\CurrentVersion\Image File Execution Options\exename. The value name is GlobalFlag.
• HeapSegmentReserve and HeapSegmentCommit indicate the initial defaults for reserved memory (in
bytes) and committed bytes for a new heap if no override is specified.
• HeapDeCommitTotalFreeThreshold and HeapDeCommitFreeBlockThreshold are used as thresholds
for decommitting memory from heaps when blocks are freed.
The above defaults are read from the Registry key HKLM\System\CurrentControlSet\Control\Session Man-
ager.
• GdiSharedHandleTable is the shared handle table for GDI objects for this session (not just this process).
• LoaderLock is a critical section tasked with protecting certain loader operations from concurrent access.
This concludes the PEB members I will cover at this point. Newer versions of the book will likely cover more
members.
There is no direct counterpart to these APIs in the Windows API. Suspending a process means suspending
all the threads in the process, since threads are the ones actually executing.
The handle provided must have the PROCESS_SUSPEND_RESUME access mask for these functions to succeed.
NTSTATUS NtGetNextProcess(
_In_opt_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_In_ ULONG Flags,
_Out_ PHANDLE NewProcessHandle);
The idea behind this function is to get the “next” process whose handle can be obtained with the requested
access mask. If the next process in line is not accessible by the caller with that access masked, it will be
skipped. This function is typically invoked in a loop until it fails, which means no more processes can be
retrieved with the requested access.
To begin iteration, the input ProcessHandle is set to NULL. The output handle (if successful) is stored in
NewProcessHandle. This handle should then passed as ProcessHandle in the next iteration. It’s important
not to forget to close each handle once you’re doing using it - the API opens handles, and the client’s job to
close them at some point, usually after using a returned handle.
ObjectAttributes can be zero or a set of flags defined for the OBJECT_ATTRIBUTES structure discussed in
chapter 2; a value of zero is typical.
The only flag currently supported in Flags has a value of 1, and if specified, traverses the processes in reverse
order.
Here is an example that iterates through processes with a specific access mask (see the ProcList sample for
full code):
int main() {
HANDLE hProcess = nullptr;
for (;;) {
HANDLE hNewProcess;
auto status = NtGetNextProcess(hProcess,
PROCESS_QUERY_LIMITED_INFORMATION, 0, 0, &hNewProcess);
//
// close previous handle
//
if(hProcess)
NtClose(hProcess);
if (!NT_SUCCESS(status))
break;
PROCESS_EXTENDED_BASIC_INFORMATION ebi;
if (NT_SUCCESS(NtQueryInformationProcess(hNewProcess,
ProcessBasicInformation, &ebi, sizeof(ebi), nullptr))) {
auto& bi = ebi.BasicInfo;
Chapter 5: Processes 115
hProcess = hNewProcess;
}
return 0;
}
if (!flags.empty())
return flags.substr(0, flags.length() - 2);
return "";
}
Chapter 5: Processes 116
6.6: Summary
This chapter dealt with several native APIs related to processes. In the next chapter, we’ll take a look at native
APIs related to threads.
Chapter 6: Threads
Threads are the entities scheduled by the Windows kernel to execute code on processors. This chapter explores
the native APIs related to threads.
In this chapter:
• Creating Threads
• Thread Information
• The Thread Environment Block (TEB)
• Asynchronous Procedure Calls (APC)
• Thread Pools
• More Thread APIs
7.1.1: RtlCreateUserThread
This is the simpler of the two:
NTSTATUS RtlCreateUserThread(
_In_ HANDLE Process,
_In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
_In_ BOOLEAN CreateSuspended,
_In_opt_ ULONG ZeroBits,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ SIZE_T CommittedStackSize,
_In_ PUSER_THREAD_START_ROUTINE StartAddress,
_In_opt_ PVOID Parameter,
_Out_opt_ PHANDLE Thread,
_Out_opt_ PCLIENT_ID ClientId);
Chapter 6: Threads 118
This API mimics very closely the CreateRemoteThread Windows API. Here are the parameters:
• Process is a handle to the process where the thread should be created in. The handle must have the
PROCESS_CREATE_THREAD access mask bit. If the handle is set to NtCurrentProcess(), the thread is
created in the caller’s process. Note that NULL is not a valid value for this parameter.
• ThreadSecurityDescriptor is an optional security descriptor to apply to the newly created thread.
• CreateSuspended is a Boolean flag that if set, indicates the thread should be created in suspended
state. In that case, NtResumeThread should be called when appropriate to spring the thread into action.
• ZeroBits is typically set to zero. This gives the kernel full freedom as where to allocate the thread’s
stack.
• MaximumStackSize and CommittedStack are the maximum reserved and the initial commit stack size
in bytes, respectively, rounded up to a page boundary if needed. If zero is specified, the default is taken
from the PE header. Noe that the Windows APIs accept the initial commit or the maximum reserved,
but not both.
• StartAddress is the thread’s start address, that has essentially the same prototype as expected by the
Windows API functions:
void SomeFunction() {
HANDLE hThread;
status = RtlCreateUserThread(NtCurrentProcess(), nullptr,
FALSE, // not suspended
0, 128 << 10, // maximum size: 128 KB
16 << 10, // initial size: 16 KB
DoWork, nullptr, // function and argument
&hThread, nullptr);
//...
NtClose(hThread);
}
Chapter 6: Threads 119
7.1.2: NtCreateThreadEx
NtCreateThreadEx provides more flexibility than RtlCreateUserThread:
NTSTATUS NtCreateThreadEx(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine, // PUSER_THREAD_START_ROUTINE
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags, // THREAD_CREATE_FLAGS_*
_In_ SIZE_T ZeroBits,
_In_ SIZE_T StackSize,
_In_ SIZE_T MaximumStackSize,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList);
ThreadHandle is the returned thread handle on successful invocation. DesiredAccess specifies the desired
access mask for the returned handle, the typical value being THREAD_ALL_ACCESS (this is always the case
with the Windows APIs). ObjectAttributes is an optional pointer to the standard OBJECT_ATTRIBUTES,
typically passed as NULL.
ProcessHandle, StartRoutine, Argument, and ZeroBits serve the same purpose as in
RtlCreateUserThread. StackSize is the initial committed stack size, and MaximumStackSize is the
maximum reserved stack size.
CreateFlags is a set of optional flags that can be specified:
Note that phnt has different defines for some of the above constants.
Finally, AttributeList is an optional attribute list to apply to the created thread. You can find more possible
attributes by examining the documentation of the UpdateProcThreadAttribute Windows API, although
the native uses a different way of building attribute lists.
Here are the associated structures:
Chapter 6: Threads 120
An attributes list structure has room for just one attribute, but it can dynamically allocated based on the
actual number needed, or event statically by introducing a union.
The following example uses a group affinity attribute to set a specific group affinity for a thread:
The following example uses two attributes, showing a way to statically allocate the required structures:
Chapter 6: Threads 121
union {
PS_ATTRIBUTE_LIST List;
//
// force big enough stack allocation
//
BYTE Buffer[FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes)
+ 2 * sizeof(PS_ATTRIBUTE)];
} attributes;
GROUP_AFFINITY affinity{};
affinity.Group = 0;
affinity.Mask = 0;
{
//
// first attribute
//
auto& attribute = attributes.List.Attributes[0];
attribute.Attribute = PS_ATTRIBUTE_GROUP_AFFINITY;
attribute.Size = sizeof(affinity);
attribute.ValuePtr = &affinity;
attribute.ReturnLength = 0;
}
PROCESSOR_NUMBER ideal{};
ideal.Group = 0;
ideal.Number = 5; // set CPU 5 as the ideal CPU
{
//
// second attribute
//
auto& attribute = attributes.List.Attributes[1];
attribute.Attribute = PS_ATTRIBUTE_IDEAL_PROCESSOR;
attribute.Size = sizeof(ideal);
attribute.ValuePtr = &ideal;
attribute.ReturnLength = 0;
}
attributes.List.TotalLength = sizeof(attributes.Buffer);
NTSTATUS NtQueryInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_Out_writes_bytes_(ThreadInformationLength) PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength,
_Out_opt_ PULONG ReturnLength);
NTSTATUS NtSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_In_reads_bytes_(ThreadInformationLength) PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength);
The THREADINFOCLASS is an enumeration used to specify the type of information to set or query. In this
section, we’ll take a look at some of these information classes. Unless otherwise specified, the thread handle
used must have the THREAD_QUERY_LIMITED_INFORMATION or THREAD_QUERY_INFORMATION access mask for
querying and THREAD_SET_LIMITED_INFORMATION or THREAD_SET_INFORMATION for setting.
• ExitStatus is the exit code of the thread (if terminated). Otherwise, the value STATUS_PENDING (0x103)
is returned. The Windows API defines this as STILL_ACTIVE.
• TebBaseAddress is the address of the Thread Environment Block (TEB). See the next section for more
on the TEB.
Chapter 6: Threads 123
This information class returns the thread creation and exit times, kernel-mode and user-mode execution
times. The structure used, KERNEL_USER_TIMES, is the same one used in NtQueryInformationProcess with
ProcessTimes in chapter 5. The Windows API GetThreadTimes provides the same information.
• ThreadPriority (2) sets the current thread priority. If the priority is in the real-time range (16-31), the
SeIncreaseBasePriorityPrivilege is required.
• ThreadBasePriority (3) sets the base priority of the thread, on top of which a boost can be applied
by the kernel and kernel drivers. Csrss.exe processes and processes running with the Realtime priority
class can change to any priority without restriction.
• ThreadPriorityBoost (14) allows settings or querying whether priority boosts should apply to the
thread (ULONG used with 0 to disable, 1 to enable). By default, boosts are enabled. Note, however, that
boots are never applied in the Realtime range (16-31).
• ThreadActualBasePriority (25) allows setting the current thread priority similarly to Thread-
BasePriority, but also changes the current priority to have the same value. Querying is supported as
well.
Priority boots (if enabled) occur for the following reasons (not an exhaustive list):
• Thread released after a wait is satisfied on an event or semaphore gets a +1 boost by the kernel. Device
drivers can specify their own boost when they call KeSetEvent or KeReleaseSemaphore.
• GUI threads that receive a message get a +2 boost.
• Threads in the foreground process (one of its windows are in focus) get a +2 boost as well. Boosts are
cumulative, so a GUI thread in such a process may get a +4 boost.
• A starved thread (being in the Ready state for more than 4 seconds) might get a boost to priority 15 for
a single quantum.
Except for the last boost that drops to the thread’s base priority after one quantum of running, the other
boosts cause a decay of 1 for the thread’s priority after each quantum the thread executed, until the priority
reaches the thread’s base priority. For more information about thread priorities and scheduling, see chapter
4 in the “Windows Internals part 1” book.
Chapter 6: Threads 124
This functionality is available with the Windows API functions SetThreadDescription and
GetThreadDescription.
For a query operation, this is exactly what is returned. For a set operation, an extended structure can be
specified:
If specified, BoostOutstanding indicates if existing I/O operations started from this thread should be boosted
immediately (if higher than current).
Chapter 6: Threads 125
This information class returns the suspend count of a thread. Zero means the thread is not suspended, while
any positive value indicates the thread is suspended. NtSuspendThread and NtResumeThread can be used
to suspend/resume a thread. If a thread is suspended with calls to NtSuspendThread, the same number of
calls to NtResumeThread must be made to resume the thread. Here are the APIs:
NTSTATUS NtSuspendThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
NTSTATUS NtResumeThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
The THREAD_SUSPEND_RESUME access mask is required for the handle used to suspend a thread.
7.3: Synchronization
Threads sometimes need to coordinate work by waiting on various kernel dispatcher (“waitable”) objects,
which maintain a state of signaled or non-signaled. A thread can wait on one or more objects using the
following basic APIs:
NTSTATUS NtWaitForSingleObject(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout);
NTSTATUS NtWaitForMultipleObjects(
_In_ ULONG Count,
_In_reads_(Count) HANDLE Handles[],
_In_ WAIT_TYPE WaitType,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout);
Example dispatcher object types are: process, thread, mutex, event, semaphore, job, file. We’ll look at the
APIs available for working with various object types in chapter 7.
Both functions require a Timeout parameter to indicate how long the wait should last at most. Specifying
NULL means forever (as long as it takes). Negative values indicate relative time (in the usual 100 nsec units),
while positive values indicate absolute time with the same units measured from January 1, 1601 at midnight
UT.
The handles provided point to the object(s) to wait on. NtWaitForSingleObject waits for a single object,
while NtWaitForMultipleObjects can wait on at most 64 objects. The WaitType indicates whether all
objects must become signaled for the wait to succeed (WaitAll), or just one (WaitAny).
The Aletrable flag indicates if the wait should be alterable. If TRUE, then if APCs are queued to the thread
while waiting, they execute, and the wait is satisfied. See the section Asynchronous Procedure Calls later in
this chapter for more on APCs.
The common return values for both single and multiple waits are the following:
• STATUS_TIMEOUT - the timeout has elapsed, but the object(s) have not become signaled. This value
cannot be returned if the the timeout is NULL (infinite wait).
• STATUS_SUCCESS - the object waited upon has become signaled before the timeout elapsed. This applies
to a single object wait and a multiple object wait with WaitAll.
• STATUS_USER_APC - an alertable wait is satisfied because APC(s) executed.
For single waits, STATUS_ABANDONED may be returned if the wait was satisfied because a mutex was
abandoned. An abandoned mutex is one that was not released by a thread before that thread terminated.
The kernel forcefully released the mutex (making it signaled), but the next thread to successfully acquire
the mutex receives a STATUS_ABANDONED to indicate the previous owner thread of that mutex terminated
prematurely.
For a multiple wait with WaitAny, a return value in the range STATUS_WAIT_0 (0) to STATUS_WAIT_63 (63)
indicates the index of the object which became signaled. In the rare case where an abandomed mutex is one
of the object becoming signaled, STATUS_ABANDONED (0x80) to STATUS_ABANDONED + 63 is returned.
See chapter 7 for more on dispatcher objects.
Inside Wow64 processes, there are two TEBs for a thread: a 64-bit and a 32-bit one. There are two
variants of TEB - TEB32 and TEB64. The differences are around sizes of pointers and some 32/64 bit
size changes.
Chapter 6: Threads 127
The first member of TEB named NtTib, and is of type TIB. It’s small enough to repeat here:
ExceptionList is an exception list created by this thread (implemented on x86 only). StackBase and
StackLimit are the current used stack addresses for this thread. SubSystemTib does not seem to be used.
FiberData contains something if the thread has been converted to fiber. In most cases, the Version member
is the “used” one, which seems to store the value 0x1e00 (latest version of the support for OS2 Windows
had). ArbitraryUserPointer seems like a location to store some thread-specific data, and it is, but not by
developers. Some scenarios use this pointer, so don’t use it yourself. There is Thread Local Storage for that.
Finally, Self points to the beginning of NT_TIB which is also the beginning of the TEB.
Moving to other members of the TEB:
It points to a CSR_PROCESS shown below, along with CSR_NT_SESSION pointed to by the CSR_PROCESS:
} CSR_NT_SESSION;
These data structures can be obtained by loading Csrss.exe into a WinDbg instance and using the
standard dt command.
• CurrentLocale is the current locale used by the thread. For example, 0x409 is “en-US”, equivalent to
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US).
• ExceptionCode stores the last exception code that occurred on this thread.
• LastStatusValue stores the last status value returned from a system call on this thread.
• SubProcessTag is non-zero for threads that handle a service. The service tag information is maintained
by the Service Control Manager (SCM, running in services.exe). The exported I_QueryTagInformation
function from AdvApi32.dll can be used to obtain the mapping to a service name. Here is its definition
with supporting structures and enums:
DWORD I_QueryTagInformation(
_In_opt_ LPCWSTR pszMachineName,
_In_ TAG_INFO_LEVEL eInfoLevel,
_Inout_ PVOID pTagInfo);
Chapter 6: Threads 130
You could use code like the following to get a service name based on a SubProcessTag:
TAG_INFO_NAME_FROM_TAG info = { 0 };
info.InParams.dwPid = pid;
info.InParams.dwTag = tag;
auto err = QueryTagInformation(nullptr, eTagInfoLevelNameFromTag, &info);
if (err)
return L"";
return info.OutParams.pszName;
}
Reading the tag itself can be done by getting the TEB address and then reading from the process memory. For
example:
if (tbi.TebBaseAddress == 0)
return 0;
uint32_t tag = 0;
BOOL isWow = FALSE;
//
// assume a 64-bit OS
//
IsWow64Process(hProcess, &isWow);
if (isWow) {
auto teb = (TEB32*)tbi.TebBaseAddress;
Chapter 6: Threads 131
ReadProcessMemory(process->GetHandle(),
(BYTE*)teb + offsetof(TEB32, SubProcessTag),
&tag, sizeof(ULONG), nullptr);
}
else {
auto teb = (TEB*)tbi.TebBaseAddress;
ReadProcessMemory(process->GetHandle(),
(BYTE*)teb + offsetof(TEB, SubProcessTag),
&tag, sizeof(tag), nullptr);
}
return tag;
}
• TlsSlots is the array of Thread Local Storage used for this thread. The Windows APIs TlsSetValue
and TlsGetValue use this array to store/query TLS values.
• ReservedForOle stores a pointer to a data structure used to manage the Component Object Model
(COM) state for this thread. If the thread has not called CoInitialize(Ex), then this pointer is NULL.
The structure type is SOleThreadData:
typedef enum {
OLE_LOCALTID = 0x01, // TID is in current process
OLE_UUIDINITIALIZED = 0x02,
OLE_INTHREADDETACH = 0x04,
OLE_CHANNELTHREADINITIALZED = 0x08,
OLE_WOWTHREAD = 0x10,
OLE_THREADUNINITIALIZING = 0x20, // thread in CoUninitialize
OLE_DISABLE_OLE1DDE = 0x40, // thread can't use a DDE
OLE_APARTMENTTHREADED = 0x80, // STA thread
OLE_MULTITHREADED = 0x100, // MTA thread
OLE_IMPERSONATING = 0x200, // impersonating
OLE_DISABLE_EVENTLOGGER = 0x400,
OLE_INNEUTRALAPT = 0x800, // thread in NTA
OLE_DISPATCHTHREAD = 0x1000,
OLE_HOSTTHREAD = 0x2000, // host STA
OLE_ALLOWCOINIT = 0x4000,
OLE_PENDINGUNINIT = 0x8000,
OLE_FIRSTMTAINIT = 0x10000,
OLE_FIRSTNTAINIT = 0x20000,
OLE_APTINITIALIZING = 0x40000,
OLE_UIMSGSINMODALLOOP = 0x80000, // Thread has messages in modal loop
OLE_MARSHALING_ERROR_OBJECT = 0x100000,
OLE_WINRT_INITIALIZE = 0x200000,
Chapter 6: Threads 132
LONG TlsMapIndex;
void *ppTlsSlot;
DWORD cComInits;
DWORD cOleInits;
DWORD cCalls;
ServerCall *pServerCall;
ThreadCallObjectCache *pCallObjectCache;
ContextStackNode* pContextStack;
CObjServer* pObjServer;
DWORD dwTIDCaller;
PVOID pCurrentCtxForNefariousReaders;
CPolicySet* pPS;
PVOID pvPendingCallsFront;
PVOID pvPendingCallsBack;
Chapter 6: Threads 133
CAptCallCtrl* pCallCtrl;
CSrvCallState* pTopSCS;
HWND hwndSTA; // STA window
LONG cORPCNestingLevel;
DWORD cDebugData;
UUID LogicalThreadId; // logical thread id
HWND hwndDdeServer;
HWND hwndDdeClient;
ULONG cServeDdeObjects;
PVOID pSTALSvrsFront;
HWND hwndClip; // Clipboard window
IDataObject* pDataObjClip; // Current Clipboard DataObject
DWORD dwClipSeqNum;
DWORD fIsClipWrapper;
IUnknown* punkState;
DWORD cCallCancellation;
DWORD cAsyncSends;
CAsyncCall* pAsyncCallList;
CSurrogatedObjectList* pSurrogateList;
PRWLOCKTLSEntry pRWLockTlsEntry;
CallEntryBuffer CallEntry;
InitializeSpyNode* pFirstSpyReg;
InitializeSpyNode* pFirstFreeSpyReg;
CVerifierTlsData* pVerifierData;
DWORD dwMaxSpy;
BYTE cCustomMarshallerRecursion;
PVOID pDragCursors;
IUnknown* punkError;
ULONG cbErrorData;
OutgoingCallData outgoingCallData;
IncomingCallData incomingCallData;
OutgoingActivationData outgoingActivationData;
Chapter 6: Threads 134
ULONG cReentrancyFromUserAPC;
ModernSTAWaitContext* pModernSTAWaitContext;
DWORD dwCrossThreadFlags;
DWORD dwNestedRemRelease;
ULONG cIncomingTouchedASTACalls;
PushLogicalThreadId* pTopPushedLogicalThreadId;
ULONG iXslockOwnerTableHint;
OLETLS_PREVENT_RUNDOWN_MITIGATION currentPreventRundownMitigation;
BOOL fOweForcedBulkUpdateForCurrentMitigation;
IUnknown* pClipboardBroker;
DWORD dwActivationType;
ULONG cTouchedAstasInActiveCall;
OXID* pTouchedAstasInActiveCall;
UnmarshalForQueryInterface* pTopmostUnmarshalForQueryInterface;
CoGetStandardMarshalInProgress* pTopmostCoGetStandardMarshalInProgress;
WireContainerThis* requestContainerPassthroughData;
ULONG requestContainerPassthroughDataSize;
BOOL freeRequestContainerPassthroughData;
WireContainerThat* responseContainerPassthroughData;
ULONG responseContainerPassthroughDataSize;
ComTlsFlags comTlsFlags;
ThreadLockOrderVerifier<ComLockOrder> lockOrderVerifier;
} SOleTlsData;
You can find this definition (and other referenced by it) by loading ComBase.Dll into WinDbg and
using the dt command.
NTSTATUS NtDelayExecution(
_In_ BOOLEAN Alertable, // alertable?
_In_opt_ PLARGE_INTEGER DelayInterval);
NTSTATUS NtQueueApcThread(
_In_ HANDLE ThreadHandle,
_In_ PPS_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcArgument1,
_In_opt_ PVOID ApcArgument2,
_In_opt_ PVOID ApcArgument3);
The ThreadHandle must have the THREAD_SET_CONTEXT access mask for this to work. The target thread can
be in a different process than the caller, but a 32-bit process cannot queue an APC to a thread that belongs
to a 64-bit process. Regardless, the ApcRoutine must be valid in the target process. The APC routine itself
accepts three arbitrary arguments specified by the caller.
What happens if the target thread never enters an alertable wait? The APCs will never execute. At some
point, if more APCs are queued to the thread, queuing will fail. This means that normally the queuer needs
to somehow “know” that the target thread goes into an alertable wait from time to time so the APCs can
execute.
What happens if a thread waits in an alertable state, but no APCs ever arrive? If the wait is non-infinite, then
it will eventually end when the timeout elapses. If the wait is infinite, the thread will wait forever. However,
the native API provides ways to release the thread from an alertable wait regardless of APCs:
If the thread is in a alertable waiting state, it will be alerted (woken up), and continues execution. The return
value from the wait function (or NtDelayExecution) is STATUS_ALERTED (0x101), rather than STATUS_-
USER_APC (0xc0). The last variant (NtAlertResumeThread) also resumes the thread before alerting it. This
could be useful if the target thread is known to be suspended.
Finally, NtTestAlert tests if the current thread has the “alerted” flag set. If so, STATUS_ALERTED is returned,
and pending APCs execute.
Chapter 6: Threads 136
NTSTATUS NtTestAlert();
Note that to get the NtAlert* APIs to work, the target thread must be waiting in an alertable with
a native API (NtWait... or NtDelayExecution), rather than one of the rough equivalents from
Windows APIs (this is one reason the equivalence is not exact).
The native API functions related to thread pooling begin with Tp. The documented Windows APIs are
thin wrappers around the native APIs. The parameters are virtually identical, except the native API returns
NTSTATUS, and any result is provided as an indirect first argument, while the Windows API returns the result
directly (or a Boolean if there is no special result). For example, here are two equivalent APIs:
Table 6-1 lists the many of the Windows thread pool APIs and their native API equivalents. Refer to the
Windows SDK documentation for the description of these APIs.
Chapter 6: Threads 137
NTSTATUS NtGetContextThread(
_In_ HANDLE ThreadHandle, // requires THREAD_GET_CONTEXT
_Inout_ PCONTEXT ThreadContext);
NTSTATUS NtSetContextThread(
_In_ HANDLE ThreadHandle, // requires THREAD_SET_CONTEXT
_In_ PCONTEXT ThreadContext);
To safely set the context (or read it in some predictable manner), the thread should be suspended first by
calling NtSuspendThread:
NTSTATUS NtSuspendThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
NTSTATUS NtResumeThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
A suspend count is maintained for each thread, optionally returned by the above APIs. The maximum
suspend count is 127.
Sometimes a handle to a thread needs to be obtained. This is where NtOpenThread can help:
NTSTATUS NtOpenThread(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId);
The similarities to NtOpenProcess should be evident. The ClientId should contain the thread ID in
UniqueThread, and zero for UniqueProcess.
NTSTATUS NtTerminateThread(
_In_opt_ HANDLE ThreadHandle, // requires THREAD_TERMINATE
_In_ NTSTATUS ExitStatus);
NTSTATUS NtGetNextThread(
_In_ HANDLE ProcessHandle, // requires PROCESS_QUERY_INFORMATION
_In_opt_ HANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_In_ ULONG Flags, // must be zero
_Out_ PHANDLE NewThreadHandle);
This is similar to NtGetNextProcess we encountered in chapter 5. The enumeration starts with ThreadHan-
dle set to NULL, and ends when the returned status is unsuccessful. Don’t forget to close each handle you
receive to prevent a handle leak.
7.8: Summary
This chapter looked at thread-related native APIs. In the next chapter, we’ll deal with kernel objects and
handles.
Chapter 7: Objects and Handles
Much of the functionality in Windows is provided through kernel objects. Accessing kernel objects from
user-mode is always performed indirectly through handles. This chapter deals with several common types
of objects, as well as more generally working with objects and handles.
In this chapter:
• Objects
• Enumerating Objects
• Object Manager Namespace
• Handles
• Enumerating Handles
• Specific Object Types
8.1: Objects
The Windows kernel supports a set of object types, out of which objects can be created. Each object belongs
to a type (in itself an object). The Windows API provides many functions to create objects or open handles to
existing objects. Each object type has its own set of open and/or create functions, as well as specific functions
for manipulating existing objects.
The objects themselves are data structures residing in kernel space, which is one reason user-mode cannot
access objects directly, even if an object’s address is known.
The set of object types supported on a particular version of Windows can be viewed with a tool like Object
Explorer (figure 7-1).
Chapter 7: Objects and Handles 141
For each object types, Object Explorer shows the total number of objects and handles, the peak number of
objects and handles, and a few more details discussed shortly.
Getting the information seen in Object Explorer is a matter of calling NtQueryObject:
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_writes_bytes_opt_(ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
Chapter 7: Objects and Handles 142
Normally, calling NtQueryObject requires a valid handle, with one exception. Getting object types
information is done by specifying a NULL handle with ObjectTypesInformation information class. The
returned structure are as follows:
Each object type has a different size because of the type name (TypeName member), so moving to the next
object type is somewhat tricky, as it always starts on a pointer-aligned address. The first type starts on such
an aligned address as well. The following code shows how to iterate over the types, displaying some key
information (error handling omitted):
Chapter 7: Objects and Handles 143
//
// move to next type object
//
auto temp = (PBYTE)type + sizeof(OBJECT_TYPE_INFORMATION) +
type->TypeName.MaximumLength;
//
// round up to the next pointer-size address
//
type = (OBJECT_TYPE_INFORMATION*)(((ULONG_PTR)temp +
sizeof(PVOID) - 1) / sizeof(PVOID) * sizeof(PVOID));
}
printf("Total Types: %u\n", types->NumberOfTypes);
For each object type, the meaning of GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, and GENERIC_-
ALL is different - this mapping provides the details. For example, Process objects map GENERIC_READ to
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | READ_CONTROL.
• ValidAccessMask indicates the valid access mask bits for handles to objects of this type (for example,
for Process object type, this is PROCESS_ALL_ACCESS).
• SecurityRequired indicates if objects of this type must have a security descriptor if they are created
with default security. Normally, objects that can be named have this set to TRUE.
• MaintainHandleCount is set if the number of handles in each process for this object type is maintained
by the kernel.
Chapter 7: Objects and Handles 145
To enable object tracking, a global flag must be set. The easiest way to set it is by running the Gflags utility
from the Windows SDK, and check “Maintain a list of objects for each type” (figure 7-2). This changes a value
in the Registry, and the system must be restarted for this flag to take effect. Once restarted, enumerating
objects can work.
Chapter 7: Objects and Handles 146
Why isn’t this option enabled by default? Because of some overhead - this was especially important to avoid
in the (distant) past, when hardware wasn’t as powerful as it is now, and memory sizes were smaller. It’s
difficult to estimate the cost of this overhead, but it exists. More CPU time must be spent when objects are
created, and more memory is needed as well to maintain this extra information.
Setting “Maintain a list of objects for each type” causes the value 0x4000 to be added to the GlobalFlag
value in the Registry key HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. This value
is read when the system boots.
Chapter 7: Objects and Handles 147
The enumeration starts by allocating a large enough buffer. The API will not return the necessary buffer size
if you pass zero. This is because calculating the number of bytes required requires going over all objects,
which is inefficient. Instead, the API tries to fill the provided buffer with as many objects as possible, and
if the buffer is not big enough, the call fails with a STATUS_INFO_LENGTH_MISMATCH status. It’s your job to
allocate more memory and try again. Here is one way to go about it:
The code starts with a 4MB (1 << 22) allocation, and doubles it every time the length is not enough. Expect
at least 12MB of memory to be allocated for this call to succeed.
The above code also takes care of the case where the call fails for a different reason, which would mean the
global flag is not set. Of course, the code ignores allocation failures for simplicity.
Now the enumeration can begin. The returned information is in two layers: the first is a SYSTEM_OB-
JECTTYPE_INFORMATION structure, providing information about a type, very similar (but not identical) to
OBJECT_TYPE_INFORMATION. Then, the list of objects for this type follows as SYSTEM_OBJECT_INFORMATION
structures. This is non-trivial, because the structures don’t have fixed sizes, as there are names (for types,
and possibly for objects).
SYSTEM_OBJECTTYPE_INFORMATION is defined like so:
• The next type object is pointed NextEntryOffset bytes from this one.
• The WaitableObject flag is added, indicating whether objects of this type are waitable (dispatcher
objects).
• There are some details that are missing compared to OBJECT_TYPE_INFORMATION.
• NextEntryOffset stores the number of bytes to move forward from the beginning of the result buffer
to get to the next object.
• Object is the object’s address in kernel space.
• CreatorUniqueProcess is the process ID of the creator of this object.
• CreatorBackTraceIndex seems to be always zero.
• Flags is the flags set in the object’s header. The possible flags are shown below:
typedef enum {
OBF_NEW_OBJECT = 0x01,
OBF_KERNEL_OBJECT = 0x02,
OBF_KERNEL_ONLY_ACCESS = 0x04,
OBF_EXCLUSIVE_OBJECT = 0x08,
OBF_PERMANENT_OBJECT = 0x10,
OBF_DEFAULT_SECURITY_QUOTA = 0x20,
OBF_SINGLE_HANDLE_ENTRY = 0x40,
OBF_DELETED_INLINE = 0x80
} OBJECT_HEADER_FLAGS;
Chapter 7: Objects and Handles 149
• PointerCount is the reference count of the object mixed with an inverted usage count (Windows 8.1
and later). This value should not be relied upon because of that usage count. See the “Windows Internals
7th edition, Part 2” book for more details.
• HandleCount is the number of handles open to this object (could be zero if it’s being used by kernel
code only).
• PagedPoolCharge and NonPagedPoolCharge are the memory allocation charges for this object. These
are the same as the ones reported for their type.
• ExclusiveProcessId is usually zero, but is the process ID that owns the object if it’s exclusive. An
exclusive object cannot be used by any other process. This kind of power is available to kernel-mode
code only.
• SecurityDescriptor is the security descriptor (if any) that is attached to the object.
• NameInfo is the name of the object (if any).
Given the above definitions, here is a full iteration over all types and all objects within each type:
//
// get the first object
//
auto obj = (SYSTEM_OBJECT_INFORMATION*)
((PBYTE)type + sizeof(*type) + type->TypeName.MaximumLength);
for (;;) {
//
// do something with the object
//
printf("0x%p (%wZ) P: 0x%X H: %u PID: %u EPID: %u\n",
obj->Object, &obj->NameInfo, obj->PointerCount, obj->HandleCount,
HandleToULong(obj->CreatorUniqueProcess),
HandleToULong(obj->ExclusiveProcessId));
//
// if no more objects, break
//
if (obj->NextEntryOffset == 0)
break;
Chapter 7: Objects and Handles 150
//
// move to next object
//
obj = (SYSTEM_OBJECT_INFORMATION*)(buffer.get() + obj->NextEntryOffset);
}
//
// if no more types, break
//
if (type->NextEntryOffset == 0)
break;
//
// move to next type
//
type = (SYSTEM_OBJECTTYPE_INFORMATION*)(buffer.get() + type->NextEntryOffset);
}
Note that the type “ObjectType” and its objects are not provided by the enumeration as separate
objects, because the type objects themselves are these objects.
The “directories” shown in WinObj have nothing to do with the file system; rather, these are Directory kernel
objects, which are containers for other kernel objects (including other directories). The following briefly
describes some of the more “known” Directory objects:
Querying the contents of a directory object requires opening a handle to the directory in question:
Chapter 7: Objects and Handles 152
NTSTATUS NtOpenDirectoryObject(
_Out_ PHANDLE DirectoryHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
The ObjectAttributes is where the directory name is specified with the usual InitializeObjectAt-
tributes.The following code opens a handle to the root directory:
HANDLE hDirectory;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\");
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &name, 0, nullptr, nullptr);
status = NtOpenDirectoryObject(&hDirectory, DIRECTORY_QUERY, &attr);
if(!NT_SUCCESS(status)) {
// do something with hDirectory
NtClose(hDirectory);
}
The AccessMask parameter indicates the requested access to the directory. These can be a combination of
the following access flags:
With a directory handle in hand, the contents of the directory can be obtained with NtQueryDirectoryOb-
ject:
NTSTATUS NtQueryDirectoryObject(
_In_ HANDLE DirectoryHandle,
_Out_writes_bytes_opt_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_ BOOLEAN ReturnSingleEntry,
_In_ BOOLEAN RestartScan,
Chapter 7: Objects and Handles 153
To simplify the use of NtQueryDirectoryObject, one could pass a big buffer, where it’s unlikely a second
call would be required. But there is no good way to tell what size would be large enough, as it heavily depends
on the directory in question and the system on which the code runs; so, it’s best to be prepared for multiple
calls.
The returned buffer is an array of OBJECT_DIRECTORY_INFORMATION objects, providing the object’s name
and its type name. Fortunately, simple array access works, because even though the name and type of the
object are provided, the strings themselves are stored after the array itself so that no special calculation is
needed.
Assuming hDirectory is a valid directory handle, the following example shows how to correctly get all
objects in that directory with a (possibly) limited buffer:
ULONG index = 0;
bool first = true;
ULONG size = 1 << 12; // example size (4KB)
auto buffer = std::make_unique<BYTE[]>(size);
auto data = (POBJECT_DIRECTORY_INFORMATION)buffer.get();
int start = 0;
for(;;) {
status = NtQueryDirectoryObject(hDirectory, buffer.get(), size,
FALSE, first, &index, nullptr);
if (!NT_SUCCESS(status))
break;
first = false;
//
Chapter 7: Objects and Handles 154
The SymbolicLink object type is a kernel object that points to another kernel object (similar in spirit to a
shell shortcut). Given a symbolic link object name, its target can be retrieved by opening a handle to it
(NtOpenSymbolicLinkObject) and then querying for the target (NtQuerySymbolicLinkObject):
NTSTATUS NtOpenSymbolicLinkObject(
_Out_ PHANDLE LinkHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
NTSTATUS NtQuerySymbolicLinkObject(
_In_ HANDLE LinkHandle,
_Inout_ PUNICODE_STRING LinkTarget,
_Out_opt_ PULONG ReturnedLength);
The parameters should be self-explanatory at this point. Here is an example of getting a symbolic link target
given a C_string symbolic link object name:
//
// open a handle to the symbolic link
//
HANDLE hLink;
Chapter 7: Objects and Handles 155
//
// query for the target
//
status = NtQuerySymbolicLinkObject(hLink, &target, nullptr);
NtClose(hLink);
return buffer;
}
You can also create a symbolic link, if you have enough permissions:
NTSTATUS NtCreateSymbolicLinkObject(
_Out_ PHANDLE LinkHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ PUNICODE_STRING LinkTarget);
The ObjDir sample puts most of what we’ve seen in this section, by querying the contents of a specified
directory on the command line, showing the objects names, type names, and symbolic link targets (for
symbolic link objects only, of course).
The main function obtains a directory, and calls a helper function to get the results. If successful, it displays
them:
Chapter 7: Objects and Handles 156
NTSTATUS status;
auto objects = EnumDirectoryObjects(argv[1], status);
if (objects.empty() && !NT_SUCCESS(status)) {
printf("Error: 0x%X\n", status);
}
else {
printf("Directory: %ws\n", argv[1]);
printf("%-20s %-50s Link Target\n", "Type", "Name");
printf("%-20s %-50s -----------\n", "----", "----");
for (auto& obj : objects) {
printf("%-20ws %-50ws %ws\n",
obj.TypeName.c_str(), obj.Name.c_str(),
obj.LinkTarget.c_str());
}
}
return 0;
}
struct ObjectInfo {
std::wstring Name;
std::wstring TypeName;
std::wstring LinkTarget;
};
ULONG index = 0;
bool first = true;
NtClose(hDirectory);
return objects;
}
The code is identical to the example shown earlier, except the results are accumulated in a
vector<ObjectInfo>, and the status is returned as a second argument. The last piece is
GetSymbolicLinkTarget, which is called only if the current object’s type name is “SymbolicLink”:
std::wstring
GetSymbolicLinkTarget(std::wstring const& directory, std::wstring const& name) {
auto fullName = directory == L"\\" ? (L"\\" + name) : (directory + L"\\" + name);
UNICODE_STRING linkName;
RtlInitUnicodeString(&linkName, fullName.c_str());
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &linkName, 0, nullptr, nullptr);
HANDLE hLink;
auto status = NtOpenSymbolicLinkObject(&hLink, GENERIC_READ, &attr);
if (!NT_SUCCESS(status))
return L"";
WCHAR buffer[512]{};
UNICODE_STRING target;
RtlInitUnicodeString(&target, buffer);
target.MaximumLength = sizeof(buffer);
status = NtQuerySymbolicLinkObject(hLink, &target, nullptr);
NtClose(hLink);
return buffer;
}
The only difference from the earlier example is that the directory and object names are provided, and the
function is forced to concatenate them together, taking care of not duplicating a backslash for the root
directory.
Here are a few examples when running ObjDir (some formatting applied to better fit a page):
Chapter 7: Objects and Handles 159
c:\>objdir \
Directory: \
Type Name Link Target
---- ---- -----------
Event DSYSDBG.Debug.Trace.Memory.530
Mutant PendingRenameMutex
Directory ObjectTypes
FilterConnectionPort storqosfltport
FilterConnectionPort MicrosoftMalwareProtectionRemoteIoPortWD
SymbolicLink SystemRoot \Device\BootDevice\Wi\
ndows
...
Section LsaPerformance
ALPC Port SmApiPort
FilterConnectionPort CLDMSGPORT
FilterConnectionPort DtdSel03
FilterConnectionPort MicrosoftMalwareProtectionPortWD
SymbolicLink OSDataRoot \Device\OSDataDevice
Event SAM_SERVICE_STARTED
Directory Driver
Directory DriverStores
c:\>objdir \KernelObjects
Directory: \KernelObjects
Type Name Link Target
---- ---- -----------
SymbolicLink MemoryErrors
Event LowNonPagedPoolCondition
Session Session1
Event SuperfetchScenarioNotify
Event SuperfetchParametersChanged
SymbolicLink PhysicalMemoryChange
SymbolicLink HighCommitCondition
Mutant BcdSyncMutant
SymbolicLink HighMemoryCondition
Event HighNonPagedPoolCondition
Partition MemoryPartition0
KeyedEvent CritSecOutOfMemoryEvent
Event SystemErrorPortReady
SymbolicLink MaximumCommitCondition
SymbolicLink LowCommitCondition
Event HighPagedPoolCondition
Chapter 7: Objects and Handles 160
SymbolicLink LowMemoryCondition
Session Session0
Event LowPagedPoolCondition
Event PrefetchTracesReady
You may notice in the above output that some symbolic links have no target. These are called dynamic
symbolic links, and their target is set by a kernel callback. All the above non-target links actually
point to Event objects (e.g. “HighMemoryCondition”).
Some directories are inaccessible without elevated permissions (for example, “\ObjectTypes”). Run
ObjDir elevated to get that extra access.
8.4: Handles
Accessing kernel objects from user-mode must be done through handles. A handle is an index into a handle
table, maintained on a per-process basis. In this section, we’ll look at native APIs related to handles in general,
applicable to any (or many) object types.
Once a handle is no longer needed, it should be closed with NtClose that we have used many times before:
It is sometimes useful to get another handle to the same object based on an existing handle. For example, a
handle in a different process cannot be used by the calling process to access the same object unless the handle
resides in the caller’s process handle table. Handle duplication provides a solution.
The function name is NtDuplicateObject, which is the one invoked by the Windows API DuplicateHandle
function:
NTSTATUS NtDuplicateObject(
_In_ HANDLE SourceProcessHandle,
_In_ HANDLE SourceHandle,
_In_opt_ HANDLE TargetProcessHandle,
_Out_opt_ PHANDLE TargetHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_In_ ULONG Options);
Chapter 7: Objects and Handles 161
The API name (NtDuplicateObject) is misleading - no object is duplicated, only a handle is duplicated. The
Windows API name is accurate.
A handle from a source process is duplicated into the process handle of a target process. Both source and
target processes may be the same one, which is sometimes useful if a different access mask is needed.
SourceHandleProcess and TargetHandleProcess must have PROCESS_DUP_HANDLE access mask available
(NtCurrentProcess() always has that). SourceHandle is the handle to duplicate, and the result is returned
in TargetHandle (if successful).
DesiredAccess can be zero if Options includes the flag DUPLICATE_SAME_ACCESS; otherwise, it specifies
the required access mask (if that can be obtained). If the access mask requested is a subset of the original
access mask, this always succeed.
HandleAttributes can include the following optional flags:
• DUPLICATE_SAME_ACCESS (2) - as mentioned, uses the access mask of the source handle
• DUPLICATE_CLOSE_SOURCE (1) - the source handle is closed after duplication (essentially “moving” the
handle from the source to the target process). If this flag is used, the source process handle and the
target handle pointer can be NULL, which causes the source handle to be closed without any duplication
actually occurring.
The target process gets the duplicated handle, but if the target process is not the caller process, it doesn’t
“know” that a new handle has been created in its handle table. This means that the duplicating process must
somehow tell the target process about the new handle value (presumabely, these two processes would like
to share access to the object). In such a case there must be some form of Interprocess Communication (IPC)
mechanism in place between the caller process and the target process.
A general way to get information about an object (through a handle) and the handle entry itself is
NtQueryObject, repeated here for convenience:
MaxObjectInfoClass
} OBJECT_INFORMATION_CLASS;
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_writes_bytes_opt_(ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength);
Some of the members provide information on the handle entry maintained in the process handle table, and
some details are for the object “pointed to” by the handle. Here is a description of the members:
• Attributes includes the handle and object attributes. It could be zero, or a combination of the
following flags:
NameInfoSize is returned as zero for objects that have no name, but also objects that are nowhere
in the Object Manager’s namespace (even though they have some form of name). Primary examples
are file and desktop objects.
The object’s name (if any) can be retrieved using the ObjectNameInformation information class. This
returns an OBJECT_NAME_INFORMATION, which is a glorified UNICODE_STRING:
The catch is that the buffer provided must be large enough to accomodate the name - the API will not allocate
it for you. One way to get that information is with ObjectBasicInformation from the previous subsection.
The following example displays an object’s name given a handle and a process ID:
//
// open handle to target process
//
HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(pid) };
Chapter 7: Objects and Handles 164
//
// get basic information
//
OBJECT_BASIC_INFORMATION info;
status = NtQueryObject(hDup, ObjectBasicInformation,
&info, sizeof(info), nullptr);
if (!NT_SUCCESS(status)) {
name = accessDeniedName;
break;
}
if (info.NameInfoSize == 0) // no name
break;
//
// query the actual name
//
auto buffer = std::make_unique<BYTE[]>(info.NameInfoSize);
status = NtQueryObject(hDup, ObjectNameInformation, buffer.get(),
info.NameInfoSize, nullptr);
if (!NT_SUCCESS(status))
break;
if (hProcess)
NtClose(hProcess);
if (hDup)
NtClose(hDup);
return name;
}
The above code retrieves object names for accessible objects (you can run it elevated to gain more access),
which are within the object manager’s namespace. File objects, for example, are not reported as having a
name (NameInfoSize is set to zero).
How do we get names of such objects, like files and desktops, which have names, but are not in the
hierarchy maintained by the Object Manager’s namespace? We could just call NtQueryObject with
ObjectNameInformation, disregarding the fact that NameInfoSize is zero.
File objects, in particular, pose an unexpected problem: querying a file object’s name may hang the
NtQueryObject call. The reason has to do with internal synchronization-related operations deep within
the Executive’s I/O system (file names can change at any time, file systems have their own caches, etc.).
The best approach I have found for this case is to spawn a thread to get the file object name, and if after a
predefined time the name is unavailable (call is stuck), terminate the thread and move on. Here is some code
that does that:
HANDLE hThread;
OBJECT_ATTRIBUTES threadAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
//
// create thread
//
auto status = RtlCreateUserThread(NtCurrentProcess(), nullptr, FALSE, 0, 0, 0,
[](auto p) -> NTSTATUS {
Chapter 7: Objects and Handles 166
//
// extract the name
//
auto uname = (UNICODE_STRING*)buffer.get();
data->Name.assign(uname->Buffer, uname->Length / sizeof(WCHAR));
return 0;
}, &data, &hThread, nullptr);
if (!NT_SUCCESS(status))
return L"";
LARGE_INTEGER timeout;
timeout.QuadPart = -10 * 10000; // 10 msec
//
// wait for the thread to terminate
//
if (STATUS_TIMEOUT == NtWaitForSingleObject(hThread, FALSE, &timeout)) {
//
// terminate forefully
//
NtTerminateThread(hThread, 1);
}
NtClose(hThread);
return data.Name;
}
To make this work we need to know the object type for which the name is required. Getting this information
get be done with ObjectTypeInformation, discussed next.
Using the ObjectTypeInformation information class with a given handle returns OBJECT_TYPE_INFORMA-
TION, which we looked at earlier in this chapter. Refer to the section Object Types for the details.
Chapter 7: Objects and Handles 167
Write an application that display’s an object’s name given a handle value and a process ID. Make
sure you handle file objects correctly.
The ObjectHandleInformation returns a simple structure indicating the state of the “protect from close”
and “inherit” flags of the given handle:
As we’ve seen, these details are also available with ObjectBasicInformation as part of the Attributes
member.
The last two members of OBJECT_INFORMATION_CLASS (ObjectSessionInformation and ObjectSes-
sionObjectInformation) are for setting information only.
NTSTATUS NtSetInformationObject(
_In_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_In_reads_bytes_(ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength);
ObjectSessionInformation only applied to Directory objects. It can be used to associate a Directory object
to the caller’s session. No buffer is needed in this case. However, it does require the TCB privilege, normally
not given to any user. Services, however, are able to request it if so desired.
ObjectSessionObjectInformation does the same thing as “ObjectSessionInformation‘, but it only works
for Directory objects not currently associated with any session. The upside is that no special privilege is
needed.
Chapter 7: Objects and Handles 168
ULONG size = 0;
std::unique_ptr<BYTE[]> buffer;
auto status = STATUS_SUCCESS;
do {
if(size)
buffer = std::make_unique<BYTE[]>(size);
status = NtQuerySystemInformation(SystemExtendedHandleInformation,
buffer.get(), size, &size);
if (status == STATUS_INFO_LENGTH_MISMATCH) {
size += 1 << 12;
continue;
}
} while (!NT_SUCCESS(status));
if (!NT_SUCCESS(status)) {
printf("Error: 0x%X\n", status);
return 1;
}
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
PROCESS_HANDLE_TABLE_ENTRY_INFO Handles[1];
} PROCESS_HANDLE_SNAPSHOT_INFORMATION, *PPROCESS_HANDLE_SNAPSHOT_INFORMATION;
Although it seems good enough, there are a couple of issues when using the process-specific call:
• A process handle is required with PROCESS_QUERY_INFORMATION, which is not always possible to get.
For example, protected processes can never be opened with this access mask.
• The PROCESS_HANDLE_TABLE_ENTRY_INFO structure is missing the object’s address in kernel space.
Although technically useless to user-mode directly, it may still be needed by the caller.
If these two issues are not a problem, then this method can certainly be used.
Tools like Process Explorer use the NtQuerySystemInformation call so they can enumerate handles
for all processes - no need to obtain a handle to any process.
The Handles sample project shows how to perform a system-wide handle enumeration. It also shows object
names. Feel free to add support for process filtering, show object types, filter based on object types or names,
etc.
8.6.1: Mutex
Mutex objects are used for thread synchronization. A mutex is either free (signaled), or owned by a thread
(non-signaled). The wait functions discussed in chapter 6 apply to mutexes (as they do to all dispatcher
objects). Creating a named mutex is achieved with the following:
NTSTATUS NtCreateMutant(
_Out_ PHANDLE MutantHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ BOOLEAN InitialOwner);
The desired access for mutex creation is typically MUTEX_ALL_ACCESS. If the mutex is not named (using
ObjectAttributes), a new mutex is created. If a name is provided, and the mutex with that name exists -
the function fails with STATUS_OBJECT_NAME_COLLISION. The name (if specified) must be either a full name
Chapter 7: Objects and Handles 171
The term “mutant” used by the native API is the original name for mutexes. The kernel object type is still
called “Mutant” for compatibility reasons. For all practical purposes, these are one and the same.
The following example shows how to create a named mutex in the Directory “KernelObjects” (requires
running elevated):
HANDLE hMutex;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\KernelObjects\\MySuperMutex");
OBJECT_ATTRIBUTES mutexAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
auto status = NtCreateMutant(&hMutex, MUTEX_ALL_ACCESS, &mutexAttr, FALSE);
NTSTATUS NtOpenMutant(
_Out_ PHANDLE MutantHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
ObjectAttributes must store a name for the mutex, otherwise the call fails. The call may also fail for
security reasons - access may not be granted to the caller.
The following example opens a handle to the named mutex created above to allow waiting only:
HANDLE hMutex;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\KernelObjects\\MySuperMutex");
OBJECT_ATTRIBUTES mutexAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
auto status = NtOpenMutant(&hMutex, SYNCHRONIZATION, &mutexAttr);
Once a mutex is successfully acquired, the owner thread must eventually release it:
Chapter 7: Objects and Handles 172
NTSTATUS NtReleaseMutant(
_In_ HANDLE MutantHandle,
_Out_opt_ PLONG PreviousCount);
When a mutex is created unowned, it’s internal count is once. If it’s created owened, its count is zero. Each
acuisition drops the count by one, and each release increments the count by one.
A mutex may be acquired recusrively more than once by the same thread. The number of waits must match
the number of NtRelaseMutant calls to actually free the mutex. The optional PreviousCount returns the
previous count of ownership. NtReleaseMutant fails if the caller thread is not the owner of the mutex.
Finally, a mutex state can be queried with NtQueryMutant:
NTSTATUS NtQueryMutant(
_In_ HANDLE MutantHandle,
_In_ MUTANT_INFORMATION_CLASS MutantInformationClass,
_Out_writes_bytes_(MutantInformationLength) PVOID MutantInformation,
_In_ ULONG MutantInformationLength,
_Out_opt_ PULONG ReturnLength);
The members are mostly self-explanatory. AbandonedState indicates if the mutex is abandoned - a thread
owned this mutex and terminated without releasing it. Since only the mutex owner can release a mutex, the
kernel steps in and releases the mutex forcefully upon the thread’s termination and changes the mutex state
to abandoned - it’s the same as free in the sense that the next thread to acquire it will do so, but the abandoned
state is an indication of that abnormal situation that may hint some bug in the application. Once the mutex
is owned again, its abandoned state is removed (the wait function returns STATUS_ABANDONED rather than
STATUS_SUCCESS)
Remember that querying the state of dispatcher objects (like mutexes) is not a safe operation, in
the sense that its state may change immediately after the call by another thread; it’s still useful for
debugging purposes.
Chapter 7: Objects and Handles 173
8.6.2: Sempahore
Sempahore objects maintain a maximum and a current count, initailized at creation time. A semaphore is
signaled while its current count is above zero. Once it drops to zero, it becomes non-signaled. Every successful
wait decrements its current count. Creating and opening a semaphore object are similar to a mutex:
NTSTATUS NtCreateSemaphore(
_Out_ PHANDLE SemaphoreHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ LONG InitialCount,
_In_ LONG MaximumCount);
NTSTATUS NtOpenSemaphore(
_Out_ PHANDLE SemaphoreHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
The purpose of a typical semaphore is to limit the size of something given that multiple threads work with
that “something” (e.g. a queue).
Acquiring one of the semaphore’s counts is done by calling a waiting function. Releasing a semaphore is
done with NtReleaseSemaphore.
NTSTATUS NtReleaseSemaphore(
_In_ HANDLE SemaphoreHandle,
_In_ LONG ReleaseCount,
_Out_opt_ PLONG PreviousCount);
A thread may release more than one “count” at a time (ReleaseCount). The optional PreviousCount returns
the previous count of the semaphore before the current operation.
Just like a mutex, a semaphore’s state may be queried (mostly for debugging purposes):
Chapter 7: Objects and Handles 174
NTSTATUS NtQuerySemaphore(
_In_ HANDLE SemaphoreHandle,
_In_ SEMAPHORE_INFORMATION_CLASS SemaphoreInformationClass,
_Out_writes_bytes_(SemaphoreInformationLength) PVOID SemaphoreInformation,
_In_ ULONG SemaphoreInformationLength,
_Out_opt_ PULONG ReturnLength);
8.6.3: Event
Event objects are simple flags, that may be in the signaled state (set, TRUE) or non-signaled (reset, FALSE).
However, there are two types of events:
A notification event releases any number of threads when set, while a synchronization event releases just one
thread when set and goes back to the non-signaled (reset) state immediately; it must be set again to release
another thread.
If, at the time of setting a synchronization event, no thread is waiting on it, it will remain signaled
until a thread waits, which would release it immediately and go to the non-signaled state.
NTSTATUS NtCreateEvent(
_Out_ PHANDLE EventHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ EVENT_TYPE EventType,
_In_ BOOLEAN InitialState);
NTSTATUS NtOpenEvent(
_Out_ PHANDLE EventHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
NTSTATUS NtSetEvent(
_In_ HANDLE EventHandle,
_Out_opt_ PLONG PreviousState);
NtClearEvent and NtResetEvent do the same thing, but the latter returns the previous state of the event
(can be zero or one).
Another API is available for setting an event:
This function works only on synchronization events and increases a released thread to the priority of the
current thread (if it’s higher). The standard behavior of NtSetEvent is to provide a boost of +1 to released
threads.
Yet another option is NtPulseEvent, having the same parameters as NtSetEvent. It sets and immediately
resets the event. It has potential issues, though (search online), and thus is not recommended.
Finally, an event’s information can be queried:
Chapter 7: Objects and Handles 176
NTSTATUS NtQueryEvent(
_In_ HANDLE EventHandle,
_In_ EVENT_INFORMATION_CLASS EventInformationClass,
_Out_writes_bytes_(EventInformationLength) PVOID EventInformation,
_In_ ULONG EventInformationLength,
_Out_opt_ PULONG ReturnLength);
These object types and others will be covered in a future edition of the book.
8.8: Summary
Kernel objects wrap the core functionality in Windows including processes, threads, jobs, mutexes and many
others. This chapter showed how to work in general with objects and handles, and focused on some useful
object types, more of which we’ll meet in future chapters.
Chapter 8: Memory (Part 1)
Memory is obviously crucial to any computer system. The Windows executive Memory Manager component
is responsible for managing the various data structures required to allow the CPU to correctly translate virtual
addresses to physical ones (RAM) by managing a set of page tables. Memory (from the CPU and Memory
Manager’s perspective) is always managed in chunks called pages.
In this chapter:
• Introduction
• The Virtual Functions
• Querying Memory
• Reading and Writing
• Other Virtual APIs
• Heaps
• Heap Information
9.1: Introduction
Table 8-1 shows the page sizes for each architecture supported by Windows. The default page size is 4 KB.
• There is some mapping from a virtual address to a physical address that is transparently followed by
the processor. Code accesses memory using virtual addresses only.
• The page may not be in physical memory at all, in which case the CPU raises a page fault exception,
handled by the Memory Manager. If the page happens to be in a file (such as a page file), the Memory
Manager will allocate a free physical page, read the data in, fix the page tables and tell the processor to
try again. This is completely transparent to the calling code.
Chapter 8: Memory (Part 1) 178
• Free - there is nothing there, no mapping. Accessing such a page causes an Access Violation exception.
• Committed - the opposite of free. A page that is definitely accessible, even though it may reside in a
file at various times. It may still be inaccessible because of page protection conflicts.
• Reserved - same as free from an access perspective - there is nothing there. However, the page has been
reserved for some future use, and will not be considered for new allocations.
On top of the Virtual API is the Heap API. The purpose of this API is to manage small or non-page sized
chunks efficiently. They call into the Virtual APIs when needed. Finally, the highest level of APIs are part of
the C/C++ runtime - functions such as malloc, free, operator new, and similar. Their implementation is
compiler-dependent, but Microsoft’s compiler currently uses the heap API (working with the default process
heap).
NTSTATUS NtAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect);
This function is actually documented in the WDK, and most of its parameters correspond to similar ones in
the Windows API VirtualAllocEx. Here is a brief description:
• ProcessHandle is the process where the allocation should be made. The typical case is NtCur-
rentProcess, but it’s possible to make an allocate in a different process if the handle has the
PROCESS_VM_OPERATION access mask.
• BaseAddress provides the address to use. For new allocations, *BaseAddress is typically set to NULL,
indicating to the Memory Manager there is no preferred address. If the region in question is reserved,
then *BaseAddress can specify an address within this region for (say) committing memory. On a
successful return of this function, *BaseAddresd* indicates the address actually used, which may be
rounded down to the nearest page boundary from its input value, or if NULL was specified on input -
returns the actual address selected by the Memory Manager.
• ZeroBits is typically zero, but can be used to affect the final address selected. See the description in
chapter 6.
• *RegionSize is the size of the region affected, in bytes. It may be rounded up (depending on
‘BaseAddress) so as to align on page boundary.
• AllocationType is a set of flags (some mutually exclusive) indicating the operation to perform. The
most common ones are:
• Protect is the protection for the allocated pages, e.g. PAGE_READWRITE, PAGE_READONLY, PAGE_EXE-
CUTE_READWRITE, etc.
See the documentation of VirtualAlloc in the Windows SDK for more details on these parameters.
The inverse function to NtAllocateVirtualMemory is NtFreeVirtualMemory:
NTSTATUS NtFreeVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG FreeType);
The first 3 parameters are the same as in NtAllocateVirtualMemory. However, *BaseAddress cannot be
NULL - it must specify a valid address to de-commit or release (opposite of reserve). The address and region
size will be rounded down and up, respectively, so the range is page aligned. Finally, the most common flags
follow:
NTSTATUS NtAllocateVirtualMemoryEx (
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG PageProtection,
_Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParameCount);
#define MEM_EXTENDED_PARAMETER_TYPE_BITS 8
Each value in MEM_EXTENDED_PARAMETER_TYPE represents one type of customization, some of which are
documented as part of CreateFileMapping2. CreateFileMapping2 only supports MemSectionExtend-
edParameterUserPhysicalFlags and MemSectionExtendedParameterNumaNode values. Here are the
currently available parameter types:
• MemExtendedParameterAddressRequirements
This allows specifying the minimum and maximum addresses to use for the memory allocation, as well as
the minimum alignment.
• MemExtendedParameterNumaNode - specifies the desired Numa node (in the ULong member).
• MemExtendedParameterPartitionHandle - specifies the desired memory partition handle (memory
partitions are beyond the scope of this chapter).
• MemExtendedParameterUserPhysicalHandle - specifies a handle to another section object to copy
information from related to Address Windowing Extensions (AWE), which always identify memory
mapped physically (beyond the scope of this chapter).
• MemExtendedParameterAttributeFlags - specifies a set of additional flags, most of which are defined
in WinNt.h, shown in table 8-2:
NTSTATUS NtQueryVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
_Out_writes_bytes_(MemoryInformationLength) PVOID MemoryInformation,
_In_ SIZE_T MemoryInformationLength,
_Out_opt_ PSIZE_T ReturnLength);
As you can see, the function supports several information classes, described below.
The buffer is of type MEMORY_BASIC_INFORMATION, documented in the Windows SDK. In fact, using this
information class is equivalent to the Windows API VirtualQueryEx. It returns information about a region
of address space that has the same attributes in terms of page state, mapping type, and page protection. The
access mask needed for the process handle is PROCESS_QUERY_LIMITED_INFORMATION or PROCESS_QUERY_-
INFORMATION. Consult the documentation of VirtualQueryEx for the details.
At the time of this writing, the documentation states that PROCESS_QUERY_INFORMATION access mask
is needed, but this is incorrect.
NtQueryVirtualMemory fails eventually when the address goes beyond the user-mode address space.
DisplayMemoryInfo is a local function showing details of a MEMORY_BASIC_INFORMATION structure:
switch (protect) {
case 0: break;
case PAGE_NOACCESS: text += "No Access"; break;
case PAGE_READONLY: text += "RO"; break;
case PAGE_READWRITE: text += "RW"; break;
case PAGE_EXECUTE_READWRITE: text += "RWX"; break;
case PAGE_WRITECOPY: text += "Write Copy"; break;
case PAGE_EXECUTE: text += "Execute"; break;
case PAGE_EXECUTE_READ: text += "RX"; break;
case PAGE_EXECUTE_WRITECOPY: text += "Execute/Write Copy"; break;
default: text += "<other>";
}
return text;
}
The main structure is just a container for entries describing working set ranges in the given process. The
BaseAddress value has no effect. This also means the size of the provided data is not fixed, and may fail
with STATUS_INFO_LENGTH_MISMATCH, in which case the buffer should be enlarged and the call repeated.
The members of MEMORY_WORKING_SET_BLOCK are described below:
• Protection describes the region protection. The values are not the same as the standard protection
constants we’ve seen in the previous subsection.
• ShareCount holds the share count of this region (if shared); the maximum value is 7.
• Shared is set if this region can be shared with other processes.
• Node is the NUMA node where this physical memory page is located.
• VirtualPage is the virtual page number (shift 12 bits to the left to get to the actual address).
The functionality provided by this information class is the same as with the QueryWorkingSet Windows
API. Consult the SDK docs for more information on the data members.
The following example shows how to output all working set pages given a handle to a process:
Chapter 8: Memory (Part 1) 188
do {
buffer = std::make_unique<BYTE[]>(size);
status = NtQueryVirtualMemory(hProcess, nullptr,
MemoryWorkingSetInformation, buffer.get(), size, nullptr);
if(NT_SUCCESS(status))
break;
size *= 2; // size too small
} while (status == STATUS_INFO_LENGTH_MISMATCH);
if (!NT_SUCCESS(status))
return false;
auto ws = (MEMORY_WORKING_SET_INFORMATION*)buffer.get();
for (ULONG_PTR i = 0; i < ws->NumberOfEntries; i++) {
//
// get single item
//
auto& info = ws->WorkingSetInfo[i];
//
// show some details
//
printf("%16zX Prot: %-17s Share: %d Shareable: %s Node: %d\n",
info.VirtualPage << 12, ProtectionToString(info.Protection),
(int)info.ShareCount, (int)info.Shared ? "Y" : "N", (int)info.Node);
}
return true;
}
MemoryRegionInformation can be called with a structure that excludes the last two members; it’s best to
use the extended version.
Here is a description of the members:
• AllocationBase is the base of the initial allocation, which could be different from the provided
BaseAddress in the call if BaseAddress is the beginning of the allocation block.
• AllocationProtect is the protection given to the memory block in the initial allocation. Note that
the current protection could be different - in fact, it could be anything for each page of this allocation
region.
• Private is set if the memory region is private to the process (not shared.
• MappedDataFile is set if the region maps to a file as data (not as a PE image).
• MappedImage is set if the region is mapped to a PE image.
• MappedPageFile is set if this region is mapped to a page file.
• MappedPhysical is set if the region is mapped to non-paged memory (such as with large pages).
• DirectMapped is set if the region is mapped by a kernel driver to a device memory.
• SoftwareEnclave is set if the region is mapped to a memory enclave (such as Virtualization Based
Security (VBS) protected enclave).
• PageSize64K is set if the region is mapped with 64 KB page size (possible when working with section
objects).
• PlaceholderReservation is set if the region is reserved by the kernel.
• MappedWriteWatch is set if the memory is registered for write watch (see InitializeProcessFor-
WsWatch Windows API or NtSetInformationProcess with ProcessWorkingSetWatch).
• PageSizeLarge is set if the region is mapped using large pages.
• PageSizeHuge is set if the region is mapped with huge pages.
if (mbi.State == MEM_COMMIT) {
MEMORY_REGION_INFORMATION ri;
if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, mbi.BaseAddress,
MemoryRegionInformationEx, &ri, sizeof(ri), nullptr))) {
details += MemoryRegionFlagsToString(ri.RegionType);
}
if (!result.empty())
return result.substr(0, result.length() - 2);
return L"";
}
caller selects the virtual addresses it’s interested in (could be just one) passed as part of the output buffer. The
buffers are updated with the attributes of the address(es) provided. Each item is of type MEMORY_WORKING_-
SET_EX_INFORMATION that contains a MEMORY_WORKING_SET_EX_BLOCK:
VirtualAddress is the input address which must be set prior to the call. The adjacent VirtualAttributes
is the result. VirtualAttributes has two parts of a union:
• If the address is valid (committed and present in RAM), the first structure should be consulted ( Valid
is 1).
• If the address is invalid, the second structure (Invalid) should be consulted. In this case, In-
valid.Valid is zero.
Some of the flags were present in MEMORY_WORKING_SET_BLOCK. Here is a description of those which were
not:
This information just returns the shared committed memory size in the given process (as SIZE_T), in pages
(4KB chunks). The following example shows the shared committed memory in KB:
Chapter 8: Memory (Part 1) 194
SIZE_T committed;
if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, nullptr,
MemorySharedCommitInformation, &committed, sizeof(committed), nullptr))) {
printf("Shared commit: %llu KB\n", committed << 2);
}
This information class gives details about an image that is mapped to the provided address (if any). The
returned structure is the following:
ImageBase is the virtual address the image is mapped to in the process. SizeOfImage is the size
of the image in bytes. ImagePartialMapis set if only part of the image is mapped within
this page.ImageNotExecutableis set if the mapping do not include execute permissions.
Finally,ImageSigningLevel‘ is one value from the following definitions found in <WinNt.h>:
This information class provides the same details as MemoryBasicInformation, but applies to “secure”
(Isolated User Mode - IUM) processes that are protected using Virtualization Based Security (VBS), such as
LsaIso.exe (if Credential Guard is running on the system). The secure kernel is the one providing the results,
at its complete discretion.
For normal processes, it behaves the same as MemoryBasicInformation.
This information class provides information on images stored in enclaves. It doesn’t seem to be accessible
from user-mode.
This information class is the same as MemoryBasicInformation, but only supported on ARM64. It doesn’t
seem to be much different from MemoryBasicInformation, except the region size provided determines the
maximum address range even if rounding down occurrs.
This information class returns physical memory information for a range of addresses as a MEMORY_PHYSI-
CAL_CONTIGUITY_INFORMATION structure:
Chapter 8: Memory (Part 1) 196
MemoryBadInformation returns a process’ bad pages list - pages that are either physically defective, or are
used for memory enclaves. BaseAddress must be set to NULL, and the returned buffer contains an array of
the following structure:
Chapter 8: Memory (Part 1) 197
union {
struct {
ULONG_PTR Poisoned : 1; // should not be accessed
ULONG_PTR Physical : 1;
};
ULONG_PTR AllInformation;
};
} MEMORY_BAD_IDENTITY_INFORMATION, *PMEMORY_BAD_IDENTITY_INFORMATION;
The Physical flag indicates whether PageFrameIndex should be used (if set), or VirtualAddress (if clear).
MemoryBadInformationAllProcesses provides similar information for all processes, but is not accessible
from user-mode.
NTSTATUS NtReadVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_Out_writes_bytes_(BufferSize) PVOID Buffer,
_In_ SIZE_T BufferSize,
_Out_opt_ PSIZE_T NumberOfBytesRead);
NTSTATUS NtWriteVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_reads_bytes_(BufferSize) PVOID Buffer,
_In_ SIZE_T BufferSize,
_Out_opt_ PSIZE_T NumberOfBytesWritten);
These functions are the ones being invoked by the Windows API functions ReadProcessMemory and
WriteProcessMemory. The parameters should be mostly self-explanatory; BaseAddress is the virtual
Chapter 8: Memory (Part 1) 198
address in the target process. ProcessHandle must have PROCESS_VM_READ access mask (for NtReadVir-
tualMemory), or PROCESS_VM_WRITE (for NtWriteVirtualMemory).
A “classic” example of writing to another process memory is injecting a DLL into a target process by creating
a thread in that process, and pointing it to a LoadLibrary function in that process. Here is an example of
how this could be done using the Windows API (error handling ommitted for brevity):
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}
Let’s see how we can create a “native” version of this technique. First we’ll accept the process ID and DLL
path just like in the Windows API version, but we’ll switch to Unicode:
Chapter 8: Memory (Part 1) 199
This is not mandatory, but we’ll use LoadLibraryW as the target function for the remote thread to execute,
and in that case simplifies the code.
The next step is to open a handle to the target process with the required access mask:
HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(wcstol(argv[1], nullptr, 0)) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
auto status = NtOpenProcess(&hProcess,
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
&procAttr, &cid);
if(!NT_SUCCESS(status)) {
printf("Error opening process (status: 0x%X)\n", status);
return status;
}
Next, we need to allocate memory in the target process and write the full DLL path into that memory. First,
the allocation:
Next, we copy the DLL path to be loaded into the allocated remote buffer:
Now we need to get the module handle for kernel32 from which we can locate LoadLibraryW:
Chapter 8: Memory (Part 1) 200
UNICODE_STRING kernel32Name;
RtlInitUnicodeString(&kernel32Name, L"kernel32");
PVOID hK32Dll;
status = LdrGetDllHandle(nullptr, nullptr, &kernel32Name, &hK32Dll);
if (!NT_SUCCESS(status)) {
printf("Error getting kernel32 module (status: 0x%X)\n", status);
return status;
}
LdrGetDllHandle is a rough equivalent to the Windows API GetModuleHandle function. Now we can locate
LoadLibraryW:
ANSI_STRING fname;
RtlInitAnsiString(&fname, "LoadLibraryW");
PVOID pLoadLibrary;
status = LdrGetProcedureAddress(hK32Dll, &fname, 0, &pLoadLibrary);
if (!NT_SUCCESS(status)) {
printf("Error locating LoadLibraryW (status: 0x%X)\n", status);
return status;
}
You may be wondering why are we not going “all native” and using LdrLoadDll (the equivalent of
LoadLibrary). The reason is that a thread function accepts one argument only, and that argument must
be the DLL path. Unfortunately, LdrLoadDll accepts the DLL path in its third parameter, which we have no
good way of providing:
NTSTATUS LdrLoadDll(
_In_opt_ PWSTR DllPath,
_In_opt_ PULONG DllCharacteristics,
_In_ PUNICODE_STRING DllName, // DLL path
_Out_ PVOID *DllHandle);
For now we’ll stick with LoadLibraryW which accepts a single parameter - the DLL path to load.
The final step is creating the actual thread to execute LoadLibraryW and pass the argument to be the allocated
buffer in the target process:
Chapter 8: Memory (Part 1) 201
HANDLE hThread;
status = RtlCreateUserThread(hProcess, nullptr, FALSE, 0, 0, 0,
(PUSER_THREAD_START_ROUTINE)pLoadLibrary, buffer,
&hThread, nullptr);
if (!NT_SUCCESS(status)) {
printf("Error creating thread (status: 0x%X)\n", status);
return status;
}
printf("Success!\n");
If this succeeds, then the thread is executing and will load the DLL based on its path. Of course that could
fail if the path is bad, for example. From the injector’s perspective all that’s left is to clean up:
NtClose(hThread);
NtClose(hProcess);
Here is the complete main function (InjectDllRemoteThread project) with error handling removed:
HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(wcstol(argv[1], nullptr, 0)) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
auto status = NtOpenProcess(&hProcess,
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
&procAttr, &cid);
UNICODE_STRING kernel32Name;
RtlInitUnicodeString(&kernel32Name, L"kernel32");
PVOID hK32Dll;
status = LdrGetDllHandle(nullptr, nullptr, &kernel32Name, &hK32Dll);
ANSI_STRING fname;
RtlInitAnsiString(&fname, "LoadLibraryW");
PVOID pLoadLibrary;
status = LdrGetProcedureAddress(hK32Dll, &fname, 0, &pLoadLibrary);
HANDLE hThread;
status = RtlCreateUserThread(hProcess, nullptr, FALSE, 0, 0, 0,
(PUSER_THREAD_START_ROUTINE)pLoadLibrary, buffer,
&hThread, nullptr);
printf("Success!\n");
NtClose(hThread);
NtClose(hProcess);
return 0;
}
In this section we’ll examine some more APIs from the “Virtual” family.
The initial page protection for newly allocated region is set at NtAllocateVirtualMemory time. If memory
protection needs to be changed, NtProtectVirtualMemory can be used:
NTSTATUS NtProtectVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG NewProtect,
_Out_ PULONG OldProtect);
Chapter 8: Memory (Part 1) 203
Most of the parameters should be familiar by now. ProcessHandle can be NtCurrentProcess(), changing
protection in the caller’s process (always succeeds). If a different process is used, the handle must have
the PROCESS_VM_OPERATION access mask. NewProtect is the requested protection using the same constants
used with the Windows API which we met earlier (PAGE_READWRITE, PAGE_READONLY, etc.). The returned
OldProtect contains the previous protection of the first page in the region.
Memory can be “locked” into physical memory, prevent it from being paged out until “unlocked”. The
following APIs can be used:
NTSTATUS NtLockVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG MapType);
NTSTATUS NtUnlockVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG MapType);
MapType can be one of two values: MAP_PROCESS (1) or MAP_SYSTEM (2). If MAP_SYSTEM is specified, then the
caller must have the SeLockMemoryPrivilege in its token. There doesn’t seem to be a difference in operation
between these two flags - but one must be specified.
Locking memory tells the memory manager to keep these pages in the process working set as long as there
are threads running (not waiting) in the process.
9.6: Heaps
The “Virtual” APIs always work in chunks of pages (4 KB), which is not what’s needed for most applications.
Heaps provide fine grained memory management, on top of the Virtual APIs. The Heap Manager is the entity
responsible for managing memory within heaps.
The Windows API has a bunch of functions related to heaps, such as HeapAlloc, HeapFree, and more. These
are thin layers on top of the native heap APIs.
Every process starts with one heap known as the Process Default Heap (created by NtDll as part of process
initialization). This heap can be retrieved with RtlProcessHeap:
Chapter 8: Memory (Part 1) 204
More heaps can be created, which could be useful for several reasons, briefly discussed later.
The Heap Manager is also implemented within the kernel, and some of the functions we’ll look at next are
documented in the WDK.
PVOID RtlAllocateHeap(
_In_ PVOID HeapHandle,
_In_opt_ ULONG Flags,
_In_ SIZE_T Size);
HeapHandle is a handle to a heap (RtlProcessHeap() is a valid value). Flags can be zero or a combination
of the following values:
• HEAP_NO_SERIALIZE (1) - the allocation should not be synchronized with possibly other threads
accessing the same heap.
• HEAP_ZERO_MEMORY (8) - the returned buffer is zero-initialized (without this flag, its contents are
unchanged).
• HEAP_GENERATE_EXCEPTION (4) - if the allocation fails, NULL is normally returned. With this flag
specified, a STATUS_NO_MEMORY exception is raised instead.
• HEAP_SETTABLE_USER_VALUE (0x100) - allows setting a user-defined value associated with the alloca-
tion (see next section for details).
A “missing” flag does not mean the opposite of the flag; it means that the heap defaults are used. For example,
if HEAP_NO_SERIALIZE is not specified, the allocation may still be unsynchronized if the heap was created
with that flag.
Size is the allocation size in bytes. The return value is the pointer to the allocated block, or NULL on failure
(unless HEAP_GENERATE_EXCEPTION is specified in Flags, in which case an exception is raised on failure).
Once the memory is no longer needed, RtlFreeHeap can be called to free the block:
Chapter 8: Memory (Part 1) 205
BOOLEAN RtlFreeHeap(
_In_ PVOID HeapHandle,
_In_opt_ ULONG Flags,
_Frees_ptr_opt_ PVOID BaseAddress);
The only valid flag is HEAP_NO_SERIALIZE. BaseAddress must be an address previously returned from
RtlAllocateHeap.
PVOID RtlReAllocateHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_Frees_ptr_opt_ PVOID BaseAddress,
_In_ SIZE_T Size);
The new block can be larger or smaller than the existing block, specified with BaseAddress. If a larger block
is requested, and cannot be specified within the existing block, the data is copied to the new block. Even
with a smaller allocation, there is no guarantee the same block is returned, which means the return value is
the one pointing to the block (whether moved or not).
The Flags accepted are the same as for RtlAllocateHeap.
• The default process heap is serialized, which reduces performance if there is no contention on the heap.
Creating a separate heap provides control over synchronization. In general, creating a heap provides
customization opportunities.
• A heap may become fragmented over time if different size blocks are requested a lot. One possible
solution is to create a heap that caters to fixed sized allocations if these are common in the application.
• Bugs accessing the heap (such as heap corruptions) are easier to find if these happen accessing a specific
heap.
• It’s sometimes easier to destroy a heap in one swoop rather than freeing all individual allocations.
Windows supports two types of heaps. The first, referred to as the NT Heap is the original heap
management implemented. Windows 8 introduced a new type of heap, Segment Heap, that provides a better
implementation in terms of block management and security. Still, normal processes by default use the NT
Heap because of compatibility concerns. UWP applications and certain system processes use the Segment
heap.
System process image names that use the segment heap include: Svchost.exe, Smss.exe, Csrss.exe,
Lsass.exe, Services.exe, RuntimeBroker.exe, dwm.exe, WinInit.exe, WinLogon.exe, MsMpEng.exe,
NisSrv.exe, SiHost.exe, and others.
Chapter 8: Memory (Part 1) 206
An executable can opt-in to use the segment heap by adding a value named FrontEndHeapDebugOptions to
the HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\exename key,
with a value of 8 to use the segment heap, or a value of 4 to disable the segment heap.
RtlHeapCreate is the heap creation function, documented in the WDK (with a kernel bias):
PVOID RtlCreateHeap(
_In_ ULONG Flags,
_In_opt_ PVOID HeapBase,
_In_opt_ SIZE_T ReserveSize,
_In_opt_ SIZE_T CommitSize,
_In_opt_ PVOID Lock,
_In_opt_ PRTL_HEAP_PARAMETERS Parameters);
BaseAddress is either NULL (HEAP_GROWABLE must be specified as flag), in which case the heap manager
allocates a new region of memory to serve the heap, or if non- NULL, the address is used as the basis for the
new heap. That address must have been allocated previously by the process (e.g., with NtAllocateVir-
tualMemory).
If ReservedSize is non-zero, it indicates the initial amount of memory to reserve for the heap, rounded up
to pages. CommitSize indicates the initial committed size of the heap. The relationship of ReservedSize
and CommittedSize is described in table 8-3.
Next, the Lock parameter is an optional pointer to an initialized CRITICAL_SECTION structure to serve as the
lock to use to synchronize heap access. If the HEAP_NO_SERIALIZE flag is specified, Lock must be NULL, as
the intent is to prevent heap serialization.
Finally, Parameters is an optional pointer to more customization options with this structure:
The CommitRoutine allows customizing allocation of blocks. This is only valid if the heap is not growable,
and BaseAddress is not NULL. In such a case, the routine is invoked by the Heap Manager with the following
arguments:
The return value from RtlCreateHeap is a heap handle. It’s not a handle in the kernel object sense; rather,
it’s an opaque pointer to an undocumented structure. It could be one of two structures: HEAP for an NT heap
or SEGMENT_HEAP for a Segment heap. Although undocumented, these structures are available as part of the
public Microsoft symbols.
Here is the output from a WinDbg session attached to a user-mode process (any will do) that shows these
two structures (truncated for brevity):
Chapter 8: Memory (Part 1) 209
0:000> dt ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : Uint4B
+0x014 SegmentFlags : Uint4B
+0x018 SegmentListEntry : _LIST_ENTRY
+0x028 Heap : Ptr64 _HEAP
+0x030 BaseAddress : Ptr64 Void
+0x038 NumberOfPages : Uint4B
+0x040 FirstEntry : Ptr64 _HEAP_ENTRY
+0x048 LastValidEntry : Ptr64 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : Uint4B
+0x054 NumberOfUnCommittedRanges : Uint4B
+0x058 SegmentAllocatorBackTraceIndex : Uint2B
+0x05a Reserved : Uint2B
+0x060 UCRSegmentList : _LIST_ENTRY
...
+0x238 Counters : _HEAP_COUNTERS
+0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS
0:000> dt ntdll!_SEGMENT_HEAP
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : Uint4B
+0x014 GlobalFlags : Uint4B
+0x018 Interceptor : Uint4B
+0x01c ProcessHeapListIndex : Uint2B
+0x01e AllocatedFromMetadata : Pos 0, 1 Bit
+0x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020 ReservedMustBeZero1 : Uint8B
+0x028 UserContext : Ptr64 Void
+0x030 ReservedMustBeZero2 : Uint8B
+0x038 Spare : Ptr64 Void
+0x040 LargeMetadataLock : Uint8B
+0x048 LargeAllocMetadata : _RTL_RB_TREE
+0x058 LargeReservedPages : Uint8B
+0x060 LargeCommittedPages : Uint8B
+0x068 Tag : Uint8B
+0x070 StackTraceInitVar : _RTL_RUN_ONCE
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0x0d8 GlobalLockCount : Uint2B
+0x0dc GlobalLockOwner : Uint4B
+0x0e0 ContextExtendLock : Uint8B
Chapter 8: Memory (Part 1) 210
This means you can cast a heap handle to one of these structures and examine its member if so desired.
How can you tell the type of heap given some heap you did not create? You can case to SEGMENT_HEAP and
examine the Signature member. If it’s 0xddeeddee - it’s a Segment heap. Here is an example assuming the
above structures have been converted to their C declaration:
When using WinDbg, you can use the !heap command to get details for all heaps in the process or
a specific heap of interest.
If any allocations remain on the heap, they are “released” as the heap is completely destroyed. The return
value is NULL on successful destruction, or HeapHandle otherwise.
The following example creates a new heap and performs an allocation against it, frees the allocation, and
then destroys the heap. Obviously, in a real scenario the created heap would be used for many allocations.
Chapter 8: Memory (Part 1) 211
// use buffer...
RtlFreeHeap(heap, 0, buffer);
RtlDestroyHeap(heap);
SIZE_T RtlSizeHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ PVOID BaseAddress);
The returned size in bytes is for the original allocation size specified, and does not include any padding bytes
or metadata bytes.
Sometimes for security purposes, it’s useful to zero out any freed blocks because normally the data is still
there until overwritten. This is the job of RtlZeroHeap:
9.6.3.2: Protection
NTSTATUS RtlZeroHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags);
The memory of a heap is normally protected with PAGE_READWRITE or PAGE_EXECUTE_READWRITE (if HEAP_-
CREATE_ENABLE_EXECUTE is specified at heap creation time). It’s possible to change the protection to read-
only with RtlProtectHeap:
VOID RtlProtectHeap(
_In_ PVOID HeapHandle,
_In_ BOOLEAN MakeReadOnly);
It can be reverted later by specifying calling the function again with MakeReadOnly set to FALSE.
Chapter 8: Memory (Part 1) 212
9.6.3.3: Locking
It’s possible to lock access to the heap to the current thread, preventing other threads from accessing the heap.
This could be more performant than making multiple calls, each causing a lock/unlock internally. It’s also
necessary if heap walking is attempted, as any changes from other threads will corrupt the state of the walk.
The following allow locking and unlocking a heap:
These work recursively, which means that multiple Lock calls require the same number of Unlock calls to
actually unlock the heap for normal access.
It’s possible to attach a user-defined value with heap entries. The allocation itself must specify the HEAP_-
SETTABLE_USER_VALUE flag for this to work:
BOOLEAN RtlSetUserValueHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ PVOID BaseAddress,
_In_ PVOID UserValue);
It’s also theoretically possible to set 2 bits of flags by calling RtlSetUserFlagsHeap, but this is too fragile as
it does not work with the Segment heap nor if the Low Fragmentation Heap (LFH) layer is active.
To retrieve the value back given a heap allocation, call RtlGetUserInfoHeap, which returns the user-defined
value and/or the flags:
BOOLEAN RtlGetUserInfoHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ PVOID BaseAddress,
_Out_opt_ PVOID *UserValue,
_Out_opt_ PULONG UserFlags);
Here is an example of setting the value 0x1234 as a user-defined value, and retrieving it back:
Chapter 8: Memory (Part 1) 213
// read back
It’s possible to make multiple allocations of the same size in a single call:
ULONG RtlMultipleAllocateHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ SIZE_T Size,
_In_ ULONG Count,
_Out_ PVOID* Array);
Size is the size in bytes for each allocation. Count is the number of allocations. Array is the returned array
of pointers. The return value is Count on success. If smaller, it indicates the number of successful allocations.
As you might expect, it’s possible to free multiple allocations as well:
ULONG RtlMultipleFreeHeap(
_In_ PVOID HeapHandle,
_In_ ULONG Flags,
_In_ ULONG Count,
_In_ PVOID *Array);
ULONG RtlGetProcessHeaps(
_In_ ULONG NumberOfHeaps,
_Out_ PVOID *ProcessHeaps);
RtlGetProcessHeaps accepts the number of heap handles to retrieve, and an array to place the results in.
It returns the actual number of heaps in the process. This means that if the return value is larger than
NumberOfHeaps, not all heap handles have been retrieved.
RtlEnumProcessHeaps invokes a callback (EnumRoutine) for each heap in the process, which makes it easier
perhaps, since no prior allocation is necessary nor “guessing” the number of heaps.
Given a heap handle, it’s possible to “walk” the heap, meaning the various blocks the heap is composed of can
retrieved, mostly useful for debugging purposes. RtlWalkHeap is the function to use, with entries returned
as RTL_HEAP_WALK_ENTRY:
struct {
SIZE_T Settable;
USHORT TagIndex;
USHORT AllocatorBackTraceIndex;
ULONG Reserved[2];
} Block;
struct {
ULONG CommittedSize;
ULONG UnCommittedSize;
PVOID FirstEntry;
PVOID LastEntry;
} Segment;
};
} RTL_HEAP_WALK_ENTRY, *PRTL_HEAP_WALK_ENTRY;
NTSTATUS RtlWalkHeap(
_In_ PVOID HeapHandle,
_Inout_ PRTL_HEAP_WALK_ENTRY Entry);
Heap walking begins with a RTL_HEAP_WALK_ENTRY structure where DataAddress is set to NULL. Subsequent
calls should use the last address returned to get the next item. Each returned item is either a block or a
segment, based on the flag RTL_HEAP_SEGMENT in the Flags member. A Segment is a management entity,
containing multiple blocks, each block representing a chunk of memory (not to be confused with the Segment
Heap).
A simple walk of the default process heap could be done as follows:
RTL_HEAP_WALK_ENTRY entry{};
while (NT_SUCCESS(RtlWalkHeap(RtlProcessHeap(), &entry))) {
printf("Addr: 0x%p Size: 0x%08X Flags: 0x%04X\n",
entry.DataAddress, entry.DataSize, (int)entry.Flags);
}
DataSize is the size of the block or segment. For a block, if busy (used, RTL_HEAP_BUSY), it’s the size of
the allocation handed to a client. The OverheadBytes is the extra bytes used to maintain this allocation.
SegmentIndex is a segment index in this heap, starting with zero.
For a Segment (RTL_HEAP_SEGMENT flag set), the Segment part of the union has the following members:
• CommittedSize is the number of committed bytes in the segment (always multiple of page size).
• UncommittedSize is the number of bytes not currently committed (always multiple of page size).
• FirstEntry is the first entry in the segment.
• LastEntry is the last valid entry in the segment.
Chapter 8: Memory (Part 1) 216
For a Block (no RTL_HEAP_SEGMENT flag), the following members are available in the Block union:
• Settable is the value attached to this block (if RTL_HEAP_SETTABLE_VALUE flag is set).
• TagIndex is the tag index of the block is tagged.
• AllocatorBackTraceIndex is used for debugging purposes (outside the scope of this chapter).
Heap information can be retrieved with RtlQueryHeapInformation, and some heap details can be changed
with RtlSetHeapInformation:
NTSTATUS RtlQueryHeapInformation(
_In_ PVOID HeapHandle,
_In_ HEAP_INFORMATION_CLASS HeapInformationClass,
_Out_opt_ PVOID HeapInformation,
_In_opt_ SIZE_T HeapInformationLength,
_Out_opt_ PSIZE_T ReturnLength);
NTSTATUS RtlSetHeapInformation(
_In_ PVOID HeapHandle,
_In_ HEAP_INFORMATION_CLASS HeapInformationClass,
_In_opt_ PVOID HeapInformation,
_In_opt_ SIZE_T HeapInformationLength);
The patterns used by these functions is very similar to other query/set functions we’ve seen. This is the
information classes supported (some are defined in <WinNt.h>):
Querying with this information class returns ULONG describing some aspects of the heap in question. For a
segment heap, the value is always 2, which means the Low Fragmentation Heap (LFH) layer is active. For a
standard heap, this could be zero (LFH not applied) or 2 (LFH applied).
For set operations, LFH can be enabled for standard heaps only. This also requires the heap to be growable
and created without HEAP_NO_SERIALIZE. There is no need to enable the LFH as it’s enabled automatically
if the Heap Manager determines it’s beneficial. The following enumeration from phnt can be used:
This information class allows querying/setting the option that if a heap corruption is detected, the process
should terminate immediately rather than raise an exception which may be handled in some way, and is
unlikely to be fruitful.
The expected type is ULONG, with non-zero meaning termination on corruption is enabled. It’s currently
enabled by default for all heaps.
This information class returns more details about the heap in question or all heaps in a process using the
HEAP_EXTENDED_INFORMATION structure and friends:
} HEAP_INFORMATION, *PHEAP_INFORMATION;
On input, the heap handle to RtlQueryHeapInformation is not used. Instead, the first five members
of HEAP_EXTENDED_INFORMATION determine what information is requested. One of the powers of this
information class is the ability to get heap details from another process, passed as a handle in the
Process member. This member can be NtCurrentProcess() if the calling process is the target. If it’s
a different process, the handle to that process has to be pretty powerful; it requires PROCESS_QUERY_IN-
FORMATION, PROCESS_CREATE_THREAD, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_DUP_HANDLE,
and PROCESS_VM_WRITE. Sometimes, even this is not enough, and you may need PROCESS_SET_QUOTA and
PROCESS_SET_INFORMATION as well. This is because in the cross-process case, a thread is created in the target
process to get the information, and a Section object is shared with the target process.
I find that for this kind of query, PROCESS_ALL_ACCESS is easier to ask for, or use MAXIMUM_ALLOWED,
and see if that gives you access.
The Level member indicates the details required by the call. The most generic level is HEAP_INFORMATION_-
LEVEL_PROCESS (1), which fills the ProcessHeapInformation member. Here is an example (error handling
omitted):
#define HEAP_INFORMATION_LEVEL_PROCESS 1
RtlQueryHeapInformation(nullptr, HeapExtendedInformation,
&info, sizeof(info), nullptr);
printf("Total heaps: %u Commit: 0x%zX Reserved: 0x%zX\n",
info.ProcessHeapInformation.NumberOfHeaps,
Chapter 8: Memory (Part 1) 219
info.ProcessHeapInformation.CommitSize,
info.ProcessHeapInformation.ReserveSize);
}
The phnt headers have constants like HeapExtendedInformation defined as macros instead of an
enum, because such enum exists from <WinNt.h> but does not include the full list. I’ve simplified
the code not to include a hard cast to HEAP_INFORMATION_CLASS, although I would have preferred
that cast to be part of the macros, or alternatively define a slightly different enum name. I’ll leave
that to the interested reader.
The next Level is HEAP_INFORMATION_LEVEL_HEAP (2). This can be used to get details for each heap in a
process, and also includes the total process heaps details shown above. If you need both, it’s best to make a
single call to get that instead of two separate calls. The returned result consists of a ProcessHeapInforma-
tion being filled up just like the previous case. However, NextHeapInformationOffset stores the offset to
the first heap details (with HEAP_INFORMATION_LEVEL_PROCESS it’s just zero).
Using that offset, the next items are all HEAP_INFORMATION structures, each describing a single heap, where
NextHeapInformationOffset points to the next heap information. The following code snippet uses a single
call to get the general details for the process and all heaps:
Here is some example output from the previous code (full source part of the Heaps sample):
The Heap member of HEAP_EXTENDED_INFORMATION can be used to get details for a specific heap
rather than all heaps in the process (if zero). The value is the heap handle (in the context of the
target process).
The next level of details has to do with memory regions within a heap, which provide another structure like
so:
Chapter 8: Memory (Part 1) 221
#define HEAP_INFORMATION_LEVEL_REGION 3
The next level of detail is a range, described with another level value and structure:
#define HEAP_INFORMATION_LEVEL_RANGE 4
#define HEAP_RANGE_TYPE_COMMITTED 1
#define HEAP_RANGE_TYPE_RESERVED 2
ULONG_PTR FirstBlockInformationOffset;
ULONG_PTR NextRangeInformationOffset;
} HEAP_RANGE_INFORMATION, *PHEAP_RANGE_INFORMATION;
#define HEAP_INFORMATION_LEVEL_BLOCK 5
#define HEAP_BLOCK_BUSY 1
#define HEAP_BLOCK_EXTRA_INFORMATION 2
#define HEAP_BLOCK_LARGE_BLOCK 4
#define HEAP_BLOCK_LFH_BLOCK 8
The pattern should be pretty clear by now. A range points to its first block via FirstBlockInformationOff-
set. Here is an example for enumerating blocks given a range:
block = (HEAP_BLOCK_INFORMATION*)(buffer +
block->NextBlockInformationOffset);
}
}
std::string text;
for (auto& d : data) {
if ((d.flag & flags) == d.flag)
(text += d.text) += ", ";
}
return text.empty() ? "" : text.substr(0, text.length() - 2);
}
Chapter 8: Memory (Part 1) 224
The block level is the last, but if the HEAP_BLOCK_EXTRA_INFORMATION is set, it means there is extra
information for the block in question, immediately following HEAP_BLOCK_INFORMATION. Its header looks
like this:
The Next member indicates whether there is another extra information block following this one. Type is the
extra information type which currently can be only 1. The extra information structure itself is as follows:
9.7.5.1: Callback
} HEAP_INFORMATION_ITEM, * PHEAP_INFORMATION_ITEM;
NTSTATUS HeapInformationCallback(
_In_ PHEAP_INFORMATION_ITEM item,
_In_ PVOID context);
Returning STATUS_SUCCESS from the callback continues enumeration, while returning a failure status aborts
further calls and that status is returned from RtlQueryHeapInformation.
The above members should be self explanatory. The sizes must be in bytes, but multiples of a page size.
The remaining information classes either don’t work or require more research.
Chapter 8: Memory (Part 1) 226
9.8: Summary
This chapter covered a lot of ground as it related to memory. From the Virtual APIs to heaps. This is not all
that is available, however. In chapter 13, we’ll cover more memory related APIs, including Section objects,
memory zones, and lookaside lists.
Chapter 9: I/O
In this chapter:
The well-known drive names users are accustomed to, such as “C:”, “D:”, etc. are nothing more than symbolic
links - an object type we looked at in chapter 7. You can use the Windows API QueryDosDevice to get the
target device name given a symbolic link. Here is an example:
WCHAR buffer[256];
QueryDosDevice(L"c:", buffer, _countof(buffer));
On my system I get “\Device\HarddiskVolume3” as the result. This is the device object that represents the C:
drive. The symbolic links that are directly accessible by the CreateFile Windows API can be viewed with
tools like WinObj from Sysinternals or my own Object Explorer. Figure 9-2 shows the “Global??” Object
Manager directory in Object Explorer, which contains the symbolic links that can be used with CreateFile.
Except for drive letters and certain old DOS names (like CON), symbolic links passed to CreateFile
must be prefixed with “\\.\” so they are interpreted as symbolic links and not as file names.
Contrary to CreateFile, the native API allows direct access to any object in the Object Manager namespace.
NTSTATUS NtCreateFile(
_Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_opt_ PLARGE_INTEGER AllocationSize,
_In_ ULONG FileAttributes,
_In_ ULONG ShareAccess,
_In_ ULONG CreateDisposition,
_In_ ULONG CreateOptions,
_In_reads_bytes_opt_(EaLength) PVOID EaBuffer,
_In_ ULONG EaLength);
NTSTATUS NtOpenFile(
_Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
Chapter 9: I/O 230
Fortunately, these APIs are officially documented in the WDK (under ZwCreateFile and ZwOpenFile) since
device driver developers need these APIs for I/O work. NtOpenFile is the simpler API, providing a subset of
the capabilities of NtCreateFile. Internally, both call a common function in the kernel (IopCreateFile),
where NtOpenFile passes some defaults that are configurable with NtCreateFile.
The CreateFile Windows API calls NtCreateFile internally; this can be easily observed with a
user-mode debugger.
Most of NtOpenFile parameters should be familiar by now. ObjectAttributes is where the device name
is specified. IoStatusBlock is an output parameter filled with the following details:
Information contains one of the following values indicating the type of operation that was performed. This
is relevant for true files/directories in a file system, but not for devices (devices must always exist before an
attempt to open them can succeed):
DesiredAccess is the access mask requested. This can include specific access masks for file objects (e.g.
FILE_READ_DATA), file generic ones (FILE_GENERIC_READ), and even object-generic ones (e.g. GENERIC_-
READ); this is no different in principle than any other object type.
ShareAccess is the share mode to open/create the file; it only has meaning if a new file is actually created.
Values include FILE_SHARE_READ, FILE_SHARE_WRITE, and FILE_SHARE_DELETE. Finally, OpenOptions
allows specifying more flags to customize the operation. Many such flags exist, documented in the WDK
under ZwCreateFile.
The following example opens the Kernel32.dll file from the System32 directory:
Chapter 9: I/O 231
UNICODE_STRING path;
RtlInitUnicodeString(&path, L"\\SystemRoot\\System32\\kernel32.dll");
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&path, 0);
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
status = NtOpenFile(&hFile, FILE_READ_DATA, &attr, &ioStatus,
FILE_SHARE_READ, 0);
Why does that even work? SystemRoot is a symbolic link that points to the root directory of where
Windows is installed, typically C:\Windows from a user’s perspective, but not quite what that looks like
underneath. Looking at Object Explorer or the ObjDir tool we developed in chapter 7, we can see that
SystemRoot is a symbolic link pointing to Device\BootDevice\Window. What is Device\BootDevice? Going to
the Device directory and locating BootDevice shows that BootDevice is yet another symbolic link, pointing
to Device\HarddiskVolume3. Looking at \Global??\C: shows it points to the same device, representing the C:
drive on this machine.
Run WinObj or ObjExp with admin privileges to see the above symbolic link target.
To use common user-mode paths, like “c:\Something”, prepend “\??\” to the path. For example, “c:\Something”
should be written as “\??\c:\Something”; “\??\” is the same as “\Global??\”.
Of course we could get to the C:\Windows directory directly with Device\HarddiskVolume3\Windows, but that
device may not be the same on every Windows system. That’s why using the symbolic links is beneficial -
they are guaranteed to be the same on every Windows system.
The example for opening the file above seems simple enough. Unfortunately, it’s not that simple. Suppose
we want to read the first 1024 bytes from the file. We can use the NtReadFile API:
NTSTATUS NtReadFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
Chapter 9: I/O 232
The FileHandle parameter is the handle returned from NtOpenFile or NtCreateFile. Event is an optional
event handle to signal when the operation completes. ApcRoutine is an optional APC callback that is used
to construct an APC on the calling thread when the I/O operation completes. The thread must enter an
altertable state if the APC is to be eventually executed. ApcContext is an optional context value to pass to
the APC routine. Event, ApcRoutine, and ApcContext only have meaning if the operation is asynchronous.
We’ll get to that in a moment.
Buffer is the buffer to read the data into, and Length is the number of bytes to read. ByteOffset is an
optional offset to read from. Typically, NULL is provided which indicates to read from the current file position.
Key is an optional key value to use for the operation, which has no use in user-mode.
Here is simple code to read the first 1KB from the previously opened file:
BYTE buffer[1024];
status = NtReadFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
buffer, sizeof(buffer), nullptr, nullptr);
The call mysteriously fails with STATUS_INVALID_PARAMETER. What’s going on? The problem is that the file
object was opened for asynchronous access. We didn’t specify anything special in the call to NtCreateFile,
but the default is asynchronous access. This is in contrast to the CreateFile function, which creates a file
object for synchronous access by default.
To create a file object for synchronous access, we need to specify the FILE_SYNCHRONOUS_IO_NONALERT or
FILE_SYNCHRONOUS_IO_ALERT flag in the OpenOptions parameter of NtOpenFile. Here is the correct call
to NtOpenFile:
Notice the access mask includes the SYNCHRONIZE flag. This is necessary because this flag is required to
wait on the file object, which is what happens internally in for synchronous operations. Now the call to
NtReadFile can succeed and data is available when the call returns.
What do we need to do to make an asynchronous call work? In this case, the call to NtReadFile must specify
a file position in the ByteOffset parameter. This is because a file object does not keep track of a file position
if opened for asynchronous access. This makes sense, since multiple operations could start concurrently, so
there is no meaning to a single file position. Optionally, an event handle should be provided (or an APC
callback) so that a thread can tell when the operation is complete and data is available. The status returned
for a successful initiation of an async operation is STATUS_PENDING (0x103).
Here is a simple example that waits immediately after the asynchronous call:
Chapter 9: I/O 233
HANDLE hEvent;
NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE);
LARGE_INTEGER pos{}; // pos is set to zero (beginning of file)
status = NtReadFile(hFile, hEvent, nullptr, nullptr, &ioStatus,
buffer, sizeof(buffer), &pos, nullptr);
if (status != STATUS_PENDING) {
// some real error occurred
}
else {
NtWaitForSingleObject(hEvent, FALSE, nullptr);
// data is available in buffer
}
NtClose(hEvent);
In a real scenario, the waiting would not occur immediately after initiation, as that would defeat the purpose
of asynchronous I/O. Instead, the thread would do other work and periodically check the status of the
operation, or another thread would wait on the event, or the thread pool would be used to wait on the
event and execute a callback when the operation completes.
Keep in mind that for asynchronous operations, the IO_STATUS_BLOCK and buffer provided should
be kept alive until the operation completes. If these are stack-allocated, then the thread must not exit
the current function until the operation completes.
NTSTATUS NtWriteFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_reads_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_opt_ PLARGE_INTEGER ByteOffset,
_In_opt_ PULONG Key);
The function is virtually identical to NtReadFile, except for SAL annotations and the fact that the Buffer
parameter can be declared as const.
For more information about these APIs, including NtCreateFile, consult the WDK documentation.
Chapter 9: I/O 234
NTSTATUS NtQueryInformationFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass);
NTSTATUS NtSetInformationFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_reads_bytes_(Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass);
NTSTATUS NtQueryInformationByName(
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass);
As you might expect, there is a long list of file information classes. Some apply to files only, some to directories
only, and others to both. In the following subsections we’ll examine some of them.
Some of the information classes and associated structures are documented in the WDK and/or SDK. Look for
ZwQueryInformationFile in the WDK and GetFileInformationByHandleEx in the SDK.
This information class returns the basic details for a file or directory:
Chapter 9: I/O 235
The members are mostly self-explanatory, where times are expressed in 100nsec units since January 1, 1601.
FileAttributes is a set of file attributes, such as FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_HIDDEN,
FILE_ATTRIBUTE_READONLY, etc. The difference between ChangeTime and LastWriteTime is that Change-
Time can mean change in file name or file attributes, whereas LastWriteTime is only the last time the file
contents were modified.
The same information class can be used to set these attributes. The file handle must include the FILE_-
WRITE_ATTRIBUTES access mask (FILE_GENERIC_WRITE and GENERIC_WRITE include it).
The following example sets a file’s creattion time to the given value:
fbi.CreationTime = createTime;
status = NtSetInformationFile(hFile, &ioStatus, &fbi,
sizeof(fbi), FileBasicInformation);
NtClose(hFile);
return status;
}
The following example sets the creation time of a file to 24 hours in the future:
Chapter 9: I/O 236
LARGE_INTEGER time;
NtQuerySystemTime(&time);
time.QuadPart += 10000000ULL * 60 * 60 * 24;
SetCreateTime(L"\\??\\C:\\Temp\\myfile.txt", time);
AllocationSize is the file allocation size, which is typically a multiple of the underlying sector size of the
physical device. EndOfFile is the file size (zero for directories). NumberOfLinks is the number of hard links
to the file. DeletePending indicates whether the file is pending deletion. For example, if it was opened with
the FILE_DELETE_ON_CLOSE flag. Directory indicates whether the file is a directory.
FileNameLength is the length of the filename in bytes. FileName is the filename itself, which is not
necessarily null-terminated. If the supplied buffer is not large enough to receive the full name, the function
returns STATUS_BUFFER_OVERFLOW and FileNameLength is set to the required size (at least this is what file
system drivers and filters suppose to do).
The returned file name always starts with a backslash. It does not include a drive letter or
volume name. For example, the file name returned for “C:\Windows\System32\kernel32.dll” is
“\Windows\System32\kernel32.dll”. The same file name is returned if the file was opened by the
name “\SystemRoot\System32\kernel32.dll”.
How do you get the volume name? See next information class.
The return value is something like “\Device\HarddiskVolume3”. Combining it with the FileNameInforma-
tion information class produces the full path name in device form. If a drive letter is desired, you’ll need
to lookup drive letters as symbolic link names and see which points to the device name. I’ll leave that as an
exercise for the interested reader. Note that this may fail - not all volumes are mapped to drive letters - there
is no such requirement. A quick way to see what is mapped is to the fltmc.exe command line tool, which
must be run from an elevated command prompt. For example (formatted for clarity):
ReplaceIfExists indicates whether to replace an existing file if the new file name exists. RootDirectory
is a handle to the directory where the new file should reside. If RootDirectory is NULL, then the file is
renamed in the same directory. FileNameLength is the length of the new name in bytes and FileName is the
new name, which is not necessarily null-terminated.
Here is an example of renaming a file in the same directory:
Notes:
• The file must be opened with the DELETE access mask. CreateOptions should be zero (not something
like FILE_SYNCHRONOUS_IO_NONALERT).
• The buffer size provided to NtSetInformationFile in the above code is larger than needed, but that’s
not an issue. Still, the buffer should be allocated dynamically based on the length of the new name.
RenameFile(L"\\??\\C:\\Temp\\myfile.txt", L"somefile.txt");
We can extend the example to support moving a file to a different directory by specifying a root directory
handle. A directory handle can be opened for this purpose like so:
The target directory must be in the same volume as the file being moved.
DeleteFile is set to TRUE to delete the file. The file must be opened with the DELETE access mask. The
following example deletes a file:
The file is actually deleted when the handle is closed. A simple way to delete a file is to open it with the flag
FILE_DELETE_ON_CLOSE (as part of CreateOptions), and closing the handle.
This information class can be used to query or set the file pointer as a LARGE_INTEGER:
This information class returns the mode the file was opened with. The structure returned is FILE_MODE_-
INFORMATION, which is a glorified set of flags:
This information class is also partially supported with NtSetInformationFile, especially contradicting the
way the file was opened in relation to synchronous/asynchronous open.
This information class returns many of the structures we’ve seen before as one:
Chapter 9: I/O 241
This is more efficient than making multiple calls, assuming most of these details are of interest. Note that
this is a variable length structure, as the last member is a variable-length string.
NTSTATUS NtQueryDirectoryFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass,
_In_ BOOLEAN ReturnSingleEntry,
_In_opt_ PUNICODE_STRING FileName,
_In_ BOOLEAN RestartScan);
The last three parameters are new. ReturnSingleEntry indicates whether to return a single entry or all
entries (remember this is a directory). FileName can be used to filter the returned items. If NULL is specified,
no filtering is performed. If non-NULL, it indicates the items of interest, supporting wildcards. For example,
specifying “Hello.*” returns all files/directories whos name is “hello” with any extension. RestartScan
indicates whether to restart the scan from the beginning or continue from the last entry. This is useful
when the buffer provided is not large enough to receive all entries. The first time NtQueryDirectoryFile
is called with a particular handle, RestartScan is ignored. It is respected on subsequent calls.
The next subsections examine the directory-contents information classes.
Chapter 9: I/O 242
These information classes are similar, with the difference being the details returned. The provided handle
must be of a directory (not a file). The FileDirectoryInformation class returns the following structure:
It’s a variable-size structure, as the filename appears as the last member. Moving to the next structure is done
using NextEntryOffset, which indicates the number of bytes to move forward to the next entry. When
NextEntryOffset is zero, there are no more entries. The structure is described in the WDK documentation.
Here is a brief description of the members:
• FileIndex is a byte offset of the file from the parent directory. It should normally be ignored, as it’s
not used by NTFS, for example.
• CreationTime, LastAccessTime, LastWriteTime, and ChangeTime should be familiar by now.
• EndOfFile is the size of the file in bytes.
• AllocationSize is the file allocation size, which is typically a multiple of the underlying sector size
of the physical device.
• FileAttributes is a set of file attributes, such as FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_-
HIDDEN, FILE_ATTRIBUTE_READONLY, etc.
• FileNameLength is the length of the filename in bytes.
• FileName is the filename itself, which is not necessarily null-terminated.
//
if(info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
printf(" <DIR>");
else
printf(" [%llu KB]", info->EndOfFile.QuadPart >> 10);
printf("\n");
if (info->NextEntryOffset == 0)
break;
info = (FILE_DIRECTORY_INFORMATION*)((BYTE*)info +
info->NextEntryOffset);
}
}
NtClose(hDir);
return status;
}
EaSize is the size of the extended attributes in bytes. See the subsection “Extended Attributes” later in this
chapter for more information.
Finally, FileBothDirectoryInformation returns an even extended structure that contains the “short name”
(in the old DOS format 8.3):
Chapter 9: I/O 245
HANDLE hFile;
UNICODE_STRING name;
IO_STATUS_BLOCK ioStatus;
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
RtlInitUnicodeString(&name, L"\\??\\C:\\Temp\\myfile.txt:mystream");
NtCreateFile(&hFile, GENERIC_WRITE | SYNCHRONIZE,
&attr, &ioStatus, nullptr, 0,
FILE_SHARE_WRITE, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, nullptr, 0);
char data[] = "Hello, stream!";
NtWriteFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
data, (ULONG)strlen(data), nullptr, nullptr);
NtClose(hFile);
Note that NtCreateFile must be used to create alternate streams. If a stream exists, NtOpenFile works
as well. The size of the file as reflected in standard tools like Windows Explorer show the size of the main
stream only. This means you can create a file with zero size, but have alternate streams with data of any size,
practically invisible by standard tools. The Streams tool from Sysinternals will list the streams in a file:
Chapter 9: I/O 246
My own NTFS Streams tool can show the contents of streams as well as their name and size (figure 9-3).
As you can see, reading and writing to alternate streams is no different than “normal” file. Enumerating
streams in a file is a different story. The NtQueryInformationFile function can be used with the
FileStreamInformation (22) information class to enumerate the streams in a file. The expected structure
is FILE_STREAM_INFORMATION:
The following example shows how to enumerate the streams in a file (see full example in the Streams sample):
UNICODE_STRING name;
RtlInitUnicodeString(&name, path.c_str());
HANDLE hFile;
Chapter 9: I/O 247
IO_STATUS_BLOCK ioStatus;
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
auto status = NtOpenFile(&hFile, FILE_READ_ACCESS | SYNCHRONIZE,
&attr, &ioStatus, FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_SYNCHRONOUS_IO_NONALERT);
if (!NT_SUCCESS(status))
return status;
NtClose(hFile);
return status;
}
Here is what the output looks like for an “empty” myfile.txt from the earlier example:
C:\winnativeapibooksamples\Chapter09\>Streams.exe c:\temp\myfile.txt
Name: ::$DATA Size: 0 bytes
Name: :mystream:$DATA Size: 14 bytes
There are a couple of information information classes that deal with EA. The first is FileEaInformation (7),
which returns the size of the EA in bytes as a ULONG. The second, and more interesting, is FileFullEaIn-
formation (15), which returns the EA in a (possibly) chain of FILE_FULL_EA_INFORMATION structures:
NextEntryOffset indicates the number of bytes to move to the next attribute, where zero means there are no
more attributes. Flags is either zero or FILE_NEED_EA (0x80) to indicate that the EA is required for proper
operation of the file. EaNameLength is the length of the EA name in bytes, and the name itself (EaName)
consists of ASCII characters stored internally as uppercase. EaValueLength is the length of the EA value in
bytes. The EA value follows the structure. This is depicted in figure 9-4.
There are two ways to query the EA of a file. The first is calling NtQueryInformationFile with
FileFullEaInformation and interpreting the returned buffer as a chain of FILE_FULL_EA_INFORMATION
structures. This works, but is limited in flexibility. The second is calling NtQueryEaFile, which returns the
EA, which provides more flexibility as the expense of complexity:
Chapter 9: I/O 249
NTSTATUS NtQueryEaFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_ BOOLEAN ReturnSingleEntry,
_In_reads_bytes_opt_(EaListLength) PVOID EaList,
_In_ ULONG EaListLength,
_In_opt_ PULONG EaIndex,
_In_ BOOLEAN RestartScan);
The first four parameters should be familiar. ReturnSingleEntry indicates whether to return a single EA or
all EAs. EaList is a list of EA names to return. If NULL is specified, all EAs are returned. EaListLength is
the length of the list in bytes. EaIndex indicates the index of the EA to return. If NULL is specified, the first
EA or all EAs are returned (based on ReturnSingleEntry).
RestartScan indicates whether to restart the scan from the beginning or continue from the last entry. This
is useful when the buffer provided is not large enough to receive all entries. The first time NtQueryEaFile
is called with a particular handle, RestartScan is ignored. It is respected on subsequent calls.
The following example shows how to retrieves all EAs of a file:
}
}
NTSTATUS NtSetEaFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_reads_bytes_(Length) PVOID Buffer,
_In_ ULONG Length);
The expected buffer is a chain of FILE_FULL_EA_INFORMATION structures, as described earlier. Each one
must be formatted as shown in figure 9-4. The following example shows how to set a single EA (assuming
the value is a Unicode string):
//
// copy name
//
strcpy_s(info.EaName, info.EaNameLenth + 1, name);
info.NextEntryOffset = 0;
info.EaValueLength = USHORT(1 + info.EaNameLength) * sizeof(WCHAR);
wcscpy_s((PWSTR)(info.EaName + info.EaNameLength + 1),
info.EaValueLength / sizeof(WCHAR), value);
return NtSetEaFile(hFile, &ioStatus, &info, sizeof(buffer));
}
It’s ok to indicate the buffer is bigger than it actually is, because the file system driver will follow the chain
based on NextEntryOffset.
Chapter 9: I/O 251
HANDLE hFile;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\Device\\Beep");
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
IO_STATUS_BLOCK ioStatus;
status = NtOpenFile(&hFile, FILE_WRITE_DATA | SYNCHRONIZE, &attr,
&ioStatus, FILE_SHARE_WRITE | FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
The path “\Device\Beep” can be viewed with WinObj or Object Explorer (figure 9-5).
Chapter 9: I/O 252
We’ve seen this kind of code multiple times. Once we have a valid handle, how do we “tell” the Beep device
to play a sound? With files in a file system we called NtReadFile or NtWriteFile. With devices, it depends
on the driver for that device. It may expect some call to NtReadFile or NtWriteFile with some formatted
data that it understands, but more often than not a third API is used, NtDeviceIoControlFile:
NTSTATUS NtDeviceIoControlFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG IoControlCode,
_In_reads_bytes_opt_(InputBufferLength) PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_writes_bytes_opt_(OutputBufferLength) PVOID OutputBuffer,
_In_ ULONG OutputBufferLength);
NtDeviceIoControlFile provides more flexibility when talking to devices. The IoControlCode is a 32-bit
value that is interpreted by the driver as an operation to perform. Of course, if the value is unrecognized by
Chapter 9: I/O 253
the driver, the call will fail. The InputBuffer optional buffer can be used to send input data to the driver,
while OutputBuffer may be used to receive results. This all depends on the driver and the way it expects to
be communicated with.
How do we know the correct way to communicate with the Beep device? Except for reverse engineering
its operation, we need documentation. Fortunately, for the Beep device, Microsoft provides the Ntddbeep.h
header with the information needed.
Within that file, we find the following definitions (slightly edited for clarity):
#define IOCTL_BEEP_SET \
CTL_CODE(FILE_DEVICE_BEEP, 0, METHOD_BUFFERED, FILE_ANY_ACCESS)
The header includes the device name in ASCII and Unicode formats. BEEP_SET_PARAMETERS is the expected
input buffer to the driver. IOCTL_BEEP_SET is the expected control code; no need to guess anything. The
following example plays a single sound with a frequency of 600Hz for 2 seconds:
BEEP_SET_PARAMETERS params;
params.Duration = 2000; // msec
params.Frequency = 600; // Hz
Although the device was opened for synchronous access, as it happens, the Beep driver works asynchronously;
the call to NtDeviceIoControlFile returns immediately, while the sound is played in the background. We
can “prove” that by waiting for the sound to complete while displaying something from time to time:
Chapter 9: I/O 254
typedef struct {
LARGE_INTEGER VolumeSerialNumber;
LARGE_INTEGER NumberSectors;
LARGE_INTEGER TotalClusters;
LARGE_INTEGER FreeClusters;
LARGE_INTEGER TotalReserved;
DWORD BytesPerSector;
DWORD BytesPerCluster;
DWORD BytesPerFileRecordSegment;
DWORD ClustersPerFileRecordSegment;
LARGE_INTEGER MftValidDataLength;
LARGE_INTEGER MftStartLcn;
LARGE_INTEGER Mft2StartLcn;
LARGE_INTEGER MftZoneStart;
LARGE_INTEGER MftZoneEnd;
} NTFS_VOLUME_DATA_BUFFER, *PNTFS_VOLUME_DATA_BUFFER;
Here is an example:
Chapter 9: I/O 255
NTFS_VOLUME_DATA_BUFFER data;
status = NtFsControlFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
FSCTL_GET_NTFS_VOLUME_DATA, nullptr, 0, &data, sizeof(data));
The above file handle can be to any file or directory for the requested volume. Curiously enough, calling
NtDeviceIoControlFile with the same control code fails with STATUS_INVALID_PARAMETER. However,
since NtFsControlFile is not part of the documented Windows API, calling DeviceIoControl (the
documented API) with the same control code works just fine.
All standard control codes and structures that may be supported by file systems are defined in
<WinIoCtl.h>, many of which are officially documented.
NTSTATUS NtCreateIoCompletion(
_Out_ PHANDLE IoCompletionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ ULONG Count);
DesiredAccess is the desired access for the new object, typically IO_COMPLETION_ALL_ACCESS (defined in
<WinNt.h>). ObjectAttributes can be provided and can contain a name. Finally, Count indicates how
many threads at most can handle I/O completions. Zero is interpreted as the number of logical processors on
the system.
Curiously enough, the Windows API CreateIoCompletionPort does not allow specifying a name.
This makes some sense, as sharing I/O completion objects between processes is not very practical, so
the value of having a name is diminished; a name is supported nonetheless.
Creating named objects outside the session namespace (such as the global namespace) requires the
SeCreateGlobalPrivilege privilege, which is granted by default to the Administrators group.
Since a name is allowed, it’s possible to open a handle to an existing I/O completion object with NtOpenIo-
Completion:
NTSTATUS NtOpenIoCompletion(
_Out_ PHANDLE IoCompletionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
The call can be made multiple times with different file handles but the same port. Port is the port handle
returned from NtCreateIoCompletion or NtOpenIoCompletion. Key is an arbitrary value that can be used
to identify the specific file.
When an I/O operation is performed, any number of threads can call NtRemoveIoCompletion to wait
for an operation to complete. Once completed, the thread is unblocked and can do whatever is needed.
The maximum number of threads released from NtRemoveIoCompletion is based on what has been
specified in the I/O completion object creation. Here is NtRemoveIoCompletion and its extended version,
NtRemoveIoCompletionEx:
Chapter 9: I/O 257
NTSTATUS NtRemoveIoCompletion(
_In_ HANDLE IoCompletionHandle,
_Out_ PVOID *KeyContext,
_Out_ PVOID *ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_opt_ PLARGE_INTEGER Timeout);
NTSTATUS NtRemoveIoCompletionEx(
_In_ HANDLE IoCompletionHandle,
_Out_writes_to_(Count, *Removed) PFILE_IO_COMPLETION_INFORMATION Information,
_In_ ULONG Count,
_Out_ PULONG Removed,
_In_opt_ PLARGE_INTEGER Timeout,
_In_ BOOLEAN Alertable);
The simpler function waits until an asynchronous I/O operation completes (or the timeout elapses). If
completed, it can run any code needed at that time. The information returned for the completed operation
is the *KeyContext (the key supplied to NtSetInformationFile), *ApcContext (value passed to the
asynchronous I/O operation), and *IoStatusBlock, indicating the result of the operation.
The extended function can wait for multiple operations to complete, up to Count operations. The *Removed
parameter indicates how many operations have completed. The information for each completed operation is
returned in the Information array, of the following type:
This basically bundles the same information returned individually by NtRemoveIoCompletion. Finally,
Alertable indicates whether the wait should be alertable or not.
The return values for these functions can be STATUS_SUCCESS (if an operation completed), STATUS_TIMEOUT
(if the timeout elapsed), or STATUS_USER_APC (if the wait was alertable and APC(s) executed).
As mentioned earlier, I/O completion objects can be used to post operations that can be waited on by multiple
threads regardless of (or in addition to) any I/O operation. This is done with NtSetIoCompletion or its
extended version, NtSetIoCompletionEx:
Chapter 9: I/O 258
NTSTATUS NtSetIoCompletion(
_In_ HANDLE IoCompletionHandle,
_In_opt_ PVOID KeyContext,
_In_opt_ PVOID ApcContext,
_In_ NTSTATUS IoStatus,
_In_ ULONG_PTR IoStatusInformation);
NTSTATUS NtSetIoCompletionEx(
_In_ HANDLE IoCompletionHandle,
_In_ HANDLE IoCompletionPacketHandle,
_In_opt_ PVOID KeyContext,
_In_opt_ PVOID ApcContext,
_In_ NTSTATUS IoStatus,
_In_ ULONG_PTR IoStatusInformation);
NtSetIoCompletion queues an item to the I/O completion object that can be picked up by one of threads
calling NtRemoveIoCompletion(Ex). The meaning of the parameters other than the completion object handle
are up to the caller. The extended function uses a I/O Reserve object handle, which is out of scope for this
chapter.
Finally, a thread pool can be used to handle I/O completion object’s “completions”. This requires creating a
thread pool I/O with TpAllocIoCompletion. This is left as an exercise to the interested reader.
10.9.1: Drivers
Where DriverServiceName is the name of the driver’s Registry key stored under \HKLM\System\CurrentControlSet\Services.
Figure 9-7 shows this key and some of its subkey in RegEdit.exe.
Chapter 9: I/O 259
Drivers are typically installed using INF files, or command line tools like Sc.Exe. Loading a driver is a privilege
operation, granted by default to the Administrators group.
After a driver is loaded, its Registry key can be deleted without any adverse effect.
NTSTATUS NtLockFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ PLARGE_INTEGER ByteOffset,
_In_ PLARGE_INTEGER Length,
Chapter 9: I/O 260
The first five parameters should be familiar by now. *ByteOffset is the offset to start locking from. Note
that NULL is not a valid value, and will cause an access violation. *Length is the number of bytes to lock
(NULL is illegal). Key is an arbitrary value used to identify the lock. FailImmediately indicates whether
to fail if the lock cannot be acquired immediately - even if the lock operation is specified as asynchronous.
ExclusiveLock indicates whether to acquire an exclusive lock (TRUE) or a shared lock (FALSE).
Once the lock is no longer needed, NtUnlockFile can be used to release it:
NTSTATUS NtUnlockFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ PLARGE_INTEGER ByteOffset,
_In_ PLARGE_INTEGER Length,
_In_ ULONG Key);
NTSTATUS NtNotifyChangeDirectoryFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer, // FILE_NOTIFY_INFORMATION
_In_ ULONG Length,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree);
NTSTATUS NtNotifyChangeDirectoryFileEx(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
Chapter 9: I/O 261
The associated structures are defined in <WinNt.h>. FileHandle must be a directory handle (rather than
a file handle). The operation completes when a change is detected. Buffer is where the information is
returned when the operation completes, and Length is the size of the buffer in bytes. CompletionFilter
is a set of flags that indicate the type of changes to detect (defined in <WinNt.h> as well). For example:
FILE_NOTIFY_CHANGE_FILE_NAME indicates to detect file name changes, FILE_NOTIFY_CHANGE_DIR_NAME
indicates to detect directory name changes, and FILE_NOTIFY_CHANGE_LAST_WRITE indicates to detect
changes to the last write time. WatchTree indicates whether to watch the directory tree recursively.
If called synchronously, the call blocks until a change is detected. A simple way to use this function is to call
it synchronously in a loop, handling each change as they come in.
10.10: Summary
I/O APIs are about working with files and devices. This chapter looked at the most common native I/O
APIs, some of which are officially documented in the WDK. The I/O system supports synchronous and
asynchronous access, where the requested mode is provided in the NtOpenFile or NtCreateFile call.
In the next chapter, we’ll examine one of Windows undocumented features - Asynchronous Local Procedure
Calls (ALPC).
Chapter 10: ALPC
Windows has many inter-process communication mechanisms that allow processes to communicate with
each other by passing data. Examples include window messages, shared memory, pipes, mailslots, and
COM. Advanced (or Asynchronous) Local Procedure Calls (ALPC) is another such mechanism that is used
by Windows components to communicate across process boundaries. Contrary to the other mentioned
mechanisms, ALPC is completely undocumented. This chapter provides an overview of the ALPC API and
how it can be used for inter-process communication.
ALPC is a big topic, and my research on it is not complete, so this chapter does not cover everything
about ALPC. Future editions of the book will cover more of ALPC.
In this chapter:
• ALPC Concepts
• Simple Client/Server
• Creating Server Ports
• Connecting to Ports
• Message Attributes
• Sending and Receiving Messages
• Server connection port - a named port that listens for incoming connections.
• Server communication port - an unnamed port used to communicate with a client.
• Client port - an unnamed port a client uses to communicate with a server.
Chapter 10: ALPC 263
Server connection ports can be viewed by tools such as WinObj, Object Explorer, Process Explorer, and
others since they are named. Figure 10-1 shows many ALPC port objects in the RPC Control object manager
directory viewed in WinObj.
With Object Explorer, you can see handles to each port object (Figure 10-2). Double-click an object to see its
properties.
Chapter 10: ALPC 264
With Process Explorer and Object Explorer you can examine ALPC port handles in a given process (figure
10-3).
Chapter 10: ALPC 265
The kernel debugger can provide more details about ALPC ports, messages, and connections. For example,
the !alpc /lpp with a process address command shows all ALPC ports used by the given process:
ffff968f2ff90aa0('umpo') 0, 32 connections
ffff968f2ca9ed70 0 ->ffff968f306a2d70 0 ffff968f2ff130c0('services.exe')
ffff968f3094bb30 0 ->ffff968f30998df0 0 ffff968f2ff62300('svchost.exe')
ffff968f2c15bc60 0 ->ffff968f2c15ba00 0 ffff968f3143d080('svchost.exe')
ffff968f2c73e6e0 0 ->ffff968f2c73e070 0 ffff968f3150e080('svchost.exe')
ffff968f3534f070 0 ->ffff968f3534f530 0 ffff968f34f6e080('esif_uf.exe')
ffff968f2479add0 0 ->ffff968f243b6dd0 0 ffff968f3066c080('WUDFHost.exe')
ffff968f35fbe070 0 ->ffff968f354edce0 0 ffff968f423d0080('NVDisplay.Cont')
ffff968f45bee580 0 ->ffff968f351e4c30 0 ffff968f31260300('sihost.exe')
ffff968f24af4a80 0 ->ffff968f24af3ce0 0 ffff968f47af41c0('explorer.exe')
...
ffff968f21a18090('actkernel') 0, 19 connections
Chapter 10: ALPC 266
Using port addresses (the first and second values on the left in the above output) can be used to get more
details about the port:
Security : Dynamic
Wow64CompletionList : No
968fe1048580 WAIT
THREAD ffff968f4ee19080 Cid a0a4.c494 Teb: 000000a170060000 Win32Thread: 00000\
00000000000 WAIT
...
Check out the documentation for the !alpc command for more details.
More information on ALPC can be found in the “Windows Internals, 7th edition Part 2” book in
chapter 8.
Let’s begin with the client, as it’s simpler. The first step is connecting to a server port by name. To make it
slightly more interesting, the client will make at most 10 attempts to connect, waiting for one second between
attempts:
Chapter 10: ALPC 269
HANDLE hPort;
UNICODE_STRING portName;
RtlInitUnicodeString(&portName, L"\\RPC Control\\SimpleServerPort");
NTSTATUS status;
for (int i = 0; i < 10; i++) {
status = NtAlpcConnectPort(&hPort, &portName, nullptr, nullptr,
ALPC_MSGFLG_SYNC_REQUEST, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr);
if (NT_SUCCESS(status))
break;
printf("NtAlpcConnectPort failed: 0x%X\n", status);
Delay(1);
}
if(!NT_SUCCESS(status))
return status;
printf("Client port connected: 0x%p\n", hPort);
NtAlpcConnectPort is used to connect to the server with the name “\RPC Control\SimpleServerPort” - our
server will use this name when registering its port. There is no requirement to use the “RPC Control” object
manager directory, but it’s as good a choice as any. The ALPC_MSGFLG_SYNC_REQUEST indicates that the client
wants to send a synchronous request to the server. The function returns a handle to the server port in hPort.
Delay is a simple function that sleeps for the specified number of seconds:
Assuming the connection was successful, the client can now send requests to the server. This client will send
a string with the current time every second in an infinite loop until it fails. Every ALPC message must start
with a PORT_MESSAGE structure. We’ll create a simple structure to extend that with some text:
LARGE_INTEGER time;
TIME_FIELDS tf;
for (;;) {
Message msg{};
NtQuerySystemTime(&time);
RtlSystemTimeToLocalTime(&time, &time);
RtlTimeToTimeFields(&time, &tf);
sprintf_s(msg.Text, "The Time is %02d:%02d:%02d.%03d",
tf.Hour, tf.Minute, tf.Second, tf.Milliseconds);
//
// set the data size and total size of the message
//
msg.u1.s1.DataLength = sizeof(msg.Text);
msg.u1.s1.TotalLength = sizeof(msg);
We use some functions we met before to get the current local time and convert it to the human-friendly
TIME_FIELDS structure before formatting it as a string. DataLength must be set to the length of the message
body (that is, without the PORT_MESSAGE header), and TotalLength must be set to the total length of the
message (including the header).
Now we can send the message with a possible reply:
Message reply;
SIZE_T msgLen = sizeof(reply);
status = NtAlpcSendWaitReceivePort(hPort, ALPC_MSGFLG_SYNC_REQUEST,
&msg, nullptr, &reply, &msgLen, nullptr, nullptr);
if (!NT_SUCCESS(status)) {
printf("NtAlpcSendWaitReceivePort failed: 0x%X\n", status);
break;
}
printf("Sent message %s\n", msg.Text);
printf("Received reply from PID: %u TID: %u\n",
HandleToULong(reply.ClientId.UniqueProcess),
HandleToULong(reply.ClientId.UniqueThread));
Delay(1);
}
NtAlpcSendWaitReceivePort has a lot of functionality built into it, but the above code makes a synchronous
request (ALPC_MSGFLG_SYNC_REQUEST) and prints the reply’s sender PID and TID.
The server is a bit more complicated. First, we’ll create a named port with NtAlpcCreatePort:
HANDLE hServerPort;
UNICODE_STRING portName;
RtlInitUnicodeString(&portName, L"\\RPC Control\\SimpleServerPort");
OBJECT_ATTRIBUTES portAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&portName, 0);
auto status = NtAlpcCreatePort(&hServerPort, &portAttr, nullptr);
if (!NT_SUCCESS(status)) {
printf("NtAlpcCreatePort failed: 0x%X\n", status);
return 1;
}
printf("Server port created: 0x%p\n", hServerPort);
There is no requirement to create ALPC ports in the “RPC Control” object manager directory. But it is
accessible for non-admin callers. Creating ALPC ports in the root object manager namespace is only allowed
for admin-level callers by default.
Next, we need to prepare a Message structure to receive messages from clients, and optionally send a message:
Message msg;
SIZE_T size = sizeof(msg);
PPORT_MESSAGE sendMessage = nullptr;
PPORT_MESSAGE receiveMessage = &msg;
The server initially has nothing to send (sendMessage is NULL). Now we can start the loop, waiting for calls:
for (;;) {
status = NtAlpcSendWaitReceivePort(hServerPort, ALPC_MSGFLG_RELEASE_MESSAGE,
sendMessage, nullptr, receiveMessage, &size, nullptr, nullptr);
if (!NT_SUCCESS(status))
break;
The API is the same as the one used by the client. It sends sendMessage (initially NULL) and waits for a
reply in receiveMessage. ALPC_MSGFLG_RELEASE_MESSAGE indicates (if sendMessage is non-NULL) that
the server is not expecting a reply from the client, so the message can be released after sending.
Once the reply message is received, the server needs to process it by checking its type. We’ll handle just two
types of messages. The first is a connection request from a client:
Chapter 10: ALPC 272
The type of message may contain flags starting with 0x100, so we mask them out.
The server receives details of the client (PID and TID) and any custom data sent by the client. In this example,
the server ignores all that, and allows any client to connect:
HANDLE hCommPort;
status = NtAlpcAcceptConnectPort(&hCommPort, hServerPort, 0,
nullptr, nullptr, nullptr, receiveMessage, nullptr, TRUE);
if (!NT_SUCCESS(status)) {
printf("NtAlpcAcceptConnectPort failed: 0x%X\n", status);
}
else {
printf("Client port connected: 0x%p\n", hCommPort);
}
sendMessage = nullptr;
break;
NtAlpcAcceptConnectPort accepts or denies the connection request (based on the last argument), and
creates a port for communicating with that specific client (hCommPort). Even though it’s the port to be
used with that client, the server will keep listening on the original named server port ( hServerPort). Any
message sent to that client will use the correct unnamed port under the hood.
The second message handled is a “normal” synchronous message from a client:
case LPC_REQUEST:
printf("\t%s\n", msg.Text);
sendMessage = receiveMessage;
sendMessage->u1.s1.DataLength = 0;
sendMessage->u1.s1.TotalLength = sizeof(PORT_MESSAGE);
break;
Nothing fancy here - the server just prints the message and prepares an “empty” message to reply to the client
(in the next loop iteration) to acknowledge receipt and mark the message as handled.
Chapter 10: ALPC 273
Running the server process and the client process shows output similar to the following:
(Server)
(Client)
NTSTATUS NtAlpcCreatePort(
_Out_ PHANDLE PortHandle,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes);
Contrary to other Create APIs, this function cannot open a handle to an existing named port - it can only
create a new one.
ObjectAttributes contains the name of the port, while the optional PortAttributes can point to a ALPC_-
PORT_ATTRIBUTES structure that contains additional information about the port:
The Flags field can contain a combination of the the following flags:
If PortAttributes is NULL, default port attributes are initialized to the following values:
Chapter 10: ALPC 275
DupObjectTypes is a bitmask indicating which object types handles can be marshalled (duplicated) to the
other process:
As part of port creation, the process owner of the port is set. If the ALPC_PORT_FLAG_SYSTEM_PROCESS flag is
specified, or the caller is from kernel mode, then the System process becomes the owner. Otherwise, the caller
process becomes the owner. The owner process is the only process that can listen for client connections.
NTSTATUS NtAlpcConnectPort(
_Out_ PHANDLE PortHandle,
_In_ PUNICODE_STRING PortName,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes,
_In_ ULONG Flags,
_In_opt_ PSID RequiredServerSid,
_Inout_updates_bytes_to_opt_(*Length, *Length) PPORT_MESSAGE ConnectionMessage,
_Inout_opt_ PULONG Length,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES OutMessageAttributes,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES InMessageAttributes,
_In_opt_ PLARGE_INTEGER Timeout);
NTSTATUS NtAlpcConnectPortEx( // Win 8+
_Out_ PHANDLE PortHandle,
_In_ POBJECT_ATTRIBUTES ConnectionPortObjectAttributes,
_In_opt_ POBJECT_ATTRIBUTES ClientPortObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes,
_In_ ULONG Flags,
_In_opt_ PSECURITY_DESCRIPTOR ServerSecurityRequirements,
_Inout_updates_bytes_to_opt_(*Length, *Length) PPORT_MESSAGE ConnectionMessage,
_Inout_opt_ PSIZE_T Length,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES OutMessageAttributes,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES InMessageAttributes,
_In_opt_ PLARGE_INTEGER Timeout);
PortName is the returned handle if the call succeeds. PortName is the full name of the port (e.g. “\RPC
Control\MyPort”). For NtAlpcConnectPortEx, ConnectionPortObjectAttributes provides the name of
the port using the standard OBJECT_ATTRIBUTES structure. PortAttributes (NtAlpcConnectPort) and
ClientPortAttributes (NtAlpcConnectPortEx) provide optional client port attributes.
Next, PortAttributes is an optional ALPC_PORT_ATTRIBUTES object specifying ALPC port specific at-
tributes for the port being created. The Flags parameter can be zero or a combination of the following
flags:
RequiredServerSid is an optional SID that must be part of the server process token, or the connection
fails. If NULL is specified, any process server SID would work. ServerSecurityRequirements is an optional
security descriptor the server will be evaluated against to make the connection.
ConnectionMessage is an optional message sent from the client to the server that can contain anything. This
may be used by the server for any purpose, such as identifying the client in some way, getting a password, or
Chapter 10: ALPC 277
whatever. Length is an address that contains on input the size of ConnectionMessage in bytes, which the
server may overwrite if it returns some information back to the client.
OutMessageAttributes is an optional ALPC_MESSAGE_ATTRIBUTES structure that contains the outgoing
message attributes. Similarly, InMessageAttributes (if not NULL) receives the reply from the server.
ALPC_MESSAGE_ATTRIBUTES is a variable-sized structure that looks like this:
//
// build simple connection message
//
Message connMsg{};
strcpy_s(connMsg.Text, "Abracadabra");
connMsg.u1.s1.DataLength = sizeof(connMsg.Text);
connMsg.u1.s1.TotalLength = sizeof(connMsg);
The real attributes follow the structure in a certain order that must be adhered to. The provided APIs hide
this complexity from the developer. The following are the attribute flags in order:
Any combination of attributes can be specified in the order shown above. Each attribute has its own structure
that represents its data. The AlpcInitializeMessageAttribute initialize a set of attributes after a buffer
is initialized, or returns the required buffer size:
NTSTATUS AlpcInitializeMessageAttribute(
_In_ ULONG AttributeFlags,
_Out_opt_ PALPC_MESSAGE_ATTRIBUTES Buffer,
_In_ SIZE_T BufferSize,
_Out_ PSIZE_T_ RequiredBufferSize);
You could calculate the required size manually by summing up the sizes of the structures associated with the
needed attributes, but AlpcInitializeMessageAttribute can do that for you if two calls are made as in
the following example:
ULONG size;
//
// assume ALPC_FLG_MSG_CONTEXT_ATTR and ALPC_FLG_MSG_HANDLE_ATTR needed
//
auto status = AlpcInitializeMessageAttribute(
ALPC_FLG_MSG_CONTEXT_ATTR | ALPC_FLG_MSG_HANDLE_ATTR,
nullptr, 0, &size);
assert(status == STATUS_BUFFER_TOO_SMALL);
To get the pointer to the correct part of the buffer for a particular attribute, call AlpcGetMessageAttribute:
PVOID AlpcGetMessageAttribute(
_In_ PALPC_MESSAGE_ATTRIBUTES Buffer,
_In_ ULONG AttributeFlag);
Buffer is the attribute buffer pointer and AttributeFlag is the specific attribute of interest. For example:
Don’t specify more than one flag, as the function will fail and return NULL.
NTSTATUS NtAlpcCreateSecurityContext(
_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_Inout_ PALPC_SECURITY_ATTR SecurityAttribute);
When the security context information object is no longer needed, it should be destroyed with
NtAlpcDeleteSecurityContext:
Chapter 10: ALPC 280
NTSTATUS NtAlpcDeleteSecurityContext(
_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_In_ ALPC_HANDLE ContextHandle);
NTSTATUS NtAlpcRevokeSecurityContext(
_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_In_ ALPC_HANDLE ContextHandle);
NTSTATUS NtAlpcCancelMessage(
_In_ HANDLE PortHandle,
_In_ ULONG Flags,
_In_ PALPC_CONTEXT_ATTR MessageContext);
ULONG Handle;
ULONG ObjectType;
union {
ULONG DesiredAccess;
ULONG GrantedAccess;
};
} ALPC_HANDLE_ATTR32, *PALPC_HANDLE_ATTR32;
// the structure
typedef struct _ALPC_HANDLE_ATTR {
union {
ULONG Flags;
struct {
ULONG Reserved0: 16;
ULONG SameAccess: 1;
ULONG SameAttributes: 1;
ULONG Indirect:1;
ULONG Inherit :1;
ULONG Reserved1: 12;
};
};
union {
HANDLE Handle;
ALPC_HANDLE_ATTR32* HandleAttrArray;
};
union {
ULONG ObjectType;
ULONG HandleCount;
Chapter 10: ALPC 282
};
union {
ACCESS_MASK DesiredAccess;
ACCESS_MASK GrantedAccess;
};
} ALPC_HANDLE_ATTR, *PALPC_HANDLE_ATTR;
This structure can be used to pass a handle from one side to the other. Handle is the handle to share (duplicate)
and ObjectType must be set the object type (constants ALPC_OBJ_*). Alternatively, an array of handles can
be passed by filling HandleCount and HandleAttrArray.
Here is an example for packing an event handle into message attributes (error handling omitted):
//
// create an event to share with the server
//
HANDLE hEvent;
NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE);
//
// put the event handle information
//
auto handleAttr = (PALPC_HANDLE_ATTR)AlpcGetMessageAttribute(msgAttr,
ALPC_FLG_MSG_HANDLE_ATTR);
handleAttr->Flags = 0;
handleAttr->Handle = hEvent;
handleAttr->ObjectType = OB_EVENT_OBJECT_TYPE;
handleAttr->DesiredAccess = EVENT_MODIFY_STATE | SYNCHRONIZE;
These are provided to the server automatically if requested. The fields mean the same thing as the
corresponding properties of token objects. For example, AuthenticationId is the Logon Session Id of the
client.
This attribute allows sharing a section between a client and a server. The structure requires to be filled is the
following:
The members need to be filled after a section and a view are created. The first step is to call NtAlpcCre-
atePortSection:
NTSTATUS NtAlpcCreatePortSection(
_In_ HANDLE PortHandle,
_In_ ULONG Flags,
_In_opt_ HANDLE SectionHandle,
_In_ SIZE_T SectionSize,
_Out_ PALPC_HANDLE AlpcSectionHandle,
_Out_ PSIZE_T ActualSectionSize);
The client typically makes this call with PortHandle being the opened port returned from NtAlpcConnect-
Port. Flags is zero or ALPC_VIEWFLG_SECURED_ACCESS, which makes accessing the shared section private
to the processes involved only. The SectionHandle can be a handle to an existing section object (see chapter
12 for more on Section objects), or NULL, it which case the function will create a Section object with the
size SectionSize. The return values from the call are an internal handle in *AlpcSectionHandle, which is
Chapter 10: ALPC 284
then stashed in the ALPC_DATA_VIEW_ATTR structure, and the actual size of the section (may be rounded up
compared to the requested size).
Next, the ALPC_DATA_VIEW_ATTR part of the message attributes is filled, and then a view must be created by
the client with NtAlpcCreateSectionView:
NTSTATUS NtAlpcCreateSectionView(
_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_Inout_ PALPC_DATA_VIEW_ATTR ViewAttributes);
ALPC_HANDLE hPortSection;
SIZE_T actualSize;
//
// create a 4KB section
//
NtAlpcCreatePortSection(hPort, 0, nullptr,
1 << 12, &hPortSection, &actualSize);
auto dataView = (PALPC_DATA_VIEW_ATTR)AlpcGetMessageAttribute(
msgAttr, ALPC_FLG_MSG_DATAVIEW_ATTR);
//
// initialize section view attributes
//
dataView->Flags = 0;
dataView->SectionHandle = hPortSection;
dataView->ViewSize = actualSize;
dataView->ViewBase = nullptr; // must be NULL
//
// create the view
//
NtAlpcCreateSectionView(hPort, 0, dataView);
The server, in turn, can extract the information and use the shared section:
Chapter 10: ALPC 285
NTSTATUS NtAlpcSendWaitReceivePort(
_In_ HANDLE PortHandle,
_In_ ULONG Flags,
_In_reads_bytes_opt_(SendMessage->u1.s1.TotalLength) PPORT_MESSAGE SendMessage,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES SendMessageAttributes,
_Out_writes_bytes_to_opt_(*Length, *Length) PPORT_MESSAGE ReceiveMessage,
_Inout_opt_ PSIZE_T Length,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ReceiveMessageAttributes,
_In_opt_ PLARGE_INTEGER Timeout);
The function is used by the client and the server. It combines two operations - send and receive (and optionally
wait in between) to minimize user/kernel transitions.
PortHandle is the port handle for communication, either from the client or server side. Flags is zero or a
combination of the following:
• ALPC_MSGFLG_SYNC_REQUEST (0x20000) - the request is synchronous. The call returns when the
message was handled or there is an error.
• ALPC_MSGFLG_RELEASE_MESSAGE (0x10000) - the message does not require a reply from the other side.
• ALPC_MSGFLG_DIRECT_RECEIVE (0x1000000) - direct messaging (async only) - further research is
required.
SendMesssage is the message to send to the receiver, which must start with a header of type PORT_MESSAGE:
Chapter 10: ALPC 286
A typical approach would be to extend this structure with the message specific details. In C++ one could
write:
Of course, a more dynamic structure may be needed in some scenarios, as long as the beginning of the
memory block is treated as PORT_MESSAGE.
There is no need to fill most members, except DataLength and TotalLength. Here is an example based on
the above definitions:
Message msg;
msg.u1.s1.DataLength = sizeof(msg) - sizeof(PORT_HEADER);
msg.u1.s1.TotalLength = sizeof(msg);
The other details will be filled by the call before it reaches the receiver.
Next comes SendMessageAttributes, which is an optional pointer to the sent message attributes as
described in the section Message Attributes, earlier in this chapter. It’s perfectly legal to provide no attributes.
Next, ReceiveMessage is an optional pointer to a return message from the receiver, whose maximum size
is provided in *BufferLength. If the return message is bigger than the provided buffer, the message is not
retrieved, an error is returned (STATUS_BUFFER_TOO_SMALL), and the caller must allocate a bigger buffer (the
required size is provided in *Length) before calling NtAlpcSendWaitReceivePort again.
Next up, ReceiveMessageAttributes is an optional pointer to message attributes associated with the
returned message (if any). Finally, Timeout is an optional time to wait until the call returns for synchronous
calls. If NULL is specified, the caller waits as long as it takes or an error occurs.
Once a message is received, the server must handle it by examining its type in the u2.s2.Type member (the
lower 8 bits only). The list of possible messages are defined below:
#define LPC_REQUEST 1
#define LPC_REPLY 2
#define LPC_DATAGRAM 3
#define LPC_LOST_REPLY 4
#define LPC_PORT_CLOSED 5
#define LPC_CLIENT_DIED 6
#define LPC_EXCEPTION 7
#define LPC_DEBUG_EVENT 8
#define LPC_ERROR_EVENT 9
Chapter 10: ALPC 288
#define LPC_CONNECTION_REQUEST 10
#define LPC_CONNECTION_REPLY 11
#define LPC_CANCELED 12
#define LPC_UNREGISTER_PROCESS 13
// flags
#define LPC_KERNELMODE_MESSAGE (CSHORT)0x8000
#define LPC_NO_IMPERSONATE (CSHORT)0x4000
NTSTATUS NtAlpcAcceptConnectPort(
_Out_ PHANDLE PortHandle,
_In_ HANDLE ConnectionPortHandle,
_In_ ULONG Flags,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes,
_In_opt_ PVOID PortContext,
_In_reads_bytes_(ConnRequest->u1.s1.TotalLength) PPORT_MESSAGE ConnRequest,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ConnectionMessageAttributes,
_In_ BOOLEAN AcceptConnection);
The last parameter, AcceptConnection is the decision to accept or reject the connection. Even if denied,
a message can be passed via ConnRequest that may provide more information. PortHandle is the return
value for an unnamed port used by the server internally to communicate with that client (if the connection
is accepted). ConnectionPortHandle is the server’s port handle returned from NtAlpcCreatePort.
Flags is normally zero, but could be ALPC_PORFLG_WOW64_CALL (0x80000000) to force handles to be treated
as 32-bit, so that HANDLE pointers would not be overwritten. ObjectAttributes is normally NULL, as well as
PortAttributes (see the discussion on port attributes earlier in this chapter). PortContext is a user-defined
value, stored as part of the port object. It’s returned in any received message attributes in ALPC_CONTEXT_-
ATTR (if used). Finally, ConnectionMessageAttributes is an optional connection message attributes to
return.
The LPC_REQUEST message is a “normal” request message with a potential reply. LPC_DATAGRAM on the other
hand, is a “fire and forget” kind of message, where no reply is expected nor needed. This message occurs when
an asynchronous call is made (no ALPC_MSGFLG_SYNC_REQUEST), but with ALPC_MSGFLG_RELEASE_MESSAGE,
and no reply message.
Chapter 10: ALPC 289
Once a client connects with NtAlpcConnectPort, it can send and receive messages with NtAlpcSendWait-
ReceivePort.
One way to be notified when an asynchronous request completes is by using an I/O completion port. One
can be associated with an ALPC port with NtAlpcSetInformation:
NTSTATUS NtAlpcSetInformation(
_In_ HANDLE PortHandle,
_In_ ALPC_PORT_INFORMATION_CLASS PortInformationClass,
_In_reads_bytes_opt_(Length) PVOID PortInformation,
_In_ ULONG Length);
A couple of information classes are supported for set operations, one of which is AlpcAssociateComple-
tionPortInformation (2) that expects the following structure:
CompletionKey is an arbitrary value to store, while CompletionPort is a handle to a valid I/O completion
port. Such an object can be created by the native NtCreateIoCompletion or the Windows API CreateIo-
CompletionPort. The following example creates one and associates it with an ALPC port:
ALPC_PORT_ASSOCIATE_COMPLETION_PORT iocp{};
NtCreateIoCompletion(&iocp.CompletionPort, IO_COMPLETION_ALL_ACCESS, nullptr, 0);
// or with the Windows API:
//iocp.CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
NtAlpcSetInformation(hPort, AlpcAssociateCompletionPortInformation,
&iocp, sizeof(iocp));
I/O completion ports native APIs are not described in detail, saved for a second edition of the
book. You can read more about them in the Windows SDK docs or my book “Windows 10 System
Programming part 1”.
An asynchronous operation does not specify a reply message or a length. Instead, the client makes a second
call once the I/O completion port is triggered. Here is some conceptual code:
Chapter 10: ALPC 290
11.7: Summary
This chapter examined some aspects of ALPC. It’s feature rich, which adds to its complexity and flexibility.
The coverage is by no means exhaustive, and further research is needed. Hopefully, future editions of the
book will contain more details about ALPC.
Chapter 11: Security
Security is a cornerstone of Windows, and critical to any modern operating system. In this chapter we’ll
examine the native APIs related to security. We’ll start with a brief overview, followed by examination of
access tokens, used as security credentials for processes and threads. Next, we’ll examine security descriptors
APIs, used to protect access to kernel objects.
In this chapter:
• Overview
• SIDs
• Tokens
• Security Descriptors
• Access Control Lists
12.1: Overview
Principals are entities that can be referred to in the security system, such as users or groups. A principal
is identified by a Security Identifier (SID) that is a variable-sized structure. A set of Well Known SIDs are
defined that represent the same conceptual entities on every system. Examples include the Everyone group
(SID S-1-1-0, also called World SID), and the Administrators alias ( S-1-5-32-544).
access tokens (or simply tokens) are kernel objects that store the security context of a process or a thread.
This context includes the user’s SID, its privileges, groups it belongs to, and other details. Two types of tokens
exist: primary and impersonation. Every process is created with a primary token that cannot be replaced.
Threads automatically use their process’ token when accessing secure objects, unless they obtain their own
(impersonation) token.
Privileges represent operations that bypass security. For example, loading a driver into a system requires a
privilege (SeLoadDriver*), by default given to members of the Administrators alias. Privileges are stored in a
token, and cannot be added or removed. Administrators can add or remove privileges for a user in the user
database; the next time the user logs off and logs in again, these changes will take effect in tokens created
based on that user account.
Security descriptors are used to protect kernel objects (of all kinds). The primary elements in a security
descriptor are the object’s owner SID (the creator’s SID by default), and a DACL (Discretionary Access Control
List), that specifies who can do what with the object.
Chapter 11: Security 292
12.2: SIDs
A SID is a variable size binary structure that looks like figure 11-1.
It can be stored as a string for readability and persistence. Converting a binary SID to a string can be done
with RtlConvertSidToUnicodeString:
NTSTATUS RtlConvertSidToUnicodeString(
_Inout_ PUNICODE_STRING UnicodeString,
_In_ PSID Sid,
_In_ BOOLEAN AllocateDestinationString);
The provided UNICODE_STRING can be pre-allocated to a size that would be large enough and passing FALSE
for AllocateDestinationString. Alternatively, pass TRUE for AllocateDestinationString so that the
internal Buffer in the UNICODE_STRING is allocated for you. In that case, call RtlFreeUnicodeString to
free the allocated buffer.
SE_SID sid{};
DWORD size = sizeof(sid);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &sid, &size);
UNICODE_STRING ssid;
RtlConvertSidToUnicodeString(&ssid, &sid.Sid, TRUE);
printf("SID of Administrators: %wZ\n", &ssid);
RtlFreeUnicodeString(&ssid);
The maximum size of a SID in its binary form is defined as SECURITY_MAX_SID_SIZE (68 bytes) and
in its string form as SECURITY_MAX_SID_STRING_CHARACTERS (187 characters).
NTSTATUS RtlLengthSidAsUnicodeString(
_In_ PSID Sid,
_Out_ PULONG StringLength);
Some simple SID-related APIs are about checking the validity of a SID ( RtlValidSid), and comparing two
SIDs for equality (RtlEqualSid and RtlEqualPrefixSid):
RtlEqualPerfixSid compares the given SIDs with all components except for the last sub-authority ID.
There are a few APIs dedicated to creating SIDs. The most direct one is RtlAllocateAndInitializeSid:
Chapter 11: Security 294
NTSTATUS RtlAllocateAndInitializeSid(
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount,
_In_ ULONG SubAuthority0,
_In_ ULONG SubAuthority1,
_In_ ULONG SubAuthority2,
_In_ ULONG SubAuthority3,
_In_ ULONG SubAuthority4,
_In_ ULONG SubAuthority5,
_In_ ULONG SubAuthority6,
_In_ ULONG SubAuthority7,
_Outptr_ PSID *Sid);
The function accepts the authority as an array of 6 bytes. One authority that is defined in <Winnt.h> is the
so-called NT Authority:
The maximum number of sub-authorities in a SID is 15, but this function only allows 8, which is mostly fine,
as very long SIDs not as common. SubAuthorityCount specifies the valid sub-authorities in the call.
The returned SID (last Sid parameter) is allocated by this function, which means it must be freed explicitly
by calling RtlFreeSid:
NTSTATUS RtlAllocateAndInitializeSidEx(
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount,
_In_reads_(SubAuthorityCount) PULONG SubAuthorities,
_Outptr_ PSID *Sid);
SubAuthorities is an array of SubAuthorityCount ULONG values used to initialize the SID. Just like
RtlAllocateAndInitializeSid, RtlFreeSid must be used to dispose of the SID allocated memory.
NTSTATUS RtlInitializeSid(
_Out_ PSID Sid,
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount);
NTSTATUS RtlInitializeSidEx( // Windows 10+
_Out_writes_bytes_(SECURITY_SID_SIZE(SubAuthorityCount)) PSID Sid,
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount,
...);
The extended function accepts any number of arguments (…) to initialize the sub-authorities. RtlInitial-
izeSid only initializes the authority value and the count of sub-authorities.
A SID can be created for a Windows Service so that specific access for this service can be specified even if
the service is running under a generic account like the Local System, Network Service, or Local Service. This
is accomplished with RtlCreateServiceSid:
NTSTATUS RtlCreateServiceSid(
_In_ PUNICODE_STRING ServiceName,
_Out_writes_bytes_opt_(*ServiceSidLength) PSID ServiceSid,
_Inout_ PULONG ServiceSidLength);
The function uses the service name (ServiceName) converted to upper case as input to the SHA-1 algorithm
to generate the final SID in the form: S-1-5-80-hash1-hash2-hash3-hash4-hash5. ServiceSidLength is
an input/output parameter indicating on input the number of bytes the SID buffer is able to accept. Upon
successful return, the value indicates the actual number of bytes written.
Here is an example:
SE_SID sid{};
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"MyService");
ULONG len = sizeof(sid);
RtlCreateServiceSid(&name, &sid, &len);
// display as string
UNICODE_STRING ssid;
RtlConvertSidToUnicodeString(&ssid, &sid.Sid, TRUE);
printf("SID of MyService: %wZ\n", &ssid);
// displays S-1-5-80-517257762-1253276234-605902578-3995580692-1133959824
RtlFreeUnicodeString(&ssid);
Chapter 11: Security 296
See the Windows SDK documentation for more information on Service SIDs.
12.3: Tokens
Token objects must be attached to a process or thread to have any effect. The typical way of getting a token
is by opening one from a process with NtOpenProcessToken(Ex):
NTSTATUS NtOpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_Out_ PHANDLE TokenHandle);
NTSTATUS NtOpenProcessTokenEx(
_In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_Out_ PHANDLE TokenHandle);
NTSTATUS NtOpenThreadToken(
_In_ HANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ BOOLEAN OpenAsSelf,
_Out_ PHANDLE TokenHandle);
NTSTATUS NtOpenThreadTokenEx(
_In_ HANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ BOOLEAN OpenAsSelf,
_In_ ULONG HandleAttributes,
_Out_ PHANDLE TokenHandle);
Another way to get a token is by duplicating an existing token with NtDuplicateToken (and possibly making
some modifications):
NTSTATUS NtDuplicateToken(
_In_ HANDLE ExistingTokenHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ BOOLEAN EffectiveOnly,
_In_ TOKEN_TYPE TokenType,
_Out_ PHANDLE NewTokenHandle);
ExistingTokenHandle represents the token to duplicate. This handle must have the TOKEN_DUPLICATE
access mask. DesiredAccess is the type of access needed for the new token (TOKEN_QUERY, TOKEN_ALL_-
ACCESS, etc.). ObjectAttributes is the usual attributes structure, that if provided is used to specify the
following:
EffectiveOnly indicates whether the entire source token should be duplicated ( FALSE), or only the enabled
parts of the token (TRUE), such as only the enabled privileges. Typically, FALSE is passed for this parameter.
Finally, TokenType indicates what type should the new token be: TokenPrimary or TokenImpersonation.
A primary token can be attached to a process, while an impersonation token can be attached to a thread.
One of the reasons to call NtDuplicateToken is to get the other token type. For example, the Windows API
CreateProcessAsUser requires a primary token to succeed.
The new token handle is returned in *NewTokenHandle*, and should eventually be closed normally with
NtClose.
A new token can be created from an existing token by removing privileges, disabling SIDs, and/or restricting
SIDs. This is the role of NtFilterToken:
NTSTATUS NtFilterToken(
_In_ HANDLE ExistingTokenHandle,
_In_ ULONG Flags,
_In_opt_ PTOKEN_GROUPS SidsToDisable,
_In_opt_ PTOKEN_PRIVILEGES PrivilegesToDelete,
_In_opt_ PTOKEN_GROUPS RestrictedSids,
_Out_ PHANDLE NewTokenHandle);
NtFilterToken is the workhorse of the CreateRestrictedToken Windows API. Refer to that function’s
documentation for more information. The parameters of CreateRestrictedToken are conceptually the
same as NtFilterToken, but slightly different structures are used. NtFilterToken leverages the count fields
in TOKEN_GROUPS and TOKEN_PRIVILEGES, while the Windows API uses a SID_AND_ATTRIBUTES structure,
with separate parameters for length. Other than that, the functionality is identical. The newly created token
is returned in *NewTokenHandle.
For information the the security Windows API can also be found in chapter 16 of my book “Windows
10 System Programming, part 2”.
NTSTATUS NtQueryInformationToken(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_Out_writes_bytes_to_opt_(Length, *ReturnLength) PVOID TokenInformation,
_In_ ULONG Length,
_Out_ PULONG ReturnLength);
The Windows API GetTokenInformation calls NtQueryInformationToken with exactly the same param-
eters. Refer to the Windows SDK documentation for this API for all the various options.
The converse function exists as well:
NTSTATUS NtSetInformationToken(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_In_reads_bytes_(TokenInformationLength) PVOID TokenInformation,
_In_ ULONG TokenInformationLength);
The Windows API SetTokenInformation calls NtSetInformationToken with the same arguments. Refer
to the Windows SDK documentation for the details.
NtSetInformationToken provides many of the token attributes that can be changed. However, there are
specialized functions for other changes. One is NtAdjustPrivilegesToken:
NTSTATUS NtAdjustPrivilegesToken(
_In_ HANDLE TokenHandle,
_In_ BOOLEAN DisableAllPrivileges,
_In_opt_ PTOKEN_PRIVILEGES NewState,
_In_ ULONG BufferLength,
_Out_writes_bytes_to_opt_(BufferLength, *Length) PTOKEN_PRIVILEGES PreviousState,
_Out_opt_ PULONG Length);
There is one difference that requires explanation. Privileges identifiers are exposed to the Windows API as
strings. For example, SE_DEBUG_NAME is defined as the string “SeDebugPrivilege”. AdjustTokenPrivileges
and NtAdjustPrivilegesToken need PTOKEN_PRIVILEGES, where privileges are identifier by a number. The
Windows API function LookupPrivilegeValue is used to retrieve the number corresponding to the privilege
string.
With the native API, privilege identifiers are given as follows:
Chapter 11: Security 300
Enabling or disabling a single privilege for the current process token is a common operation, and NtAdjust-
PrivilegesToken is certainly capable of doing that. However, the native API offers a much more convenient
API for this purpose (internally calling NtAdjustPrivilegesToken):
Chapter 11: Security 301
NTSTATUS RtlAdjustPrivilege(
_In_ ULONG Privilege,
_In_ BOOLEAN Enable,
_In_ BOOLEAN Client,
_Out_ PBOOLEAN WasEnabled);
Privilege is one of the constants listed above. Enable indicates whether the privilege should be enabled
or disabled. Client indicates whether to adjust the current process token (FALSE) or the current thread’s
token (TRUE). If TRUE is specified, but the current thread has no token, the function fails. Finally, WasEnabled
returns the previous state of the privilege. This could be useful for restoring the privilege state later.
Similarly to adjusting privileges, NtAdjustGroupsToken allows adjusting groups within a token:
NTSTATUS NtAdjustGroupsToken(
_In_ HANDLE TokenHandle,
_In_ BOOLEAN ResetToDefault,
_In_opt_ PTOKEN_GROUPS NewState,
_In_opt_ ULONG BufferLength,
_Out_writes_bytes_to_opt_(BufferLength, *Length) PTOKEN_GROUPS PreviousState,
_Out_opt_ PULONG Length);
This function is called by the Windows API AdjustTokenGroups, with basically identical parameters (the
only difference is BOOL used by the Windows API instead of BOOLEAN). See the Windows SDK documentation
for the details.
12.3.2: Impersonation
A thread can impersonate by attaching a token to itself, which must be an impersonation token. Once such
a token is obtained (e.g., by calling NtDuplicateToken), the caller thread can impersonate.
One impersonation that does not require an explicit token is for an anonymous user:
The thread handle must have the THREAD_IMPRESONATE access mask, which is always available if the current
thread handle is used (NtCurrentThread).
To impersonate an arbitrary token (i.e., assign an impersonation token to a thread), call NtSetInformation-
Thread with the ThreadImpersonationToken information class. For example:
The thread handle (NtCurrentThread() in the above example) must have the THREAD_SET_THREAD_TOKEN
access mask.
To revert the thread to using the process’ token, call NtSetInformationThread again, but set the provided
token handle to NULL, like so:
NTSTATUS NtImpersonateThread(
_In_ HANDLE ServerThreadHandle,
_In_ HANDLE ClientThreadHandle,
_In_ PSECURITY_QUALITY_OF_SERVICE SecurityQos);
The “server” thread is the one who impersonates - the handle must have the THREAD_IMPERSONATE access
mask. The “client” thread is the one being impersonated - its handle must have the THREAD_DIRECT_IMPER-
SONATION access mask. SecurityQos provides more details for the impersonation required:
This structure is documented in the Windows SDK. Briefly, Length must be set to the size of the structure.
ImpersonationLevel is probably the most important member, indicating the “power” of the impersonating
thread:
This indicates the “trust” provided to the impersonator - what can be done on the behalf of the impersonated
thread. ContextTrackingMode indicates if the the impersonator gets a snapshot of the security context of the
impersonated thread (static tracking, SECURITY_STATIC_TRACKING=0), or the context is updated dynamically
(dynamic tracking, SECURITY_DYNAMIC_TRACKING=1).
Finally, EffectiveOnly indicates if the impersonator is allowed enabling or disabling privileges.
The RtlImpersonateSelf is the workhorse of the Windows API ImpersonateSelf, which gets a copy of
the process token as an impersonation and assigns it to the current thread:
This could be useful for the thread to enable/disable privileges privately, not effecting the process token,
which may be used by other threads.
An extended version is available as well:
NTSTATUS RtlImpersonateSelfEx(
_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
_In_opt_ ACCESS_MASK AdditionalAccess,
_Out_opt_ PHANDLE ThreadToken);
The functionality is identical to RtlImpersonateSelf, but allows asking for more access for the new token
(beyond TOKEN_IMPERSONATE), and optionally receiving the token handle directly.
ALPC ports provide some impersonation functions of their own (see chapter 10 for more on ALPC):
NTSTATUS NtImpersonateClientOfPort(
_In_ HANDLE PortHandle,
_In_ PPORT_MESSAGE Message);
NTSTATUS NtAlpcImpersonateClientOfPort(
_In_ HANDLE PortHandle,
_In_ PPORT_MESSAGE Message,
_In_ PVOID Flags);
NTSTATUS NtAlpcImpersonateClientContainerOfPort(
_In_ HANDLE PortHandle,
_In_ PPORT_MESSAGE Message,
_In_ ULONG Flags);
Chapter 11: Security 304
We’ve seen token creation based on existing tokens - NtDuplicateToken and NtFilterToken. Creating a
token that is not based on an existing token is possible with NtCreateToken and NtCreateTokenEx defined
like so:
NTSTATUS NtCreateToken(
_Out_ PHANDLE TokenHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ TOKEN_TYPE TokenType,
_In_ PLUID AuthenticationId,
_In_ PLARGE_INTEGER ExpirationTime,
_In_ PTOKEN_USER User,
_In_ PTOKEN_GROUPS Groups,
_In_ PTOKEN_PRIVILEGES Privileges,
_In_opt_ PTOKEN_OWNER Owner,
_In_ PTOKEN_PRIMARY_GROUP PrimaryGroup,
_In_opt_ PTOKEN_DEFAULT_DACL DefaultDacl,
_In_ PTOKEN_SOURCE TokenSource);
NTSTATUS NtCreateTokenEx(
_Out_ PHANDLE TokenHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ TOKEN_TYPE TokenType,
_In_ PLUID AuthenticationId,
_In_ PLARGE_INTEGER ExpirationTime,
_In_ PTOKEN_USER User,
_In_ PTOKEN_GROUPS Groups,
Chapter 11: Security 305
Creating a token with CreateToken(Ex) requires the SeCreateToken privilege, normally available to the
Lsass.exe process only. One way to get this privilege is to duplicate Lsass’s token and use it for impersonation;
this can only be achieved by an admin level caller. Another way is to have an administrator add this privilege
to the user account - the next time that user logs in, the privilege will be available in its token.
Here is the rundown of parameters to NtCreateToken:
Other logon sessions are created as needed. To view the entire list of logon sessions, you can use the
logonsessions Sysinternals command line tool, or graphically in my System Explorer tool (System/Logon
Sessions menu item). Here is an example output from logonsessions:
Logon server:
DNS Domain:
UPN:
...
You can think of a logon session as the “source” of tokens. Put another way, a token points to its logon session.
Figure 11-2 illustrates this.
• ExpirationTime indicates when should the resulting token expire. If the value in the LARGE_INTEGER
is zero, the token never expires. Note that passing NULL as an argument here is invalid.
• User is the user of the resulting token, specified as the standard TOKEN_USER structure pointer, holding
the user’s SID. Refer to the documentation of GetTokenInformation on the definition of TOKEN_USER
and related structures.
• Groups is the list of groups the token should have, given as a TOKEN_GROUPS pointer, documented in
the Windows API.
• Privileges provides the list of privileges to place in the resulting token. This is where we can add any
power we’d like to be part of the token.
• Owner is the default owner to set for security attributes created using this token ( TOKEN_OWNER). It’s
optional, but recommended. A simple argument would be the same SID used for User.
• PrimaryGroup is the primary group of this token - it must be one of the groups listed in Groups. A
primary group does not carry much meaning in Windows, since it was created for compatibility with
the POSIX subsystem; nevertheless, it is required.
• DefaultDacl is an optional DACL to be used to protect the token.
• TokenSource is the source of the token, identified by a simple string and a LUID. These can be set to
anything - it’s used to identify log entries that involve the returned token.
• UserAttributes is an optional set of user claims, documented in the Windows SDK. The type is TOKEN_-
SECURITY_ATTRIBUTES_INFORMATION used by the native API and the kernel, but is in fact the same as
the user mode structure CLAIM_SECURITY_ATTRIBUTES_INFORMATION. Check out the SDK docs for the
details.
• DeviceAttributes is an optional set of device claims, documented in the Windows SDK.
• DeviceGroups is an optional set of groups the device the user is using is a member of.
• TokenMandatoryPolicy is an optional integrity level policy, the deualt being TOKEN_MANDATORY_POL-
ICY_NO_WRITE_UP. Check out the SDK docs for more information.
NtCreateToken calls NtCreateTokenEx with the above four argunments set to NULL.
In the following demo, we’ll create a token from scratch using NtCreateToken. For full source code is in the
CreateToken project.
Since we need the SeCreateToken privilege to successfully use NtCreateToken, we’re going to use the first
option mentioned in the previous section - duplicate Lsass’s token.
The first step is to find the Lsass process and open a handle to it. We’ll start by getting the its full path name
for comparison purposes:
Chapter 11: Security 309
HANDLE FindLsass() {
WCHAR path[MAX_PATH];
GetSystemDirectory(path, ARRAYSIZE(path));
wcscat_s(path, L"\\lsass.exe");
UNICODE_STRING lsassPath;
RtlInitUnicodeString(&lsassPath, path);
Next, we need to iterate over all processes, looking for Lsass. One way to do that is using NtQuerySystem-
Information with SystemProcessInformation, but that’s an overkill. We need to open a handle to the
Lsass process, so we may as well use NtGetNextProcess to achieve both at the same time:
BYTE buffer[256];
HANDLE hProcess = nullptr, hOld;
while (true) {
hOld = hProcess;
//
// get the next process handle
//
auto status = NtGetNextProcess(hProcess,
PROCESS_QUERY_LIMITED_INFORMATION, 0, 0, &hProcess);
if (hOld)
NtClose(hOld);
if (!NT_SUCCESS(status))
break;
//
// get path of Lsass executable
//
if (NT_SUCCESS(NtQueryInformationProcess(hProcess,
ProcessImageFileNameWin32, buffer, sizeof(buffer), nullptr))) {
auto name = (UNICODE_STRING*)buffer;
if (RtlEqualUnicodeString(&lsassPath, name, TRUE))
return hProcess;
}
}
return nullptr;
}
We call NtGetNextProcess until the correct process shows up. Finally, we return the handle to the caller.
The next step is to open Lasass process’ token, and duplicate it. Let’s open the token first:
Chapter 11: Security 310
HANDLE DuplicateLsassToken() {
auto hProcess = FindLsass();
if (hProcess == nullptr)
return nullptr;
We need the TOKEN_DUPLICATE access mask so we can duplicate it for impersonation purposes (we can’t use
it directly as it’s a primary token).
Now we can call NtDuplicateObject to get our own token but making it an impersonation token:
//
// set this token to allow impersonation
//
SECURITY_QUALITY_OF_SERVICE qos{ sizeof(qos) };
qos.ImpersonationLevel = SecurityImpersonation;
tokenAttr.SecurityQualityOfService = &qos;
The tricky part is to realize that in order to impersonate with the new token, it should support impersonation
- creating it as an impersonation token is not enough - an impersonation token just means it can be attached
to a thread. This is one of those rare cases where we have to set the SecurityQualityOfService member
of OBJECT_ATTRIBUTES, which is not available through the InitializeObjectAttributes macro.
Now we can start our main function. First, we need to enable the SeDebug privilege, without which accessing
Lsass would not work (of course the caller must be an administrator to begin with):
Chapter 11: Security 311
int main() {
BOOLEAN enabled;
auto status = RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
if (!NT_SUCCESS(status)) {
printf("Failed to enable the debug privilege! (0x%X)\n", status);
return status;
}
Next, we need to prepare for calling NtCreateToken, starting with groups we’d like to have in the newly
created token. We’ll start by creating SIDs we’ll use (as any group is represented with a SID):
SE_SID_ systemSid;
DWORD size = sizeof(systemSid);
CreateWellKnownSid(WinLocalSystemSid, nullptr, &systemSid, &size);
SE_SID adminSid;
size = sizeof(adminSid);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &adminSid, &size);
SE_SID allUsersSid;
size = sizeof(allUsersSid);
CreateWellKnownSid(WinWorldSid, nullptr, &allUsersSid, &size);
SE_SID interactiveSid;
size = sizeof(interactiveSid);
CreateWellKnownSid(WinInteractiveSid, nullptr, &interactiveSid, &size);
SE_SID authUsers;
size = sizeof(authUsers);
CreateWellKnownSid(WinAuthenticatedUserSid, nullptr, &authUsers, &size);
Some of the above groups are necessary if we would later like to create a process with the new token (which
we will).
The final group we’ll create is a integrity level, which is not a simple well known group:
Chapter 11: Security 312
PSID integritySid;
SID_IDENTIFIER_AUTHORITY auth = SECURITY_MANDATORY_LABEL_AUTHORITY;
status = RtlAllocateAndInitializeSid(&auth, 1,
SECURITY_MANDATORY_MEDIUM_RID, 0, 0, 0, 0, 0, 0, 0, &integritySid);
assert(SUCCEEDED(status));
Since we need multiple groups, we need to allocate some memory dynamically, cast it to TOKEN_GROUPS*
and use it. There is another way, however, to allocate the required size statically, which is more convenient
(and faster). To help out, I have defined the following helper templated structure:
template<int N>
struct MultiGroups : TOKEN_GROUPS {
MultiGroups() {
GroupCount = N;
}
SID_AND_ATTRIBUTES _Additional[N - 1]{};
};
It uses an integer as the template argument to allocate a static array of SID_AND_ATTRIBUTES structures.
With this definition, we can allocate the number of groups we need statically and just fill the array to the
allocated limit:
MultiGroups<6> groups;
groups.Groups[0].Sid = &adminSid;
groups.Groups[0].Attributes = SE_GROUP_DEFAULTED | SE_GROUP_ENABLED | SE_GROUP_OWNER;
groups.Groups[1].Sid = &allUsersSid;
groups.Groups[1].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[2].Sid = &interactiveSid;
groups.Groups[2].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[3].Sid = &systemSid;
groups.Groups[3].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[4].Sid = integritySid;
groups.Groups[4].Attributes = SE_GROUP_INTEGRITY | SE_GROUP_INTEGRITY_ENABLED;
Chapter 11: Security 313
groups.Groups[5].Sid = &authUsers;
groups.Groups[5].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
Next, we need to set up privileges. We’re going to add two of them, SeChangeNotify (“bypass traverse
checking”), which is always needed, and SeTcb (for demonstration purposes). This is your chance to add any
privileges you want! That’s one of the main reasons to create a token from scratch.
Privileges are added in a similar manner to groups - TOKEN_PRIVILEGES can accommodate a single priviege
with extra allocations. We’ll use the same trick for allocating privileges statically:
template<int N>
struct MultiPrivileges : TOKEN_PRIVILEGES {
MultiPrivileges() {
PrivilegeCount = N;
}
LUID_AND_ATTRIBUTES _Additional[N - 1]{};
};
MultiPrivileges<2> privs;
privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED_BY_DEFAULT;
privs.Privileges[0].Luid.LowPart = SE_CHANGE_NOTIFY_PRIVILEGE;
privs.Privileges[1].Luid.LowPart = SE_TCB_PRIVILEGE;
For a description of the various privileges, please consult the official Microsoft documentation.
TOKEN_PRIMARY_GROUP primary;
primary.PrimaryGroup = &adminSid;
The user of this token will by the Local System account (can be any user, of course):
TOKEN_USER user{};
user.User.Sid = &systemSid;
We are almost ready to call NtCreateToken. It’s time to impersonate the duplicated Lsass token so we would
have the SeCreateToken privilege:
Chapter 11: Security 314
The final initialization step is the logon session ID (sometimes referred to as authentication Id). We must
choose an existing logon session - we’ll select the one used by the Local System account, which has the fixed
number 999:
Now we’re ready to call NtCreateToken. We also need a TOKEN_SOURCE which represents the logical entity
creating the token - a short string and a number. We’ll also make the created token have no expiration:
We create the new token to be a primary token, so we can use it in process creation functions, as we’ll soon
see. The token is created with all possible powers (TOKEN_ALL_ACCESS), so we can change its attributes if we
so choose.
Let’s do something interesting with the token - creating a process that is attached to the user’s console session,
so that any UI from the new process would be visible.
We can get the active console session ID and then replace it in the token. Changing the session ID stored in
a token requires the SeTcb privilege, which fortunately we have, since Lsass has it as well; we just need to
enable it before making the change:
Next, we’ll prepare the STARTUPINFO and PROCESS_INFORMATION used in process creation, and choose
notepad as the exeutable to launch:
Chapter 11: Security 315
We can now attempt to create a process with one of the following Windows APIs: CreateProcessAsUser or
CreateProcessWithTokenW. Although these functions look similar, they are not the same. CreateProces-
sAsUser is a “local” function, which CreateProcessWithTokenW contacts a service to do the actual creation.
On the other hand, CreateProcessAsUser requires the SeAssignPrimary privilege to work.
We’ll try CreateProcessAsUser first, and if that fails, call CreateProcessWithTokenW:
if (!created) {
created = CreateProcessWithTokenW(hToken, LOGON_WITH_PROFILE, nullptr,
name, 0, nullptr, nullptr, &si, &pi);
}
if (!created) {
printf("Failed to create process (%u)\n", GetLastError());
}
else {
printf("Process created: %u\n", pi.dwProcessId);
NtClose(pi.hProcess);
NtClose(pi.hThread);
}
int main() {
BOOLEAN enabled;
auto status = RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
if (!NT_SUCCESS(status)) {
printf("Failed to enable the debug privilege! (0x%X)\n", status);
return status;
}
union Sid {
BYTE buffer[SECURITY_MAX_SID_SIZE];
SID Sid;
};
SE_SID systemSid;
DWORD size = sizeof(systemSid);
CreateWellKnownSid(WinLocalSystemSid, nullptr, &systemSid, &size);
DisplaySid(&systemSid);
SE_SID adminSid;
size = sizeof(adminSid);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &adminSid, &size);
DisplaySid(&adminSid);
SE_SID allUsersSid;
size = sizeof(allUsersSid);
CreateWellKnownSid(WinWorldSid, nullptr, &allUsersSid, &size);
DisplaySid(&allUsersSid);
SE_SID interactiveSid;
size = sizeof(interactiveSid);
CreateWellKnownSid(WinInteractiveSid, nullptr, &interactiveSid, &size);
DisplaySid(&interactiveSid);
SE_SID authUsers;
size = sizeof(authUsers);
CreateWellKnownSid(WinAuthenticatedUserSid, nullptr, &authUsers, &size);
DisplaySid(&authUsers);
Chapter 11: Security 317
PSID integritySid;
SID_IDENTIFIER_AUTHORITY auth = SECURITY_MANDATORY_LABEL_AUTHORITY;
status = RtlAllocateAndInitializeSid(&auth, 1,
SECURITY_MANDATORY_MEDIUM_RID, 0, 0, 0, 0, 0, 0, 0, &integritySid);
assert(SUCCEEDED(status));
//
// set up groups
//
MultiGroups<6> groups;
groups.Groups[0].Sid = &adminSid;
groups.Groups[0].Attributes = SE_GROUP_DEFAULTED | SE_GROUP_ENABLED
| SE_GROUP_OWNER;
groups.Groups[1].Sid = &allUsersSid;
groups.Groups[1].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[2].Sid = &interactiveSid;
groups.Groups[2].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[3].Sid = &systemSid;
groups.Groups[3].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[4].Sid = integritySid;
groups.Groups[4].Attributes = SE_GROUP_INTEGRITY | SE_GROUP_INTEGRITY_ENABLED;
groups.Groups[5].Sid = &authUsers;
groups.Groups[5].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
//
// set up privileges
//
MultiPrivileges<2> privs;
privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED_BY_DEFAULT;
privs.Privileges[0].Luid.LowPart = SE_CHANGE_NOTIFY_PRIVILEGE;
privs.Privileges[1].Luid.LowPart = SE_TCB_PRIVILEGE;
TOKEN_PRIMARY_GROUP primary;
primary.PrimaryGroup = &adminSid;
TOKEN_USER user{};
user.User.Sid = &systemSid;
//
// impersonate
//
Chapter 11: Security 318
status = NtSetInformationThread(NtCurrentThread(),
ThreadImpersonationToken, &hDupToken, sizeof(hDupToken));
if (!NT_SUCCESS(status)) {
printf("Failed to impersonate! (0x%X)\n", status);
return status;
}
if (NT_SUCCESS(status)) {
printf("Token created successfully.\n");
if (NT_SUCCESS(RtlAdjustPrivilege(SE_TCB_PRIVILEGE, TRUE, TRUE, &enabled))) {
ULONG session = WTSGetActiveConsoleSessionId();
NtQueryInformationProcess(NtCurrentProcess(),
ProcessSessionInformation, &session, sizeof(session), nullptr);
NtSetInformationToken(hToken, TokenSessionId, &session, sizeof(session));
}
STARTUPINFO si{ sizeof(si) };
PROCESS_INFORMATION pi;
WCHAR desktop[] = L"winsta0\\Default";
WCHAR name[] = L"notepad.exe";
si.lpDesktop = desktop;
status = RtlAdjustPrivilege(SE_ASSIGNPRIMARYTOKEN_PRIVILEGE,
TRUE, TRUE, &enabled);
BOOL created = FALSE;
if (NT_SUCCESS(status)) {
created = CreateProcessAsUser(hToken, nullptr, name,
nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
}
if (!created) {
created = CreateProcessWithTokenW(hToken, LOGON_WITH_PROFILE, nullptr,
name, 0, nullptr, nullptr, &si, &pi);
}
Chapter 11: Security 319
if (!created) {
printf("Failed to create process (%u)\n", GetLastError());
}
else {
printf("Process created: %u\n", pi.dwProcessId);
NtClose(pi.hProcess);
NtClose(pi.hThread);
}
RtlFreeSid(integritySid);
}
else {
printf("Failed to create token (0x%X)\n", status);
}
return status;
}
NTSTATUS NtPrivilegeCheck(
_In_ HANDLE ClientToken,
_Inout_ PPRIVILEGE_SET RequiredPrivileges,
_Out_ PBOOLEAN Result);
The Windows API PrivilegeCheck is using this function directly. Check out the docs for more details.
Two tokens can be compared with NtCompareTokens:
NTSTATUS NtCompareTokens(
_In_ HANDLE FirstTokenHandle,
_In_ HANDLE SecondTokenHandle,
_Out_ PBOOLEAN Equal);
The function checks if the given tokens are equivalent from an access check perspective. Obviously, if both
handles point to the same token object, then they are equivalent. The function checks the following pieces
of the tokens to determine equality:
• Every SID that is present in one token must be present in the other, including their attributes (such as
SE_GROUP_ENABLED).
• Both tokens must have the same list of privileges.
• Both or neither token are restricted tokens.
Chapter 11: Security 320
Security descriptors (SDs) are objects that can be attached to kernel objects to indicate “who can do what”
with the object. SDs have two formats: absolute and self-relative*. Absolute format uses absolute pointers
to point to the various pieces of an SD, while the self-relative uses offsets to point to its parts. A self-relative
SD can be easily moved in memory, while absolute SDs cannot be easily moved, but can be more economical
if some of their parts is shared with other SDs.
Absolute SDs are described by the SECURITY_DESCRIPTOR structure:
As you can see, the pointers in SECURITY_DESCRIPTOR are replaced by offsets in SECURITY_DESCRIPTOR_-
RELATIVE. Regardless, for compatibility and consistency SDs are usually treated as opaque PVOID pointers.
In fact, the PSECURITY_DESCRIPTOR type is defined to be PVOID (Notice the above definitions have a “PI”
prefix for the pointer types).
Figure 11-3 shows the layout of an absolute SD, while figure 11-4 depicts a self-relative SD.
Chapter 11: Security 321
Converting between the two formats is possible with the following APIs:
Chapter 11: Security 322
NTSTATUS RtlAbsoluteToSelfRelativeSD(
_In_ PSECURITY_DESCRIPTOR AbsoluteSD,
_Out_writes_bytes_to_opt_(*Length, *Length)
PSECURITY_DESCRIPTOR SelfSD, _Inout_ PULONG Length);
NTSTATUS RtlSelfRelativeToAbsoluteSD(
_In_ PSECURITY_DESCRIPTOR SelfRelativeSD,
_Out_writes_bytes_to_opt_(*SDSize, *SDSize) PSECURITY_DESCRIPTOR AbsoluteSD,
_Inout_ PULONG SDSize,
_Out_writes_bytes_to_opt_(*DaclSize, *DaclSize) PACL Dacl,
_Inout_ PULONG DaclSize,
_Out_writes_bytes_to_opt_(*SaclSize, *SaclSize) PACL Sacl,
_Inout_ PULONG SaclSize,
_Out_writes_bytes_to_opt_(*OwnerSize, *OwnerSize) PSID Owner,
_Inout_ PULONG OwnerSize,
_Out_writes_bytes_to_opt_(*PrimaryGroupLen, *PrimaryGroupLen) PSID PrimaryGroup,
_Inout_ PULONG PrimaryGroupLen);
These functions are the workhorses of the Windows APIs MakeSelfRelativeSD and MakeAbsoluteSD. Refer
to the SDK docs for the details. In addition, the native API offers the function RtlMakeSelfRelativeSD to
convert to a self-relative format from whatever format the original SD is (creates a copy either way):
NTSTATUS RtlMakeSelfRelativeSD(
_In_ PSECURITY_DESCRIPTOR AbsoluteSD,
_Out_writes_bytes_(*BufferLength) PSECURITY_DESCRIPTOR SelfRelativeSD,
_Inout_ PULONG BufferLength);
If the provided buffer is too small, the function fails and returns the required buffer size in *BufferLength.
As can be seen in figures 11-3 and 11-4, a SD consists of the following:
• Control flags
• Owner SID - the owner of an object.
• Primary Group SID - used in the past for group security in POSIX subsystem applications.
• Discretionary Access Control List (DACL) - a list of Access Control Entries (ACE), specifying who-can-
do-what with the object.
• System Access Control List (SACL) - a list of ACEs, indicating which operations should cause an audit
entry to be written to the security log.
The most important pieces from a protection perspective are the owner and the DACL.
Many of the security descriptor-related Windows APIs are thin wrappers around the corresponding native
APIs. Table 11-1 summarizes many of them. The parameters to these APIs are practically identical, except the
return value being NTSTATUS for native APIs instead of BOOL for Windows APIs. Also, BOOL input parameters
to Windows APIs are replaced with BOOLEAN in native APIs.
We’ll start by writing a DisplaySD function that focuses on the two important pieces of a SD: owner and
DACL.
SECURITY_DESCRIPTOR_CONTROL control;
DWORD revision;
if (NT_SUCCESS(RtlGetControlSecurityDescriptor(sd, &control, &revision))) {
printf("Revision: %u Control: 0x%X (%s)\n", revision,
control, SDControlToString(control).c_str());
}
The code gets the length of the SD (RtlLengthSecurityDescriptor) and then the control flags
(RtlGetControlSecurityDescriptor). The SDControlToString function is a little helper that shows the
control flags in human readable form.
PSID sid;
BOOLEAN defaulted;
if (NT_SUCCESS(RtlGetOwnerSecurityDescriptor(sd, &sid, &defaulted))) {
if (sid)
printf("Owner: %ws (%ws) Defaulted: %s\n",
SidToString(sid).c_str(), GetUserNameFromSid(sid).c_str(),
defaulted ? "Yes" : "No");
else
printf("No owner\n");
}
BOOLEAN present;
PACL dacl;
if (NT_SUCCESS(RtlGetDaclSecurityDescriptor(sd, &present, &dacl, &defaulted))) {
if (!present)
printf("NULL DACL - object is unprotected\n");
else {
printf("DACL: ACE count: %d\n", (int)dacl->AceCount);
PACE_HEADER header;
for (int i = 0; i < dacl->AceCount; i++) {
if (NT_SUCCESS(RtlGetAce(dacl, i, (PVOID*)&header))) {
DisplayAce(header, i);
}
}
}
}
An ACE has three pieces: access mask, type (most commonly Allow or Deny), and a SID to which it applies.
Now that we have the DisplaySD function ready, we need to feed it a SD. Getting the security descriptor of
a kernel object can be done with NtQuerySecurityObject:
Chapter 11: Security 326
NTSTATUS NtQuerySecurityObject(
_In_ HANDLE Handle,
_In_ SECURITY_INFORMATION SecurityInformation,
_Out_writes_bytes_opt_(Length) PSECURITY_DESCRIPTOR SecurityDescriptor,
_In_ ULONG Length,
_Out_ PULONG LengthNeeded);
The code prepares a (mostly empty) OBJECT_ATTRIBUTES that has the flag OBJ_CASE_INSENSITIVE so the
search by name is case insensitive, making it more convenient for the caller.
Named objects have an “open” function we can use. Here are a few:
// event
NtOpenEvent(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// mutex
NtOpenMutant(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// job
NtOpenJobObject(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// section
NtOpenSection(&hObject, READ_CONTROL, &objAttr);
Chapter 11: Security 327
if (hObject)
return hObject;
// registry key
NtOpenKey(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// semaphore
NtOpenSemaphore(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// timer
NtOpenTimer(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
The READ_CONTROL generic access mask is what we need to gain access to the SD of the object. The code
could be made somewhat more efficient if the status is captured and compared to STATUS_ACCESS_DENIED.
If that is the returned value, it’s safe to bail early since the correct object is referenced, but access cannot be
granted.
What happens if we call an “open” function for a named object that is of the wrong type? The returned status
is STATUS_OBJECT_TYPE_MISMATCH (0xC0000024), which is an error we ignore and move to the next object
type.
The last object type is a file, which requires special handling. For one, NtOpenFile has extra arguments.
Second, clients may specify a standard Win32 path, e.g. “c:\temp”, but that is not interpreted correctly by
native APIs, and we need to prepend a “\??\” to the path, so that the symbolic links directory is used as a base
for locating drive letters. Here is the code:
Chapter 11: Security 328
IO_STATUS_BLOCK ioStatus;
NtOpenFile(&hObject, FILE_GENERIC_READ, &objAttr, &ioStatus,
FILE_SHARE_READ | FILE_SHARE_WRITE, 0);
if (hObject)
return hObject;
//
// special handling for a drive letter
//
if (name.Length > 4 && path[1] == L':') {
UNICODE_STRING name2;
name2.MaximumLength = name.Length + sizeof(WCHAR) * 4;
name2.Buffer = (PWSTR)RtlAllocateHeap(RtlProcessHeap(), 0,
name2.MaximumLength);
UNICODE_STRING prefix;
RtlInitUnicodeString(&prefix, L"\\??\\");
RtlCopyUnicodeString(&name2, &prefix);
RtlAppendUnicodeStringToString(&name2, &name);
InitializeObjectAttributes(&objAttr, &name2,
OBJ_CASE_INSENSITIVE, nullptr, nullptr);
return hObject;
}
The grunt of the work is to prepend “\??\” to the given path. For flexibility, the code allocates memory
dynamically using RtlAllocateHeap (malloc would work just as well, but I’d like to stick to native APIs if
possible), and then manipulating UNICODE_STRING objects to get the desired result. Finally, NtOpenFile is
called again with the new path, before freeing the allocated buffer.
To tie up everything together, we need to implement our main function. First, getting command line
arguments:
The application accepts a process ID (-p flag), thread ID (-t flag), or an object name. If -p is specified, an
optional handle can be specified as well, as the target handle in the process.
Next, we’ll enable the SeDebug privilege (if available in the caller’s token) to give the app some more power
when trying to open handles to objects:
BOOLEAN enabled;
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
Next, we’ll prepare an empty OBJECT_ATTRIBUTES for opening processes and threads:
OBJECT_ATTRIBUTES emptyAttr;
InitializeObjectAttributes(&emptyAttr, nullptr, 0, nullptr, nullptr);
Next, we’ll start dealing with the given arguments, starting with -p:
if (argc > 2) {
if (_wcsicmp(argv[1], L"-p") == 0) {
CLIENT_ID cid{ ULongToHandle(wcstol(argv[2], nullptr, 0)) };
HANDLE hProcess = nullptr;
NtOpenProcess(&hProcess, argc > 3 ? PROCESS_DUP_HANDLE : READ_CONTROL,
&emptyAttr, &cid);
if (hProcess && argc > 3) {
NtDuplicateObject(hProcess,
ULongToHandle(wcstoul(argv[3], nullptr, 0)),
NtCurrentProcess(), &hObject, READ_CONTROL, 0, 0);
NtClose(hProcess);
}
else {
hObject = hProcess;
}
}
If a process ID is specified, NtOpenProcess is called to open a handle to the process. If an extra argument
is provided (a handle value), it means the invoker is interested in that handle rather the process itself. In
order to access a handle in an arbitrary process we must duplicate it into the current process, which is what
NtDuplicateObject does. If no extra argument is provided, then the process handle is placed in hObject.
else if (argc == 2) {
hObject = OpenNamedObject(argv[1]);
}
OpenNamedObject is the function we saw earlier that attempts to open a named object.
If we couldn’t get a valid handle, then we’re done:
if (!hObject) {
printf("Error opening object\n");
return 1;
}
We have a valid handle, so we can get the SD of that object (if any):
PSECURITY_DESCRIPTOR sd = nullptr;
BYTE buffer[1 << 12];
ULONG needed;
auto status = NtQuerySecurityObject(hObject,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
buffer, sizeof(buffer), &needed);
if (!NT_SUCCESS(status)) {
printf("No security descriptor available (0x%X)\n", status);
}
else {
DisplaySD((PSECURITY_DESCRIPTOR)buffer);
}
The code uses a simple scheme, assuming the SD size is no larger than 4KB (a reasonable assumption). More
robust code would query the SD size, allocate a buffer before retrieving it. I’ll leave that as an exercise for
the reader.
The only thing left to do is cleanup. Here is the full wmain function for easier reference:
Chapter 11: Security 331
BOOLEAN enabled;
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
OBJECT_ATTRIBUTES emptyAttr;
InitializeObjectAttributes(&emptyAttr, nullptr, 0, nullptr, nullptr);
if (argc > 2) {
if (_wcsicmp(argv[1], L"-p") == 0) {
CLIENT_ID cid{ ULongToHandle(wcstol(argv[2], nullptr, 0)) };
HANDLE hProcess = nullptr;
NtOpenProcess(&hProcess, argc > 3 ? PROCESS_DUP_HANDLE : READ_CONTROL,
&emptyAttr, &cid);
if (hProcess && argc > 3) {
NtDuplicateObject(hProcess,
ULongToHandle(wcstoul(argv[3], nullptr, 0)),
NtCurrentProcess(), &hObject, READ_CONTROL, 0, 0);
NtClose(hProcess);
}
else {
hObject = hProcess;
}
}
else if (_wcsicmp(argv[1], L"-t") == 0) {
CLIENT_ID cid{ nullptr, ULongToHandle(wcstol(argv[2], nullptr, 0)) };
NtOpenThread(&hObject, READ_CONTROL, &emptyAttr, &cid);
}
}
else if (argc == 2) {
hObject = OpenNamedObject(argv[1]);
}
if (!hObject) {
Chapter 11: Security 332
PSECURITY_DESCRIPTOR sd = nullptr;
BYTE buffer[1 << 12];
ULONG needed;
auto status = NtQuerySecurityObject(hObject,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
buffer, sizeof(buffer), &needed);
if (!NT_SUCCESS(status)) {
printf("No security descriptor available (0x%X)\n", status);
}
else {
DisplaySD((PSECURITY_DESCRIPTOR)buffer);
}
return 0;
}
>sd -p 540
SD Length: 116 (0x74) bytes
Revision: 1 Control: 0x8004 (DACL Present, Self Relative)
Owner: S-1-5-32-544 (BUILTIN\Administrators) Defaulted: No
DACL: ACE count: 3
ACE 0: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001FFFFF S-1-5-32-544 (BUILTIN\Administrators)
ACE 1: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001FFFFF S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 2: Size: 28 bytes, Flags: 0x00 Type: ALLOW
Access: 0x00121411 S-1-5-5-0-268513 (NT AUTHORITY\LogonSessionId_0_268513)
>sd c:\windows\system32
SD Length: 392 (0x188) bytes
Revision: 1 Control: 0x9404 (DACL Present, DACL Auto Inherited, DACL Protected, Self\
Relative)
Owner: S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464 (NT SERVICE\Tr\
Chapter 11: Security 333
ustedInstaller) Defaulted: No
DACL: ACE count: 13
ACE 0: Size: 40 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F01FF S-1-5-80-956008885-3418522649-1831038044-1853292631-22714\
78464 (NT SERVICE\TrustedInstaller)
ACE 1: Size: 40 bytes, Flags: 0x0A Type: ALLOW
Access: 0x10000000 S-1-5-80-956008885-3418522649-1831038044-1853292631-22714\
78464 (NT SERVICE\TrustedInstaller)
ACE 2: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001301BF S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 3: Size: 20 bytes, Flags: 0x0B Type: ALLOW
Access: 0x10000000 S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 4: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001301BF S-1-5-32-544 (BUILTIN\Administrators)
ACE 5: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0x10000000 S-1-5-32-544 (BUILTIN\Administrators)
ACE 6: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001200A9 S-1-5-32-545 (BUILTIN\Users)
ACE 7: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0xA0000000 S-1-5-32-545 (BUILTIN\Users)
ACE 8: Size: 20 bytes, Flags: 0x0B Type: ALLOW
Access: 0x10000000 S-1-3-0 (\CREATOR OWNER)
ACE 9: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001200A9 S-1-15-2-1 (APPLICATION PACKAGE AUTHORITY\ALL APPLICATION\
PACKAGES)
ACE 10: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0xA0000000 S-1-15-2-1 (APPLICATION PACKAGE AUTHORITY\ALL APPLICATION\
PACKAGES)
ACE 11: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001200A9 S-1-15-2-2 (APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED \
APPLICATION PACKAGES)
ACE 12: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0xA0000000 S-1-15-2-2 (APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED \
APPLICATION PACKAGES)
>sd \kernelobjects\memoryerrors
SD Length: 156 (0x9C) bytes
Revision: 1 Control: 0x8004 (DACL Present, Self Relative)
Owner: S-1-5-32-544 (BUILTIN\Administrators) Defaulted: No
DACL: ACE count: 5
ACE 0: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x00120001 S-1-1-0 (\Everyone)
Chapter 11: Security 334
>sd -p 57716 8
SD Length: 20 (0x14) bytes
Revision: 1 Control: 0x8000 (Self Relative)
No owner
NULL DACL - object is unprotected
12.5: Summary
This chapter examined some of the Security related native APIs. Many security-related Windows APIs
provide a thin wrapper around these native functions, but some are more specialized and don’t have a
Windows API equivalent, such as NtCreateToken.
Chapter 12: Memory (Part 2)
Chapter 8 dealt with the standard memory related native APIs. This chapter focuses on Section kernel objects,
which provide the ability to map files to memory and to share memory between processes. Less known
memory management objects provided by NtDll.Dll are described - memory zones and lookaside lists.
In this chapter
• Sections
• Memory Zones
• Lookaside Lists
13.1: Sections
Section kernel objects provide the ability to map files to memory, while at the same time allowing sharing
that memory with multiple processes. The file used by a section may be the page file - that is, the memory is
backed by the page file, which means that once the section object is destroyed, the memory is lost.
The term “the page file” implies there is only one, but in fact Windows supports up to 16 page files.
A section can use any page file space.
In this section (no pun intended), we’ll cover the native API as it pertains to section objects. Some of the APIs
are documented indirectly in the Windows Driver Kit, which we’ll point out explicitly.
NTSTATUS NtCreateSection(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PLARGE_INTEGER MaximumSize,
_In_ ULONG SectionPageProtection,
_In_ ULONG AllocationAttributes,
_In_opt_ HANDLE FileHandle);
This function is documented in the WDK (under ZwCreateSection). The Windows APIs CreateFileMap-
ping(Numa) call NtCreateSection.
SectionHandle is the resulting handle upon success. DesiredAccess is the requested access, typically
SECTION_ALL_ACCESS if creating a new section.Other section access bits are defined in WinNt.h ( SECTION_-
MAP_READ, SECTION_MAP_WRITE, SECTION_MAP_EXECUTE, SECTION_QUERY, and SECTION_EXTEND_SIZE).
ObjectAttributes is the usual OBJECT_ATTRIBUTES, that if specified can set a name for the section. If
the section exists, NtCreateSection fails, unless the attributes provided in OBJECT_ATTRIBUTES contain the
OBJ_OPENIF flag, in which case a handle will be returned (assuming the caller can have the requested access).
MaximumSize specifies the maximum memory size - if backed by the page file, or sets the file size - if backed
by a file. SectionPageProtection is one of the page protection constants, typically PAGE_READWRITE or
PAGE_READONLY (if mapping a file for read only access).
NTSTATUS NtCreateSectionEx(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PLARGE_INTEGER MaximumSize,
_In_ ULONG SectionPageProtection,
_In_ ULONG AllocationAttributes,
_In_opt_ HANDLE FileHandle,
_Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParamCount);
If a section object is to be located by name, NtOpenSection can be used, where failure to locate the object
with the desired access fails the call:
NTSTATUS NtOpenSection(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
Once a section handle is available, the next step is to use it to map the memory represented by the section,
either the entire section or just a part, into a process address space. This is the roles of NtMapViewOfSec-
tion(Ex):
NTSTATUS NtMapViewOfSection(
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_In_ SIZE_T CommitSize,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ SECTION_INHERIT InheritDisposition,
_In_ ULONG AllocationType,
_In_ ULONG Win32Protect);
NTSTATUS NtMapViewOfSectionEx( // Win 10 version 1803+
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ ULONG AllocationType,
_In_ ULONG PageProtection,
_Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParamCount);
Chapter 12: Memory (Part 2) 338
SectionHandle is the section handle representing the mapping. ProcessHandle represents the process into
which to map the memory. Typically, this will be the calling process (NtCurrentProcess), but could be
another process where the handle must have the PROCESS_VM_OPERATION access mask.
BaseAddress is an input/output value, indicating the address in that target process to use for the mapping.
If NULL is specified, the system will find a range in the process address space. Otherwise, the specific address
will be used (rounded down to a page boundary), and if address range is not available, the mapping fails.
ZeroBits indicates the number of high-order bits that must be zero in the resulting address (if *BaseAddress
is NULL). Typically, zero is specified, indicating the caller doesn’t have any restrictions.
CommitSize is the initial memory to commit for this view (rounded up to page boundary), which has meaning
only for a page file-backed section. Typical value is zero, which will cause the memory to be committed “on
demand” (when accessed).
SectionOffset is the offset to start the mapping from (rounded down to page boundary), If NULL is passed in,
the mapping starts at offset zero (beginning of the section). ViewSize is an input/output parameter indicating
the size to map, returning the actual size being mapped. If zero is specified, the view will be mapped from
SectionOffset to the end of the section. Otherwise, the size will be rounded up to a page boundary.
InheritDisposition indicates whether the view should be inherited by all child processes ( ViewShare)
or not (ViewUnmap). AllocationFlags is typically zero, but may contain flags such as MEM_LARGE_PAGES,
MEM_TOP_DOWN, and MEM_RESERVE, among others. Refer to the docs for VirtualAlloc for more details.
Finally, Win32Protect is the page protection requested for the mapped view (e.g., PAGE_READONLY, PAGE_-
READWRITE), which must be compatible with the initial section creation protection flags. The actual result
from a successful call to NtMapViewOfSection is in *BaseAddress - this is the address to use for accessing
the memory.
With NtMapViewOfSectionEx, more customization is possible using the same MEM_EXTENDED_PARAMETER
array we met before. Check out the documentation for MapViewOfFile2 for more information.
Once a view is no longer needed, it should be unmapped by calling NtUnmapViewOfSection(Ex):
NTSTATUS NtUnmapViewOfSection(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress);
NTSTATUS NtUnmapViewOfSectionEx(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ ULONG Flags);
The required parameters are the process handle and the base address that was returned from NtMapViewOf-
Section(Ex). NtUnmapViewOfSection calls NtUnmapViewOfSectionEx with Flags set to zero. Other
available flags include MEM_UNMAP_WITH_TRANSIENT_BOOST (1), which indicates that a temporary page
Chapter 12: Memory (Part 2) 339
priority boost should apply to the paged being unmapped because the caller expects to remap these pages
again soon, and MEM_PRESERVE_PLACEHOLDER (2), which would return the pages to a “placeholder” state.
Check out the docs for MapViewOfFile2 for more information.
The following demo project uses the main section APIs described previously to create a simple two process
“chat”, where each process passes a string to the other using shared memory.
We’ll use an event object to synchronize access to the shared memory for read/write. When one process is
done writing, the other process reads the data, and then they switch roles.
We need to create the section object with a name so it’s easy to share. The name should be visible in the
session’s namespace, which means we need to retrieve the base directory of the current session like so:
#include <string>
#include <format>
std::wstring GetBaseDirectory() {
ULONG session;
if (!NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(),
ProcessSessionInformation, &session, sizeof(session), nullptr)))
return L"";
int main() {
auto baseDir = GetBaseDirectory();
if (baseDir.empty())
return 1;
HANDLE hSection;
OBJECT_ATTRIBUTES secAttr;
UNICODE_STRING secName;
auto fullSecName = baseDir + L"MySharedMem";
RtlInitUnicodeString(&secName, fullSecName.c_str());
InitializeObjectAttributes(&secAttr, &secName, OBJ_OPENIF, nullptr, nullptr);
Notice the OBJ_OPENIF flag indicating that if the section name already exists, open a handle rather than
failing.
Now we can create/open the section with a size of 8KB (could be any size):
LARGE_INTEGER size;
size.QuadPart = 1 << 13; // 8KB
If the object existed before the call to NtCreateSection, the current process will start as a reader, and later
will wait for a notification:
Now we’re ready to map a view into the current process. We’ll map the entire section as it’s very small (8KB):
Chapter 12: Memory (Part 2) 341
Next, we’ll create the event object used for synchronization named “MySharedMemDataReady”:
OBJECT_ATTRIBUTES evtAttr;
UNICODE_STRING evtName;
auto fullEvtName = baseDir + L"MySharedMemDataReady";
RtlInitUnicodeString(&evtName, fullEvtName.c_str());
InitializeObjectAttributes(&evtAttr, &evtName, OBJ_OPENIF, nullptr, nullptr);
HANDLE hEvent;
status = NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &evtAttr,
SynchronizationEvent, FALSE);
if (!NT_SUCCESS(status)) {
printf("Failed to create/open event (0x%X)\n", status);
return status;
}
At this point we’re ready to exchange information by writing/reading into the shared memory:
char text[128];
for(;;) {
if (wait) {
printf("Waiting for data...\n");
NtWaitForSingleObject(hEvent, FALSE, nullptr);
printf("%s\n", (PCSTR)address);
}
else {
printf("> ");
gets_s(text);
strcpy_s((PSTR)address, sizeof(text), text);
NtSetEvent(hEvent, nullptr);
if (strcmp(text, "quit") == 0)
break;
}
wait = !wait;
}
Chapter 12: Memory (Part 2) 342
The currently waiting process calls NtWaitForSingleObject to wait for the event to be signaled, and then
displays the shared memory with printf. Otherwise, text is input from the user, and written to the shared
memory, after which the event is signaled (KeSetEvent).
When “quit” is entered, the loop exits, in which point we can do some cleanup:
NtUnmapViewOfSection(NtCurrentProcess(), address);
NtClose(hEvent);
NtClose(hSection);
int main() {
auto baseDir = GetBaseDirectory();
if (baseDir.empty())
return 1;
HANDLE hSection;
OBJECT_ATTRIBUTES secAttr;
UNICODE_STRING secName;
auto fullSecName = baseDir + L"MySharedMem";
RtlInitUnicodeString(&secName, fullSecName.c_str());
InitializeObjectAttributes(&secAttr, &secName,
OBJ_OPENIF, nullptr, nullptr);
LARGE_INTEGER size;
size.QuadPart = 1 << 13; // 8KB
if (!NT_SUCCESS(status)) {
printf("Failed to map section (0x%X)\n", status);
return status;
}
OBJECT_ATTRIBUTES evtAttr;
UNICODE_STRING evtName;
auto fullEvtName = baseDir + L"MySharedMemDataReady";
RtlInitUnicodeString(&evtName, fullEvtName.c_str());
InitializeObjectAttributes(&evtAttr, &evtName,
OBJ_OPENIF, nullptr, nullptr);
HANDLE hEvent;
status = NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &evtAttr,
SynchronizationEvent, FALSE);
if (!NT_SUCCESS(status)) {
printf("Failed to create/open event (0x%X)\n", status);
return status;
}
char text[128];
for(;;) {
if (wait) {
printf("Waiting for data...\n");
NtWaitForSingleObject(hEvent, FALSE, nullptr);
printf("%s\n", (PCSTR)address);
}
else {
printf("> ");
gets_s(text);
strcpy_s((PSTR)address, sizeof(text), text);
NtSetEvent(hEvent, nullptr);
if (strcmp(text, "quit") == 0)
break;
}
wait = !wait;
}
NtUnmapViewOfSection(NtCurrentProcess(), address);
NtClose(hEvent);
NtClose(hSection);
return 0;
Chapter 12: Memory (Part 2) 344
NTSTATUS NtQuerySection(
_In_ HANDLE SectionHandle,
_In_ SECTION_INFORMATION_CLASS SectionInformationClass,
_Out_writes_bytes_(SectionInformationLength) PVOID SectionInformation,
_In_ SIZE_T SectionInformationLength,
_Out_opt_ PSIZE_T ReturnLength);
SectionHandle must have the SECTION_QUERY access mask for the API to work.
The SectionBasicInformation information class requires a SECTION_BASIC_INFORMATION structure:
BaseAddress is the based address used if the SEC_BASED allocation attribute was specified when creating the
section (this is the address to map in any process sharing the section). AllocationAttributes are the flags
used to create the section (e.g., SEC_COMMIT, SEC_BASED). Finally, MaximumSize is the maximum size of the
memory that can be mapped with the section.
The SectionImageInformation information class returns a SECTION_IMAGE_INFORMATION:
Chapter 12: Memory (Part 2) 345
This information class is valid only for a section mapping a file as an image ( SEC_IMAGE attribute). The
Chapter 12: Memory (Part 2) 346
following example shows mapping an image and reading the image information (error handling omitted):
// open file
HANDLE hFile;
OBJECT_ATTRIBUTES fileAttr;
UNICODE_STRING fileName;
RtlInitUnicodeString(&fileName, L"\\SystemRoot\\System32\\kernelbase.dll");
InitializeObjectAttributes(&fileAttr, &fileName, 0, nullptr, nullptr);
IO_STATUS_BLOCK ioStatus;
NtOpenFile(&hFile, FILE_READ_ACCESS, &fileAttr, &ioStatus, FILE_SHARE_READ, 0);
SECTION_IMAGE_INFORMATION sii;
NtQuerySection(hSection, SectionImageInformation, &sii, sizeof(sii), nullptr);
Unfortunately, the SectionInternalImageInformation does not work - this information is not available
outside of the kernel.
The last two information classes, SectionRelocationInformation and SectionOriginalBaseInforma-
tion also apply to images only. SectionRelocationInformation returns the address into which the image
Chapter 12: Memory (Part 2) 347
is loaded (SIZE_T), while SectionOriginalBaseInformation returns the original load address of the image
(PVOID).
The NtExtendSection can extend the size of a file-backed section only (not mapped as image):
NTSTATUS NtExtendSection(
_In_ HANDLE SectionHandle,
_Inout_ PLARGE_INTEGER NewSectionSize);
The section handle must have the SECTION_EXTEND_SIZE access mask. If the provided size is smaller than
the current section size, the call is ignored.
Finally, the NtAreMappedFilesTheSame function checks if both addresses provided are mapping the same
file:
NTSTATUS NtAreMappedFilesTheSame(
_In_ PVOID File1MappedAsAnImage,
_In_ PVOID File2MappedAsFile);
The first address must be within a section mapping a file as image; it doesn’t have to be the start address.
The second address must be within a section mapping a file (either image or not). If these addresses refer to
the same file, STATUS_SUCCESS is returned. Otherwise, STATUS_NOT_SAME_DEVICE is returned.
NTSTATUS RtlCreateMemoryZone(
_Out_ PVOID *MemoryZone,
_In_ SIZE_T InitialSize,
_Reserved_ ULONG Flags); // no flags currently defined
MemoryZone is teh returned opaque pointer to the allocated memory zone object, consisting of the initial
buffer size (InitialSize bytes) and the management structure itself. The function allocates (commits) the
InitialSize bytes rounded up to the next page boundary (and taking into consideration the management
structure).
Chapter 12: Memory (Part 2) 348
The management structure’s definition, RTL_MEMORY_ZONE, can be found as part of the PHNT library,
but it’s best to keep it opaque in case it changes in future version of Windows.
Once the memory zone is created, memory can be allocated by calling RtlAllocateMemoryZone:
NTSTATUS RtlAllocateMemoryZone(
_In_ PVOID MemoryZone,
_In_ SIZE_T BlockSize,
_Out_ PVOID *Block);
The BlockSize is the requested allocation in bytes. The returned pointer on success is in *Block. The
allocation fails if the added request exceeds the current memory zone size. In such a case, the caller can use
RtlExtendMemoryZone to increase its size before trying the allocation again:
NTSTATUS RtlExtendMemoryZone(
_In_ PVOID MemoryZone,
_In_ SIZE_T Increment);
At the time of writing, RtlExtendMemoryZone is not provided in the PHNT headers. Just add the
declaration above to use it.
This call frees all the memory allocated by the memory zone. If the memory zone is to be reused, it’s possible
to reset its contents (empty memory) without destroying it:
Currently, RtlResetMemoryZone does not seem to be part of the Ntdll.lib import library provided
with Visual Studio. Unless you create your own import library, you’ll have to bind to the API
dynamically, for example like so:
Chapter 12: Memory (Part 2) 349
Finally, a memory zone can be locked into physical memory by calling RtlLockMemoryZone, and later
unlocked with RtlUnlockMemoryZone. Each lock/unlock increments/decrements an internal lock count:
int main() {
PVOID zone;
auto status = RtlCreateMemoryZone(&zone, 1 << 12, 0);
if (!NT_SUCCESS(status)) {
printf("Failed to create memory zone (0x%X)\n", status);
return status;
}
Next, we’ll create some allocations, and if any fails, extend the memory zone and try again:
PCHAR buffers[100]{};
Now we can walk over the allocations and access their contents:
Chapter 12: Memory (Part 2) 350
RtlDestroyMemoryZone(zone);
return 0;
}
NTSTATUS RtlCreateMemoryBlockLookaside(
_Out_ PVOID *MemoryBlockLookaside,
_Reserved_ ULONG Flags, // no flags currently defined
_In_ ULONG InitialSize,
_In_ ULONG MinimumBlockSize,
_In_ ULONG MaximumBlockSize);
The lookaside management object is returned via the first parameter. InitialSize is the size to allocate
(commit) upfront for the lookaside. MinimumBlockSize and MaximumBlockSize represent the range of valid
allocations using this lookaside list. These values are rounded up (maximum) / down (minimum) to the
closest power of two.
Once created, blocks can be allocated with RtlAllocateMemoryBlockLookaside:
NTSTATUS RtlAllocateMemoryBlockLookaside(
_In_ PVOID MemoryBlockLookaside,
_In_ ULONG BlockSize,
_Out_ PVOID *Block);
BlockSize is the block size in bytes to allocate, which must be in the range specified when creating the
lookaside list object. The returned value on successful allocation is in *Block. If the lookaside is full (really
the internal memory zone), the allocation fails. In that case, you can extend the lookaside list.
To free a block (mark it as available), call RtlFreeMemoryBlockLookaside:
Chapter 12: Memory (Part 2) 351
NTSTATUS RtlFreeMemoryBlockLookaside(
_In_ PVOID MemoryBlockLookaside,
_In_ PVOID Block);
Block is a returned pointer from RtlAllocateMemoryBlockLookaside. It’s returned to the pool of availale
blocks. (The blocks are managed in buckets based on their size.)
Extending the lookaside list is needed if more memory is required than was initially requested:
NTSTATUS RtlExtendMemoryBlockLookaside(
_In_ PVOID MemoryBlockLookaside,
_In_ ULONG Increment);
These are thin wrappers around the memory zone used under the hood.
Finally, a lookaside list can be destroyed completely:
13.4: Summary
This chapter concludes the APIs that deal with memory management in some way. Sections are kernel
objects, while memory zones and lookaside lists are object types implemented internally by NtDll.dll - these
are not kernel objects at all, but provide useful services for applications.
Chapter 13: The Registry
The Registry is one of the most recognizable aspects of Windows. The Registry is a hierarchical database,
used to store system and user information. This chapter looks at the structure of the Registry, and examines
the native APIs used to work with the Registry.
Many of the Registry native API are documented in the Windows Driver Kit, where Nt is replaced
with Zw.
In this chapter:
• Registry Structure
• Creating and Opening Keys
• Working with Keys and Values
• Key Information
• Other Registry Functions
• Key Persistence
• Registry Notifications
• Registry Transactions
• Higher Level Registry Helpers
This view of the Registry is just that - a view - it’s not the “true” Registry structure. We can see the true view
with my TotalRegistry tool, that shows both the “true” view and the abstraction used by the Windows API
(figure 13-2).
Chapter 13: The Registry 354
The root of the Registry is named “Registry”, and it has the following subkeys:
• MACHINE - the machine hive. Equivalent to HKEY_LOCAL_MACHINE using the abstracted Registry
view.
• USER - list of profiles for users on this machine. Equivalent to HKEY_USERS in the abstracted view.
• A - this hive is used by UWP processes for private data. It’s inaccessible by any other process. This key
may not exist on all systems.
• WC - optional key used to store information for Windows Containers (hence “WC”), also called Server
Silos.
When working with native Registry APIs, Registry paths must be specified using the “true” Registry
structure. For example, HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services (abstracted view)
must be specified as \REGISTRY\MACHINE\System\CurrentControlSet\Services for native APIs.
NTSTATUS NtOpenKey(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
NTSTATUS NtOpenKeyEx(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ ULONG OpenOptions);
KeyHandle is where the where the returned handle is placed on success. DesiredAccess is the access required
for the key, typically KEY_READ and/or KEY_WRITE. The key path itself is stored in ObjectAttributes, as
we’ve seen many times before.
The following example shows how to open a read-only handle to \Registry\Machine\System\CurrentControlSet\Services:
HANDLE hKey;
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName,
L"\\Registry\\Machine\\System\\CurrentControlSet\\Services");
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0, nullptr, nullptr);
auto status = NtOpenKey(&hKey, KEY_READ, &keyAttr);
Note that the key path is not case sensitive, regardless of the use of the OBJ_CASE_INSENSITIVE attribute
within OBJECT_ATTRIBUTES. If the key does not exist, the call fails.
Using a relative path is supported by setting the RootDirectory member of OBJECT_ATTRIBUTES and using
a relative name. Here is an example for opening the ACPI subkey of the above key using a relative path:
keyAttr.RootDirectory = hKey;
HANDLE hSubKey;
RtlInitUnicodeString(&keyName, L"ACPI");
status = NtOpenKey(&hSubKey, KEY_READ, &keyAttr);
• REG_OPTION_BACKUP_RESTORE - supports opening the key with read access if the SeBackupPrivilege
and/or write access if the SeRestorePrivilege is available and enabled in the caller’s token. In this case,
DesiredAccess is ignored.
• REG_OPTION_OPEN_LINK - if the key is a link key, don’t follow the link and instead open the key itself.
This also requires setting OBJ_OPENLINK as part of the attributes in the OBJECT_ATTRIBUTES structure.
Chapter 13: The Registry 356
• REG_OPTION_DONT_VIRTUALIZE - indicates that the key path should not be virtualized. This is related
to User Access Control (UAC) virtualization, where a virtualized process writing to local machine hive
“succeeds” by writing to a per-user location. For more information, look for “UAC virtualization” online.
NTSTATUS NtCreateKey(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Reserved_ ULONG TitleIndex, // not used
_In_opt_ PUNICODE_STRING Class,
_In_ ULONG CreateOptions,
_Out_opt_ PULONG Disposition);
NtCreateKey creates or opens a key given its path stored in the usual ObjectAttributes. The first three
parameters in fact are identical to NtOpenKey. Class is an optional string that is attached as a kind of
“metadata” to the created key - it serves no other purpose - the application can use as a somewhat hidden
string value. The value can be later retrieved with NtQueryKey (see the section Key Information later in this
chapter).
CreateOptions can be zero or a combination of flags - three of them are described in the previous section.
Additional flags follow:
• REG_OPTION_NON_VOLATILE - indicates the key is non-volatile. That is, it’s persisted across boots. This
is the default, as the value of this flag is zero.
• REG_OPTION_VOLATILE - indicates the key is volatile, which means it will be deleted when the system
shuts down.
• REG_OPTION_CREATE_LINK - creates a link key, rather than a normal key. A special value named
“SymbolicLinkName” must be set to full key path to the target of the key.
Finally, Disposition is an optional return value that indicates if the key was actually created ( REG_CRE-
ATED_NEW_KEY), or it has been opened because it existed before the call ( REG_OPENED_EXISTING_KEY).
NTSTATUS NtQueryValueKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING ValueName,
_In_ KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
_Out_writes_bytes_opt_(Length) PVOID KeyValueInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
NTSTATUS NtSetValueKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING ValueName,
_In_opt_ ULONG TitleIndex, // not used
_In_ ULONG Type,
_In_reads_bytes_opt_(DataSize) PVOID Data,
_In_ ULONG DataSize);
NtSetValueKey is pretty straightforward. KeyHandle must have the KEY_SET_VALUE access mask (included
as part of KEY_WRITE, which is more commonly used). ValueName is the value to set. If it exists, it’s
overridden. Type is the type of the value, one of the data types supported by the Registry, such as REG_SZ,
REG_DWORD, etc. See the Windows SDK documentation for the full list.
Finally, Data is a pointer to the data whose size if DataSize (in bytes). Note that strings (REG_SZ,REG_EX-
PAND_SZ, and REG_MULTI_SZ) are NULL-terminated UTF-16 strings (rather than UNICODE_STRING structures).
NtQueryValueKey is more involved. It accepts a key handle (with KEY_QUERY_VALUE access mask at
least, part of the more commonly used KEY_READ), the value name (ValueName), and an enumeration (in
KeyValueInformationClass) indicating what kind of information is requested:
As you can see, these are variable-length structures, as they hold name information and (for KEY_VALUE_-
FULL_INFORMATION) the data. This means that the caller must provide a buffer large enough to hold the
requested information. If the buffer is too small, the STATUS_BUFFER_OVERFLOW error status is returned. In
that case, *ResultLength returns the required size in bytes.
The information itself is returned in KeyValueInformation if the call succeeds. The Name field in
KEY_VALUE_BASIC_INFORMATION/KEY_VALUE_FULL_INFORMATION immediately follows the name length (in
bytes), and is not NULL-terminated. With KEY_VALUE_FULL_INFORMATION, the data itself follows the name,
although the DataOffset field should be used as the offset from the beginning of the structure to the actual
data. DataLength is the data size in bytes, and Type is the data type (REG_DWORD, REG_SZ, etc.).
The following example queries a couple of values and displays them:
HANDLE hKey;
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName,
L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\ACPI");
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0, nullptr, nullptr);
auto status = NtOpenKey(&hKey, KEY_READ, &keyAttr);
if (NT_SUCCESS(status)) {
UNICODE_STRING valueName;
RtlInitUnicodeString(&valueName, L"ImagePath");
ULONG len = 0;
//
// call NtQueryValueKey twice. First, to get the requires length
//
NtQueryValueKey(hKey, &valueName, KeyValueFullInformation, nullptr, 0, &len);
Chapter 13: The Registry 359
if (len) {
//
// allocate the buffer
//
auto info = (KEY_VALUE_FULL_INFORMATION*)RtlAllocateHeap(
NtCurrentPeb()->ProcessHeap, 0, len);
if (NT_SUCCESS(NtQueryValueKey(hKey, &valueName,
KeyValueFullInformation, info, len, &len))) {
_ASSERTE(info->Type == REG_SZ || info->Type == REG_EXPAND_SZ);
printf("ImagePath: %ws\n", (PCWSTR)((PBYTE)info + info->DataOffset));
}
RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, info);
}
RtlInitUnicodeString(&valueName, L"Type");
len = 0;
NtQueryValueKey(hKey, &valueName, KeyValueFullInformation, nullptr, 0, &len);
if (len) {
auto info = (KEY_VALUE_FULL_INFORMATION*)RtlAllocateHeap(
NtCurrentPeb()->ProcessHeap, 0, len);
if (NT_SUCCESS(NtQueryValueKey(hKey, &valueName,
KeyValueFullInformation, info, len, &len))) {
_ASSERTE(info->Type == REG_DWORD);
printf("Type: %u\n", *(DWORD*)((PBYTE)info + info->DataOffset));
}
RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, info);
}
NtClose(hKey);
}
ImagePath: System32\drivers\ACPI.sys
Type: 1
Sometimes there is a need to query multiple values in the same key. Although making multiple calls to
NtQueryValueKey is possible, it could be made more efficient if a single call would be made instead. This is
the role of NtQueryMultipleValueKey:
Chapter 13: The Registry 360
NTSTATUS NtQueryMultipleValueKey(
_In_ HANDLE KeyHandle,
_Inout_updates_(EntryCount) PKEY_VALUE_ENTRY ValueEntries,
_In_ ULONG EntryCount,
_Out_writes_bytes_(*BufferLength) PVOID ValueBuffer,
_Inout_ PULONG BufferLength,
_Out_opt_ PULONG RequiredBufferLength);
NtQueryMultipleValueKey accepts an array of KEY_VALUE_ENTRY, each one specifying a value to read. The
resulting data and data type are provided in the same structure upon success. Just like with NtQueryValueKey,
the buffer provided by the caller must be large enough to hold the results.
The following examples reads the same two values from the previous code example, but uses the more efficient
NtQueryMultipleValueKey (some error handling omitted):
}
// display results
_ASSERTE(entry[0].Type == REG_SZ || entry[0].Type == REG_EXPAND_SZ);
_ASSERTE(entry[1].Type == REG_DWORD);
printf("ImagePath: %ws\n", (PCWSTR)(info + entry[0].DataOffset));
printf("Type: %u\n", *(DWORD*)(info + entry[1].DataOffset));
NTSTATUS NtEnumerateKey(
_In_ HANDLE KeyHandle,
_In_ ULONG Index,
_In_ KEY_INFORMATION_CLASS KeyInformationClass,
_Out_writes_bytes_opt_(Length) PVOID KeyInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
The KeyHandle must have the KEY_ENUMERATE_SUB_KEY access mask (which is part of the more commonly
used KEY_READ). Index is the index of a sub key. For standard enumeration, start with zero and increment
Index by one until the function returns STATUS_NO_MORE_ENTRIES. For each key, the type of information
returned is based on KeyInformationClass, and the data itself is placed in KeyInformation. The size of the
buffer pointed to by KeyInformation is specified in Length. If the buffer is too small, the function returns
STATUS_BUFFER_OVERFLOW, and *ResultLength indicates the required buffer size.
FormatTime is a little helper to convert a 64-bit date/time value to a human readable form:
Chapter 13: The Registry 363
NTSTATUS NtEnumerateValueKey(
_In_ HANDLE KeyHandle,
_In_ ULONG Index,
_In_ KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
_Out_writes_bytes_opt_(Length) PVOID KeyValueInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
The idea is similar to key enumeration. The provided Index should start at zero, and incremented as long
as the returned status is not STATUS_NO_MORE_ENTRIES. The information retrieved is based on the KEY_-
VALUE_INFORMATION_CLASS and its associated structure(s).
The following example enumerates all values of a given key and displays each value’s name and data:
DisplayData(info);
}
return STATUS_SUCCESS;
}
RegistryTypeToString returns a string representation of registry data type, and DisplayData displays the
data itself based on its type:
case REG_MULTI_SZ:
{
auto s = (PCWSTR)p;
while (*s) {
printf("%ws ", s);
s += wcslen(s) + 1;
}
printf("\n");
break;
}
case REG_DWORD:
printf("%u (0x%X)\n", *(DWORD*)p, *(DWORD*)p);
break;
case REG_QWORD:
printf("%llu (0x%llX)\n", *(ULONGLONG*)p, *(ULONGLONG*)p);
break;
case REG_BINARY:
case REG_FULL_RESOURCE_DESCRIPTOR:
Chapter 13: The Registry 365
case REG_RESOURCE_LIST:
case REG_RESOURCE_REQUIREMENTS_LIST:
auto len = min(64, info->DataLength);
for (DWORD i = 0; i < len; i++) {
printf("%02X ", p[i]);
}
printf("\n");
break;
}
}
The full sample is in the EnumKeyValues project, where the command line accepts a key path and displays
all values within that key. Here is some example output:
c:\>EnumKeyValues \registry\machine\system\currentcontrolset\services\acpi
Name: ImagePath Type: REG_EXPAND_SZ (2) Data Size: 52 bytes Data: System32\drivers\A\
CPI.sys
Name: Type Type: REG_DWORD (4) Data Size: 4 bytes Data: 1 (0x1)
Name: Start Type: REG_DWORD (4) Data Size: 4 bytes Data: 0 (0x0)
Name: ErrorControl Type: REG_DWORD (4) Data Size: 4 bytes Data: 3 (0x3)
Name: DisplayName Type: REG_SZ (1) Data Size: 94 bytes Data: @acpi.inf,%ACPI.SvcDesc\
%;Microsoft ACPI Driver
Name: Owners Type: REG_MULTI_SZ (7) Data Size: 20 bytes Data: acpi.inf
Name: Tag Type: REG_DWORD (4) Data Size: 4 bytes Data: 2 (0x2)
Name: Group Type: REG_SZ (1) Data Size: 10 bytes Data: Core
04 00 00 00 30 00 33 00 36 00 31 00 32 00 2D 00 30 00 33 00 33 00 ...
Name: InstallTime Type: REG_QDWORD (11) Data Size: 8 bytes Data: 132760139614416808 \
(0x1D7A8A4C20C6FA8)
Name: DisplayVersion Type: REG_SZ (1) Data Size: 10 bytes Data: 22H2
Name: RegisteredOwner Type: REG_SZ (1) Data Size: 12 bytes Data: Pavel
Name: WinREVersion Type: REG_SZ (1) Data Size: 32 bytes Data: 10.0.19041.3920
NTSTATUS NtQueryKey(
_In_ HANDLE KeyHandle,
_In_ KEY_INFORMATION_CLASS KeyInformationClass,
_Out_writes_bytes_opt_(Length) PVOID KeyInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
KeyHandle is the usual key handle whose access mask can be any non-zero value for KeyNameInformation
and KeyHandleTagsInformation, and KEY_QUERY_VALUE for all other information classes. KeyInformation
is a pointer to receive the data, and Length is the maximum size expected by the caller. The actual size is
returned in *ResultLength. If the size is not known in advance, KeyInformation can be NULL, Length zero
and *ResultLength will return the needed size.
KeyBasicInformation (0) is the simplest information class requiring KEY_BASIC_INFORMATION that we
encountered before:
It provides the last write time in the key (in the usual 100 nsec units from 1/1/1601), and the name of the key
in Name; TitleIndex seems to be always zero even if it’s set explicitly to another value.
KeyNodeInformation (1) adds the “class” that was set for the key:
Chapter 13: The Registry 367
It provides the class name, maximum name length (in that key), maximum class length (in that key), the
number of subkeys (SubKeys), number of values (Values), maximum value name (in the key), and maximum
value data length (in that key).
The sample project KeyInfo displays key information taking advantage of the above information classes given
a key path.
Here is the function displaying the information given a key handle:
}
if (NT_SUCCESS(NtQueryKey(hKey, KeyFullInformation,
buffer, sizeof(buffer), &len))) {
auto info = (KEY_FULL_INFORMATION*)buffer;
if (info->ClassLength) {
printf("Class: %ws\n", std::wstring(
info->Class, info->ClassLength / sizeof(WCHAR)).c_str());
}
printf("Subkeys: %u\n", info->SubKeys);
printf("Values: %u\n", info->Values);
printf("Max class length: %u\n", info->MaxClassLen);
printf("Max name length: %u\n", info->MaxNameLen);
printf("Max value name length: %u\n", info->MaxValueNameLen);
printf("Max data length: %u\n", info->MaxValueDataLen);
}
}
c:\>keyinfo \registry\machine\system\currentcontrolset\services
Name: Services
Write time: 2024/06/09 23:16:27.020
Subkeys: 937
Values: 0
Max class length: 0
Max name length: 108
Max value name length: 0
Max data length: 0
c:\>keyinfo \registry\machine\system\currentcontrolset\services\acpi
Name: ACPI
Write time: 2024/06/07 21:53:14.006
Chapter 13: The Registry 369
Subkeys: 2
Values: 8
Max class length: 0
Max name length: 20
Max value name length: 24
Max data length: 94
This just returns the key name. Note that the returned name is the full key name. Contrast that with KEY_-
BASIC_INFORMATION, where the returned name is just the final part.
Next, we have KeyCachedInformation (4). This information class only applies to predefined keys (like
“\Registry\Machine”) and returns KEY_CACHED_INFORMATION:
The information is similar to KEY_FULL_INFORMATION, but lacks any class name information and the name
itself.
KeyFlagsInformation (5) returns a KEY_FLAGS_INFORMATION:
KeyFlags is zero or a combination of REG_FLAG_VOLATILE (=1, volatile key), and REG_FLAG_LINK (=2, link
key). ControlFlags is zero or a combination of the following values: REG_KEY_DONT_VIRTUALIZE (=2, don’t
virtualize the key), REG_KEY_DONT_SILENT_FAIL (=4, force access check to fail without a true check), REG_-
KEY_RECURSE_FLAG (=8, get properties from parent key). Wow64Flags is used for user flags, so that anyone
can use these flags for whatever purpose.
The KeyVirtualizationInformation (6) information class returns virtualization information with KEY_-
VIRTUALIZATION_INFORMATION:
// key is a virtual key. Can be 1 only if above 2 are 0. Valid only on the virtual s\
tore key handles
ULONG VirtualTarget: 1;
These flags are documented as part of ZwQueryKey in the WDK. The above comments summarize the
information.
The KeyHandleTagsInformation (7) information class returns the following simple structure:
This “tags” value is used to store extra information for keys, such as the KEY_WOW64_32KEY flag.
The KeyTrustInformation (8) information class returns fills a KEY_TRUST_INFORMATION structure:
Chapter 13: The Registry 371
The only detail is TrustedKey, which seems to be 1 for the machine hive and subkeys, and 0 for keys under
\Registry\User.
Finally, the KeyLayerInformation (9) information class fills a KEY_LAYER_INFORMATION structure:
NTSTATUS NtSetInformationKey(
_In_ HANDLE KeyHandle,
_In_ KEY_SET_INFORMATION_CLASS KeySetInformationClass,
_In_reads_bytes_(KeySetInformationLength) PVOID KeySetInformation,
_In_ ULONG KeySetInformationLength);
KeyHandle is a handle to the key to modify which can have any non-zero access for KeySetHandleTagsIn-
formation and KEY_SET_VALUE for all others. Let’s examine the various information classes.
KeyWriteTimeInformation (0) allows setting the write time of the key explicitly by passing a KEY_WRITE_-
TIME_INFORMATION, essentially a 64-bit value:
Chapter 13: The Registry 372
The KeyWow64FlagsInformation (1) information class allows changing the “user flags” associated with the
key by passing a 32-bit value:
The KeySetVirtualizationInformation (3) information class stores virtualization flags for the key:
The KeySetHandleTagsInformation (5) requires another 32-bit value that sets the “tags” for the key. This
is used internally by the kernel. Valid flags used are:
Finally, the KeySetLayerInformation information class allow setting the flags provided by KEY_SET_-
LAYER_INFORMATION (see the discussion of NtQueryKey).
NTSTATUS NtRenameKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING NewName);
KeyHandle must have the KEY_WRITE access mask for the call to succeed. The name must be simple - that is,
does not contain path separators (backslash).
Deleting a value in a key is done with NtDeleteValueKey:
NTSTATUS NtDeleteValueKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING ValueName);
KeyHandle must have the KEY_SET_VALUE access mask. If the ValueName exists, it’s deleted.
To delete an entire key, call NtDeleteKey:
KeyHandle must have the DELETE access mask. The key is not fully deleted until the last handle to it is closed.
The NtFlushKey forces writing the key information and values to disk:
The NtCompactKeys puts the referenced keys into the same bucket internally:
NTSTATUS NtCompactKeys(
_In_ ULONG Count,
_In_reads_(Count) HANDLE KeyArray[]);
The handles must have the KEY_WRITE access mask, and must belong to the same hive (such as the machine
hive). There doesn’t seem to be much use for this API in practice.
The NtCompressKey attempts to compress the provided key (which must be a hive root):
NTSTATUS NtSaveKey(
_In_ HANDLE KeyHandle,
_In_ HANDLE FileHandle);
NTSTATUS NtSaveKeyEx(
_In_ HANDLE KeyHandle,
_In_ HANDLE FileHandle,
_In_ ULONG Format);
The caller must have the SeBackupPrivilege privilege enabled in its token. By default, administrators have
this privilege.
KeyHandle is the key to write to file - the key itself and all its children are included. FileHandle must be a
handle to a file open with FILE_WRITE_DATA access mask. NtSaveKey calls NtSaveKeyEx with a Format set
to REG_STANDARD_FORMAT (1). Other values include REG_LATEST_FORMAT (2) and REG_NO_COMPRESSION (4).
Refer to the documentation of the RegSaveKeyEx Windows API for more information.
The REG file format that is accepted by RegEdit and TotalRegistry is not a truly supported format. This
format is implemented privately by these tools. The formats supported by NtSaveKeyEx are binary formats,
rather than textual, to keep data as small as possible.
NTSTATUS NtRestoreKey(
_In_ HANDLE KeyHandle,
_In_ HANDLE FileHandle,
_In_ ULONG Flags);
The caller must have the SeRestorePrivilege privilege enabled in its token. KeyHandle will be overwritten
with the information from the file given by FileHandle. Refer to the documentation of the RegRestoreKey
Windows API for more information.
Several functions allow loading Registry data from a file, and attaching it to a target key:
NTSTATUS NtLoadKey(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ POBJECT_ATTRIBUTES SourceFile);
NTSTATUS NtLoadKey2(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ POBJECT_ATTRIBUTES SourceFile,
_In_ ULONG Flags);
NTSTATUS NtLoadKeyEx(
Chapter 13: The Registry 375
The core difference between the NtLoadKey* APIs and NtRestoreKey is that NtRestoreKey uses the file to
read data, and integrates it into the Registry; after that the file is no longer used nor needed. The NtLoadKey*
functions use the provided file as the underlying backing store of the key data.
NtLoadKey calls NtLoadKeyEx with the provided TargetKey and SourceFile, and the reset of the parameters
as zero/NULL.
The TargetKey must be “\Registry\Machine” or “\Registry\User”. SourceFile is the stored key previously
saved with NtSaveKey(Ex). With NtLoadKey, that’s all that’s needed.
The sample projects SaveKey and LoadKey provided canonical examples of using NtSaveKey and NtLoadKey.
The extended NtLoadkey* functions provide more options. The Flags is either zero or a combination of
flags, some of are described below:
• REG_PROCESS_PRIVATE (0x20) - the hive cannot be mounted by any other process while it’s in use by
a process.
• REG_APP_HIVE_OPEN_READ_ONLY (0x2000) - open the hive in read only mode.
• REG_NO_IMPERSONATION_FALLBACK (0x8000) - Don’t try to fallback to impersonating the caller if the
file access check fails.
The Windows API RegLoadAppKey calls NtLoadKeyEx with the flags REG_APP_HIVE and optionally
REG_PROCESS_PRIVATE. It uses the \Registry\A subkey with a random GUID as the root of the load.
The other parameters available in the extended functions are described below:
• TrustClassKey is an optional key handle from which the trusted/not trusted property is obtained for
the hive key.
• Event is an optional event handle that gets signaled when the hive is unloaded.
• DesiredAccess is an optional access for the returned key if specified in the next parameter.
• RootHandle is an optional returned handle to the new hive key.
• IoStatus is currently unused.
• ExtendedParameters and ExtParamCount represent more customization options that could be ex-
tended in the future. They currently support the parameters that can be specified with NtLoadKeyEx:
#define CM_EXTENDED_PARAMETER_TYPE_BITS 8
ULONG ULong;
ACCESS_MASK AccessMask;
};
} CM_EXTENDED_PARAMETER, *PCM_EXTENDED_PARAMETER;
Once a key is loaded, it can be unloaded using one of the following functions:
TargetKey is the key path stored within the OBJECT_ATTRIBUTES. NtLoadKey fails if the hive is currently
used by an app. NtUnloadKey2 allows setting Flags to REG_FORCE_UNLOAD (1), which will unload the key
even if it’s currently used. NtUnloadKeyEx allows specifying an event object handle that is signaled when
the unload completes. NtUnloadKeyEx also sets “late unload”, which means if an unload cannot be done now,
it will complete in the future when the hive is no longer used.
NTSTATUS NtNotifyChangeKey(
_In_ HANDLE KeyHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree,
_Out_writes_bytes_opt_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_In_ BOOLEAN Asynchronous);
NTSTATUS NtNotifyChangeMultipleKeys(
_In_ HANDLE MasterKeyHandle,
_In_opt_ ULONG Count,
_In_reads_opt_(Count) OBJECT_ATTRIBUTES SubordinateObjects[],
_In_opt_ HANDLE Event,
Chapter 13: The Registry 378
KeyHandle (and MasterKeyHandle) is the root key to watch for, which must have the REG_NOTIFY access
mask. Event is an optional event handle that is signaled when a notification is available. This only applies
to asynchronous calls (Asynchronous set to TRUE). ApcRoutine is an optional Asynchronous Procedure Call
(APC) to be queued to the calling thread when a notification is available. If specified, then ApcContext can be
set to any value that is propagated to the APC function. IoStatusBlock is an output structure, just holding
the status of the call. It doesn’t seem to be interesting in practice (and ignored if Synchronous is TRUE), but
must be provided.
CompletionFilter indicates what kind of notifications to watch for. Possible value include REG_NOTIFY_-
CHANGE_NAME (subkeys added/deleted), REG_NOTIFY_CHANGE_ATTRIBUTES, REG_NOTIFY_CHANGE_LAST_SET
(value added/modified/deleted), REG_NOTIFY_CHANGE_SECURITY (security descriptor changed), and REG_-
NOTIFY_THREAD_AGNOSTIC (indicates any thread can wait for the event provided, not just the calling thread).
See the Windows API RegNotifyChangeKeyValue docs for more information.
WatchTree indicates wheteher to watch just the provided key (FALSE) or the entire subkey (TRUE). Buffer
and BufferSize seem to suggest that detailed information is returned when a notification is available ( REG_-
NOTIFY_INFORMATION structure), but the buffer is never used in the implementation. This is hinted by the
fact that the Windows API RegNotifyChangeKeyValue has no such buffer. The net result is that it’s up to
the caller to figure out which key/value/attribute has changed.
With SubordinateObjects, it’s possible to add more keys to that are not descendants of MasterKeyHandle.
However, at this time the implementation supports just one such object, which means that Count can be at
most 1. if Count is zero, NtNotifyChangeMultipleKeys is identical to NtNotifyChangeKey.
In practice, these notifications are not as useful as they could be, as they don’t provide the details of the
changes. To get Registry notifications from user mode, it’s more useful to use Event Tracing for Windows
(ETW) with relevant providers, such as the classic kernel provider. (ETW if beyond the scope of this book.)
NTSTATUS NtCreateKeyTransacted(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Reserved_ ULONG TitleIndex,
_In_opt_ PUNICODE_STRING Class,
_In_ ULONG CreateOptions,
_In_ HANDLE TransactionHandle,
_Out_opt_ PULONG Disposition);
NTSTATUS NtOpenKeyTransacted(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE TransactionHandle);
NTSTATUS NtOpenKeyTransactedEx(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ ULONG OpenOptions,
_In_ HANDLE TransactionHandle);
These APIs are extensions to the functions we met early in this chapter. The only difference is the addition
of a transaction object handle. Obtaining a transaction can be done in one of two ways. The first is creating
a “generic” transaction with NtCreateTransaction:
NTSTATUS NtCreateTransaction(
_Out_ PHANDLE TransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ LPGUID Uow,
_In_opt_ HANDLE TmHandle,
_In_opt_ ULONG CreateOptions,
_In_opt_ ULONG IsolationLevel,
_In_opt_ ULONG IsolationFlags,
_In_opt_ PLARGE_INTEGER Timeout,
_In_opt_ PUNICODE_STRING Description);
Transaction APIs are beyond the scope of this chapter, but the Windows API CreateTransaction calls
NtCreateTransaction - refer to that function documentation for more details.
Using the same idea, NtOpenTramnsaction can be called to obtain an existing transaction based on its name,
which is a GUID:
Chapter 13: The Registry 380
NTSTATUS NtOpenTransaction(
_Out_ PHANDLE TransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ LPGUID Uow,
_In_opt_ HANDLE TmHandle);
NTSTATUS NtCreateRegistryTransaction(
_Out_ HANDLE *RegistryTransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjAttributes,
_Reserved_ ULONG CreateOptions);
NTSTATUS NtOpenRegistryTransaction(
_Out_ HANDLE *RegistryTransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjAttributes);
NtCreateRegistryTransaction creates a new registry transaction object, which can be named (using the
usual ObjectAttributes), and returns a handle to the new transaction. NtOpenRegistryTransaction
opens a handle to an existing Registry transaction object specified by name.
With a Registry transaction in hand, calls to NtOpenKeyTransacted and the other aforementioned APIs may
be invoked with that transaction handle.
After the various changes have been made to the key(s) used with the transaction, it must be committed or
aborted (rolled back):
NTSTATUS NtCommitRegistryTransaction(
_In_ HANDLE RegistryTransactionHandle,
_Reserved_ ULONG Flags);
NTSTATUS NtRollbackRegistryTransaction(
_In_ HANDLE RegistryTransactionHandle,
_Reserved_ ULONG Flags);
If the transaction handle is closed before committing the transaction, it’s aborted.
Here is an example that shows how to use these APIs (error handling omitted):
Chapter 13: The Registry 381
//
// initialize key name and attributes
//
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName, // example key
L"\\REGISTRY\\USER\\S-1-5-21-3456612-33973779-3838822-1001\\Zebra");
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0, nullptr, nullptr);
//
// create a named transaction (does not need a name in this case)
//
HANDLE hTrans;
OBJECT_ATTRIBUTES txAttr;
UNICODE_STRING txName;
RtlInitUnicodeString(&txName, L"\\BaseNamedObjects\\MyTransaction");
InitializeObjectAttributes(&txAttr, &txName, OBJ_OPENIF, nullptr, nullptr);
NtCreateRegistryTransaction(&hTrans, TRANSACTION_ALL_ACCESS, &txAttr, 0);
//
// create a new key as part of the transaction
//
HANDLE hKey;
NtCreateKeyTransacted(&hKey, KEY_WRITE, &keyAttr, 0, nullptr, 0, hTrans, nullptr);
//
// set a value in the new key
//
UNICODE_STRING valueName;
RtlInitUnicodeString(&valueName, L"SecretToUniverse");
int data = 42; // example value
NtSetValueKey(hKey, &valueName, 0, REG_DWORD, &data, sizeof(data));
//
// commit the transaction
//
NtCommitRegistryTransaction(hTrans, 0);
NtClose(hKey);
NtClose(hTrans);
Chapter 13: The Registry 382
NTSTATUS NtQueryOpenSubKeys(
_In_ POBJECT_ATTRIBUTES TargetKey,
_Out_ PULONG HandleCount);
TargetKey is a key whose hive is queried. The key doesn’t need to be a root hive key. For example,
\Registry\Machine\Software is a hive root, but \Registry\Machine\Software\Microsoft is not, but would still
apply to the same hive.
To work, the caller must be able to open the key for KEY_READ access. If successful, it returns the number of
open handles to keys within that hive in *HandleCount.
To get more information about these keys, an extended function is available:
NTSTATUS NtQueryOpenSubKeysEx(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ ULONG BufferLength,
_Out_writes_bytes_opt_(BufferLength) PVOID Buffer,
_Out_ PULONG RequiredSize);
The function requires the SeBackupPrivilege privilege to be enabled in the caller’s token. The returned
information is provided in the following structures:
For each handle, we get a process ID and the full key name. A typical usage would be to make two calls - the
first to get the needed size, and the second to get the actual data.
The KeyHandles sample project shows how to use these APIs. First, we’ll get the key name:
Chapter 13: The Registry 383
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName, argv[1]);
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0, nullptr, nullptr);
Next, we’ll start with the basic function since it requires no special privileges:
ULONG count;
auto status = NtQueryOpenSubKeys(&keyAttr, &count);
if (NT_SUCCESS(status)) {
printf("Handle count: %u\n", count);
If this works, we can try the extended function. First, enable the privilege:
BOOLEAN wasEnabled;
status = RtlAdjustPrivilege(SE_RESTORE_PRIVILEGE, TRUE, FALSE, &wasEnabled);
if (!NT_SUCCESS(status)) {
printf("Failed to enable Restore privilege. Launching with admin rights may help\
.\n");
return status;
}
Next, make the first call. The minimum buffer size must be sizeof(KEY_OPEN_SUBKEYS_INFORMATION)
for the call to succeed. This means the count is always returned even if the buffer is too small for all the
information:
ULONG needed;
KEY_OPEN_SUBKEYS_INFORMATION dummy;
status = NtQueryOpenSubKeysEx(&keyAttr,
sizeof(dummy), &dummy, &needed);
// allocate the buffer (a bit bigger in case new handles are created in the interim)
auto buffer = std::make_unique<BYTE[]>(needed += 1024);
Finally, we can make the real call and display the results:
Chapter 13: The Registry 384
The NtFreezeRegistry is able to stop any changes to the Registry for the specified number of seconds:
The time out can not be more than 900 seconds (15 minutes). The caller must have the SeBackupPrivilege
privilege enabled in its token. To thaw the Registry before the timeout elapsed, call NtThawRegistry:
NTSTATUS NtThawRegistry();
There is also an NtLockRegistryKey which prevents modifications to a given key - but this API only works
when called from kernel mode.
NTSTATUS RtlCreateRegistryKey(
_In_ ULONG RelativeTo,
_In_ PWSTR Path);
RelativeTo is one of a set of values indicating “relative to what” should Path be interpreted:
Chapter 13: The Registry 385
#define RTL_REGISTRY_ABSOLUTE 0
// \Registry\Machine\System\CurrentControlSet\Services
#define RTL_REGISTRY_SERVICES 1
// \Registry\Machine\System\CurrentControlSet\Control
#define RTL_REGISTRY_CONTROL 2
// \Registry\Machine\Software\Microsoft\Windows NT\CurrentVersion
#define RTL_REGISTRY_WINDOWS_NT 3
// \Registry\Machine\Hardware\DeviceMap
#define RTL_REGISTRY_DEVICEMAP 4
// \Registry\User\CurrentUser
#define RTL_REGISTRY_USER 5
The function creates the key but does not return any open handle to it if successful.
The RtlCheckRegistryKey checks the existence of a key:
NTSTATUS RtlCheckRegistryKey(
_In_ ULONG RelativeTo,
_In_ PWSTR Path);
NTSTATUS RtlQueryRegistryValues(
_In_ ULONG RelativeTo,
_In_ PCWSTR Path,
_In_ PRTL_QUERY_REGISTRY_TABLE QueryTable,
_In_ PVOID Context,
_In_opt_ PVOID Environment);
Writing a value quickly without the need to get a key handle, build a value name, etc. is possible with
RtlWriteRegistryValue:
Chapter 13: The Registry 386
NTSTATUS RtlWriteRegistryValue(
_In_ ULONG RelativeTo,
_In_ PCWSTR Path,
_In_ PCWSTR ValueName,
_In_ ULONG ValueType,
_In_ PVOID ValueData,
_In_ ULONG ValueLength);
Other functions worth mentioning include RtlDeleteRegistryValue and RtlOpenCurrentUser (opens the
current user’s hive key without the need to query the user’s SID).
14.11: Summary
Working with the Registry is necessary in many applications and services. The native API provides powerful
functions for this purpose - from creating and opening keys, to keys and values enumeration, to transactions.