Before we begin debugging malicious kernel code, you need to understand how kernel code works, why malware writers use it, and some of the unique challenges it presents. Windows device drivers, more commonly referred to simply as drivers, allow third-party developers to run code in the Windows kernel.
Drivers are difficult to analyze because they load into memory, stay resident, and respond to requests from applications. This is further complicated because applications do not directly interact with kernel drivers. Instead, they access device objects, which send requests to particular devices. Devices are not necessarily physical hardware components; the driver creates and destroys devices, which can be accessed from user space.
For example, consider a USB flash drive. A driver on the system handles USB flash drives, but an application does not make requests directly to that driver; it makes requests to a specific device object instead. When the user plugs the USB flash drive into the computer, Windows creates the “F: drive” device object for that drive. An application can now make requests to the F: drive, which ultimately will be sent to the driver for USB flash drives. The same driver might handle requests for a second USB flash drive, but applications would access it through a different device object such as the G: drive.
In order for this system to work properly, drivers must be loaded into the kernel, just as
DLLs are loaded into processes. When a driver is first loaded, its DriverEntry
procedure is called, similar to DLLMain
for DLLs.
Unlike DLLs, which expose functionality through the export table, drivers must register the
address for callback functions, which will be called when a user-space software component requests a
service. The registration happens in the DriverEntry
routine.
Windows creates a driver object structure, which is passed to the DriverEntry
routine. The DriverEntry
routine is responsible for filling this structure in with its callback functions. The DriverEntry
routine then creates a device that can be accessed from user
space, and the user-space application interacts with the driver by sending requests to that
device.
Consider a read request from a program in user space. This request will eventually be routed
to a driver that manages the hardware that stores the data to be read. The user-mode application
first obtains a file handle to this device, and then calls ReadFile
on that handle. The kernel will process the ReadFile
request, and eventually invoke the driver’s callback function responsible
for handling read I/O requests.
The most commonly encountered request for a malicious kernel component is DeviceIoControl
, which is a generic request from a user-space module to a
device managed by a driver. The user-space program passes an arbitrary length buffer of data as
input and receives an arbitrary length buffer of data as output.
Calls from a user-mode application to a kernel-mode driver are difficult to trace because of all the OS code that supports the call. By way of illustration, Figure 10-1 shows how a request from a user-mode application eventually reaches a kernel-mode driver. Requests originate from a user-mode program and eventually reach the kernel. Some requests are sent to drivers that control hardware; others affect only the internal kernel state.
Some kernel-mode malware has no significant user-mode component. It creates no device object, and the kernel-mode driver executes on its own.
Malicious drivers generally do not usually control hardware; instead, they interact with the main Windows kernel components, ntoskrnl.exe and hal.dll. The ntoskrnl.exe component has the code for the core OS functions, and hal.dll has the code for interacting with the main hardware components. Malware will often import functions from one or both of these files in order to manipulate the kernel.