All About HackingBlackhat Hacking ToolsFree CoursesHacking

Hooking the System Service Dispatch Table (SSDT) 2023

In this article we will learn about Hooking the System Service Dispatch Table.

What is Hooking the System Service Dispatch Table?

In this article, we will imagine how we can connect the System Service Dispatch Table, but first we need to find out what SSDT actually is and how it is used by the operating system. To understand how and why the SSDT table is used, we must first talk about system calls.

We know of two ways in which a system call can be invoked:

int 0x2e instruction: used mainly in older versions of Windows operating systems, where the system call number is stored in the eax register, which is then called in the kernel.
sysenter instruction: The sysenter instruction uses MSR to quickly call the kernel and is mainly used in newer versions of Windows.
The SSDT table contains a pointer to kernel functions that are used when a system call is invoked by either the “int 0x2e” or sysenter instructions. The value stored in the eax register is the system call number that will be invoked in the kernel. In the image below we can see that sysenter is called in the ntdll.dll library and the system call number 0x25 will be called.

When the system call routine is called, the system call number is stored in the eax register, which is a 32-bit value. But how is this number used? It can’t be an index into a pointer table, because if 32 bits were used as an index, that would mean the table is 4GB in size, which it certainly isn’t. With a little research, we can find that the system service number is divided into three parts:

bits 0-11: System Service Number (SSN) to be invoked.
bits 12-13: service descriptor table (SDT).
bits 14-31: not used.
Only the lower 12 bits are used as an index into the table, which means that the table is 4096 bytes in size. The top 18 bits are unused and the middle 2 bits are used to select the appropriate service descriptor table – so we can have a maximum of 4 system descriptor tables (SDTs). On Windows operating systems, only two tables are used and they are called KeServiceDescriptorTable (middle bits set to 0x00) and KeServiceDescriptorTableShadow (middle bits set to 0x01).

This means that the value in the EAX register, which is the system service number, can contain the following values ​​(representing 16-bit values):

0000xxxx xxxxxxxx: KeServiceDescriptorTable is used, where x can be 0 or 1, which further means that the first table is used if the system service numbers are from 0x0 to 0xFFF.
0001yyyy yyyyyyyy: uses KeServiceDescriptorTableShadow, where y can be 0 or 1, which further means that the second table is used if the system service numbers are from 0x1000 – 0x1FFF.
This means that system service numbers in the EAX register can only be in the range 0x0000 – 0x1FFFF and all other values ​​are invalid.

We can list all symbols that begin with KeServiceDescriptor with “x nt!KeServiceDescriptor*

” in WinDbg. The result of running this command can be seen below.

Note that the KeServiceDescriptorTable is exported by the ntoskrnl.exe, while the KeServiceDescriptorTableShadow is not exported. Both Service Descriptor Tables (SDTs) contain a structure called System Service Table (SST), which have a structure like presented below [2].

Each System Service Table (SST) contains the following fields:

ServiceTable: points to an array of virtual addresses – SSDT (System Service Dispatch Table), where each entry further points to a kernel routine.
CounterTable: not used.
ServiceLimit: the number of entries in the SSDT table.
ArgumentTable: points to an array of bytes – SSDP (System Service Parameter Table), where each byte represents the number of bytes allocated for function arguments for each corresponding SSDT routine.
Let’s show the process override in the image below, where we can see that both “int 0x2e” and the sysenter instruction make a system call based on the SSN stored in the eax register. The Service Descriptor Table Number (SDTN) points to one of the 4 SDT tables where only the first two are actually used and point to the SST. KeServiceDescriptorTable points to one SST, which in turn points to the SSDT table. KeServiceDescriptorTableShadow points to two SSTs where the first points to the same SSDT table and the second points to a secondary SSDT table.

Now let’s look at the whole process from a practical point of view and actually imagine all the things described earlier on a real Windows operating system. We have already introduced KeServiceDescriptorTable and KeServiceDescriptorTableShadow, so now we need to display the SST field of KeServiceDescriptorTable as well as KeServiceDescriptorTableShadow, which we can do using dps command as below. Note that the first 4 bytes contain a pointer to the SSDT KiServiceTable, while the last 4 bytes contain a pointer to the KiArgumentTable argument table.

