System calls on Windows go through NTDLL.dll, where each system call is invoked by a syscall (x64) or sysenter (x86) CPU instruction, as can be seen from the following output of NtCreateFile from NTDLL:
0:000> u
ntdll!NtCreateFile:
00007ffc`c07fcb50 4c8bd1 mov r10,rcx
00007ffc`c07fcb53 b855000000 mov eax,55h
00007ffc`c07fcb58 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffc`c07fcb60 7503 jne ntdll!NtCreateFile+0x15 (00007ffc`c07fcb65)
00007ffc`c07fcb62 0f05 syscall
00007ffc`c07fcb64 c3 ret
00007ffc`c07fcb65 cd2e int 2Eh
00007ffc`c07fcb67 c3 ret
The important instructions are marked in bold. The value set to EAX is the system service number (0x55 in this case). The syscall instruction follows (the condition tested does not normally cause a branch). syscall causes transition to the kernel into the System Service Dispatcher routine, which is responsible for dispatching to the real system call implementation within the Executive. I will not go to the exact details here, but eventually, the EAX register must be used as a lookup index into the System Service Dispatch Table (SSDT), where each system service number (index) should point to the actual routine.
On x64 versions of Windows, the SSDT is available in the kernel debugger in the nt!KiServiceTable symbol:
lkd> dd nt!KiServiceTable
fffff804`13c3ec20 fced7204 fcf77b00 02b94a02 04747400
fffff804`13c3ec30 01cef300 fda01f00 01c06005 01c3b506
fffff804`13c3ec40 02218b05 0289df01 028bd600 01a98d00
fffff804`13c3ec50 01e31b00 01c2a200 028b7200 01cca500
fffff804`13c3ec60 02229b01 01bf9901 0296d100 01fea002
You might expect the values in the SSDT to be 64-bit pointers, pointing directly to the system services (this is the scheme used on x86 systems). On x64 the values are 32 bit, and are used as offsets from the start of the SSDT itself. However, the offset does not include the last hex digit (4 bits): this last value is the number of arguments to the system call.
Let’s see if this holds with NtCreateFile. Its service number is 0x55 as we’ve seen from user mode, so to get to the actual offset, we need to perform a simple calculation:
kd> dd nt!KiServiceTable+55*4 L1
fffff804`13c3ed74 020b9207
Now we need to take this offset (without the last hex digit), add it to the SSDT and this should point at NtCreateFile:
lkd> u nt!KiServiceTable+020b920
nt!NtCreateFile:
fffff804`13e4a540 4881ec88000000 sub rsp,88h
fffff804`13e4a547 33c0 xor eax,eax
fffff804`13e4a549 4889442478 mov qword ptr [rsp+78h],rax
fffff804`13e4a54e c744247020000000 mov dword ptr [rsp+70h],20h
Indeed – this is NtCreateFile. What about the argument count? The value stored is 7. Here is the prototype of NtCreateFile (documented in the WDK as ZwCreateFile):
NTSTATUS NtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);
Clearly, there are 11 parameters, not just 7. Why the discrepency? The stored value is the number of parameters that are passed using the stack. In x64 calling convention, the first 4 arguments are passed using registers: RCX, RDX, R8, R9 (in this order).
Now back to the title of this post. Here are the first few entries in the SSDT again:
lkd> dd nt!KiServiceTable
fffff804`13c3ec20 fced7204 fcf77b00 02b94a02 04747400
fffff804`13c3ec30 01cef300 fda01f00 01c06005 01c3b506
The first two entries look different, with much larger numbers. Let’s try to apply the same logic for the first value (index 0):
kd> u nt!KiServiceTable+fced720
fffff804`2392c340 ?? ???
^ Memory access error in 'u nt!KiServiceTable+fced720'
Clearly a bust. The value is in fact a negative value (in two’s complement), so we need to sign-extend it to 64 bit, and then perform the addition (leaving out the last hex digit as before):
kd> u nt!KiServiceTable+ffffffff`ffced720
nt!NtAccessCheck:
fffff804`1392c340 4c8bdc mov r11,rsp
fffff804`1392c343 4883ec68 sub rsp,68h
fffff804`1392c347 488b8424a8000000 mov rax,qword ptr [rsp+0A8h]
This is NtAccessCheck. The function’s implementation is in lower addresses than the SSDT itself. Let’s try the same exercise with index 1:
kd> u nt!KiServiceTable+ffffffff`ffcf77b0
nt!NtWorkerFactoryWorkerReady:
fffff804`139363d0 4c8bdc mov r11,rsp
fffff804`139363d3 49895b08 mov qword ptr [r11+8],rbx
And we get system call number 1: NtWorkerFactoryWorkerReady.
For those fond of WinDbg scripting – write a script to display nicely all system call functions and their indices.