Hooking the System Service Dispatch Table (SSDT)

Dejan Lukan
March 21, 2014 by
Dejan Lukan


In this article we'll present how we can hook the System Service Dispatch Table, but first we have to establish what the SSDT actually is and how it is used by the operating system. In order to understand how and why the SSDT table is used, we must first talk about system calls.

We know two ways that a system call can be invoked:

Earn two pentesting certifications at once!

Earn two pentesting certifications at once!

Enroll in one boot camp to earn both your Certified Ethical Hacker (CEH) and CompTIA PenTest+ certifications — backed with an Exam Pass Guarantee.

  • 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 the MSRs in order to quickly call into the kernel and is mainly used in newer versions of Windows.

The SSDT table holds the pointer to kernel functions, which are used upon system call invocation either by "int 0x2e" or sysenter instructions. The value stored in register eax is a system call number, which will be invoked in the kernel. On the picture below we can see that sysenter is called in ntdll.dll library and the system call number 0x25 will be called.

When calling the system call routine, the system call number is stored in the eax register, which is 32-bit value. But how is that number then used? It can't be an index into a table of pointers, because if 32-bits were used as an index, it would mean the table is 4GB large, which is certainly not so. With a little bit of research we can find our that the system service number is broken up unto three parts:

  • bits 0-11: the system service number (SSN) to be invoked.
  • bits 12-13: the service descriptor table (SDT).
  • bits 14-31: not used.

Only the lower 12-bits are used as an index into the table, which means the table is 4096 bytes in size. The upper 18-bits are not used and the middle 2-bits are used to select the appropriate service descriptor table – therefore we can have a maximum of 4 system descriptor tables (SDT). In 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 hold the following values (presenting the 16-bit values):

  • 0000xxxx xxxxxxxx: used by KeServiceDescriptorTable, where the x's can be 0 or 1, which further implies that the first table is used if the system service numbers are from 0×0 – 0xFFF.
  • 0001yyyy yyyyyyyy: used by KeServiceDescriptorTableShadow, where y's can be 0 or 1, which further implies that the second table is used if the system service numbers are from 0×1000 – 0x1FFF.

This means that the system service numbers in the EAX register can only be in the range of 0×0000 – 0x1FFFF, and all other values are invalid.

We can dump all the symbols which start with KeServiceDescriptor by using the "x nt!KeServiceDescriptor*

" command in WinDbg. The result of running that 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].

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

  • ServiceTable: points to an array of virtual addresses – the SSDT (System Service Dispatch Table), where each entry further points to a kernel routine.
  • CounterTable: not used.
  • ServiceLimit: number of entries in the SSDT table.
  • ArgumentTable: points to an array of bytes – the SSDP (System Service Parameter Table), where each byte represents the number of bytes allocated for function arguments for corresponding with each SSDT routine.

Let's present an overwrite of the process with a picture below, where we can see that "int 0x2e" as well as the sysenter instruction execute a system call based upon 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. The KeServiceDescriptorTable points to one SST, which further points to the SSDT table. The KeServiceDescriptorTableShadow points to two SSTs where the first one points to the same SSDT table and the second one point to a secondary SSDT table.

Let's now look at the whole process from a practical point of view and actually present all the previously described stuff on an actual Windows operating system. We're already presented the KeServiceDescriptorTable and KeServiceDescriptorTableShadow, so we must now display the SST fields of the KeServiceDescriptorTable as well as the KeServiceDescriptorTableShadow, which we can do with the dps command as presented below. Notice that the first 4 bytes contain a pointer to the SSDT table KiServiceTable, while the last 4 bytes contain the pointer to the argument table KiArgumentTable.

To summarize the values above, let's present all fields of:

  • KeServiceDescriptorTable
    • ServiceTable : 0x826af6f0
    • CounterTable : not used
    • ServiceLimit : 0x191 (401 in decimal)
    • ArgumentTable : 0x826afd38
  • KeServiceDescriptorTableShadow
    • ServiceTable : 0x967a5000
    • CounterTable : not used
    • ServiceLimit : 0x339 (825 in decimal)
    • ArgumentTable : 0x967a602c

Note that the KeServiceDescriptorTableShadow actually contains two SST entries, where the first one is the same as with KeServiceDescriptorTable, and a second SST is totally different. This is also the reason why we displayed 32 bytes in the second dps commad whereas 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 a single SSDT table is 0x400 (or 1024 in decimal). To dump the whole KiServiceTable we can use the "dps nt!KiServiceTable L poi nt!KiServiceLimit" command, which will automatically dump the whole table since we're using the "poi nt!KiServiceLimit". The "dps nt!KiServiceLimit l1" command actually prints the value of 0x191, which is exactly the number of entries in the SSDT table.

