Reverse engineering

Loading the Windows Kernel Driver

Dejan Lukan
April 12, 2013 by
Dejan Lukan

In the previous part of the article, we've explained how to compile the Windows kernel driver. Now that we know how to compile the driver, we also have to look at how to load it into the kernel. We'll be using the Service Control Manager (SCM), which is a services.exe program under Windows that is responsible for starting, stopping and interacting with Windows service processes. The picture below shows that services.exe program is indeed running:

Become a certified reverse engineer!

Become a certified reverse engineer!

Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst.

In the article, we'll see different methods of interacting with the SCM: by using OSR Driver Loader, sc.exe and of course by using the Win32 API functions. The services.exe program is started early on in the system startup. After it is started, it must launch all of the services that are configured to start automatically. When the services.exe program starts, the internal database is initialized by reading the HKLMSYSTEMCurrentControlSetControlServiceGroupOrderList registry key, which contains the names and order of service groups [9]. This can be seen on the picture below:

Another registry key is also read, the HKLMSYSTEMCurrentControlSetServices, which contains the database of services and device drivers, which is read into the SCM's internals database [9]. Some of the services are presented below:

Services that have the Type registry value set to SERVICE_KERNEL_DRIVER are device driver services that load device drivers from the C:WINDOWSSystem32drivers directory. To do that, the NtLoadDriver function call is invoked.

Let's first start the winobj.exe program to check out which drivers are currently loaded. We can see that on the picture below:

On the picture above, we have selected the DSFKSvcs device name. Since the order of devices is listed alphabetically, the Example device name should appear directly after the selected name once we load the driver. Let's first download the OSR Driver Loader and select our driver.sys (seen in the Driver Path on the picture below):

After that, click on the Register Service and Start Service. As soon as this happens, we need to refresh the drivers in winobj.exe, which will now list the Example driver, as seen on the picture below:

We can see that the driver has been loaded into the kernel, which is exactly what we're trying to achieve. But this by itself doesn't tell us much, because we can't directly interact with the driver and see whether it's doing anything or now. This is exactly why dbgview.exe comes in handy, because it should display the messages we're printing with DbgPrint in the kernel driver. Right after starting dbgview.exe, we need to enable the "Capture Kernel" option, which enables logging of kernel messages (otherwise we won't see the messages printed by our driver):

If we go back to the OSR Driver Loader and click on Stop Service, then Start Service again, we will see our DbgPrint statements written in dbgview.exe. We can see that on the picture below:

We've come to the point where all of this suddenly seems very cool, because we can actually see what we were working on. The first entry in dbgview.exe is printed by the DriverUnload function, because we've unloaded the driver. And the second entry is printed by the DriverEntry routine, since we're loading the driver again.

But there's one problem with loading the driver like this: it leaves a trail in the registry under the HKLMSystemCurrentControlSetServicesdriver key, as seen below:

We will explain this in more detail later. Let's just say that by using the above approach. the entry is written to the registry, which leaves behind a trail, so a security researcher looking for an evidence of a compromise can easily find the entry in registry.

We can also start the service and load the driver directly from the command prompt by using the sc.exe command. We can see all of the commands on the picture below, where we're first creating the service named example and then we're starting that same service. When the service is started it will print some of the information about itself: the type of service, which is KERNEL_DRIVER and the state, which is RUNNING, etc…

We used the "sc.exe create" command to create a new entry; this command calls the underlying CreateService() function. Keep in mind that there should be a space after the '=' character and before the values of the parameters. We used the start= command with the demand option, but remember that there are other options as well. They are listed below:

  • boot : the driver will be loaded by system boot loader winload.exe
  • system : the driver will be loaded by kernel ntoskrnl.exe
  • auto : the driver will be loaded by services.exe
  • demand : the driver is loaded manually
  • disabled : the driver cannot be loaded

If we have dbgview.exe open at the same time of loading the driver with "sc.exe start", we'll see a new "DriverEntry Called" message that will be printed to the debug log, which proves that the driver has been successfully loaded into the kernel. After the service is created with the "sc.exe create", it will be saved into the registry under the HKLMSystemCurrentControlSetServicesexample key as seen below:

