Under the Hood: Understanding Windows Kernel Zw vs. Nt Functions
If you have ever ventured into the depths of Windows driver development or reverse engineering, you have undoubtedly stumbled upon the Native API. You see a function named NtCreateFile, and right next to it, ZwCreateFile. They take identical parameters. They return the same NTSTATUS codes. In User Mode, they even point to the same instruction addresses.
So, why the distinction?
This is one of the most common questions for developers moving from Win32 to Kernel programming. The answer lies not in *what* these functions do, but in *how* the Windows Kernel treats the caller's permissions based on the prefix used. In this post, we are going to dissect the architecture of the System Service Dispatching mechanism to understand the critical security implications of Zw versus Nt.
The Native API: A Quick Primer
The Windows API (Win32) that we know and love—CreateFile, ReadFile, etc.—is merely a set of wrapper DLLs (kernel32.dll, user32.dll). Underneath these wrappers lies the Native API, implemented in ntdll.dll (user mode) and ntoskrnl.exe (kernel mode).
The functions exposed here usually start with two prefixes:
The User Mode Illusion
If you are writing a standard user-mode application and you link against ntdll.lib, the distinction is meaningless.
In ntdll.dll, both NtCreateFile and ZwCreateFile point to the exact same memory address. They act as trampolines (stubs) that load a specific System Service Index (SSDT Index) into the EAX register and execute the syscall (or sysenter/int 2e) instruction to transition into kernel mode.
; Pseudo-assembly of NtCreateFile / ZwCreateFile in ntdll.dll
mov eax, 0x55 ; The system service number for CreateFile
mov edx, 0x... ; Pointer to stack arguments
syscall ; Transition to Kernel Mode (Ring 0)
retThe Kernel Mode Reality
The story changes drastically once we cross the boundary into Ring 0 (Kernel Mode).
When a driver calls these functions, ntoskrnl.exe exposes both sets, but they behave differently regarding Parameter Validation and Access Checks.
The Concept of `PreviousMode`
To understand the difference, we must look at the KTHREAD structure. Every thread in Windows maintains a field called PreviousMode. This field indicates the privilege level of the code that called the current system service.
* UserMode (1): The call originated from an application (untrusted).
* KernelMode (0): The call originated from a driver or the kernel itself (trusted).
When the Kernel executes a function, it checks PreviousMode. If it is UserMode, the kernel must validate every pointer passed to it (to ensure it points to user memory, not kernel memory) and check handle access rights.
Nt Functions: "I Trust the Caller's Mode"
When you call an Nt function (e.g., NtCreateFile) directly from a kernel driver, the function executes without changing the `PreviousMode`.
* If your driver was called by a user application (e.g., via DeviceIoControl), the PreviousMode is likely UserMode.
* If you pass a kernel-allocated buffer to NtCreateFile, the function will check PreviousMode.
* Seeing UserMode, it expects the buffer to be in User Space.
* Since you passed a Kernel Space pointer, the validation fails, and you get STATUS_ACCESS_VIOLATION.
Zw Functions: "I Am the Law"
When you call a Zw function (e.g., ZwCreateFile) from kernel mode, it doesn't execute the service immediately. Instead, it goes through a stub that re-triggers the system call mechanism (essentially calling itself via the interrupt handler).
Crucially, this transition sets the `PreviousMode` to `KernelMode`.
Because the mode is now KernelMode, the internal security manager skips pointer validation and assumes the caller has full administrative rights to the handles. It assumes *you* (the driver developer) know what you are doing.
Code Example: The Difference in Action
Let's look at a scenario where a driver wants to write a log file to disk.
// Assume we are in a Driver entry point or a system thread
// We allocate a buffer in PagedPool (Kernel Memory)
PVOID pBuffer = ExAllocatePoolWithTag(PagedPool, 1024, 'LogT');
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
OBJECT_ATTRIBUTES objAttr;
// ... Initialize ObjectAttributes for "\??\C:\Log.txt" ...
// SCENARIO A: Using NtCreateFile
// If we are in the context of a User Request, PreviousMode might be UserMode.
// The Kernel checks pBuffer.
// Since pBuffer is > 0x7FFFFFFF (Kernel Address) but PreviousMode is User,
// The Kernel thinks this is a hacking attempt.
NTSTATUS statusNt = NtCreateFile(&hFile, ..., &ioStatus, ..., pBuffer, ...);
// Result: statusNt is likely STATUS_ACCESS_VIOLATION
// SCENARIO B: Using ZwCreateFile
// The Zw stub traps into the kernel again.
// The Trap Handler sees the call is from Kernel Mode, sets PreviousMode = KernelMode.
// The Kernel checks pBuffer.
// Since PreviousMode is Kernel, access to Kernel Address is ALLOWED.
NTSTATUS statusZw = ZwCreateFile(&hFile, ..., &ioStatus, ..., pBuffer, ...);
// Result: SuccessSecurity Implications: The Confused Deputy
While Zw functions are necessary for drivers to perform administrative tasks, they are dangerous if misused.
Imagine a user application passes a filename to your driver and asks you to delete it. If your driver blindly takes that string and calls ZwDeleteFile, you have bypassed security checks.
ZwDeleteFile.Zw sets PreviousMode to KernelMode.This is a classic Privilege Escalation vulnerability.
Best Practices
Zw with user data, use ProbeForRead and ProbeForWrite to validate memory ranges manually before passing them to the API.Conclusion
The difference between Zw and Nt is a masterclass in Windows architecture design. It elegantly solves the problem of how to differentiate between "a user asking for something" and "the operating system doing something internal," all while using the same underlying code.
Next time you are writing a WDF driver and reach for ZwCreateFile, remember: you are explicitly telling the kernel, "Trust me, I'm an admin." Make sure you deserve that trust.