This tutorial analyzes the malicious driver B48DADF8.sys. We assume that you have retrieved the driver from the hidden drive, following the instructions of Tutorial 28.

2. Lab Configuration

We need two windows images: one for taking notes and one for actually running the malware. Also a kernel mode WinDbg instance is needed on the host.

To set up the Notes images, you can follow the instructions of Tutorial 20. The basic idea is to start two instances of IMM, using one to debug the other. Then at 0x004E6095 to set a breakpoint and skip the instruction when there will be an illegal memory write. Once B48DADF8.sys is loaded, in the second IMM (as shown in Figure 1), if we check the executable modules (View -> Executable Modules), we can find out the entry of the driver is +1259. Jumping to that address, we can see the driver entry (which makes a bunch of calls on hooking image loading and creation of driver device).

Figure 1. Identify B48DADF8.sys Entry

The set up of the windows image for debugging and WinDbg should follow the instructions of Tutorial 28. We need to stop at the entry of the driver. This could be achieved by first finding out the starting address of the module in WinDbg, and then plus offset 1259.

You can verify that the code starts at faf0d259 (shown in the WinDbg dump above) matches the instructions in the IMM window in Figure 1. From now on, we can start the analysis. The basic approach is to execute the driver in the WinDbg instance and annotate the code in the WinNotes image.

3.Hook Up Driver with New Device and Set Image Load Notifier

We now observe the first section of the code at the beginning of the driver loading. Figure 3 shows the annotated code.

Figure 3. First Part of B48DADF8.sys

The first interesting part is that the driver takes itself (DRIVER_OBJECT) and saves to a global variable. It first reads from EBP+8, i.e., the first parameter to a driver (PDRIVER_OBJECT), as shown in the first highlighted part. We will see that the malware later will need this value.

We can verify that the value ffb81268 is really a driver object as shown below. It is clear that the driver object is not fully set up yet, e.g., the DeviceObject is null.

Next B48DADF8.sys tries to call function psSetLoadImageNotifyRoutine at to +1082. This is clearly an operation that tries to hide the loading of modules. We are not getting into the details yet, but we can set a breakpoint on it. We can see that the breakpoint will be hit multiple times and DebugService + 2bde is not hit any more.

Then, B48DADF8.sys tries to create an IO device and hooks itself up as the driver for that device. According to MSDN, IoCreateDevice() has 6 parameters: PDRIVER_OBJECT, DriverExtension, DeviceName, DeviceType, DeviceCharacteristics, Exclusive, PDEVICE_OBJECT.

From the WinDbg dump below, we can soon infer that the name of the new device is \??\EBB02C33..\#...0CFE and the device type is FILE_DEVICE_UNKNOWN. This is confirmed by the following WinDbg dump:

4.Hide Driver Module
We now discuss the efforts of B48DADF8.sys to hide itself. This part contains no more than 20 instructions, as shown in Figure 4.

Figure 4. Hide Driver Module B48DADF8.sys

At the beginning of the code, ESI points to the _DRIVER_OBJECT of B48DADF8, and then the code retrieves the word at offset 0x14 of the _DRIVER_OBJECT, and now EDX points to DriverSection (whose data type is _LDR_DATA_TABLE_ENTRY). Using WinDbg, we can easily verify its contents as below. You can see that it's full DLL name is "\??\... C2CAD...B48DADF8.sys".

The next couple of instructions (from 0x100012CE to 0x100012D2 in figure 4) clears the FullDLLName. After 0x100012D2, if you display the same DriverSection again, you would notice that the FullDllName is gone, as shown below. However, the BaseDllName is still there, I guess the malware author forgot to clear it as well.

Next, B48DADF8.sys tries to remove itself from the module list. As shown in Figure 4, at 100012D6, EAX and ECX now have the FLINK and BLINK of the first module of the InLoadOrderModule list. The next four instructions constitute a typical REMOVE_NODE operation on a doubly linked list, which removes B48DADF8 module from the list.

Challenge 2. Explain the logic of code from 100012D6 to 100012E3 in Figure 4.

4.Hook Up on PCI Device
The next step (function 0x100011D0) is to hook up on the PCI Device by copying from the original PCI driver. This is shown in Figure 5.

Figure 5. Copy from PCI Driver

As shown in Figure 5, the first step of function 0x100011D0 is to retrieve the PCI Driver object by name "driver\PCI". Then it copies several attributes of the PCI driver to the current driver, such as driver_start, driver_init, driver size, and driver name. However, the driver major functions 0xfaed514c (an array that contains the IRP handler entry addresses) is not changed. Here all entries are redirected to 0xfaed514, as shown below (which dumps the contents of the B4DADF8.sys driver object, before and after the call of 0x100011D0.

Challenge 3. Note that at this moment, the PCI device has not been completely hooked up to the new driver. Find a way to find out where the new driver is eventually set as the handling driver for the PCI device.

5.Image Loading
We now go back to study the image loading call at +1082, which is discussed earlier in section 3. Max++ sets +1082 as the call back function whenever NtImageLoad is called. This first, of course, disrupts WinDbg in monitoring image loading. But the code itself is doing a lot of malicious stuff. Let's set a breakpoint at +1082 and watch its behavior. The set up is shown as below:

Figure 6 displays the major function of +1082 (also written as 0x10001082).

Figure 6. Function Body of 0x10001082

As shown in Figure 6, the majority part of +1082 is to set up and queue an APC object (Asynchronous Procedure Call). APC is frequently used in I/O operation, it stands for an object that will be executed a while later.

See the highlighted parts in Figure 6, the control flow is very clear: Max++ first tries to call ExAllocatePool to reserve 30 bytes of kernel memory for the APC object, then it calls KeInitializeAPC and KeInsertQueue to queue the APC call. We need to look at the details of KeInitializeAPC. According to ReactOS documentation, the prototype of KeInitializeAPC is shown below:

VOID NAPI KeInitializeAPC(

IN PKAPC pApc,

IN PKTHREAD thread,

IN KAPC_ENVIRONMENT env,

IN PKKERNEL_ROUTINE kernelRoutine,

IN PKROUNDOWN_ROUTINE rundownRoutine,

IN PKNORMAL_ROUTINE normalRoutine,

IN KPROCESSOR_MODE mode,

IN VOID context

)

The dump from WinDbg can be found in the following:kd> dd espf7c88bb4 ffbb2e48 81176320 00000000 faee530cf7c88bc4 faee52f0 71a50000 00000001 00000000

Here, the kernelRoutine is faee530c (+130c), rundownRoutine is faee52f0 (+12f0), and normalRoutine is 71a50000 (your job: find out which module does it belong to), mode is 1. By MSDN documentation, if mode is 1 and normalRoutine is not 0, this is a user mode APC, which will call the normalRoutine later. However, to be safe, we want to set up breakpoints on all of the routines +130c, +12f0, 71a50000.
Pay special attention, at this moment, the normal routine is 71a50000!

Now let's study what the function +130c (0x1000130c in IMM) is doing. Figure 7 shows its function body.

Figure 7. Function Body of +1307

The first part of +130c is pretty interesting. It's a collection of exchange functions, which essentially rotates 6 words on top of the stack. In the following, we show you the contents of the stack before the rotation.

kd> dd esp
f7c88d00 804e60f1ffb9b638f7c88d48f7c88d3c

f7c88d10 f7c88d40f7c88d44 f7c88d64 0012c834

...

The stack contents after the rotation is shown below. You can find that 0x804e60f1 is shifted to the right (now the 6'th word in the stack).kd> dd espf7c88d00 ffb9b638f7c88d48f7c88d3cf7c88d40f7c88d10 f7c88d44804e60f1 f7c88d64 0012c834

Why does Max++ have to do this? The reason is that the function copyMaliciousCodeToNewVM (the call located at 10001327 in Figure 7) actually consumes 5 additional words in stack. In the following, we display the stack contents after the call of copyMaliciousCodeToNewVM is completed. kd> dd espf7c88d14 804e60f1 f7c88d64 0012c834 f7c88d64f7c88d24 ffffffff 804e2490 804f2001 7c90e4f4

You can notice that 804e60f1 is now at the top of the stack. At this
moment, the control flow (at 0x10001332) is going to jump to
ntoskrnl.ObfDereferenceObject, which when finishes, will jump to
0x804e60f1 (which is originally the return address of +130c). By
manipulating the stack this way, Max++ can successfully confuse the control flow analysis performed by static analysis tools.

Now let's observe the logic of copyMaliciousCodeToNewVM , which is located at +100f. Figure 8 shows its function body

Figure 8. Function Body of +100f

The logic of copyMalicoiusCodeToNewVM (+100f) is very simple, it first lowers the IRQ level and then it allocates a small piece from the memory and copies the contents of +1338 to the target address (0x00380000).Recall the "Stealthy 0x00380000 memory segment" in Tutorial 27,it's now your job to figure out what is copied into the region 0x00380000.

Challenge 5. Figure out what is copied by the copyMaliciousCodeToNewVM.

Then the JMP ntoskrnl.obfDerefrenceObject de-references the new driver object and returns to the system call that triggers the kernel function +130c.

Figure 9 shows the contents of the copyMalicoiusCodeToNewVM (+100f). If you look it its logic, it basically matches the description above. However, there is one thing we'd like you to pay special attention:

Challenge 6. See figure 9, where is the new VM address (allocated by ZwAllocatevirtualMemory, i.e., 0x00380000) stored at?

Figure 9. Function Body of copyMalicousCodeToNewVM (0x1000100f)

If you look at the two highlighed instructions in Figure 9, you might notice that the 3rd parameter of 0x1000100F (copyMaliciousCodeToNewVM) is used to store 0x00380000. But why?

Challenge 7. Figure out what is the motivation for storing 0x00380000 in the 3rd parameter of 0x1000100F.

To solve the above challenge, we need to go back to Figure 8, and we notice that it's f7c88d3cwhich is passed to the call of 0x1000100F (copyMaliciousCodeToNewVM).
Looking at the ReactOS information about kernel routine (search for PKKERNEL_ROUTINE on ReactOS), you will find that kernel routine (130c) accepts 5 parameters: APC, pNormal_Routine, Normal_Context, System_Arg, System_Arg2). So the f7c88d3c is actually the NORMAL_CONTEXT parameter of the KERNEL_ROUTINE.! Similarly, you would find that f7c88d48 (the second parameter) is the NORMAL_ROUTINE.

Now, look the highlighted part in Figure 9 (two yellow underlines and three thicker ones), you will find that copyMalicoiusCodeToNewVM writes value 0x00380000 into both the place holders for NORMAL_CONTEXT and NORMAL_ROUTINE!

Lab Configuration for Analyzing Code 0x00380000:

Clearly, the next step we would like to pursue is to debug the code located at 0x00380000 (originally copied from 0x10001338). Interestingly, if you set a hardware BP in WinDbg, the debugger never stops on 0x00380000. We suspect that somehow hardware BP is cleared at some point. Only the software BP works in this scenario ("bp 0x00380000"), and in some scenarios you might find that your IMM actually gets the INT3 (software BP) interrupt and you have to now debug it in IMM. Figure 10 shows you the screen of IMM when the software BP is intercepted. You can see that the "instruction" at 0x00380000 is "INT3".

Figure 10. Interesting Debugging Behavior of WinDbg/IMM

Open Challenge: Figure out why the software BP set by WinDbg is intercepted by Immunity Debugger. (conjecture: the debug port of the operating system is reset by Max++).

Figure 11. Code at 0x00380000

Figure 11 displays the logic of code at 0x00380000 (originally copied from 0x10001338). It mainly consists of three steps: (1) it searches for module "kernel32.dll", (2) it searches for funciton LoadLibraryW in the PE header, (3) it invokes function +1404 (we named it LoadMax++00x86).

The main part of function LoadMax++00x86 (located at +1404) is shown in Figure 12.

Figure 12. Load Max++.00.x86

The major bulk of the funciton + 1404 is the call of LoadLibraryW("\.\C2CAD...max++.00.x86").

Summary:
Up to now, combined with Tutorial 27, we have shown you the complete picture of the stealthy remote DLL loading technique. Max++ loads max++.x86.dll via three steps: (1) load B48DADF8.sys; (2) load max++.00.x86; and (3) load max++.x86.dll. During each step, a variety of techniques are employed to hide the trace, e.g., by modifying the kernel data structures of libraries. There are also techniques that we have not completely understand, see the open challenge in this tutorial.