A Gentle Introduction to Syscalls in Windows
Ever reverse-engineered functions in ntdll.dll
? Why are certain functions just a few opcodes long when they’re supposed to do behemoth of tasks?
That’s because these functions are just wrappers for offloading the actual tasks to the kernel. And how does the kernel know what to do? Syscalls. Syscalls are a way of signalling the kernel what you want, and give the kernel all the necessary information to carry that out.
This post is meant to introduce you to the concept of syscalls in Windows, and all the relevant prerequisite concepts surrounding it.
Prerequisite concepts
Protection Rings
Execution in Windows is architectured around the concept of rings, where the inner-most ring represents the kernel and the outermost layer represents user-mode applications (such as your web browsers, video games, etc). An outer layer abstracts away the inner layers, providing simple interfaces.
As you might have guessed, the inner layers run with much more elevated privileges than the outer layers. It also runs much more critical tasks. Failure in user-mode applications just crash the applications. Failure in kernel-level routines crash the OS.
Here’s the thing though — the user-mode applications often require help from kernel to do its tasks, because the tasks might necessitate some form of interaction with the hardware. That’s because only the kernel has raw access to the hardware.
System Services and the SSDT
The Kernel runs and exposes several services called System Services. It’s these services that are invoked by previously mentioned syscalls. These services use the same input parameters from user-mode functions to execute its routines, then pass back the result to the caller user-mode functions.
To form this bridge between user-mode and kernel-mode, the Kernel maintains a “System Service Descriptor Table” (SSDT) for each category of System Service. Each of these tables have two things —
- An index number, starting sequentially from 0
- An address to a routine (or a relative offset to a routine)
The index numbers are “System Service Numbers” (SSN), and each of them thus point to a routine. It’s these SSNs that are used by syscalls to signal the Kernel on what routine is to be executed. SSNs are of 2 bytes each (WORD).
These SSNs may vary for the same functions across different Windows versions. A good resource to get these is https://github.com/j00ru/windows-syscalls. As an example, the SSN for nt!NtCreateFile
on Windows 11 23H2 is 0x55.
Difference between Nt
and Zw
syscall functions
You might have noticed that every function prefixed with Nt
has a Zw
counterpart. The difference between them depends on where you are seeing them.
In user-land, both syscall functions are the same, since they are stored at the same address. So it does not truly matter which one you call. They are both serviced by nt!NtCreateFile
syscall.
However in kernel-mode, they are different. nt!NtCreateFile
and nt!ZwCreateFile
can both be directly called in the kernel space, but the Nt
variant performs additional validations on the input parameters, while the Zw
variant does not.
How user-mode applications use syscalls
With an understanding of Protection rings and System Services, it’s now clear that all syscalls do is to transfer the execution from Ring 3 (user-mode applications) to Ring 2–0 (device drivers and the kernel).
Let’s see an example of this. Here’s a dissassembly of ntdll!NtCreateFile
. This function is used by kernel32!CreateFileA
to perform the actual File operations.
To use a syscall
, there’s two things that need to happen:
mov r10, rcx
: The first parameter of theNtCreateFile
needs to be copied tor10
register.mov eax, 55
: A hardcoded 0x55 needs to be set toeax
register.
This 0x55
is the SSN for NtCreateFile
. When this is used for a syscall
, the Kernel knows exactly which routine to execute (nt!NtCreateFile
).
That test
and jne
instructions are supposed to execute int 2E
instead of syscall
on systems that don’t support syscalls. The logic remains the same.
Notice that this routine does not manipulate any stack variables or function parameter registers. What this means is that the function arguments are unchanged when a syscall is made. In other words, the parameters passed to ntdll!NtCreateFile
gets passed in the same form and order to nt!NtCreateFile
.
Can you use syscalls yourself?
Yes. You can. If you can execute the same sequence of instructions as the example above, then you can effectively do the same operations as, say ntdll.dll
does.
In my next post, I will showcase exactly that, with an offensive objective in mind.