System Service Descriptor Table

In this documentation we will discuss SSDT in the windows systems.

What is SSDT?

So, SSDT (System Service Descriptor Table) is a list of system calls supported by the operating system. Recall the syscalls. SSDT is the first member of the Service Descriptor Table kernel memory structure:

typedef struct tagSERVICE_DESCRIPTOR_TABLE {
    SYSTEM_SERVICE_TABLE nt;    // SSDT Tablosu kendisine etkili bir pointer
    SYSTEM_SERVICE_TABLE win32k;
    SYSTEM_SERVICE_TABLE sst3; 
    SYSTEM_SERVICE_TABLE sst4;
} SERVICE_DESCRIPTOR_TABLE;

SSDT maps system calls (syscalls) to kernel API addresses. When a syscall is made by a user-mode program, it contains a service index. This index specifies which syscall to use. The operating system uses this index to parse the address of the corresponding function from the System Service Descriptor Table (SSDT) and redirects to the correct kernel function in ntoskrnl.exe. In case it sounds confusing, let me explain it in a diagram:

Let’s say you call CreateFile from User-Mode and the syscall number is 0x2. When the syscall is executed, the flow continues from the kernel space. You can see this in the diagram.

When switching to the kernel space, the first section will be SSDT (KiServiceTable). Here I would like you to think that it typically contains a list of offsets to access Kernel Routine Addresses. The offset is calculated with the corresponding syscall number and then redirected to the corresponding Routine Address. Thus NtCreateFile is executed. The following formula is used for the calculation:

Offset = KiServiceTableAddress + 4 * SSN

In short, SSDT and syscalls act as a bridge between API calls from user-mode and the corresponding kernel routine addresses. In this way, the kernel determines which function responds to a system call from user-mode space.

Analysis with WinDBG

Now let’s take a look at this SSDT with WinDBG:

The first address given in the output is fffff800798c7cb0, the address of the SSDT.

I wanted you to think of SSDT as a table, so let’s take a look at a few offsets in the table with the address:

The first 5 offsets from the table are sorted as shown. Now let’s take 056b3400 from these offsets and calculate its address.

If you look at the diagram again, the following formula is used to access the kernel routine address:

KernelRoutineAddress = KiServiceTableAddress + ( Offset >>> 4 )

Now let’s take a look at the address of the routine using this formula:

As you can see, the offset we took as an example belongs to NtWaitForSingleObject.

Finding Routine Address with Syscall

Now let’s see how we can find the Routine address using Syscall.

I mentioned NtCreateFile as an example. With WinDBG we can find the SSN number of this API from ntdll.dll:

We see that 0x55 is passed to the eax register. This is the syscall number of NtCreateFile.

Now let’s calculate the offset using this syscall number and then access the routine address:

As we can see from the output, the routine address of NtCreateFile is fffff80431a2d240. We can recreate the diagram to better understand these steps:

Coding

Let’s simply code what we have learned using the Kernel Driver.

Our scenario is that we will create a .txt file using the NtCreateFile and NtWriteFile APIs and write some text in it, but we will take the kernel routine addresses of the APIs and run the APIs directly from the routine address.

#pragma warning(disable: 4083 4005)
#include "main.h"

#define KiServiceTableAddress 0xfffff800798c7cb0
#define SSN_NtCreateFile 0x55
#define SSN_NtWriteFile 0x8

typedef (NTAPI* My_NtCreateFile)(
	_Out_ PHANDLE FileHandle,
	_In_ ACCESS_MASK DesiredAccess,
	_In_ POBJECT_ATTRIBUTES ObjectAttributes,
	_Out_ PIO_STATUS_BLOCK IoStatusBlock,
	_In_opt_ PLARGE_INTEGER AllocationSize,
	_In_ ULONG FileAttributes,
	_In_ ULONG ShareAccess,
	_In_ ULONG CreateDisposition,
	_In_ ULONG CreateOptions,
	_In_reads_bytes_opt_(EaLength) PVOID EaBuffer,
	_In_ ULONG EaLength
);

typedef (NTAPI* My_NtWriteFile)(
	IN HANDLE FileHandle,
	IN HANDLE Event OPTIONAL,
	IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
	IN PVOID ApcContext OPTIONAL,
	OUT PIO_STATUS_BLOCK IoStatusBlock,
	_In_reads_bytes_(Length) PVOID Buffer,
	IN ULONG Length,
	IN PLARGE_INTEGER ByteOffset OPTIONAL,
	IN PULONG Key OPTIONAL
);

uint32_t ReadMemory(uint64_t Address) {
	return *(volatile uint32_t*)Address;
}

