Friday, March 23, 2012

Malware Analysis Tutorial 22: IRP Handler and Infected Disk Driver

Learning Goals:
  1. Use WinDbg for kernel debugging
  2. Understand basic inner working of disk driver
  3. Understand virtual hidden drive creation
  4. Reverse engineering Max++ driver infection technique
Applicable to:
  1. Operating Systems
  2. Assembly Language
  3. Operating System Security
1. Introduction
This tutorial continues the analysis presented in Tutorial 20. We reveal how Max++ uses a modified disk driver to handle I/O requests on the disk it created (its name is "\\?\C2CAD..."). Recall that in section 4.2.3 we showed you Max++ creates a new IO device and hooks it to the malicious driver object, so that whenever an IO request is raised on this device the request will be forwarded to driver object 8112d550, as shown below. Pay attention to the value of MajorFunction (0xfae36bde), this is where IO requests are handled. Obtaining the module base address, we can easily calculate its offset: _+2BDE.

kd> dt _DRIVER_OBJECT 8112d550
nt!_DRIVER_OBJECT
   +0x000 Type             : 0n4
  ...
   +0x02c DriverInit       : 0xfae4772b     long  +0
   +0x030 DriverStartIo    : (null)
   +0x034 DriverUnload     : (null)
   +0x038 MajorFunction    : [28] 0xfae56bde     long  +0



To replicate the experiments of this tutorial, you have to follow the instructions in Section 2 of Tutorial 20. In this tutorial, we perform analysis on the code of raspppoe.sys from _+2BDE (0x10002BDE)

2. Lab Configuration
In general we will use the instructions of Section 2 of Tutorial 20. In the following we just remind you of several important steps in the configuration:
(1) You need a separate image named "Win_Notes" to record and comment the code. You don't really need to run the malware on this instance, but just to record all your observations using the .udd file. To do this, you have to modify the control flow of IMM so that it does not crash on .sys files. See Section 2 of Tutorial 20 for details. Jump to 0x10002BDE to start the analysis.
(2) The second "Win_DEBUG" image has to be run in the DEBUG mode and there should be a WinDbg hooked from the host system using COM part -- so here, we are doing kernel debugging.
(3) Set a breakpoint "bu _+2BDE" in WinDbg to intercept the driver entry function.

3. Background: Windows Driver Development
Opferman provides an excellent introduction and sample code in [1]. In the following, we summarize of the major points here.

(1) Each driver has a driver entry function, its prototype is shown below:

NTSTATUS DriverEntry(PDRIVER_OBJECT pDrv, PUNICODE_STRING reg)

Here pDrv is a pointer to _DRIVER_OBJECT, and reg is a string that represents the registry entry where the driver could store information.

As we shown earlier in Tutorial 20, the DriverEntry function is located at _+372b.

(2) Each driver may have a collection of 28 functions to handle different types of I/O requests (such as close handle, read, write etc.) The IRP Function code can be found at [2] (typical ones are IRP_MR_CREATE and IRP_MR_READ).

You might wonder, do we have to set breakpoints on all of the 28 functions? The answer is YES and NO. Look at the following dump (combined with the dump in section 1).

kd> dd 8112d550
8112d550  00a80004 81210030 00000002 fae54000
8112d560  00008000 ffbd7d80 8112d5f8 001a001a
8112d570  e1389208 8068fa90 00000000 fae5772b
8112d580  00000000 00000000 fae56bde fae56bde
8112d590  fae56bde fae56bde fae56bde fae56bde
8112d5a0  fae56bde fae56bde fae56bde fae56bde
8112d5b0  fae56bde fae56bde fae56bde fae56bde
8112d5c0  fae56bde fae56bde fae56bde fae56bde


At offset 0x38 of the driver object  (the starting of the major function array), all IRP handlers are set to one single function _+2BDE! The malware author tries to be lazy here, and it saves us a lot of job too. We can just concentrate on _+2BDE then!

Now before we move on, we should know that each IRP handler function has the following prototype:

NTSTATUS Handler(PDEVICE_OBJECT pDevice, PIRP pIRP)

Here, the first parameter is a device object, and the second parameter represents the IRP request to handle.

When we hit the _+2BDE handler, we could easily find out the contents of the two input parameters (device located at 8112d550 and irp located at 00070000) as below:

kd> dd esp
fafb73fc  81210030 8112d550 00070000 81210030
fafb740c  fafb7460 804e37f7 81210030 ffbbe7e8
fafb741c  00000000 fb07c7a9 81210030 c000014f
fafb742c  00000000 00000000 c3a408e0 00000000
fafb743c  00000001 00000000 804e2490 fa047501
fafb744c  00000000 fafb7450 fafb7450 804fb1a9
fafb745c  00000000 fafb748c fb07ce80 81210030
fafb746c  fafb7484 ffb6fe10 81210030 ffb6fe10
kd> dt _DEVICE_OBJECT 8112d550
nt!_DEVICE_OBJECT
   +0x000 Type             : 0n4
   +0x002 Size             : 0xa8
   +0x004 ReferenceCount   : 0n-2128543696
   +0x008 DriverObject     : 0x00000002 _DRIVER_OBJECT
   +0x00c NextDevice       : 0xfae54000 _DEVICE_OBJECT
   ...