To summarize the values ​​above, we list all the fields:

Service table: 0x826af6f0
CounterTable: not used
ServiceLimit: 0x191 (401 in decimal)
Argument table: 0x826afd38
Service table: 0x967a5000
CounterTable: not used
ServiceLimit: 0x339 (825 in decimal)
Argument table: 0x967a602c
Note that the KeServiceDescriptorTableShadow actually contains two SST records, where the first is the same as the KeServiceDescriptorTable and the second SST is completely different. This is also why we displayed 32 bytes in the second dps command, while we only displayed 16 bytes in the first dps command.

We can also see that there are 0x191 entries in the first SSDT table, while there are 0x339 entries in the second SSDT table. The maximum number of entries in one SSDT table is 0x400 (or 1024 in decimal). To list the entire KiServiceTable we can use the command “dps nt!KiServiceTable L pod!KiServiceLimit” which automatically lists the entire table because we use “poi nt!KiServiceLimit”. The “dps nt!KiServiceLimit l1” command actually outputs a value of 0x191, which is exactly the number of entries in the SSDT table.

Specifically, we’re interested in KiServiceTable, which is the SSDT table we’ll be hooking into in the rest of the article. At this point, I just wanted to say that the SSDT table is very similar to the IDT table we encountered in real mode, which is still used before entering protected mode. Therefore, we only need to overwrite the pointers in the SSDT table to attach any kernel routine stored in it.

Read-only memory and SSDT table

The first problem we encounter when overwriting SSDT entries is that SSDT is located in read-only memory, which means we can’t just write a pointer to our function to the selected entry in the SSDT table.

To really understand what happens when we want to read, write or execute from some virtual address, take a look at the image below. The figure represents the entire process of translating a virtual address to its corresponding physical address and security elements along the way.

Let’s describe the picture above in detail: in code, a code segment and a virtual address are used to refer to a specific location in memory. First, the WP flag in the CR0 register is checked to see if it contains a value of 0 or 1. Basically, WP is used to protect read-only memory from being written to in kernel mode, allowing for additional protection when we access protected mode. Note that the WP bit takes effect only in kernel mode, while user mode code can never write to read-only pages, regardless of the value stored in the WP bit. The WP bit can have two values:

0: kernel can write to read-only pages regardless of R/W and U/S flags in PDE and PTE.
1: kernel must not write to read-only pages because WP is not set; instead, the R/W and U/S flags in PDE and PTE are used to determine whether the kernel can access certain pages – it can only access pages marked as writable, but never pages marked as read-only.
When checking whether certain:

Segment table: if the DPL of the segment descriptor is higher than the RPL of the code segment register, then access to that segment is allowed, otherwise it is ignored. The DPL member in the segment descriptor is used to distinguish between privileged and unprivileged instructions. There are two code and two data segments in the segment table, one with DPL=0 and the other with DPL=3. Both are mapped to the same base address of 0x00000000, but are considered different segments. This is because the CS register can contain a value of 0x08 (when executing privileged code) or 0x1B (when executing unprivileged code). If we execute the code when CS is set to 0x08, then the privileged code is executed, but if we execute the same code when CS is set to 0x1B, it is considered as unprivileged code.
Page Directory Table / Page Table: if the WP flag in the CR0 register is set to 1, then the R/W and U/S flags in the Page Directory Table and Page Table are used to define kernel access to a specific memory page. If the R/W (Read/Write) flag is set to 1, then the kernel can read and write to the page, otherwise it can only read from it. The U/S (User/Supervisor) flag is set to 1 for all pages that contain kernel addresses that are greater than 0x80000000. If unprivileged code (from user mode) attempts to access such pages, an access violation occurs: code that has the CS register set to 0x1B cannot access such pages.
We have seen that kernel address protection is implemented through a combination of segmentation and page-level protection. The CS register is basically used to determine whether code is granted read/write access to a particular page in memory. Note that we can execute privileged instructions from user mode using one of the following approaches [4]:

“int 0x2e” instruction.
sysenter instructions
long distance call
Now that we’ve cleared that all up, let’s see what kind of view we have of the SSDT table. First, we need to check the value stored in the 16-bit (WP – Write Protect) register CR0. We can easily do this by using the .formats command to print the value of the CR0 register in binary as seen below. The highlighted bit is the WP bit and is set to 1, which means that the page directory table and the page table are used to determine whether the CPU can read/write from/to the page. Because of this, the CPU will not be able to write to read-only pages.

Let’s now display a single entry from the SSDT KiServiceTable by using the “dps nt!KiServiceTable l1” command, which displays the first entry from the SSDT table that’s located at address 0x826af6f0. After that, we used the “!pte 0x826af6f0” command to display various flags about the PDE/PTE in which the address is located.

In the image above we see the first column which represents the PDE which has the following flags: DAKWEV and the second column which represents the PTE which has the following flags: GAKREV. The flags in the PDE/PTE entries printed by the !pte command are listed below [5]:

Valid (V): data resides in physical memory and has not been swapped.
Read/Write (R/W): data is read-only or write-only.
User/kernel (U/K): the page is owned by either user mode or kernel mode.
Writethrough (T): write-through caching policy.
CacheDisable (N): whether the page can be cached.
Accessed (A): set when the page was accessed either by reading from it or writing to it.
Dirty (D): the data on the page has been changed.
LargePool (L): used only in PDE and specifies whether large page sizes are used, which applies when PAE is enabled. If set, page size is 4MB, otherwise 4KB.
Global (G): affects translation cache flush and translation buffer cache.
Prototype (P): the software field used by Windows.
Executable (E): the instructions on the page can be executed.
Having studied the flags used by the !pte command, we can see that PDE is marked as writable, but PTE is read-only, but both are executable. This means that we cannot simply write some values ​​to the PTE where the SSDT table resides. Now that we know that we are dealing with a system service dispatch table located in read-only memory, we can start looking for ways to work around this limitation and mark the memory as writable. There are three ways to gain write access to the SSDT table [1]:

Changing the WP CR0 flag: if we set the WP flag in the CR0 register to 0, PDE/PTE restrictions are not taken into account when granting write access to read-only pages when we are in kernel mode.
Edit registry: we can change the registry key “HKLMSYSTEMCurrentControlSetControlSession ManagerMemoryManagementEnforceWriteProtection” which allows us to write.
MDL (Memory Descriptor List): The Windows operating system uses the MDL to describe the physical page layout for the virtual memory buffer. In order to be able to write to the SSDT table, we need to allocate our own MDL, which is associated with the physical memory where the SSDT table is stored. Since we have assigned our own MDL, we can control it however we want, and therefore we can also change its flags accordingly.
In this article we will see how we can write to SSDT using the first method by changing the WP bit in the CR0 register. I chose this method because it is the easiest to achieve and can be programmed in a few lines of assembly code. In the output below, I have introduced two functions to turn on and off the WP bit in the CR0 register.


  • Disable the WP bit in the CR0 register.