We're specifically interested in the KiServiceTable, which is the SSDT table we'll be hooking in the remainder of the article. At this point I just wanted to say that the SSDT table is very similar to the IDT table we've met in real mode, which is still used before entering protected mode. Therefore, we must just overwrite the pointers in the SSDT table in order to hook any kernel routine stored in there.

Read-Only Memory and the SSDT Table

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

To really understand what's going on when we would like to read, write or execute from some virtual address, take a look at the picture below. The picture presents the whole process of translating a virtual address to its corresponding physical address and the security features along the way.

Let's describe the picture above in detail: in the code the code segment and virtual address are used to reference certain memory location. At first, the WP flag in CR0 register is checked whether it contains the value 0 or 1. Basically the WP is used to protect the read-only memory from being written to in kernel-mode, which allows additional protection when we've gained access to protected mode. Note that the WP bit only takes effect in kernel-mode, while the user-mode code can never write to read-only pages, regardless of the value stored in WP bit. The WP bit can hold two values:

  • 0: the kernel is allowed to write to read-only pages regardless of the R/W and U/S flags in PDEs and PTEs.
  • 1: the kernel is not allowed to write to read-only pages due to the WP not being set; rather than that, the R/W and U/S flags in PDEs and PTEs are used to determine whether kernel has access to certain pages – it only has access to pages marked as writeable, but never to 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 the access to that segment is allowed, otherwise it's ignored. The DPL member in the segment descriptor is used to differentiate between privileged and unprivileged instructions. In segment table there are two code and two data segments, one having the DPL=0 and the other the DPL=3. They are both mapped to the same base address 0x00000000, but are considered as different segments. This is because the CS register can hold the value 0x08 (when privileged code executes) or 0x1B (when unprivileged code executes). If we're executing a code when CS is set to 0x08 then privileged code is being executed, but if we're executing the same code when CS is set to 0x1B, it's considered unprivileged code.
  • Page Directory Table / Page Table: if the WP flag in CR0 register is set to 1, then the R/W and U/S flags in page directory table and page table are used to defined access the kernel has to 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 merely read from it. The U/S (User/Supervisor) flag is set 1 for all pages that contain the kernel addresses, which are above 0x80000000. If an unprivileged code (from user-mode) tries to access such pages, an access violation occurs: a code that has CS register set to 0x1B doesn't have access to such pages.

We've seen that kernel address protection is realized with the combination of segmentation and page-level protection. Basically the CS register is used to determine whether the code is given read/write access to the specific page in memory. Remember that we can execute privileged instructions from user-mode by using one of the following approaches [4]:

  • "int 0x2e" instruction
  • sysenter instruction
  • far call

Now that we've got that all cleared out, let's see what kind of display we have regarding the SSDT table. First we have to check the value stored in the 16-bit (WP – Write Protect) of the CR0 register. We can easily do so by using the .formats command to print the value of CR0 register in binary form as seen below. The highlighted bit is WP bit and is set to 1, which means that page directory table and page table are used to determine whether the CPU can read/write from/to pages. Because of this, the CPU won't 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.

On the picture above we can see the first column, which represents the PDE that has the following flags: DAKWEV and the second column, which represents the PTE that has the following flags: GAKREV. The flags in PDE/PTE entries printed by the !pte command are presented below [5]:

  • Valid (V): in data is located in physical memory and has not been swapped out.
  • Read/Write (R/W): the data is read-only or writeable.
  • User/Kernel (U/K): the page is either owned by user-mode or kernel-mode.
  • Writethrough (T): a writethrough caching policy.
  • CacheDisable (N): whether or not the page can be cached.
  • Accessed (A): set when the page has been accessed by either reading from it or writing in it.
  • Dirty (D): the data in the page has been modified.
  • LargePool (L): only used in PDE and specifies whether large page sizes are used, which is true when PAE is enabled. If set, the page size is 4MB, otherwise it's 4KB.
  • Global (G): affects the translation caching flushes and translation lookaside buffer cache.
  • Prototype (P): a software field used by Windows.
  • Executable (E): the instructions in the page can be executed.

