Loading Kernel Driver

Objectives

The purpose of this lab is to explore an alternative and stealthy option of loading/unloading a kernel device driver via undocumented NTAPIs namely NtLoadDriver and NtUnloadDriver.

Loading a Kernel Driver

Till now, we have covered building exploit primitives but we still rely on 3rd party driver loaders like OSR Driver Loader to load our vulnerable and signed driver. Now, this is a luxury that we might not have in our exploitation scenarios ergo we have to learn how to load a device driver ourselves.

There are predominantly two ways by which we can load a device driver:

1. Service Control Driver Loading
2. Nt/ZwLoadDriver Driver Loading

Although we will discuss both of them, we will only implement the latter technique due to the reasons mentioned below.

There is also a third undocumented option to load kernel drivers using NtSetSystemInformation with SYSTEM_LOAD_AND_CALL_IMAGE. However, this method actually limits the capabilities of the driver and is impractical for our purposes ergo we shall not take this into consideration.

Service Control Driver Loading

We can load a kernel driver using Service Control Manager(SCM) either via a built-in tool called sc.exe or using Service Control API.

The advantages of using this method are that it is the officially supported/documented mechanism for loading kernel device drivers and it is quite easy to realize in code.

The disadvantages of this method are that it is a very noisy technique that leaves behind large forensic artefacts in the form of service creation, service start/stop, service deletion logs etc.

We can load/unload/query drivers by executing the following commands from an elevated command prompt:

sc create <service name> type= kernel bin= <full path to driver>
sc start <service name>
sc query <service name>
sc stop <service name>

Note that this method is also utilized by OSR Driver Loader.

NtLoadDriver Driver Loading

There is however a second option to load kernel drivers using an NTAPI function known as NtLoadDriver.

The advantages of using this method are that it is a stealthy alternative to load drivers without leaving behind glaring logs or alerts.

Stealth usually comes at the cost of increased complexity in terms of development and this is no different. When opting for this method, we have to take care of a couple of things manually which wasn't previously required.

The steps required to load a driver using this method are:

  1. Ensure that the driver PE/SYS file is available locally on disk.

  2. Next step is enabling a privilege in the access token that is required to load drivers known as SeLoadDriverPrivilege/SE_LOAD_DRIVER_NAME. It should be worth noting that this privilege is only available in a High-IL process token to be enabled in the first place implying non-admin processes shouldn't normally be able to load drivers(which of course makes sense right?)

  3. Our next step is creating a service registry key for the driver which is required for loading drivers via this technique. First, we create a service key at HKLM\SYSTEM\CurrentControlSet\Services\<driver name> and four subkeys called ImagePath, Type, Start and ErrorControl under it. Then the values of the subkeys are set to Driver Unicode path, 1(this is a kernel device driver), 3(we want a manual load) and finally 0(since we do not load the driver at startup) respectively.

  4. Now, we need to convert the service key path of the driver to Unicode. This can be achieved using RtlInitUnicodeString and is necessary since NtLoadDriver is required to be fed the Unicode path only.

  5. Finally, we can call NtLoadDriver with the Service Key Unicode path as the single argument to load the driver.

  6. To unload the driver, the steps remain the same through 1-4 but this time we make the call to NtUnloadDriver with the Service Key Unicode path as the single argument to unload the driver. Cleanup taskings can be performed afterwards as and when necessary.

Service registry key of driver

It should be noted that loading a driver via this technique will trigger PsSetLoadImageNotifyRoutine callback routines that might be registered by PSP and logging software such as Sysmon etc. Also, remember that the usual CI rules are also applicable while loading a driver using this technique.

Code

Now that we are done with the theory, let's implement the functions necessary for loading a kernel driver:

The below piece of code assumes that we already have SE_LOAD_DRIVER_NAME privilege available and enabled in the process token.