uint64_t GetAbsoluteAddress(int SSN, UNICODE_STRING APIName) {
	if (0 == SSN) {
		DbgPrintEx(0, 0, "SSN is 0\n");
		return 0;
	}
	DbgPrintEx(0, 0, "Target API Name: %wZ\n", APIName);

	uint64_t RoutineAbsoluteAddress = 0;
	uint64_t OffsetAddress;
	uint32_t Offset = 0;

	OffsetAddress = KiServiceTableAddress + 4 * SSN;
	DbgPrintEx(0, 0, "Offset Address: 0x%llx\n", OffsetAddress);

	Offset = ReadMemory(OffsetAddress);
	DbgPrintEx(0, 0, "Offset: 0x%08x\n", Offset);

	RoutineAbsoluteAddress = KiServiceTableAddress + (Offset >> 4);
	DbgPrintEx(0, 0, "The absolute address of %wZ is: 0x%llx\n", APIName, RoutineAbsoluteAddress);

	return RoutineAbsoluteAddress;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
	UNREFERENCED_PARAMETER(RegistryPath);
	DriverObject->DriverUnload = UnloadDriver;

	HANDLE HandleFile = NULL;
	OBJECT_ATTRIBUTES ObjAttr;
	IO_STATUS_BLOCK IoStatusBlock;
	NTSTATUS Status = STATUS_SUCCESS;
	uint64_t Address = 0;
	UNICODE_STRING FileName;
	UNICODE_STRING NtCreateFileName;
	UNICODE_STRING NtWriteFileName;
	UNICODE_STRING Data;

	RtlInitUnicodeString(&NtCreateFileName, L"NtCreateFile");
	RtlInitUnicodeString(&NtWriteFileName, L"NtWriteFile");
	RtlInitUnicodeString(&FileName, L"\\??\\C:\\ssdt.txt");
	RtlInitUnicodeString(&Data, L"Hello SSDT!");

	InitializeObjectAttributes(&ObjAttr, &FileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);	

	Address = GetAbsoluteAddress(SSN_NtCreateFile, NtCreateFileName);
	if (0 == Address) {
		return STATUS_NOT_FOUND;
	}
	My_NtCreateFile MyNtCreateFile = (My_NtCreateFile)Address;

	Status = MyNtCreateFile(&HandleFile, GENERIC_WRITE, &ObjAttr, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, \
								0, FILE_OVERWRITE_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create File! Error: 0x%08x\n", Status);
		return Status;
	}
	DbgPrintEx(0, 0, "Created File!\n\n");

	Address = GetAbsoluteAddress(SSN_NtWriteFile, NtWriteFileName);
	if (0 == Address) {
		return STATUS_NOT_FOUND;
	}
	My_NtWriteFile MyNtWriteFile = (My_NtWriteFile)Address;
	
	Status = MyNtWriteFile(HandleFile, NULL, NULL, NULL, &IoStatusBlock, Data.Buffer, Data.Length, NULL, NULL);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to ZwWriteFile! Error: 0x%08x\n", Status);

		ZwClose(HandleFile);
		return Status;
	}
	DbgPrintEx(0, 0, "Wrote to File\n\n");

	ZwClose(HandleFile);
	return STATUS_SUCCESS;
}

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject) {
	UNREFERENCED_PARAMETER(DriverObject);

	DbgPrintEx(0, 0, "Unloading the Driver...\n");

	return STATUS_SUCCESS;
}

This is our code. Let’s start analyzing in detail:

#define KiServiceTableAddress 0xfffff800798c7cb0
#define SSN_NtCreateFile 0x55
#define SSN_NtWriteFile 0x8

First, in the project, we define the address of the KiServiceTable and the SSN numbers of the two APIs. Change the address of SSDT and SSN numbers which you get from WinDbg.

Address = GetAbsoluteAddress(SSN_NtCreateFile, NtCreateFileName);
if (0 == Address) {
	return STATUS_NOT_FOUND;
}

In Driverentry, we call the GetAbsoluteAddress function with the SSN number and the name of the API. With this API, we will get the kernel routine address of the API.

OffsetAddress = KiServiceTableAddress + 4 * SSN;
DbgPrintEx(0, 0, "Offset Address: 0x%llx\n", OffsetAddress);

When we look at the codes of the GetAbsoluteAddress function, we make the calculation mentioned before to find the address of the Offset in SSDT. Then we print the offset address.

Offset = ReadMemory(OffsetAddress);
DbgPrintEx(0, 0, "Offset: 0x%08x\n", Offset);

Then we obtain the offset by reading the content of the address we received and in the same way we print the offset to the screen.

RoutineAbsoluteAddress = KiServiceTableAddress + (Offset >> 4);
DbgPrintEx(0, 0, "The absolute address of %wZ is: 0x%llx\n", APIName, RoutineAbsoluteAddress);

Finally, we apply the formula to access the corresponding routine address.

My_NtCreateFile MyNtCreateFile = (My_NtCreateFile)Address;

