# The Low-Level Way

“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.

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

&#x20;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](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp).\
Handling **IRP** requests can be done by defining the function responsible to handle a specific types of **IRP**, just as in the following example:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FbuE50LIVpKlD0PWvTGpA%2Fimage.png?alt=media&#x26;token=602b35de-79ee-4207-b0ab-94ca97836e5d" alt=""><figcaption></figcaption></figure>

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](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes?source=recommendations).\
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**.

<br>

#### 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:**

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FK1uOVJjXWN4Dc7zkjEEc%2Fimage.png?alt=media&#x26;token=2f83edd2-27e3-4783-bc16-eaa2229ab3eb" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FiXwPPGOWORebk9c2o3tM%2Fimage.png?alt=media&#x26;token=dd451913-b91d-4626-ab87-80ae42f0a4cb" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2Fv1qk698imnoJGiMqFHTu%2Fimage.png?alt=media&#x26;token=538c9e76-fa6b-42dc-95f2-fe40857bbc5f" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FykCeM2HRyaz1PW4mgcD7%2Fimage.png?alt=media&#x26;token=e2d930ff-1f76-4894-b912-0cbc08fc9ff9" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FDqygZfiunRDBXjGCajuq%2Fmiob_gif.GIF?alt=media&#x26;token=ca989b68-f187-4f7d-a909-67b82802d360" alt=""><figcaption></figcaption></figure>

#### 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.

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FZEqY1ryydKF7g4SdmyRS%2Fimage.png?alt=media&#x26;token=715e3cc4-fc4f-4ed1-8c14-4db8278b4f16" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FXJSx9ACAIBCzjLQgWdgc%2Fimage.png?alt=media&#x26;token=4c6ce1db-c4f1-43ff-b1fe-96a6fd516e5f" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FtJNNv6IwkOmVewZONBxM%2Fimage.png?alt=media&#x26;token=2947784e-fd32-4c83-a2f5-9c55ab6c8751" alt=""><figcaption></figcaption></figure>

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:

&#x20;  1.Kill the caller process (prevent the arrival of additional requests first).

&#x20;  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:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2Fg0xNiDHdpqOtu8HoN0zy%2Fimage.png?alt=media&#x26;token=16a93f71-f5e7-45f7-90d4-66b2d0afa851" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="https://3225596869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfhLztwDgT7iE72rxX2cz%2Fuploads%2FMryPQUFdxMqSeriNIHfU%2Fimage.png?alt=media&#x26;token=2f9f5b67-fa3d-430e-ad84-43a4709f8f14" alt=""><figcaption></figcaption></figure>

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
