Introduction
Unlike the pursuit for the Holy Grail, this quest will succeed but the journey will be a lil bit tricky.
Have you heard about software repositories where authors often drop code without a single line of relevant explanation? Of course you did, but here we must not do the same.
I have the obligation to provide relevant information on a difficult topic, in a go-easy way and within the space of a single page article, in the hope that in the end everything will make sense to everybody.
This may not be that easy, but let's move on through 5 steps and hope for the best:
- Requirements and Forewarnings
- What are and where are the SSDTs
- How SSDTs are used
- Why are the SSDTs so important?
- Our 32-bit and 64-bit code to find the SSDTs
Requirements and Forewarnings
- Some knowledge of C language is important in order to understand our code - if you don't have it, you can still continue reading and try to grasp some insight (if you don't already have, of course) about what we are writing about.
- For Kernel Mode snooping, it is useful to know how to use
Windbg
, or its command line brother KD. Proper Kernel debugging requires 2 computers, but the target computer can be lodged in a virtual machine (unless we are testing some hardware drivers, which is not the case) so that you don't have to mess with cables to establish a connection. - We have tested the Kernel Drivers in all the operating systems they are intended for (i.e., 32-bit Windows 7 and Windows 10 till RedStone 2 or version 1703, 64-bit releases of Windows 7, 8, 8.1 and 10 till Redstone 2, and 64-bit server editions that use the same codebases, namely 2008 R2, 2012 and 2016). Even though the tests were successful, you must only test these Drivers, and any drivers in general, in computers (or virtual machines) you use exclusively for testing.
What Are and Where Are the SSDTs
SSDTs are hidden inside some structures. We must explain and locate them first before we finally locate and explain the SSDTs.
Service Descriptor Table
A Service Descriptor Table is a Kernel structure, shown below, that contains 4 System Service Table (SST) entries.
typedef struct tagSERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE nt;
SYSTEM_SERVICE_TABLE win32k;
SYSTEM_SERVICE_TABLE sst3;
SYSTEM_SERVICE_TABLE sst4;
} SERVICE_DESCRIPTOR_TABLE;
There are two Service Descriptor Tables in the system, the nt!KeServiceDescriptorTable
and the nt!KeServiceDescriptorTableShadow
(we will use the notation of prefixing structures and function names with the module name, in this case nt!
means from ntoskrl.exe or ntkrnlmp.exe).
System Service Table (SST)
A SST structure, shown below, contains the ServiceTable
field, which is a pointer to the first element of an array of pointers to Kernel routines, in the case of 32-bit operating systems, or a pointer to an array of addresses (and some extra information about the number of arguments) relative to the base address pointed to by it, in the case of 64-bit operating systems.
typedef struct tagSYSTEM_SERVICE_TABLE {
PULONG ServiceTable;
PULONG_PTR CounterTable;
ULONG_PTR ServiceLimit;
PBYTE ArgumentTable;
} SYSTEM_SERVICE_TABLE;
Let's now use Windbg
to have a look at a 32-bit Service Descriptor Table, in this case the nt!KeServiceDescriptorTable
, and its contained SST structures:
kd> dps nt!KeServiceDescriptorTable L10
81a2a180 8190f20c nt!KiServiceTable
81a2a184 00000000
81a2a188 000001c8
81a2a18c 8190f930 nt!KiArgumentTable
81a2a190 00000000
81a2a194 00000000
81a2a198 00000000
81a2a19c 00000000
81a2a1a0 00000000
81a2a1a4 00000000
81a2a1a8 25c10ad3
81a2a1ac 01d2e256
81a2a1b0 00000000
81a2a1b4 8913bb40
81a2a1b8 89248580
81a2a1bc 890284f0
Now, let's have a look at the other Service Descriptor Table, the nt!KeServiceDescriptorTableShadow
:
kd> dps nt!KeServiceDescriptorTableShadow L10
81a2a140 8190f20c nt!KiServiceTable
81a2a144 00000000
81a2a148 000001c8
81a2a14c 8190f930 nt!KiArgumentTable
81a2a150 98f00000 win32k!W32pServiceTable
81a2a154 00000000
81a2a158 0000046c
81a2a15c 98f01628 win32k!W32pArgumentTable
81a2a160 a0cf3fff
81a2a164 00000001
81a2a168 ffffffff
81a2a16c 819245b2 nt!FinalExceptionHandlerPad58
81a2a170 8959869c
81a2a174 00000000
81a2a178 00000001
81a2a17c 00000000
SSDTs
SSDT is an acronym for System Service Dispatch Table.
As mentioned above, there is an array of pointers, in the 32-bit case, or an array of relative addresses (plus additional information), in the 64-bit case, that is pointed to by the ServiceTable
field of a SST (System Service Table) - this array is nothing more, nothing less than a SSDT.
Looking at the Windbg
outputs above, we see that there is one clearly identified SST table in the case of nt!KeServiceDescriptorTable
and two in the case of nt!KeServiceDescriptorTableShadow
. Although we see more data in there, from available information, we can only state that from the possible 4 SST entries, the nt!KeServiceDescriptorTable
uses only the first one - it describes the SSDT for the Windows Native APIs exported by ntoskrnl.exe. The nt!KeServiceDescriptorTableShadow
uses 2 SST entries, the first is a copy of the nt!KiServiceTable
from nt!KeServiceDescriptorTable
, the second one, win32k!W32pServiceTable
, describes the SSDT for the User and GDI routines exported by win32k.sys.
How SSDTs are Used
SSDTs provide functionality both to User Mode applications (indirectly, of course) and to Kernel Mode drivers.
We are going to have a look at the User Mode case.
When a User Mode application invokes, directly or indirectly, some Windows API function, many times one or more Kernel routines will be called in the process. Kernel Mode is entered from User Mode through the Sysenter (or Syscall for 64-bit) ASM instruction (in the old days, it was interrupt 0x2E - which is still there, although probably not used that much). The exact Kernel service routine will be specified by a number, called Dispatch ID, entered in the EAX register before the Sysenter
/Syscall
instruction.
The first 12 bits of the Dispatch ID is an index into one of the SSDTs. Bits 12 and 13 specify which SSDT. This means that Dispatch IDs up to 0xFFF will be serviced by the nt!KiServiceTable
SSDT and Dispatch IDs between 0x1000 and 0x1FFF will be serviced by the win32k!W32pServiceTable
SSDT.
At this point, if all this is new for you, you may feel a bit lost - the subject is indeed dense and tricky, but try to stick with us.
A 32-bit Example
To clarify a bit the ideas, let’s use Windbg
once again and imagine a User Mode call to ntdll!NtCreateFile
(ntdll.dll is a User Mode DLL containing, among other things, dispatch stubs to Kernel Mode system services):
kd> u ntdll!NtCreateFile
ntdll!NtCreateFile:
77ab3250 b875010000 mov eax,175h
77ab3255 e803000000 call ntdll!NtCreateFile+0xd (77ab325d)
77ab325a c22c00 ret 2Ch
77ab325d 8bd4 mov edx,esp
77ab325f 0f34 sysenter
As you see, the Dispatch ID is 0x175. It will be resolved in the nt!KiServiceTable
SSDT to the item in the 0x176th position (the list is zero based), which is nt!NtCreateFile
.
kd> dps nt!KiServiceTable L176
8190f20c 818c573a nt!NtAccessCheck
8190f210 818cbfd8 nt!NtWorkerFactoryWorkerReady
8190f214 81b033b8 nt!NtAcceptConnectPort
.....
190f7d8 81ad7b18 nt!NtCreateTimer2
8190f7dc 81af323a nt!NtCreateIoCompletion
8190f7e0 81a66958 nt!NtCreateFile
A 64-bit Example
Again using Windbg
, let's imagine a User Mode call to ntdll!NtCreateFile
:
kde> u ntdll!NtCreateFile
ntdll!ZwCreateFile:
00000000`777ac080 4c8bd1 mov r10,rcx
00000000`777ac083 b852000000 mov eax,52h
00000000`777ac088 0f05 syscall
00000000`777ac08a c3 ret
Here, we see that the Dispatch ID that ntdll!NtCreateFile
(same as ntdll!ZwCreateFile
in User Mode) is using is 0x52.
This Dispatch ID corresponds to 0x53th item in the nt!KiServiceTable
:
kd> dd /c 1 nt!KiServiceTable L53
fffff800`02a78000 04170500
fffff800`02a78004 02f80700
......
fffff800`02a78140 02d4b140
fffff800`02a78144 046f9202
fffff800`02a78148 030b4fc7 <- This is our entry
Bits 4-31 of 0x030b4fc7 correspond to the relative address to the base of the nt!KiServiceTable
. Bits 0-3 are related to the number of arguments and will not be used here.
To obtain the function address, we shift the value 4 bits to the right and add the result to the table base linear address:
So, 0x030b4fc7 >> 4 = 0x30b4fc
Now add it to the table linear base address:
0xfffff80002a78000 + 0x30b4fc = 0xfffff80002d834fc
Let’s confirm:
kd> u nt!NtCreateFile
nt!NtCreateFile:
fffff800`02d834fc 4c8bdc mov r11,rsp
fffff800`02d834ff 4881ec88000000 sub rsp,88h
fffff800`02d83506 33c0 xor eax,eax
...
Yes, on target!
Why are the SSDTs so Important?
There are many important data structures in the System, they are all central and contribute to the expected functionality of the whole.
Still, SSDTs have been deserving more hype and attention by the general public than the others.
By now, you may already have a feeling about the reasons for that. In the 32-bit times (not so old, actually) bad people from the dark corners of the Internet used to produce (actually they still do, although 32-bit Operating Systems are unfortunately for them in short demand these days) malicious software running in Kernel Mode (so called Rootkits) that modify entries in either the nt!KiServiceTable
or the win32k!W32pServiceTable
diverting System calls to their own code in order to cause troubles. Not only bad people got used to play with the SSDTs, many security products, namely antivirus, used to hook the SSDTs as well in order to receive an immediate alert on virus attacks.
However, 64-bit Windows releases, since Windows XP 64-bit and Windows Server 2003 64-bit SP1, introduced a strong protection feature called Kernel Patch Protection (generally known by the term PatchGuard). PatchGuard makes periodic checks to make sure that a certain number of critical System structures, including the SSDTs, were not modified in the meantime. Security software, namely antivirus, was forced to search for less efficient alternatives. Authors of Rootkits suffered a violent backlash but not a complete defeat - from time to time, they come up with new but short-lived (at least, we want to think they are) ways to bypass the PatchGuard - when aware, Microsoft will produce a new patch to their PatchGuard and issue a new Security Update.
In addition to PatchGuard, compulsory Driver Signature with Class 3 certificates from selected Certification Authorities was a great contribution to System safety.
Our 32-bit and 64-bit Code to Find the SSDTs
The purpose of our code is not to hook any System functions in the SSDTs, actually it is relatively easy to do that when feasible (PatchGuard makes things much less feasible) - there is a vast literature on the subject, even published in books that sell well and hundreds of conferences hosted by specialists have been held on the subject.
Somewhat surprisingly, the tricky part in all this is to find out where the SSDTs really are. Particularly, the win32k!W32pServiceTable
SSDT pointed to by the second SST entry of the nt!KeServiceDescriptorTab1eShadow
is indeed shadowed. On the other hand, locate the nt!KiServiceTable
SSDT for 32-bit Operating Systems is straightforward, the symbol is exported and can be linked with - just read its value. However, the symbol for nt!KiServiceTable
is not exported in 64-bit Windows.
What we will do in our code is locate the nt!KeServiceDescriptorTab1eShadow
. If we succeed (of course, we will), we will know the whereabouts of both the nt!KiServiceTable
and the win32k!W32pServiceTable
- two birds with one shot.
We have built both the Kernel Mode driver and the User Mode application (which also launches the driver and unloads it in the end) with a single Visual Studio 2015 solution - one project for the driver (both 32-bit and 64-bit) and another for the driver (both 32-bit and 64-bit).
The Windows Driver Kit version 10 must be installed in the computer in order to compile our project without modifications.
The User Mode application has slightly different behaviors when compiled for 32-bit or for 64-bit.
So, when testing you must launch the 32-bit driver with the 32-bit application and the 64-bit driver with the 64-bit application.
The code for the driver uses undocumented (or opaque, as Microsoft uses to say) structures and System functions. These undocumented elements work for the range of Operating Systems (mentioned above) we have tested the driver on.
The Kernel Driver
The driver is loaded on demand and does not run at boot time - this means that even if the System crashes due to a Bugcheck, you will never remain in an endless catastrophic loop.
After performing its job, the driver will be unloaded and the Registry entries self-cleaned. In addition, if it is the 64-bit driver, it will be deleted from the %SYSTEM32%\Drivers folder (where it was copied to). The 32-bit driver will launch from the same folder the launcher application is in and, of course, will not be deleted.
If you want to build the driver for 64-bit and test it without an acceptable Class 3 Certificate (Test Certificates do not work here), beware that you need to set the machine into Test Mode. This is done by launching bcdedit.exe from an elevated command prompt and issue the command:
bcdedit.exe -set TESTSIGNING ON
then reboot.
When you are over with the tests, launch again bcdedit
, issue the same command with OFF
instead of ON
and reboot.
To accomplish its mission, the driver uses different methods in 32-bit and 64-bit.
In 32-bit
- Enumerates all running process looking for the process PID sent from User Mode (by default, it will be the
PID
of csrss.exe, a process always present). - Once found, it enumerates all csrss.exe's threads, looking for a GUI thread -they have the
Win32Thread
entry in KTHREAD
(part of ETHREAD
- Executive Thread Block) not NULL
. - GUI threads are guaranteed to have the
ServiceTable
entry in KTHREAD
pointing to nt!KeServiceDescriptorTab1eShadow
.
Very easy indeed, however the positions of the Win32Thread
and ServiceTable
entries within the KTHREAD
structure may change between Windows releases (they are opaque structures).
Let's use Windbg
and see how the KTHREAD
structure has been in Windows 10 till Redstone2.
kd> dt _KTHREAD
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 SListFaultAddress : Ptr32 Void
+0x018 QuantumTarget : Uint8B
+0x020 InitialStack : Ptr32 Void
+0x024 StackLimit : Ptr32 Void
+0x028 StackBase : Ptr32 Void
+0x02c ThreadLock : Uint4B
+0x030 CycleTime : Uint8B
+0x038 HighCycleTime : Uint4B
+0x03c ServiceTable : Ptr32 Void <------
....
+0x0e0 WaitBlockFill10 : [68] UChar
+0x124 Win32Thread : Ptr32 Void <------
In 64-bit
Here, we use an even easier approach:
- Read the MSR CPU register using the value
IA32_LSTAR
(1) (0xC0000082)
. This will return the address of the 64-bit service call dispatcher (nt!kiSystemCall64
). No farther than 512 bytes away is the function nt!KiSystemServiceRepeat
. When we disassemble it, we find a reference to nt!KeServiceDescriptorTableShadow
:
kd> u nt!KiSystemServiceRepeat
nt!KiSystemServiceRepeat:
fffff803`f28060a4 4c8d15d5e72700 lea r10,[nt!KeServiceDescriptorTable (fffff803`f2a84880)]
fffff803`f28060ab 4c8d1d4eb42600 lea r11,[nt!KeServiceDescriptorTableShadow (fffff803`f2a71500)]
fffff803`f28060b2 f7437840000000 test dword ptr [rbx+78h],40h
- All we have to do is search for the pattern, extract the RIP-Relative address and then calculate the linear address of
nt!KeServiceDescriptorTableShadow
.
The User Mode Application
The User Mode Application will perform the following tasks:
- Load the Kernel Driver.
- Issue an IOCTL command to request it to find the addresses of
nt!ServiceDescriptorTableShadow
, nt!KiServiceTable
and win32k!W32pServiceTable
. - Collect the answer and display it on screen.
- Finally, unload the Kernel Driver leaving the System clean.
This application must be run as Administrator.
References
(1) Intel® 64 and IA-32 Architectures Software Developer’s Manual
Updates
20th July 2017
- Code has been updated to deal as well with 32-bit Windows 8.0 and 8.1. Now all Windows releases, 32-bit and 64-bit, from Windows 7 to Windows 10 Redstone 2 are covered.
- Fixed some sentences in the text.
Jose Pascoa is the owner of AtelierWeb Software (http://www.atelierweb.com). We produce security and network software and mixed utilities since 1999. The first program I published (in a BBS) was a MS-DOS utility, had the size of 21 KB and was done in Assembly Language. Nowadays, my low level languages are more likely to be "C", "C++" and "Delphi" rather than Assembly Language but I still like it. I have nothing against more fashionable languages like C# and technologies like WPF, actually I have played with them and published software with them.