Utils.h
Utils.h
#define DRIVER_NAME L"MsIo64"
#define DRIVER_PATH L"\\??\\C:\\Windows\\System32\\drivers\\MsIo64.sys"
// Create registry entry for driver
// ------------------------------------------------------------------------
BOOL create_driver_reg_entry(LPCWSTR driverName, LPCWSTR driverPath) {
// Init some important stuff
BOOL ok = FALSE;
wchar_t keyPath[MAX_PATH];
UNICODE_STRING keyPathU;
NTSTATUS status;
HANDLE keyHandle = NULL;
OBJECT_ATTRIBUTES objectAttributes = { 0 };
UNICODE_STRING errorControlU;
DWORD errorControlValue = 0; // Do not show warning
UNICODE_STRING startU;
DWORD startValue = 3; // Load on demand
UNICODE_STRING typeU;
DWORD typeValue = 1; // Kernel device driver
UNICODE_STRING imagePathU;
LPCWSTR imagePathValue = driverPath;
SIZE_T imagePathSize = ((((DWORD)lstrlenW(imagePathValue) + 1)) * 2);
// Convert driver registry service key to unicode
swprintf(keyPath, MAX_PATH, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\%ls", driverName);
RtlInitUnicodeString(&keyPathU, keyPath);
// Create driver registry service key
objectAttributes.Length = sizeof(OBJECT_ATTRIBUTES);
objectAttributes.ObjectName = &keyPathU;
status = NtCreateKey(&keyHandle, KEY_ALL_ACCESS, &objectAttributes, 0, NULL, REG_OPTION_NON_VOLATILE, NULL);
if (status != STATUS_SUCCESS) {
printf("[-] NtCreateKey error: 0x%X\n", status); // [DBG]
goto cleanup;
}
// Convert ErrorControl subkey to unicode
RtlInitUnicodeString(&errorControlU, L"ErrorControl");
// Set up ErrorControl subkey
status = NtSetValueKey(keyHandle, &errorControlU, 0, REG_DWORD, &errorControlValue, sizeof(errorControlValue));
if (status != STATUS_SUCCESS) {
printf("[-] NtSetValueKey1 error: 0x%X\n", status); // [DBG]
goto cleanup;
}
// Convert Start subkey to unicode
RtlInitUnicodeString(&startU, L"Start");
// Set up Start subkey
status = NtSetValueKey(keyHandle, &startU, 0, REG_DWORD, &startValue, sizeof(startValue));
if (status != STATUS_SUCCESS) {
printf("[-] NtSetValueKey2 error: 0x%X\n", status); // [DBG]
goto cleanup;
}
// Convert Type subkey to unicode
RtlInitUnicodeString(&typeU, L"Type");
// Set up Type subkey
status = NtSetValueKey(keyHandle, &typeU, 0, REG_DWORD, &typeValue, sizeof(typeValue));
if (status != STATUS_SUCCESS) {
printf("[-] NtSetValueKey3 error: 0x%X\n", status); // [DBG]
goto cleanup;
}
// Convert ImagePath subkey to unicode
RtlInitUnicodeString(&imagePathU, L"ImagePath");
// Set up ImagePath subkey
status = NtSetValueKey(keyHandle, &imagePathU, 0, REG_EXPAND_SZ, (LPVOID)imagePathValue, imagePathSize);
if (status != STATUS_SUCCESS) {
printf("[-] NtSetValueKey4 error: 0x%X\n", status); // [DBG]
goto cleanup;
}
ok = TRUE;
// Cleanup
cleanup:
if (keyHandle)
NtClose(keyHandle);
return ok;
}
// To load a driver from disk
// Needs SE_LOAD_DRIVER_NAME privilege enabled in process token
// Needs driver registry service key to be set
// ------------------------------------------------------------------------
BOOL load_driver(LPCWSTR driverName) {
// Init some important stuff
wchar_t keyPath[MAX_PATH];
UNICODE_STRING keyPathU;
NTSTATUS status;
// Convert driver registry service key to unicode
swprintf(keyPath, MAX_PATH, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\%ls", driverName);
RtlInitUnicodeString(&keyPathU, keyPath);
// Load driver
status = NtLoadDriver(&keyPathU);
if (status == STATUS_IMAGE_ALREADY_LOADED || status == STATUS_OBJECT_NAME_COLLISION || status != STATUS_SUCCESS) {
printf("[-] Driver may already be loaded, please unload it to continue!\n"); // [DBG]
printf("[-] NtLoadDriver error: 0x%X\n", status); // [DBG]
return FALSE;
}
return TRUE;
}
// To unload a loaded driver
// Needs SE_LOAD_DRIVER_NAME privilege enabled in process token
// Needs driver registry service key to be set
// ------------------------------------------------------------------------
BOOL unload_driver(LPCWSTR driverName) {
// Init some important stuff
wchar_t keyPath[MAX_PATH];
UNICODE_STRING keyPathU;
NTSTATUS status;
// Convert driver registry service key to unicode
swprintf(keyPath, MAX_PATH, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\%ls", driverName);
RtlInitUnicodeString(&keyPathU, keyPath);
// Unload driver
status = NtUnloadDriver(&keyPathU);
if (status != STATUS_SUCCESS) {
printf("[-] NtUnloadDriver error: 0x%X\n", status); // [DBG]
return FALSE;
}
return TRUE;
}

There's not much to explain in this short code snippet since I have taken the effort to comment on every line so as to prevent any confusions.

And here is the output(+ some DebugViewfu to verify our code)

Loading kernel driver

Note that this test was conducted with a different test driver that uses DbgPrint to print debug messages on load/unload which are captured via DebugView. This is of course done for easier verification and demonstration.

Links

Credits

  1. loadup by _xeroxz