Once we've studied the flags used by the !pte command, we can see that the PDE is marked as writeable, but PTE is read-only, but both are executable. This means that we cannot simply write some values into the PTE where the SSDT table is located. Now that we know that we're dealing with a system service dispatch table located in read-only memory, we can start looking at a ways to bypass that limitation and mark the memory as writeable. There are three ways to get write access to the SSDT table [1]:

  • Change CR0 WP Flag: if we set the WP flag in CR0 register to 0, the PDE/PTE restrictions are not considered when granting write access to the read-only pages when we're in kernel-mode.
  • Modify Registry: we can alter the "HKLMSYSTEMCurrentControlSetControlSession ManagerMemoryManagementEnforceWriteProtection" registry key, which allows us write access.
  • MDL (Memory Descriptor List): the Windows operating system uses MDL to describe the physical page layout for a virtual memory buffer. To make the SSDT table writeable, we need to allocate our own MDL, which is associated with the physical memory of where the SSDT table is stored. Because we have allocated our own MDL, we can control it in any way we want and therefore can also change its flags accordingly.

In this article, we'll take a look at how we can make the SSDT writeable through the first method by changing the WP bit in the CR0 register. I chose this method, because it's the easiest to achieve and can be programming a few lines of assembly code. In the output below, I presented two functions for enabling and disabling the WP bit in the CR0 register.



* Disable the WP bit in CR0 register.