Status = MyNtCreateFile(&HandleFile, GENERIC_WRITE, &ObjAttr, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, \
								0, FILE_OVERWRITE_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(Status)) {
	DbgPrintEx(0, 0, "Failed to Create File! Error: 0x%08x\n", Status);
	return Status;
}
DbgPrintEx(0, 0, "Created File!\n\n");

After getting the routine address of NtCreateFile, we give this address to the MyNtCreateFile structure we created and then call the NtCreateFile API using the routine address.

// NtWriteFile'in Routine adresini al 
Address = GetAbsoluteAddress(SSN_NtWriteFile, NtWriteFileName);
if (0 == Address) {
	return STATUS_NOT_FOUND;
}
My_NtWriteFile MyNtWriteFile = (My_NtWriteFile)Address;

// NtWriteFile'i çağır
Status = MyNtWriteFile(HandleFile, NULL, NULL, NULL, &IoStatusBlock, Data.Buffer, Data.Length, NULL, NULL);
if (!NT_SUCCESS(Status)) {
	DbgPrintEx(0, 0, "Failed to ZwWriteFile! Error: 0x%08x\n", Status);

	ZwClose(HandleFile);
	return Status;
}
DbgPrintEx(0, 0, "Wrote to File\n\n");

After running NtCreateFile, we get the routine address of NtWriteFile with the same codes and give the address to the structure we created and then run the NtWriteFile API.

Analyzing Driver

Now let’s follow Driver step by step and see what it does. I think this will help us to better understand it.

Firstly add the bp to the driver name, then run it. After running it, we need to get an output on the WinDBG screen that bp is triggered:

Then add a bp to GetAbsoluteAddress function to see the addresses we obtained and continue the program:

kd> bp SSDT!GetAbsoluteAddress
kd> g
Breakpoint 1 hit
SSDT!GetAbsoluteAddress:
fffff802`333c12f0 4889542410      mov     qword ptr [rsp+10h],rdx

We can examine the GetAbsoluteAddress function in the Disassembly screen. Our first section will be the part where we calculate the address of Offset:

First, we see that the value of the SSN variable, which is the first parameter of the function, is transferred to the eax register. Since the program enters the GetAbsoluteAddress function for the first time, we know that 0x55, NtCreateFile, contains the SSN number.

Then the received SSN number shl eax,2 shifts the value of eax 2 bits to the left. Let’s look at the formula for this part again to understand what it does:

Offset = KiServiceTableAddress + 4 * SSN

Now you may ask this: “The formula is multiplying, but in the background it is shifting 2 bits to the left”.

The shift left (shl) operation is equivalent to multiplying a number by a multiple of 2. Here shl eax, 2 multiplies eax by 4. To verify this, we can add a bp in this section and see what eax gets:

In this image, we can see the value of eax before and after the operation. Before the operation the value of eax is 0x55 (Dec: 85), after the operation the result is 0x154 (Dec: 340).

In the last part of this formula, the address of KiServiceTable is passed to rcx and the value of KiServiceTableAddress is subtracted from the rax register which stores the result 4 * SSN. Now I realize that you are confused here again.

Notice that it subtracts the value rcx from rax, even though subtraction is typically done using sub. So in other words, this means subtracting KiServiceTableAddress from rax. So this will give the same result as KiServiceTableAddress + 4 * SSN. I realize that you are confused, but you can keep in mind that it will give the same result as the formula we want to do.

To see the address of the offset, mov qword ptr OffsetAddress (rsp+40h), add a bp at the address corresponding to rax and continue the program. Let’s take a look at the address we get:

We see that the address is fffffff8046a6c7e04. We already did an example with NtCreateFile above, so the addresses match.

Our last section will be the formula part where we calculate the Routine address. After taking the offset of NtCreateFile from SSDT, we give the offset value to eax and then we see that it shifts 4 bit right. This is part of our formula.

Then rcx is given the address of KiServiceTable and then we see that it subtracts rcx from rax (as I mentioned above, don’t get confused, think of it as addition). Thus, we have accessed the routine address of NtCreateFile. Let’s take a look at the resulting address:

As you can see, we successfully get the result fffffff8046ac2d240. Let’s add a bp where the routing address is called and see where it redirects the flow:

Theen let’s see the result:

Finally, let’s continue the driver and see if a .txt is actually created and data is written into it:

That’s all!

Conclusion

In this documantetion, we analyzed SSDT in WinDBG and coded the findings. In the coding phase, we calculated the routine addresses of NtCreateFile and NtWriteFile with the formulas we learned. Using these addresses, we created a .txt file and wrote text in it. Finally, we analyzed the driver we coded in windbg.

I hope this documentation was useful for you. SSDT may seem confusing at first glance, but I have tried to explain the details as much as possible so that it does not remain theoretical and can be better understood.

References

Last updated on