void DisableWP() {
__asm ​​{
push edx;
mov edx, cr0;
and edx, 0xFFFEFFFF;
mov cr0, edx;
pop edx;


  • Enable the WP bit in the CR0 register.

void EnableWP() {
__asm ​​{
push edx;
mov edx, cr0;
or edx, 0x00010000;
mov cr0, edx;
pop edx;


The DisableWP function stores the edx registry value on the stack so we can restore it later: push and pop instructions. We then move the value of the CR0 register into the edx register and perform an AND operation with 0xFFFEFFFF (note the middle E). This effectively ANDs the binary number [1111 1111 1111 1110 1111 1111 1111 1111], which means we keep all the bits except the WP bit from the original CR0 register – essentially clearing the WP register. After the AND operation, we overwrite the value in the CR0 register.

The EnableWP function does something very similar to the DisableWP function, except it performs an OR operation with 0x00010000. This means that the binary number [0000 0000 0000 0001 0000 0000 0000 0000] is ORed – this sets the WP flag back to 1.

The DisableWP function allows you to write to memory where the SSDT table is contained, and in the next part of the article we can actually hook the function call by overwriting the address in the SSDT table. Note that hooking functions without somehow enabling writing to read-only memory in kernel mode would not be possible.

Let’s see what happens if we don’t disable the WP bit in the CR0 register, but still try to write to read-only memory. We can try the same program as defined in the hooksssdt project, except we comment out the DisableWP() function call in the HookSSDT function. At the time the InterlockedExchange function is called, the system crashes and we are left with the following message written in the WinDbg debugger.

From the message, we can see that the previous DbgPrint was still executed successfully, but the system crashed at the time InterlockedExchange was called. At this point it is not immediately clear why the crash occurred, but we can run the command “!analyze -v” to see the details of the error. A portion of the output can be seen below where we can see that the driver wanted to write to read-only memory. This happened because we didn’t call the DisableWP() function, which would have disabled the WP bit in the CR0 register, allowing kernel mode to write to read-only memory.

Since we obviously don’t have access to write to read-only memory, the system crashed and gave us the above error. We can also do “r cr0” after the crash to display the contents of the CR0 register to make sure the WP bit is set to 1. If you look at the image below, you can notice the middle 1 in the register value, which means that the WP bit is set and the kernel does not have read-write access to read-only memory.

In this section of the article, we have shown why we need to use one of three techniques to enable kernel mode to write to read-only memory. In addition, we also introduced how the operating system performs checks when transitioning from user mode to kernel mode to disable user mode code from accessing privileged memory.

Environment settings

At this point we also need to introduce what environment we will be working with and how to set it up. I was working with Windows 7 operating system. We have to realize that we need two Windows operating systems to debug the kernel (basically with SoftICE we would need only one, but we will do everything with WinDbg).

The first Windows operating system needs to be configured to start in debug mode – this can be done by executing the following instructions in Windows cmd.exe under administrator privileges. The commands below will set Windows to start in debug mode where we will be able to debug Windows over the serial port.


bcdedit /set debug on

bcdedit /set debugtype serial

bcdedit /set debugport 1

bcdedit /set baudrate 115200

bcdedit /set {bootmgr} displaybootmenu yes

bcdedit /timeout 10


In order to debug the Windows OS, we first need to start another Windows VM with WinDbg installed and go to File – Kernel Debugging and accept the default settings as shown below. If we didn’t use the exact same commands as above, we need to change the settings in the Kernel Debug dialog accordingly.

After pressing OK, WinDbg will listen for incoming connections on the serial port. Since we’ve set up the Windows operating system on the second VM to connect to the same serial port, we’ll be able to debug Windows from the WinDbg debugger running. Additionally, we will be able to monitor the execution of the entire operating system, not just user mode code. When debugging with Ida Pro, OllyDbg, or ImmunityDebugger, we do not see execution of kernel-mode instructions located at virtual addresses 0x80000000-0xFFFFFFFF; we skip right over them because we’re running the debugger in user mode. In this case, we specifically instructed our Windows operating system to connect to the serial port where the WinDbg debugger listens for an incoming connection. Therefore, we are able to easily debug instructions in both user mode and kernel mode.

At this point we have effectively started the Windows operating system in debug mode and can start/stop it at will using the WinDbg debugger. First, we pause Windows execution by clicking Debug – Break in WinDbg as shown below. This effectively stops the debugged Windows OS and gives us a chance to run WinDbg commands.

Once we break into the system, we will be able to input WinDbg commands at the “kd>” shell, as seen on the picture below.

We have listed how we can go about debugging the kernel in the Windows operating system. We need to use this knowledge in the next section, where we actually hook a function whose pointer is stored in the SSDT table. Note that without kernel tuning, this kind of effort would be much harder, if not impossible, to achieve.

Connecting SSDT

Up until now, we’ve been setting the stage for actually hooking up SSDT, and at this point we’ve done all the preparations and can actually do it.

Remember we mentioned that KeServiceDescriptorTable exports ntoskrnl.exe while KeServiceDescriptorTableShadow doesn’t? We will need to know this detail later in the article, so you should make a note of it. Whenever the kernel exports a symbol, we can access it using the “__declspec(dllimport)” declaration. Dllimport can be used to tell the compiler that a given function is exported by the kernel and should not throw an error when used in a program – normally the compiler would throw an error because it knows nothing about the function. , so we have to specifically tell her not to worry about it. When using such an exported symbol, it is the linker’s job to find its address and make the appropriate changes by replacing the symbol with its actual address. So to use the KeServiceDescriptorTable symbol we need the following code.

/* A structure representing the system services table. */

typedef struct SystemServiceTable {
UINT32* service table;
UINT32* CounterTable;
UINT32 ServiceLimit;
UINT32* Argument table;
} SST;

/* Declaration of KeServiceDescriptorTable exported by ntoskrnl.exe. */

__declspec(dllimport) SST KeServiceDescriptorTable;

Since the KeServiceDescriptorTable symbol is just a location in memory, we need to define a structure and apply it to that location in memory. This means that when KeServiceDescriptorTable is referenced, the 16-byte SystemServiceTable will automatically be applied to contiguous memory to create a meaningful structure. Therefore, we will access the SystemServiceTable member using dot notation.

We also need to discuss a few features that we need to be aware of when connecting SSDT records. The first function is InterlockedExchange, which sets a 32-bit variable to a specified value as an atomic operation. An atomic operation is an operation that will be completed in one move no matter what. This means that when the InterlockedExchange function is called, the 32-bit value will be written to the specified location without being interrupted by any other processor. Note that there is only one SSDT table for all processors, so we need to ensure that the second processor cannot interrupt the first processor while writing values, because we could end up with the first processor only writing a word to the destination, not a dword, which could result in a system crash or other error. A prototype of the InterlockedExchange function can be seen below [7].

The InterlockedExchange function takes 2 arguments as input [7]:

Target: a pointer to the value to be changed.
Value: the value to be swapped with the value pointed to by the target.
The InterlockedExchange function returns the initial value of the Target parameter.

The second function we will also hook into is ZwQuerySystemInformation which retrieves the specified system information. Note that this feature is no longer available in Windows 8. A prototype of the feature is shown below [8]:

The function has the following parameters [8]

SystemInformationClass: specifies the type of system information we would like to get. The parameter can be one of the following values ​​defined in the SYSTEM_INFORMATION_CLASS enumeration type; the most important are the highlighted arguments that can be used to retrieve the relevant system data.
SystemBasicInformation: number of processes in the system.
SystemPerformanceInformation: returns information that can be used to generate the source for the random number generator.
SystemTimeOfDayInformation: returns information that can be used to generate a seed for a random number generator.
SystemProcessInformation: returns an array of structures for each process running on the system, which can be used to get various information about the process, such as its number of open handles, page file usage, number of pages of allocated memory, etc.
SystemProcessorPerformanceInformation: returns an array of structures for each processor in the system, which is used to obtain information about each processor.
SystemInterruptInformation: returns information that can be used to generate a seed for a random number generator.
SystemExceptionInformation: returns information that can be used to generate a seed for a random number generator.
SystemRegistryQuotaInformation: returns a SYSTEM_REGISTRY_QUOTA_INFORMATION structure.
SystemLookasideInformation: returns information that can be used to generate the source for the random number generator.
SystemInformation: a pointer to the buffer that will receive the requested information – the size and structure of the returned buffer depends entirely on the SystemInformationClass.
SystemInformationLength: the size of the buffer pointed to by the SystemInformation parameter, specified in bytes.
ReturnLength: an optional parameter where the actual size of the requested information is written.
When we wire a function, the very first thing we need to do is define its original prototype, which we can do using the code below [1].


ULONG SystemInformationClass,
PVOID system information,
ULONG length of system information,
PULONG ReturnLength


We also need to store the address of the old ZwQuerySystemInformation function, so we need another definition. This is needed so that we can call the old routine from an existing hook routine, so the functionality of the functions won’t change, just a little bit.

typedef NTSTATUS (*ZwQuerySystemInformationPrototype)(

ULONG SystemInformationCLass,
PVOID system information,
ULONG length of system information,
PULONG ReturnLength


ZwQuerySystemInformationPrototype oldZwQuerySystemInformation = NULL;

The global variable oldZwQuerySystemInformation is used as a placeholder to store the old address from the SSDT that we overwrote. This variable is set in the DriverEntry function when the HookSSDT function is called. The actual function call can be seen below where we can see that the HookSSDT function returns the address of the old pointer.

oldZwQuerySystemInformation = (ZwQuerySystemInformationPrototype)HookSSDT((PULONG)ZwQuerySystemInformation, (PULONG)Hook_ZwQuerySystemInformation);

The HookSSDT function is seen below and accepts two parameters: the first parameter is a pointer to the system function we would like to hook, and the second parameter is a pointer to the function that will hook the syscall routine. In the function, we first reserve some space for local variables, then we call the DisableWP function.

Then we use KeServiceDescriptorTable.ServiceTable to get a pointer to the SSDT table. This is exactly why we had to assign the SST structure to the KeServiceDescriptorTable – this way we can use dot notation to access the data members of the structure.

The “*((PULONG)(syscall + 0x1));” line of code looks quite complicated, but it really isn’t. It basically identifies the number of the system call we are trying to hang up on. It does this in a pretty quick and novel way: it adds a single byte to the pointer to the system call we’re trying to connect to. The way system calls are structured, the first instruction is always “mov eax, 105h”, where 105h is the system number. The system number is different for each system call, but basically it is stored in the first byte of the system call address – so adding 1 to the system call pointer actually accesses the system call number. To read a number from an address, we need to dereference the pointer using the *() syntax.

We then calculate the address to the function pointer in the SSDT table and store it in the target variable. At the end of HookSSDT, we call the InterlockedExchange function to store a pointer to our hooking function in the SSDT table and return the previous value: a pointer to the old hook function.

PULONG HookSSDT(PUCHAR syscall, PUCHAR hookaddr) {

/* local variables */
UINT32 index;
PLONG ssdt;
PLONG target;

/* disable WP bit in CR0 to enable writing to SSDT */
DbgPrint(“WP flag in CR0 was disabled.rn”);

/* identify address of SSDT table */
ssdt = KeServiceDescriptorTable.ServiceTable;
DbgPrint(“System call address is %x.rn”, syscall);
DbgPrint(“Address of hook function is %x.rn”, hookaddr);
DbgPrint(“Address of SSDT is: %x.rn”, ssdt);

/* identify the ‘syscall’ index into the SSDT table */
index = *((PULONG)(syscall + 0x1));
DbgPrint(“The index to the SSDT table is: %d.rn”, index);

/* get address of service routine in SSDT */
target = (PLONG)&(ssdt[index]);
DbgPrint(“Address of SSDT to be connected is: %x.rn”, target);

/* plug service routine into SSDT */
return (PUCHAR)InterlockedExchange(target, hookaddr);

There is still a Hook_ZwQuerySystemInformation function that will be called instead of the original ZwQuerySystemInformation. The function accepts the same arguments as the original function. The function basically just prints a message so we know it’s being called, but we could have run anything now. At the end of the function, we still need to call oldZwQuerySystemInformation, which stores a pointer to the old ZwQuerySystemInformation function.

NTSTATUS Hook_ZwQuerySystemInformation(ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength) {

/* local variables */
status NTSTATUS;

/* invoke new instructions */
DbgPrint(“ZwQuerySystemInformation hook named.rn”);

/* old function call */
status = oldZwQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
if(!NT_SUCCESS(status)) {
DbgPrint(“Failed to call native ZwQuerySystemInformation.rn”);
return status;

When pulling a driver from the kernel, we need to restore the old feature to clean up after ourselves.

Once we unload the driver from the kernel, the following is printed in WinDbg output, which clarifies that the hooked SSDT entry was restored to its original value.

Detecting SSDT Hooks

Here we will try to describe how we can go about detecting SSDT hooks. We can download and remove the GMER rootkit detector from [9]. From its official website, we can see that GMER is capable of detecting and removing rootkits while checking for malicious activity in the following items:

  • hidden processes
  • hidden threads
  • hidden modules
  • hidden services
  • hidden files
  • hidden disk sectors (MBR)
  • hidden Alternative data streams
  • hidden registry keys
  • SSDT hooking drivers
  • IDT hacking drivers
  • IRP call hooking drivers
  • inline hooks

After we have downloaded and installed GMER, we can run it normally. If we have already loaded the hooksssdt driver into the kernel, ZwQuerySystemInformation is already attached at GMER startup time. If this is the case, GMER will quickly detect that our ZwQuerySystemInformation has been attached, as we can see in the image below.

In order to test the system for rootkits, we need to run GMER and click on the Rootkit/Malware tab and then click on the Scan button. The image above also shows that it is mydriver.sys that was used to connect the ZwQuerySystemInformation.

Related article:Ethical Hacking Interview Questions 2023

If we wanted to detect it manually with our own program, we can do it quite easily. Note that we don’t need to run a thread on every processor in the system because there is only one SSDT table and all processors share it. Also, we don’t have to worry about writing to read-only memory, since we only need to read from it – this greatly simplifies the code. Therefore, the program used to find out if the SSDT has been connected to the SSDT can be quite simple: all we have to do is get the address of the SSDT table by reading KeServiceDescriptorTable.KiServiceTable and traversing it. Just make sure all pointers point to the ntoskrnl.exe module and not somewhere else. When connecting ZwQuerySystemInformation with mydriver.sys, it points to the mydriver.sys code, which makes the connection suspect. All pointers not pointing to ntoskrnl.exe driver memory space are considered hung and can be detected.


In the article, we saw how we can hook pointers to SSDT functions to take over execution when a system call is invoked. We looked at a real implementation where we plugged in the ZwQuerySystemInformation function. In order to do this, we first had to disable the WP bit in the CR0 register so that the kernel had read-only access to the memory. After we could write to read-only memory, we overwrote the ZwQuerySystemInformation function call pointer in the SSDT table with a pointer to our custom routine. This allows us to take control of the execution each time the ZwQuerySystemInformation function is called.

Later in the article we also saw how we can detect that the SSDT pointer is connected. We used the GMER rootkit detector to detect our mydriver.sys driver. It’s pretty easy to tell that the SSDT entry has hung because the pointer doesn’t point to the memory space of the ntoskrnl.exe module.

We have covered quite a few things in this article, which is important when we want to hang SSDT pointers. Note that SSDT hooks are easy to detect and remove, so they are not used much anymore, but they can still be useful when analyzing the code of some user-mode applications. If we need to analyze a program that is protected by various anti-debugging tricks, we can try hooking the SSDT table to get various information about which system calls it uses. This can provide a lot of information in figuring out what the program is actually doing and will certainly help in analyzing it.

There are many cases where this information can be quite valuable, we just have to determine when to use it. All code samples are also included in my Github account so they can be easily downloaded and modified for specific needs that may arise.


[1] Writing drivers to perform kernel-level SSDT hooking, AndrewThomas,

[2]Rootkit Analysis: Hiding SSDT hooks,

[3]New reverse engineering technique using API hooking and sysenter hooking, and

capturing of cash card access, NetAgent Co., Ltd, Kenji Aiko,

[4] Entering the kernel without a driver and getting interrupt information from APIC,

[5] Understanding !PTE, Part2: Flags and Large Pages,

[6] Using MDLs,

[7] InterlockedExchange function,

[8] ZwQuerySystemInformation function,

[9] GMER,

Leave a Reply

Your email address will not be published. Required fields are marked *