We can see that the "sc.exe create" command created a new entry example with the key-value pair as seen on the picture above. The macro names for the numbers above can be seen in the winnt.h header file located in the C:WinDDK7600.16385.1incapi folder. The Type name has the value 0x1, which is the SERVICE_KERNEL_DRIVER macro as seen below:

Once the service is started, another folder will be created, the Enum folder, as seen on the picture below:

But there's also a third option to load the driver into the kernel mode: that is by using the code accessible in the LoadDriver/ directory of the example at [1]. To compile the example, we have to delete the makefile and create a sources file with the following contents:

Once we've started the build environment and issued the bcz command, the main.exe executable program will be created. Let's present the whole code taken from [1] that does this:

[cpp]

#include <windows.h>

#include <stdio.h>

int _cdecl main(void)

{

HANDLE hSCManager;

HANDLE hService;

SERVICE_STATUS ss;

hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

printf("Load Drivern");

if(hSCManager)

{

printf("Create Servicen");

hService = CreateService(hSCManager, "Example", "Example Driver", SERVICE_START | DELETE | SERVICE_STOP, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, "C:example.sys", NULL, NULL, NULL, NULL, NULL);

if(!hService)

{

hService = OpenService(hSCManager, "Example", SERVICE_START | DELETE | SERVICE_STOP);

}

if(hService)

{

printf("Start Servicen");

StartService(hService, 0, NULL);

printf("Press Enter to close servicern");

getchar();

ControlService(hService, SERVICE_CONTROL_STOP, &amp;ss);

CloseServiceHandle(hService);

DeleteService(hService);
}

CloseServiceHandle(hSCManager);
}

return 0;

}

[/cpp]

When compiling the code, you should change the path to the sys driver, so the driver can be found and loaded into the kernel. After the compilation phase is done, we can start the program and it will load the driver into the kernel, so our user application can use its services. Let's analyze the code now.

First we're calling the OpenSCManager function to establish a connection to the service control manager and its database. The syntax of the function is presented below and was taken from [5]:

The lpMachineName is the name of the target computer, which is NULL in our case. That means that we're connecting to the service control manager on the local computer. The lpDatabaseName specifies service control manager database, which is also NULL in our case. That means that the SERVICES_ACTIVE_DATABASE database is opened by default. The dwDesiredAccess specifies the access to the service control manager: we used the SC_MANAGER_CREATE_SERVICE, which requests permissions to call the CreateService function to create a service object and add it to the database. The OpenSCManager function returns NULL on failure, otherwise a handle is returned.

[cpp]

hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

[/cpp]

Then we're calling the CreateService function that creates a service object and adds it to the specified service control manager database. The syntax can be seen below [6]:

[cpp]

hService = CreateService(hSCManager, "Example", "Example Driver", SERVICE_START | DELETE | SERVICE_STOP, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, "C:example.sys", NULL, NULL, NULL, NULL, NULL);

[/cpp]

The parameters to the function are pretty much self-explanatory. Let's specifically mention only the lpBinaryPathName parameter that should contain a path to our driver. The function returns NULL on failure, otherwise it returns a handle to the service.

At last, we're also calling the OpenService function that opens an existing service. The syntax is as follows [7]:

The lpServiceName specifies the name of the service to be opened and should be the same as specified as the lpServiceName when calling the CreateService function. The unction returns NULL on failure, otherwise it returns a handle to the service.

[cpp]

hService = OpenService(hSCManager, "Example", SERVICE_START | DELETE | SERVICE_STOP);

[/cpp]

At last, the StartService function is called to actually start the service. The syntax of the function call is as follows [8]:

The hService is a handle to the server, which is returned by the OpenService function call. The function returns zero on failure, otherwise a non-zero number is returned.

[cpp]

StartService(hService, 0, NULL);

[/cpp]

When we compile and run the program, it will create the same entry as before in the registry, so even with this option there are forensic evidences left in registry, which can arise suspicion in a system administrator.