void DisableWP() {

__asm {

push edx;

mov edx, cr0;

and edx, 0xFFFEFFFF;

mov cr0, edx;

pop edx;




* Enable the WP bit in CR0 register.


void EnableWP() {

__asm {

push edx;

mov edx, cr0;

or edx, 0x00010000;

mov cr0, edx;

pop edx;




The DisableWP function is storing the value of register edx on the stack, so we can later restore it: the push and pop instructions. Then we're moving the value of register CR0 into register edx and performing an AND operation with 0xFFFEFFFF (notice the middle E). This effectively performs the AND operation with a binary number [1111 1111 1111 1110 1111 1111 1111 1111], which means that we're keeping all the bits except the WP bit from the original CR0 register – basically we're clearing the WP register. After the AND operation, we're overwriting the value in CR0 register.

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

The DisableWP function makes the memory where the SSDT table is contained writeable and in the next part of the article we can actually hook a function call by overwriting the address in the SSDT table. Remember that hooking the functions without somehow enabling kernel-mode to write to the read-only memory wouldn't 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 the read-only memory. We can try the same program as defined in hookssdt project, except that we comment out the DisableWP() function call in HookSSDT function. At the time of the InterlockedExchange function call, the system will crash and we'll be left with the following message written in WinDbg debugger.

From the message, we can see that the previous DbgPrint was still successfully executed, but the system crashed at the time of calling InterlockedExchange. At this time it's not immediately clear why the crash occurred, but we can execute the "!analyze -v" command, where we can see the details about the error. Part 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 disable the WP bit in the CR0 register and thus enable the kernel-mode to write to read-only memory.

Because we clearly don't have write access to the read-only memory, the system crashed giving us the above error. We can also execute the "r cr0" after the crash to display the contents of the CR0 register to make sure that the WP bit is set to 1. If you look at the picture below, you can notice the middle 1 in the register value, which indicates the WP bit is set and the kernel doesn't have read-write access to the read-only memory.

In this part of the article we've shown why we need to use one of the three techniques to enable kernel-mode to be able to write to read-only memory. Additionally, we also presented how the operating system performs the checks when going from user-mode to kernel-mode to disable user-mode code to access the privileged memory.

Setting up the Environment

At this point we also have to present what kind of environment we'll be working with and how to set it up. I was working with the Windows 7 operating system. We need to be aware of the fact that for kernel debugging we need two Windows operation systems (basically with SoftICE we would only need one, but we'll be doing everything with WinDbg).

The first Windows operation system needs to be configured so it will start in debugging mode – this can be done by executing the following instructions in Windows cmd.exe under Administrator privileges. Commands below will set Windows to start in debugging mode where we'll be able to debug Windows over a 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 operating system, we must first start another Windows virtual machine with WinDbg installed and go to File – Kernel Debugging and accept the defaults as presented below. If we didn't use exactly the same commands as outlined above, we need to change the settings in the Kernel Debugging dialog appropriately.

After pressing the OK button, the WinDbg will listen for incoming connections on a serial port. Because we've setup the Windows operating system in the other virtual machine to connect to the same serial port, we'll be able to debug Windows from the started WinDbg debugger. More than that, we'll be able to follow the execution of the whole operating system, not just the user-mode code. When debugging with Ida Pro, OllyDbg or ImmunityDebugger, we can't see the kernel-mode instructions located at virtual addresses 0x80000000-0xFFFFFFFF being executed; we're jumping right over them, because we're running a user-mode debugger. In this case, we've specifically instructed our Windows operating system to connect to the serial port, where the WinDbg debugger is listening for an incoming connection. Therefore, we're able to debug user-mode as well as kernel-mode instructions with ease.

At this point, we've effectively started Windows operating system in debug mode and we can start/stop it at will though WinDbg debugger. Let's first pause Windows execution by clicking on Debug – Break in WinDbg as shown below. That will effectively stop the debugged Windows operating system and give us a chance to execute 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've presented how we can go about kernel debugging in a Windows operating system. We need to use that knowledge in the next section where we'll actually hook a function whose pointer is stored in the SSDT table. Remember that without kernel debugging, this kind of endeavour would have been much harder, if not impossible to achieve.

Hooking the SSDT

Up until now, we've been laying the ground preparing for the actual SSDT hooking and at this time we've done all preparations and can actually do it.

Remember that we mentioned that KeServiceDescriptorTable is exported by the ntoskrnl.exe, while the KeServiceDescriptorTableShadow is not? We're going to need to know this detail later in the article, so you should make a note of it. Whenever a symbol is exported by the kernel, we can access it by using the "__declspec(dllimport)" declaration. The dllimport can be used to tell the compiler that a specified function is exported by the kernel and shouldn't throw an error when using it in the program – usually the compiler would throw an error, because it doesn't know anything about that function, so we must specifically tell it not to worry about it. When using an exported symbol like that, it is the job of a linker to find out its address and make appropriate changes by exchanging the symbol with its actual address. So, in order to be able to use the KeServiceDescriptorTable symbol, we need the following code.

/* The structure representing the System Service Table. */

typedef struct SystemServiceTable {

UINT32* ServiceTable;

UINT32* CounterTable;

UINT32 ServiceLimit;

UINT32* ArgumentTable;

} SST;

/* Declaration of KeServiceDescriptorTable, which is exported by ntoskrnl.exe. */

__declspec(dllimport) SST KeServiceDescriptorTable;

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

We also have to discuss a few functions that we need to be aware of when hooking SSDT entries. The first function is InterlockedExchange, which sets a 32-bit variable to the specified value as an atomic operation. Atomic operation is an operation which will be complete in one go, no matter what. That means that when calling the InterlockedExchange function, the value 32-bit value will be written to the specified location without being interrupted by some other processor. Note that there's only one SSDT table for all processors, so we need to ensure that the second processor cannot interrupt the first processor while writing the values, because we could end up with first processor writing just a word, not a dword to the destination, which could result in a system crash or some other error. The 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 exchanged.
  • Value: the value to be exchanged with the value pointed to by the Target.

The InterlockedExchange function returns the initial value of the Target parameter.

The second function, which we'll also be hooking, is the ZwQuerySystemInformation, which retrieves the specified system information. Note that the function is no longer available on the Windows 8 operating system. The prototype of the function is presented below [8]:

The function takes the following parameters [8]

  • SystemInformationClass: specifies the type of system information that we would like to retrieve. The parameter can be one of the following values defined in a SYSTEM_INFORMATION_CLASS enum type; the most important are the highlighted arguments, which can be used to retrieve relevant system data.
    • SystemBasicInformation: the number of processes in the system.
    • SystemPerformanceInformation: returns information that can be used to generate a seed for a 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 in the system, which can be used to get various information about a process, like its number of open handles, page-file usage, number of allocated memory pages, etc.
    • SystemProcessorPerformanceInformation: returns an array of structure for each processor in the system used to get 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 a seed for a random number generator.
  • SystemInformation: a pointer to the buffer, which will receive the requested information – the size and structure of returned buffer depends entirely upon the SystemInformationClass.
  • SystemInformationLength: the size of the buffer pointed to by the SystemInformation parameter, specified in bytes.
  • ReturnLength: optional parameter, where the actual size of the requested information is written.

When hooking a function, the very first thing we must do is define its original prototype, which we can do with the code presented below [1].


ULONG SystemInformationClass,

PVOID SystemInformation,

ULONG SystemInformationLength,

PULONG ReturnLength


We also need to store the address of the old ZwQuerySystemInformation function, which is why we need additional definition. This is needed so we can call the old routine from the existing hooking routine, so the functionality of the functions is not changed, just altered a little bit.

typedef NTSTATUS (*ZwQuerySystemInformationPrototype)(

ULONG SystemInformationCLass,

PVOID SystemInformation,

ULONG SystemInformationLength,

PULONG ReturnLength


ZwQuerySystemInformationPrototype oldZwQuerySystemInformation = NULL;

The oldZwQuerySystemInformation global variable is used as a placeholder for saving the old address from SSDT that we've overwritten. That variable is set in the DriverEntry function when calling the HookSSDT function. 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 can be seen below and accepts two parameters: the first parameter is a pointer to system function that we would like to hook and the second parameter is a pointer to a function which will be hooking the syscall routine. In the function, we're first reserving some space for local variables, after which we're calling the DisableWP function.

Then we're using the KeServiceDescriptorTable.ServiceTable to get a pointer to the SSDT table. This is exactly why we had to assign the SST structure to the KeServiceDescriptorTable – by doing that we can use the dot notation to access the data members of the structure.

The "*((PULONG)(syscall + 0x1));" code line looks quite complicated, but it actually isn't. Basically it identifies the number of the system call we're trying to hook. It does so in quite a quick and hacky way: it adds one byte to the pointer of the system call we're trying to hook. The way system calls are structured, the first instruction is always "mov eax, 105h", where the 105h is a system number. The system number is different in every system call, but basically it's stored in the first byte at the address of the system call – therefore by adding 1 to the system call pointer, we're actually accessing the system call number. To read the number from the address, we must de-reference the pointer by using the *() syntax.

After that, we're calculating the address to the function pointer in SSDT table and storing it in the target variable. At the end of the HookSSDT we're calling the InterlockedExchange function to store the pointer to our hooking function into the SSDT table and return the previous value: the pointer to the old hooked 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("The WP flag in CR0 has been disabled.rn");

/* identify the address of SSDT table */

ssdt = KeServiceDescriptorTable.ServiceTable;

DbgPrint("The system call address is %x.rn", syscall);

DbgPrint("The hook function address is %x.rn", hookaddr);

DbgPrint("The address of the SSDT is: %x.rn", ssdt);

/* identify 'syscall' index into the SSDT table */

index = *((PULONG)(syscall + 0x1));

DbgPrint("The index into the SSDT table is: %d.rn", index);

/* get the address of the service routine in SSDT */

target = (PLONG)&(ssdt[index]);

DbgPrint("The address of the SSDT routine to be hooked is: %x.rn", target);

/* hook the service routine in SSDT */

return (PUCHAR)InterlockedExchange(target, hookaddr);



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

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

/* local variables */
NTSTATUS status;

/* calling new instructions */
DbgPrint("ZwQuerySystemInformation hook called.rn");

/* calling old function */

status = oldZwQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);

if(!NT_SUCCESS(status)) {

DbgPrint("The call to original ZwQuerySystemInformation did not succeed.rn");


return status;



When unloading the driver from the kernel, we must restore the old function to clean up after ourselves. If we don't restore the original pointer to the old ZwQuerySystemInformation, then the system would probably crash while trying to execute the function, which doesn't exist. If we unload the driver from the kernel, its code is no longer present in the kernel, but the SSDT still points to it. So if the ZwQuerySystemInformation is called, it would try to execution the function from a memory address with unknown instruction, which means the system would probably crash completely.

/* restore the hook */

if(oldZwQuerySystemInformation != NULL) {

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


DbgPrint("The original SSDT function restored.rn");



Once we compile and load the driver into the kernel, the following will be printed into the WinDbg output. Notice how our DbgPrint is invoked every time the ZwQuerySystemInformation function is called? This means that our code is working and we're able to intercept function calls to ZwQuerySystemInformation function.

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'll try to describe how we can go about detecting the SSDT hooks. We can download the GMER rootkit detector and remove from [9]. From its official web page, we can see that GMER is able to detect and remove rootkits while it scans for malicious activity in the following items:

  • hidden processes
  • hidden threads
  • hidden modules
  • hidden services
  • hidden files
  • hidden disk sectors (MBR)
  • hidden Alternate Data Streams
  • hidden registry keys
  • drivers hooking SSDT
  • drivers hooking IDT
  • drivers hooking IRP calls
  • inline hooks

After we've downloaded and installed GMER, we can start it normally. If we've already loaded the hookssdt driver into the kernel, the ZwQuerySystemInformation is already hooked at the time of running GMER. If that is the case, GMER will quickly identify that our ZwQuerySystemInformation was hooked, as we can see on the picture below.

In order to scan the system for rootkits, we have to run GMER and click on the Rootkit/Malware tab, then click the Scan button. The picture above also discloses that it is the mydriver.sys, which was used to hook the ZwQuerySystemInformation.

If we would like to detect this manually by our own program, we can do that quite easily. Remember that we don't have to run the thread on every processor on 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, because we only need to read from it – this greatly simplifies the code. Therefore, the program used to detect whether SSDT has been hooked can be quite simple: all we need to do is get the address of the SSDT table by reading the KeServiceDescriptorTable.KiServiceTable and traversing it. We only need to look whether all the pointers are pointing to the ntoskrnl.exe module and not somewhere else. When hooking the ZwQuerySystemInformation with mydriver.sys driver, the pointer is pointing to the mydriver.sys code, which makes it a suspect to hooking. All the pointers not pointing to the ntoskrnl.exe driver memory space are considered hooked and can be detected.


In the article we've seen how we can hook SSDT function pointers in order to take over the execution when a system call is invoked. We've looked at an actual implementation, where we've hooked the ZwQuerySystemInformation function. In order to do so, we first had to disable the WP bit in CR0 register, so the kernel was able to access read-only memory. After being able to write to a read-only memory, we've overwritten the pointer to the ZwQuerySystemInformation function call in the SSDT table with a pointer to our own routine. This enables us to take control of execution every time the ZwQuerySystemInformation function is called.

Later in the article we also saw how we can detect the SSDT pointer being hooked. We used the GMER rootkit detector to detect our mydriver.sys driver. It's quite easy to detect that the SSDT entry has been hooked, because the pointer doesn't point to the ntoskrnl.exe module's memory space.

We've covered quite some ground in this article, which is important when we would like to hook SSDT pointers. Remember that SSDT hooks can be easily detected and removed, so they are not extensively used anymore, but can still be useful when analyzing the code of some user-mode application. If we have to analyze a program, which is protected with various anti-debugging tricks, we can try to hook the SSDT table to get various information about which system calls it's using. This can provide a lot of information in figuring out what the program actually does and will certainly help in its analysis.

There are a lot of cases where this information can be quite valuable, we just have to identify when to use it. All the code samples are also contained in my Github account, so it can easily be downloaded and modified for specific needs that may arise.


[1] Writing drivers to perform kernel-level SSDT hooking, AndrewThomas, http://www.unknowncheats.me/forum/c-and-c/59147-writing-drivers-perform-kernel-level-ssdt-hooking.html.

[2]Rootkit Analysis: Hiding SSDT hooks, http://www.securabit.com/wp-content/uploads/2010/03/Rootkit-Analysis-Hiding-SSDT-Hooks1.pdf.

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

capturing of cash card access, NetAgent Co., Ltd, Kenji Aiko, https://www.blackhat.com/presentations/bh-jp-08/bh-jp-08-Aiko/bh-jp-08-Aiko-EN.pdf.

[4] Entering the kernel without a driver and getting interrupt information from APIC, http://www.codeproject.com/Articles/11363/Entering-the-kernel-without-a-driver-and-getting-i.

[5] Understanding !PTE, Part2: Flags and Large Pages, http://blogs.msdn.com/b/ntdebugging/archive/2010/04/14/understanding-pte-part2-flags-and-large-pages.aspx.

[6] Using MDLs, http://msdn.microsoft.com/en-us/library/windows/hardware/ff565421(v=vs.85).aspx.

[7] InterlockedExchange function, http://msdn.microsoft.com/en-us/library/windows/desktop/ms683590(v=vs.85).aspx.

[8] ZwQuerySystemInformation function, http://msdn.microsoft.com/en-us/library/windows/desktop/ms725506(v=vs.85).aspx.

What should you learn next?

What should you learn next?

From SOC Analyst to Secure Coder to Security Manager — our team of experts has 12 free training plans to help you hit your goals. Get your free copy now.

[9] GMER, http://www.gmer.net/.

Dejan Lukan
Dejan Lukan

Dejan Lukan is a security researcher for InfoSec Institute and penetration tester from Slovenia. He is very interested in finding new bugs in real world software products with source code analysis, fuzzing and reverse engineering. He also has a great passion for developing his own simple scripts for security related problems and learning about new hacking techniques. He knows a great deal about programming languages, as he can write in couple of dozen of them. His passion is also Antivirus bypassing techniques, malware research and operating systems, mainly Linux, Windows and BSD. He also has his own blog available here: http://www.proteansec.com/.