OsderdaDev

Time:

UTC+3

W

Under the Hood: Understanding Windows Kernel Zw vs. Nt Functions

OsderdaDev
January 15, 2026
10 views

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:

  • Nt: Stands for "Native" (New Technology).
  • Zw: The etymology is debated (some say "Zero Wait", others say it was just the next letters after Nt), but functionally, it represents a system call stub.
  • 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.

    assembly
    ; 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)
    ret

    The 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.

    cpp
    // 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: Success

    Security 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.

  • The user might pass a path to a system file they don't have permission to delete.
  • Your driver calls ZwDeleteFile.
  • Zw sets PreviousMode to KernelMode.
  • The file system sees the request coming from the Kernel (System) and allows the deletion.
  • This is a classic Privilege Escalation vulnerability.

    Best Practices

  • Use `Zw` functions when your driver initiates an operation on its own behalf (e.g., logging, reading configuration) and uses its own kernel-allocated memory.
  • Use `Nt` functions when you are processing a request on behalf of a user application and you want the kernel to enforce security checks on the pointers and handles provided by that user.
  • Always Validate: If you must use 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.

    Osderda

    Senior Software Engineer

    With a diverse background in Windows system programming, scalable Backend infrastructure, native Android development, and server management, I strive to bridge the gap between different technology stacks. My goal is to simplify complex coding concepts and share actionable knowledge through concise, technical documentation.

    Discussion (0)