When we have loaded our driver with Service Control Manager (SCM), there was an entry saved to the registry, which leaves a trail of our driver being loaded. Since we want to be stealth, we want to load the driver with as little evidence as possible. To do that without an entry being added to the registry, we need to use an export driver.

A disport driver supports a subset of features of real kernel driver; it doesn't have a dispatch table, it doesn't have a place in the driver stack and it doesn't have an entry in the SCM database that defines it as a system service [10]. Because it doesn't have an entry in the SCM database, there is no proof of our driver being loaded in the registry. When we build an export driver, we must place it in the C:WINDOWSSystem32drivers directory in order for its functions to be accessible. Also, the driver is only loaded into the kernel when we're using it from another drivers; thus, if no driver is using the exported driver, then the exported driver is not loaded into the kernel. Because we need to have another driver that loads the exported driver, this is not exactly the solution we're looking for, because there's considerably more evidence left on the system when using an exported driver. This is because we have an exported driver present in the C:WINDOWSSystem32drivers directory and yet another driver that must load the exported driver that leaves a footprint in the registry. This is also the reason why we don't want to use an exported driver in the first place; we must find another way to load the drivers into the kernel, presumably one that leaves less footprints on the system. One way we can go about it is find a vulnerability in the Windows operating system itself to leverage our kernel driver into loading in the kernel mode.

User Mode Application
Let's also present an example also taken from [1], which is a user application that can communicate with the kernel driver. The whole code of the user application can be seen below:

[cpp]

#include <windows.h>

#include <stdio.h>

int _cdecl main(void)

{

HANDLE hFile;

DWORD dwReturn;

hFile = CreateFile("\.Example", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

if(hFile)

{

WriteFile(hFile, "Hello from user mode!", sizeof("Hello from user mode!"), &amp;dwReturn, NULL);

CloseHandle(hFile);

}

return 0;

}

[/cpp]

We can see that we're dealing with a very simple program. We can compile the program with the same sources file as we used in the previous cases. Once the compilation is complete, we'll have a main.exe program, which we can run. At the same time we have to have dbgview.exe open, so we can see any messages printed to the debugger console log. Once we run the main.exe program, we can see the following printed to the console log:

The first entry is present because the driver was loaded into the kernel. All the other entries are printed because we've run the user mode application main.exe. We can see that the message from user mode was printed to the console, which means that we've successfully passed a string from the user application to the kernel mode driver. Notice that the "Hello from user mode!" message is exactly the message we're inputting in the WriteFile function in our user mode application? This is the message that's being printed by the kernel by using the DbgPrint function.

Conclusion

In this article we've seen how to load the kernel driver into the kernel and explained a user application that used the driver's services to do some action.

References:

[1] Driver Development Part 1: Introduction to Drivers, accessible at http://www.codeproject.com/Articles/9504/Driver-Development-Part-1-Introduction-to-Drivers.

[2] IoGetCurrentIrpStackLocation routine, accessible at http://msdn.microsoft.com/en-us/library/windows/hardware/ff549174(v=vs.85).aspx.

[3] MmGetSystemAddressForMdlSafe macro, accessible at http://msdn.microsoft.com/en-us/library/windows/hardware/ff554559(v=vs.85).aspx.

[4] IRP, accessible at http://msdn.microsoft.com/en-us/library/windows/hardware/ff550694(v=vs.85).aspx.

[5] OpenSCManager function, accessible at http://msdn.microsoft.com/en-us/library/windows/desktop/ms684323(v=vs.85).aspx.

[6] CreateService function, accessible at http://msdn.microsoft.com/en-us/library/windows/desktop/ms682450(v=vs.85).aspx.

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

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

[9] Service Control Manager, accessible at http://en.wikipedia.org/wiki/Service_Control_Manager.

Become a certified reverse engineer!

Become a certified reverse engineer!

Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst.

[10] Creating Export Drivers, accessible at http://msdn.microsoft.com/en-us/library/windows/hardware/ff542891(v=vs.85).aspx.

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/.