In this section, we’ll examine a program that writes to files from kernel space. For
malware authors, the benefit of writing to files from kernel space is that it is more difficult to
detect. This isn’t the stealthiest way to write to a file, but it will get past certain
security products, and can mislead malware analysts who are looking for telltale calls in the user
space to CreateFile
or WriteFile
functions. The normal Win32 functions are not easily accessible from kernel
mode, which presents a challenge for malware authors, but there are similar functions that are used
regularly in malware written from the kernel. Since the CreateFile
and WriteFile
functions are not available
in the kernel mode, the NtCreateFile
and NtWriteFile
functions are used instead.
In our example, a user-space component creates a driver that will read and write the files in the kernel. First we look at our user-space code in IDA Pro to investigate what functions it calls to interact with a driver as shown in Example 10-4.
Example 10-4. Creating a service to load a kernel driver
04001B3D push esi ; lpPassword
04001B3E push esi ; lpServiceStartName
04001B3F push esi ; lpDependencies
04001B40 push esi ; lpdwTagId
04001B41 push esi ; lpLoadOrderGroup
04001B42 push [ebp+lpBinaryPathName] ; lpBinaryPathName
04001B45 push 1 ; dwErrorControl
04001B47 push 3 ; dwStartType
04001B49 push ❶1 ; dwServiceType
04001B4B push 0F01FFh ; dwDesiredAccess
04001B50 push [ebp+lpDisplayName] ; lpDisplayName
04001B53 push [ebp+lpDisplayName] ; lpServiceName
04001B56 push [ebp+hSCManager] ; hSCManager
04001B59 call ds:__imp__CreateServiceA@52
We see in the service manager routines that a driver is being created with the CreateService
function. Note the parameter for dwService
type ❶ is 0x01
. This value indicates that this is a kernel driver.
Then we see in Example 10-5 that a file is being
created to get a handle to a device with a call to CreateFileA
at
❶. The filename pushed onto the stack is stored in EDI
at ❷. (Not pictured is the EDI being loaded with the
string \.FileWriterDevice
, which is the name of the object
created by the driver for the user-space application to access.)
Example 10-5. Obtaining a handle to a device object
04001893 xor eax, eax 04001895 push eax ; hTemplateFile 04001896 push 80h ; dwFlagsAndAttributes 0400189B push 2 ; dwCreationDisposition 0400189D push eax ; lpSecurityAttributes 0400189E push eax ; dwShareMode 0400189F push ebx ; dwDesiredAccess 040018A0 ❷push edi ; lpFileName 040018A1 ❶call esi ; CreateFileA
Once the malware has a handle to the device, it uses the DeviceIoControl
function at ❶ to send data to
the driver as shown in Example 10-6.
Example 10-6. Using DeviceIoControl
to communicate from user space to
kernel space
04001910 push 0 ; lpOverlapped
04001912 sub eax, ecx
04001914 lea ecx, [ebp+BytesReturned]
0400191A push ecx ; lpBytesReturned
0400191B push 64h ; nOutBufferSize
0400191D push edi ; lpOutBuffer
0400191E inc eax
0400191F push eax ; nInBufferSize
04001920 push esi ; lpInBuffer
04001921 push 9C402408h ; dwIoControlCode
04001926 push [ebp+hObject] ; hDevice
0400192C call ds:DeviceIoControl❶
At this point, we’ll switch gears to look at the kernel-mode code. We will
dynamically analyze the code that will be executed as a result of the DeviceIoControl
call by debugging the kernel.
The first step is to find the driver in the kernel. If you’re running WinDbg with a kernel debugger attached and verbose output enabled, you will be alerted whenever a kernel module is loaded. Kernel modules are not loaded and unloaded often, so if you are debugging your malware and a kernel module is loaded, then you should be suspicious of the module.
When using VMware for kernel debugging, you will see KMixer.sys frequently loaded and unloaded. This is normal and not associated with any malicious activity.
In the following example, we see that the FileWriter.sys driver has been loaded in the kernel debugging window. Likely, this is the malicious driver.
ModLoad: f7b0d000 f7b0e780 FileWriter.sys
To determine which code is called in the malicious driver, we need to find the driver object.
Since we know the driver name, we can find the driver object with the !drvobj
command. Example 10-7 shows example
output:
Example 10-7. Viewing a driver object for a loaded driver
kd> !drvobj FileWriter
Driver object (❶827e3698) is for:
Loading symbols for f7b0d000 FileWriter.sys -> FileWriter.sys
*** ERROR: Module load completed but symbols could not be loaded for FileWriter.sys
DriverFileWriter
Driver Extension List: (id , addr)
Device Object list:
826eb030
Sometimes the driver object will have a different name or !drvobj
will fail. As an alternative, you can browse the driver
objects with the !object Driver
command.
This command lists all the objects in the Driver
namespace, which is one of the root namespaces discussed in Chapter 7.
The driver object is stored at address 0x827e3698
at
❶. Once we have the address for the driver object, we
can look at its structure using the dt
command, as shown in Example 10-8.
Example 10-8. Viewing a device object in the kernel
kd>dt nt!_DRIVER_OBJECT 0x827e3698 nt!_DRIVER_OBJECT +0x000 Type : 4 +0x002 Size : 168 +0x004 DeviceObject : 0x826eb030 _DEVICE_OBJECT +0x008 Flags : 0x12 +0x00c DriverStart : 0xf7b0d000 +0x010 DriverSize : 0x1780 +0x014 DriverSection : 0x828006a8 +0x018 DriverExtension : 0x827e3740 _DRIVER_EXTENSION +0x01c DriverName : _UNICODE_STRING "DriverFileWriter" +0x024 HardwareDatabase : 0x8066ecd8 _UNICODE_STRING "REGISTRYMACHINE HARDWAREDESCRIPTIONSYSTEM" +0x028 FastIoDispatch : (null) +0x02c DriverInit : 0xf7b0dfcd long +0 +0x030 DriverStartIo : (null) +0x034 DriverUnload : 0xf7b0da2a void +0 +0x038 MajorFunction : [28] 0xf7b0da06 long +0
The entry for MajorFunction
in this structure is a
pointer to the first entry of the major function table. The major function table tells us what is
executed when the malicious driver is called from user space. The table has different functions at
each index. Each index represents a different type of request, and the indices are found in the file
wdm.h and start with IRP_MJ_
. For example,
if we want to find out which offset in the table is called when a user-space application calls
DeviceIoControl
, we would look for the index of IRP_MJ_DEVICE_CONTROL
. In this case, IRP_MJ_DEVICE_CONTROL
has a value of 0xe
, and the
major function table starts at an offset of 0x038
from the
beginning of the driver object. To find the function that will be called to handle the DeviceIoControl
request, use the command dd
827e3698+0x38+e*4 L1
. The 0x038
is the offset to the
beginning of the table, 0xe
is the index of the IRP_MJ_DEVICE_CONTROL
, and it’s multiplied by 4 because each pointer
is 4 bytes. The L1
argument specifies that we want to see only
one DWORD
of output.
The preceding command shows that the function called in the kernel is at 0xf7b0da66, as shown
in Example 10-9. We can check to see if the
instructions at that address look valid by using the u
command.
In this case they do, but if they did not, it could mean that we made an error in the address
calculation.
Example 10-9. Locating the function for IRP_MJ_DEVICE_CONTROL
in a
driver object
kd> dd 827e3698+0x38+e*4 L1 827e3708 f7b0da66 kd> u f7b0da66 FileWriter+0xa66: f7b0da66 6a68 push 68h f7b0da68 6838d9b0f7 push offset FileWriter+0x938 (f7b0d938) f7b0da6d e822faffff call FileWriter+0x494 (f7b0d494)
Now that we have the address, we can either load the kernel driver into IDA Pro or set a
breakpoint on that function and continue to analyze it within WinDbg. It’s usually easier to
start by analyzing the function in IDA Pro and then use WinDbg if further analysis is needed. While
scanning through the IDA Pro output of our malicious example driver, we found the code in Example 10-10, which calls ZwCreateFile
and ZwWriteFile
to write to a file from
kernel space.
Example 10-10. Code listing for IRP_MJ_DEVICE_CONTROL
function
F7B0DCB1 push offset aDosdevicesCSec ; "\DosDevices\C:\secretfile.txt"
F7B0DCB6 lea eax, [ebp-54h]
F7B0DCB9 push eax ; DestinationString
F7B0DCBA call ❶ds:RtlInitUnicodeString
F7B0DCC0 mov dword ptr [ebp-74h], 18h
F7B0DCC7 mov [ebp-70h], ebx
F7B0DCCA mov dword ptr [ebp-68h], 200h
F7B0DCD1 lea eax, [ebp-54h]
F7B0DCD4 mov [ebp-6Ch], eax
F7B0DCD7 mov [ebp-64h], ebx
F7B0DCDA mov [ebp-60h], ebx
F7B0DCDD push ebx ; EaLength
F7B0DCDE push ebx ; EaBuffer
F7B0DCDF push 40h ; CreateOptions
F7B0DCE1 push 5 ; CreateDisposition
F7B0DCE3 push ebx ; ShareAccess
F7B0DCE4 push 80h ; FileAttributes
F7B0DCE9 push ebx ; AllocationSize
F7B0DCEA lea eax, [ebp-5Ch]
F7B0DCED push eax ; IoStatusBlock
F7B0DCEE lea eax, [ebp-74h]
F7B0DCF1 push eax ; ObjectAttributes
F7B0DCF2 push 1F01FFh ; DesiredAccess
F7B0DCF7 push offset FileHandle ; FileHandle
F7B0DCFC call ds:ZwCreateFile
F7B0DD02 push ebx ; Key
F7B0DD03 lea eax, [ebp-4Ch]
F7B0DD06 push eax ; ByteOffset
F7B0DD07 push dword ptr [ebp-24h] ; Length
F7B0DD0A push esi ; Buffer
F7B0DD0B lea eax, [ebp-5Ch]
F7B0DD0E push eax ; IoStatusBlock
F7B0DD0F push ebx ; ApcContext
F7B0DD10 push ebx ; ApcRoutine
F7B0DD11 push ebx ; Event
F7B0DD12 push FileHandle ; FileHandle
F7B0DD18 call ds:ZwWriteFile
The Windows kernel uses a UNICODE_STRING
structure,
which is different from the wide character strings in user space. The RtlInitUnicodeString
function at ❶ is used to
create kernel strings. The second parameter to the function is a NULL-terminated wide character
string of the UNICODE_STRING
being created.
The filename for the ZwCreateFile
function is
DosDevicesC:secretfile.txt. To create a file from within the kernel, you
must specify a fully qualified object name that identifies the root device
involved. For most devices, this is the familiar object name preceded by
DosDevices.
DeviceIoControl
is not the only function that can send data
from user space to kernel drivers. CreateFile
, ReadFile
, WriteFile
, and other
functions can also do this. For example, if a user-mode application calls ReadFile
on a handle to a device, the IRP_MJ_READ
function is called. In our example, we found the function for DeviceIoControl
by adding
0xe*4
to the beginning of the major function table because
IRP_MJ_DEVICE_CONTROL
has a value of 0xe
. To find the function for read requests, we add 0x3*4
to the beginning of the major function table instead of 0xe*4
because the value of IRP_MJ_READ
is 0x3
.
In the previous example, we saw that a driver was loaded in kernel space when we ran our
malware, and we assumed that it was the infected driver. Sometimes the driver object will be more
difficult to find, but there are tools that can help. To understand how these tools work, recall
that applications interact with devices, not drivers. From the user-space application, you can
identify the device object and then use the device object to find the driver object. You can use the
!devobj
command to get device object information by using the
name of the device specified by the CreateFile
call from the
user-space code.
kd> !devobj FileWriterDevice Device object (826eb030) is for: Rootkit DriverFileWriter DriverObject 827e3698 Current Irp 00000000 RefCount 1 Type 00000022 Flags 00000040 Dacl e13deedc DevExt 00000000 DevObjExt 828eb0e8 ExtensionFlags (0000000000) Device queue is not busy.
The device object provides a pointer to the driver object, and once you have the address for the driver object, you can find the major function table.
After you’ve identified the malicious driver, you might still need to figure out which
application is using it. One of the outputs of the !devobj
command that we just ran is a handle for the device object. You can use that handle with the
!devhandles
command to obtain a list of all user-space
applications that have a handle to that device. This command iterates through every handle table for
every process, which takes a long time. The following is the abbreviated output for the !devhandles
command, which reveals that the
FileWriterApp.exe application was using the malicious driver in this
case.
kd>!devhandles 826eb030 ... Checking handle table for process 0x829001f0 Handle table at e1d09000 with 32 Entries in use Checking handle table for process 0x8258d548 Handle table at e1cfa000 with 114 Entries in use Checking handle table for process 0x82752da0 Handle table at e1045000 with 18 Entries in use PROCESS 82752da0 SessionId: 0 Cid: 0410 Peb: 7ffd5000 ParentCid: 075c DirBase: 09180240 ObjectTable: e1da0180 HandleCount: 18. Image: FileWriterApp.exe 07b8: Object: 826eb0e8 GrantedAccess: 0012019f
Now that we know which application is affected, we can find it in user space and analyze it using the techniques discussed throughout this book.
We have covered the basics of analyzing malicious kernel drivers. Next, we’ll turn to techniques for analyzing rootkits, which are usually implemented as a kernel driver.
18.221.126.56