kd> dt _IRP 00070000
nt!_IRP
   +0x000 Type             : 0n193
   +0x002 Size             : 0
   +0x004 MdlAddress       : 0x00000100 _MDL
  ...




4. Anatomy of Infected Disk Driver
Figure 1 shows you the first part of the IRP handler function at _+2BDE.
Figure 1. Infected Disk Driver

As shown in Figure 1, the control flow  is a very simple decision procedure. First it takes out the PDEVICE_OBJECT pointer from EBP+8 (1st parameter) and compare it with a global variable stored at 100061B0 (see highlighted area). Clearly, the global variables stores the newly created infected device (for \??\C2CAD...). If it is not a request to \??\C2CAD, the flow jumps to 10002BFD (second highlighted area), which calls PoCallDriver to relay the request to low level (real) drivers to do the work; otherwise it calls a self-defined function handleIRPForVirtualVolume which performs the real operation to simulate the virtual disk.

Challenge 1. Analyze the logic between 10002BFD and 10002C25 (highlighted area in Figure 1). Especially, explain the instructions at 0x10002C16 and 0x10002C19.

5. Simulating the Virtual Disk Operations
Now we will analyze the function handleIRPForVirtualVolume. It is located at _+292A. In this case, you need to set a breakpoint using "bp _+292A" in WinDbg. Figure 2 shows its major function body. Notice that you can easily infer from the context that EBX is an input parameter of the function, EBX points to the IRP request right now!

Figure 2. Function body of handleIRPForVirtualVolum


Now comes the interesting part. Look at Figure 2, at 0x1000293C EAX now has the "MajorFunction" of _IO_STACK_LOCATION  (the value is one of the IRP_MJ_xxx types). Then there is a big switch case statement (see the highlighted area in Figure 2), which redirects the control flow to handle each of the different IRP requests such as READ, WRITE, etc.

Challenge 2. Argue that the statement about "0x1000293C EAX now has the "MajorFunction" (the value is one of the IRP_MJ_xxx types" is true. You may need to find out the definition of IRP_MJ_xyz values.

As an example of how Max++ simulates the disk volume operation, we show how it handles the IRP_MJ_READ request. Figure 3 shows the handler code.

Figure 3. Simulate the Disk Operation on File
  First, let's look at the definition of _IO_STACK_LOCATION which represents an I/O operation task. Note that at this moment, ESI points to the current _IO_STACK_LOCATION, the following is its contents. You can easily infer that it's current Device Object is \??\C2CAD...

kd> dt _IO_STACK_LOCATION ff9c7fd8
nt!_IO_STACK_LOCATION
   +0x000 MajorFunction    : 0x3 ''
   +0x001 MinorFunction    : 0 ''
   +0x002 Flags            : 0x2 ''
   +0x003 Control          : 0 ''
   +0x004 Parameters       : __unnamed
   +0x014 DeviceObject     : 0xffb746d8 _DEVICE_OBJECT
   +0x018 FileObject       : (null)
   +0x01c CompletionRoutine : (null)
   +0x020 Context          : (null)


Now look at the first instruction LEA EAX, [ESI-24] in Figure 3. The purpose here is to move 0x24 bytes away (note the direction of stack) and the size of _IO_STACK_LOCATION (0x24). So EAX is now pointing to a new _IO_STACK_LOCATION instance. The next couple of instructions copy the first 9 words of the existing _IO_STACK_LOCATION to the new.

Then at 0x10002B10 (look at the highlighted area of Figure 3), it assigns the value of ECX (from global variable at DS:[1000614C]) to offset 0x18 of the new _IO_STACK_LOCATION. Notice that 0x18 is the FileObject attribute (see above dump of _IO_STACK_LOCATION!). The following is the dump of  the File Object pointed by ECX:

kd> dt _FILE_OBJECT 811b25d0
nt!_FILE_OBJECT
   +0x000 Type             : 0n5
   +0x002 Size             : 0n112
   ...
   +0x02c Flags            : 0x40040
   +0x030 FileName         : _UNICODE_STRING "\WINDOWS\system32\config\yknueenf.sav"
   +0x038 CurrentByteOffset : _LARGE_INTEGER 0x0

   ...





Now it's pretty clear that the READ operation on the disk volume is actually achieved by CONSTRUCTING A NEW _IO_STACK_LOCATION task on the "*.sav" file created by Max++ earlier!

The last interesting point is at 0x10002B17: Max++ hooks up a function for the CompleteRoutine (offset 0x1c of _IO_STACK_LOCATION), the intention is pretty clear: the data stored on the *.sav file is encrypted, and Max++ now decodes it when reading it out.

We've finished a very challenging and interesting analysis of a portion of the infected disk driver. Now it's your job to finish the rest:

Challenge 3. What happens when FormatEx operation is performed on the virtual disk volume?

Challenge 4. Analyze all the other IRP_MJ_ operations supported by the infected disk driver (hint: this could take considerable efforts).




References
[1] T. Opferman, "Driver Development Introduction Part I", available at http://codeproject.com
[2] MSDN, "IRP Function Code", available at

1 comment: