The Low-Level Way

Driver Mitigation - MIOB

“There is always a bigger fish”, even though security products didn’t detect or prevent our attacks, we always believe that there is a bigger fish out there able to detect those types of attacks.

In order to defend against those types of attacks, we developed the MIOB (Malicious IO Blocker):

A driver that is able to detect and prevent malicious (by define) IRP requests from being executed.

Introduction & Terminology

The MIOB is a driver places a hook on the function defined to handle specific IRP requests sent to a specific driver, analyzes the IRP stack request, and decides if it should continue to the target driver or be dropped due to a malicious activity indicator. To put the theory into practical use, we need to get familiar with several structures and objects, the first is the object used to communicate with a driver from the user-mode: IRP, or I/O Request Packet.

_IRP

An IRP is a special packet used to communicate with a driver and make it perform certein actions. For those of us who are familiar with Windows drivers, the handling of IRPs is defined in the DriverEntry function, which is the entry point of any driver in the kernel. It is important to know that the type of the request is defined within the _IRP struct, we won’t dig deep into this, since most of them aren’t required for the PoC, but you can further read about the structure of the request in MSDN. Handling IRP requests can be done by defining the function responsible to handle a specific types of IRP, just as in the following example:

The 2 lines of code have been taken from the initialization stage of MIOB (within DriverEntry) and define that the function responsible for handling IRP from a type of IRP_MJ_CREATE and IRP_MJ_CLOSE is MalIOBlockCreateClose, so when our driver will receive an IRP request from those types, the function defined will handle them. We’ll not explain here what the types of IRP in the code represent, since we have more important types to focus on here.

IRP_MJ_DEVICE_CONTROL

The type of request we want to hook , as the title of this section implies, IRP_MJ_DEVICE_CONTROL. An IRP of this type is used to allow the execution of some functionalities implemented within the driver using IOCTL code. IOCTL code (or I/O Control Code) is sent to a driver using the DeviceIoControl function and specifies the operation (mostly defined by the driver’s authors) to perform (custom-made functionalities), we recommend you read the following article from MSDN on defining IOCTRL for a driver. So in the case of DBUtil_2_3, there are 2 IOCTL codes defined for reading and writing from/to the kernel memory. As explained earlier each type of IRP has its own routine, e.g. IRP_MJ_DEVICE_CONTROL, the important thing is that those types of requests contains IOCTL code, and every code has its routine defined for it.

I/O Stack Location

One last important term is I/O Stack Location. Usually, when an IRP is sent, the treatment is handled by several drivers in a chain of drivers, each driver handles another aspect of the request. By receiving an IRP, each driver in the layered chain receives an I/O Stack Location, which is represented by the IO_STACK_LOCATION and contains information about the I/O operation related to the driver. In our case, the I/O Stack Location contains the IOCTL code which is used to determine the action required from the vulnerable driver. Specifically in the IO_STACK_LOCATION you can find the IOCTL code at the following member: irpStack->Parameters.DeviceIoControl.IoControlCode

Summarize the Target of MIOB

The functionality that allowed KernelCactus to perform the attacks is the ability to read and write from and to the kernel by exploiting an arbitrary Read/Write vulnerability in DBUtil_2_3, in practice the vulnerability is exploited by using the IOCTL code defined within the vulnerable driver and sending the operation request using DeviceIoControl function. You’ll see that we can enumerate the process that has sent the IRP, but for our PoC, we would like to detect attempts to write using DBUtil_2_3 and block them from being completed by a vulnerable driver. For this purpose, we’ve utilized a technique called IRP Hook which is also used in the wild by rootkits when they prevent security products or the user from deleting their files . In our case, if the IRP is considered malicious we’ll redirect it to be canceled by MIOB.

MIOB in Practice

We’ll give up on the initialization of MIOB since the stage has no value for the main subject, the only step in the initialization that required attention is the installation of the hook on the function used to handle IRPs from the type of IRP_MJ_DEVICE_CONTROL. The installation is the last part of the initialization and the function is called InstallVulnerDriverHook:

Remember that the purpose of the hooking is to redirect IRPs sent to the vulnerable driver to our driver for inspection, Let's jump to the hooking code to better understand the technique.

IRP Hooking

First things first, this driver is written for PoC and target DBUtil_2_3, so we should get a pointer to the device object, which allows us to access the driver object and its properties:

The function IoGetDeviceObjectPointer is populating pFile_vul & pDev_vul with a pointer to the device and file objects related to the driver. Notice that the function is receiving a Boolean parameter called “REMOVE” if the parameter is set to true the function will remove the hook, and false to install the hook, let's continue with the installation procedure.

Once we got the required pointers, we can finally access the driver object:

As you can see, we save a pointer to the driver object, then from the driver object we just extracted, we save a pointer to the major function responsible for dealing with IRP from type IRP_MJ_DEVICE_CONTROL, the type of request required to make the driver execute some code based on its IOCTL.

The purpose of saving the pointer is to restore the driver’s function to its usual state during our driver’s unloading routine.

Now it’s time to set up the hook, once we acquire the address of the Major Function responsible for dealing with IRP_MJ_DEVICE_CONTROL requests, we replace it with a pointer to an exported function within our driver called HookUp, from now, every time that an IRP from a type of IRP_MJ_DEVICE_CONTROL will arrive to the vulnerable driver, it will be handled first by our driver.

With this method you can create your filter function and implement a logic that decides if this request should be passed to the vulnerable driver or not, in the HookUp function we implemented a logic that checks if the IOCTL is for a write operation, and if does our driver will block the request in a method that makes this interruption stable and also kill the process which calls the request.

Our Filter Function

The function will decide if an IRP will be forwarded to the driver or dropped and marked as malicious. The condition for that is simple, if the request contains IOCTL for writing, the request will be dropped. But first, we need to know who is the process that made the request, so from the IRP we can get the _ETHREAD structure of the thread that called the request, then with a WinAPI function called PsGetThreadProcessId, which gets an _ETHREAD and return the PID of the owner process.

Next, we need to get the related IRP Stack of our request, and it's done also by using one WinAPI function called IoGetCurrentIrpStackLocation, which only receives an IRP to operate:

Remember from the introduction section, the IRP Stack Location contains the control code sent with an IRP from a type of IRP_MJ_DEVICE_CONTROL. Now that we have all the necessary data, let’s check if the IRP is considered malicious by our terms:

So what’s happened here and why the function required to deal with IRP requests sent to our driver is involved in line 61? Once an IRP has landed, the function check within the I/O Stack Location if the Major Function for dealing with the request is IRP_MJ_DEVICE_CONTROL, if does it will check if the IOCTL code is for writing operation (line 58). If the condition is false or the request type is not IRP_MJ_DEVICE_CONTROL, we’ll give the execution back to the vulnerable driver by executing oldDevMajorFunc (which we got the pointer earlier), else, we want to block the operation with the following steps:

1.Kill the caller process (prevent the arrival of additional requests first).

2.Send the request to be handled by our driver’s function MalIOBlockCreateClose

Let’s see what happened in our function MalIOBlockCreateClose that dropped the request:

Long story short, as defined earlier in our driver, this function will handle every IRP to our function by canceling it, first, we use the IoCancelIrp, which attempts to cancel the IRP by setting a bit called Cancel into IRP to true, and by setting the status of the IRP to STATUS­_CANCELLED, which and send it to CompleteRequest function, which is responsible to just completing the request based on the data we provide from this function:

Nothing special, just take the arguments provided by MalIOBlockCreateClose and complete the request.

Why we used this flow and not build some special function to cancel the IRP? The flow of completing IRP is defined in every driver and it is part of the core logic that drivers are supposed to contain, after several attempts and some theory collected from several great sources (Thank you Pavel Yosovich and MSDN) we found out that this is the most stable way of performing the task. If this is part of the natural procedure of completing IRP, why not take advantage of that to cancel each one of them? Take it as something to think about…

Now that we have all the logic implemented, exploiting drivers vulnerable to arbitrary read/write by utilizing IOCTL required for writing and using the DeviceIoControl IS NOT POSSIBLE.

Note: the IOCTL defined here are the Control Codes collected from DBUtil_2_3, only changing the IOCTL (defined in the code) and the target driver (string…), you can monitor with the same method on each vulnerable driver you desire.

With love, MIOB.<3

Last updated