exploit reversing
exploit reversing
com
0. Quote
“Success. It's got enemies. You can be successful and have enemies or you can be unsuccessful and have
friends.”. (Dominic Cattano | “American Gang er” movie - 2007)
1. Introduction
Welcome to the first article of Exploiting Reversing (ER) series, where I will review concepts, techniques
and practical steps related to binaries and, eventually, analyze vulnerabilities in general. If readers have
not read past articles about my other series (MAS – Malware Analysis Series) yet all of them are available
on the following links:
▪ MAS_1: https://exploitreversing.com/2021/12/03/malware-analysis-series-mas-article-1/
▪ MAS_2: https://exploitreversing.com/2022/02/03/malware-analysis-series-mas-article-2/
▪ MAS_3: https://exploitreversing.com/2022/05/05/malware-analysis-series-mas-article-3/
▪ MAS_4: https://exploitreversing.com/2022/05/12/malware-analysis-series-mas-article-4/
▪ MAS_5: https://exploitreversing.com/2022/09/14/malware-analysis-series-mas-article-5/
▪ MAS_6: https://exploitreversing.com/2022/11/24/malware-analysis-series-mas-article-6/
▪ MAS_7: https://exploitreversing.com/2023/01/05/malware-analysis-series-mas-article-7/
In different opportunities we have to analyze kernel drivers or mini-filter drivers to understand a
vulnerability or even a malicious driver (as known as rootkit), and this topic is usually complex and presents
many details eventually deserves to be explained. However, I still needed a better motivation to start this
new series and it came up while I was analyzing details on Microsoft Security Events Component Minifilter
(C:\Windows\system32\drivers\mssecflt.sys), which it is a required dependency that enables FltMgr
service (fltmgr.sys) to be started, and stumbled with functions from this driver that, indirectly,
remembered me about techniques used to detect different kind of evasions using NtCreateProcessEx( )
that I had read from a good article delivered by Microsoft last year:
https://www.microsoft.com/security/blog/2022/06/30/using-process-creation-properties-to-catch-
evasion-techniques/.
At that point I realized that I could really start a new series of article, covering topics as reversing
engineering and vulnerability research and, effectively, moving away from malware analysis, which it is a
1|Page
https://exploitreversing.com
tuff that I don’t work with for a long time, but also keep writing to offer information to other professionals
who need it. Somehow, this series of articles offers me this freedom and opportunity to produce
something that, eventually, could be useful for people in the area.
While I am not concerned to analyze malicious code itself in this series, I will be using a malicious driver to
illustrate a few concepts about a section that will be presented later in this article, but it will be an
exception in this series. As I mentioned previously, the main purpose of this series is being focused on
reversing engineering, vulnerability research and, eventually, something about operating system internals.
Certainly, there is nothing new here and the idea is to provide correlated information that might help
readers to understand subtle details which could go unnoticed while reading articles, books and references
on the Internet. Mainly, while doing research, we usually learn a lot, but most of the time the information
is spread over multiple sources so that it could be hard to put everything together.
Readers from my previous articles could wonder whether I have plans to continue the MAS (Malware
Analysis Series) and, definitely, I will keep writing it. The only difference is that I will alternate between
series according to inspiration and spare time, of course.
Finally, and the more important fact by far, this article will have mistakes, typos and so on, and soon I know
about them, so I will release a fixed version of this article.
2. Acknowledgments
I could not write this series and the MAS (Malware Analysis Series) without receiving the decisive help from
Ilfak Guilfanov (@ilfak), from Hex-Rays SA (@HexRaysSA), becaue I didn’t have an own IDA Pro licene,
and he kindly provided everything I needed to write this series about reversing and vulnerabilities, and
other one that are coming. However, hi help didn’t top in 2021, and he and Hex-Rays have continuously
helped until the present moment by providing immediate support for everything I need to keep these
public projects. Additionally, Ilfak is always truly kind replying to me every single time that I send a
message to him. This section, about acknowledgments, can be translated to one word: gratitude.
Personally, all messages from Ilfak and Hex-Rays expressing their trust and praises on my previous articles
are one of most motivation to keep writing as well readers who send me even a single message thanking
me. Once again: thank you for everything, Ilfak.
I have chosen a quote to start each article to subtly show my thinking about life and information security in
general, sometimes mirroring the present days and all challenges that have forced me to make a deep
reflection over. At the end of day, we should invest in the work that we really love doing, no matter our
age, because life is short, and the ahead day is our future. Enjoy the journey!
3. References
It is always a complex task to provide references and recommendations to any topic, but I want to leave
few references I have used in the last years, and which might help readers to learn about the theme,
independently whether working on vulnerability research or malware analysis:
2|Page
https://exploitreversing.com
I don’t have an perpective to get into detail about kernel driver programming here and, certainly, it
would be impossible to touch a complex theme over a simple article, but I will try to do a minimum
revision about the topic and hopefully these words not only will help readers now, but will provide the
necessary foundation to the future ones. Actually, learning about drivers will help readers a lot while
researching for vulnerabilities in kernel drivers, as also using fuzzing tools to prospect such bugs.
To our context and concern (far away from formal WDM classification), we have distinct types of drivers:
▪ device driver: it communicates with hardware devices like printers, USB sticks and other ones.
▪ software kernel driver: this type of driver runs and establishes communication with the kernel
through resources offered by the system. Additionally, it is not the goal of this type of driver to
communicate directly with a physical device.
▪ mini-filter driver: it is a software driver that can monitor, intercept and change data transferred
between applications and/or drivers and the system (kernel or file system, for example). At the
same way, this kind of driver doen’t communicate directl with the device driver.
Certainl, we aren’t intereted in learning about device driver in thi article (although it is a fascinating
topic), but referring to device drivers is still a broad term, which could cause some confusion. In fact, a
more precise name would be function drivers, and without forgetting that we also have bus drivers that
are responsible for establishing communication between a device a PCI-X or USB bus, for example.
Anyway, in this section we will review the main concepts about kernel drivers, and in the next one we’ll
refresh concepts related to minifilter drivers.
If reader get involved in developing kernel drivers, so they will quickly learn that the development process
brings a series of challenges because as driver run on the kernel side, so any unhandled exception probably
will crash the system and, according to my experience, finding bad lines of code is not always something
trivial. One of many things that will be explained later in this article is that kernel drivers can run in
DISPATCH_LEVEL (IRQL 2), which presents a different consequence from userland applications that always
run in PASSIVE_LEVEL (IRQL 0). In fact, there is a quite extensive list of changes while programming and
writing kernel drivers than while writing user mode application, starting by the fact that most standard
libraries that help us a lot while writing userland applications are not available in kernel mode. We also
have the same concerns about security and, for example, if a driver is unloaded from memory without
3|Page
https://exploitreversing.com
doing the necessary cleaning, so there will be a memory leakage that only will be released in the next
reboot, which is also a standard issue while writing user mode programs. Unfortunately, there is an
extensive list of other programming hurdles. Of course, all of these concerns do not arise while reversing
code and understanding about internals, but they continue to be relevant aspects for differentiating user
mode and kernel mode code. Regardless of this difficulties, kernel drivers continue being an import stuff
while researching vulnerabilities and also used by criminals as an infection vector.
Another critical point is that, while writing and even analyzing a driver, we have to know that there are
different driver models that can used, which can interfere in our understanding about main characteristics:
▪ kernel drivers: Windows NT driver model and KMDF (Kernel-Mode Driver Framework).
▪ file system mini-filter drives: minidriver model.
▪ device drivers: KMDF (Kernel-Mode Driver Framework) and UMDF (User-Mode Framework Model),
and WDM (Windows Driver Model).
We need to choose a starting point, so explaining concepts related to the code, which will help while
reversing kernel drivers, could also be useful to initiate a brief discussion about the theme. Readers will
find over all kernel drivers the DriverEntry( ) routine, which is similar to the main function in C programs
that operate on the userland. This routine serves as a pivotal point to other functionalities called by the
driver. Actually, one of the main tasks performed by the DriverEntry routine is initializing structures and
resources that will be used by the driver at a later moment. In other words, it works like a midway point to
invoke other routines and prepare data structure for them.
Eventually, we also will find an unload routine that is associated with a driver object’s member named
DriverUnload, which is called automatically when the driver is unloaded and, as readers might expect, it is
responsible for performing cleaning tasks. I will be discussing about driver object, device objects and other
concepts in the next paragraphs, but for now you should know that a driver object is the parent of any
other object, and different objects such as timers, spinlocks, device objects and so on are included in this
list and, at the same way that happens for user mode application, synchronization is also a critical
component on the kernel side.
Drivers can be installed as service (sc create <driver name> type= kernel binPath= <driver path>) and, as
other services, an entry in created under HKLM\System\CurrentControlSet\Services. For sure, if Microsoft
did not sign this driver, it is necessary to setup the machine to booting in testing mode by executing bcedit
/set testsigning on followed by shutdown /r /t 0. Furthermore, whether you want to load the driver
without installing it, so there is the option to use OSR loader (available on
https://www.osronline.com/article.cfm%5Earticle=157.htm). Being honest, I haven’t ued it for a long
time, but probably it still works for legacy drivers and older versions of Windows.
We should remember that there are three main different types of memory given by POOL_TYPE
enumeration (for legacy APIs) from wdm.h (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/ne-wdm-_pool_type) or POOL_FLAGS enumeration for new APIs
(https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/pool_flags) that are used by
drivers: Paged Pool (pages can be paged out), Non-Paged Pool (pages always are kept on memory) and
NonPagedPoolNx (page alwa are kept on memor and don’t have execute permiion). Additionall, it
makes sense to mention Session Paged Pool, which can be paged but it is session independent.
4|Page
https://exploitreversing.com
Therefore, while analyzing kernel drivers, we will see routine invocations of several kernel specific memory
pool allocation functions like ExAllocatePool( ) (deprecated in Windows 10 version 2004),
ExAllocatePoolWithTag( ) (deprecated in Windows 10 version 2004), ExAllocatePool2
(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepool2),
ExAllocatePool3 (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-
exallocatepool3) and so on. It is a well-known fact that memory regions allocated with most of these
functions (deprecated and new ones) might have an associated tag, with up to four-byte value (usually in
ASCII) in reversing order, to label (tag) the allocated memory.
When a malicious drivers infects a system and allocates kernel non-paged pool memory, we might have a
chance to track these regions of memory used by the threat by looking for a specific tag if it is using one,
although it is not so common nowadays. Even without using a specific framework like Volatility, readers
can track these pools through commands such as poolmon (from WDK) and !lookaside (on WinDbg).
An essential point about kernel drivers is to understand that a single driver does not do everything alone.
Actually, when an I/O request is sent by an application, there will probably be drivers organized in a stack,
which each one is responsible for receiving the request, doing something or not, and passing the request
down to the next driver. Thus, important concepts come up from this point. After drivers are loaded, each
one is represented by a driver object, which has the following structure:
5|Page
https://exploitreversing.com
6|Page
https://exploitreversing.com
iocreatedevice), which can not be accessed from the user mode, so it is necessary to invoke
IoGetDeviceObjectPointer( ) to get the access to them (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-iogetdeviceobjectpointer).
About APIs mentioned in the last two paragraphs, we have the following one:
[Figure 3] IoCreateDevice( )
A brief summary about its parameters follows:
▪ DriverObject: it holds a pointer to driver object, which is received as parameter of DriverEntry( )
routine.
▪ DeviceExtensionSize: it represents the number of bytes reserved for the device extension of the
driver object. A device extension can be used to store private data structure associated to device,
but it is usually used with device drivers and not kernel drivers.
▪ DeviceName: optionally, it points to a buffer that holds the name of device object, as expected.
▪ DeviceType: it determines the device type, which is given by FILE_DEVICE_* constants. To add
them into IDA Pro as enumeration:
o Add the type library named ntddk64_win10 (SHIFT+11 and INS hotkeys).
o Go to Enumerations tab (SHIFT+F10), inert a new enumeration, chooe “add standard
enum by symbol name” and pick up FILE_DEVICE_DISK.
7|Page
https://exploitreversing.com
▪ DeviceCharacteristics: this parameter specifies one or more constants, but in the kernel driver’
context, it will be zero (0) or FILE_DEVICE_SECURE_OPEN in most cases. Repeating the same steps,
we have done for DeviceType, but this time add FILE_DEVICE_SECURE_OPEN.
The first parameter is a pointer to DRIVER_OBJECT and the second parameter is a pointer to RegistryPath
structure, which is a UNICODE_STRING, and that specifies the Parameters key of the driver in the Registry:
8|Page
https://exploitreversing.com
Besides core tasks performed (actually, invoked) in DriverEntry, there is another still more relevant role
performed by the same routine that is the initialization of the Dispatch Routines, which is an array of
function pointers, and that makes part of the _DRIVER_OBJECT structure (MajorFunction member).
All indexes of this array have IRP_MJ_ prefix and, as expected, they represent the IRP major function
codes. Drivers must set entry pointers into this array, which set up associated and responsible routines for
handling and manipulating each one of planned operations and, finally, attending IRP requests.
We still have a pending list of concepts that need to be explained and cleared. An IRP (I/O Request Packet)
is a structure that represents an I/O request packet, and it is used by drivers to carry information and
communicate with other drivers. In other words, it works like a data format to be used in a well-defined
standard for communication between driver layers.
The IRP, defined in wdm.h file, is a really large structure and has many fields, but most of them are unions.
If the readers want to examine the struct using Internet, so the following reference could be interesting:
https://www.vergiliusproject.com/kernels/x64/Windows%2011/22H2%20(2022%20Update)/_IRP
Personally, I prefer retrieving the _IRP structure from IDA Pro by performing the following steps:
1. open a PE format binary in IDA Pro
2. go to Type Libraries (SHIFT+F11)
3. add ntddk64_win10 or any other similar library (ntddk_win7).
Now go to Structures tab (SHIFT+F9) and add the standard structure named _IRP, as shown below:
9|Page
https://exploitreversing.com
There are fields that provide us with important context and information about kernel driver operation,
which few of them will be explained as necessary, and need to be complemented with new concepts that
will be introduced later. Even it is not shown on the previous image, an IRP has fixed part containing the
header (caller’ rea ID evice objec ’ a re I/O a u block an o on) that is used by I/O manager
to manage the IRP and a second part that is specific to each driver (I/O stack location), which holds
parameters such as function code of the requested operation and its respective context:
IO_STACK_LOCATION
IO_STACK_LOCATION
......
[Figure 8] IRP representation
We are going to make new notes on this topic later. Focusing on the IRP major codes topic again, there is a
series of IRP major codes that are used by drivers to call the respective dispatch routine in reaction to a
specific I/O request. These IRP major codes work as indexes in an array of function pointers.
As each kernel driver offers different functionalities, so they provide different dispatch routines to handle
I/O requests passing the IRP major codes shown below:
▪ IRP_MJ_CLEANUP: this IRP major code is used for invoking a DispatchCleanup routine when the
driver needs to release resources as memory and any other object whose respective reference
counter has reached zero, so it is an appropriate and recommended routine for cleanup that is not
related to file handles.
▪ IRP_MJ_CLOSE: this IRP major code is used for invoking a DispatchClose routine when the last
handle to a file object associated with a device object has been closed and released, and any
request has been closed or cancelled.
▪ IRP_MJ_CREATE: this IRP major code is used for calling a DispatchCreate routine to open a handle
to a device or file object. A well-known example occurs when a kernel driver calls functions like
NtCreateFile | ZwCreate, and an IRP_MJ_CREATE is sent to accomplish the open operation.
10 | P a g e
https://exploitreversing.com
(it could be a well-known or a private one) to the target device driver. In most situations, the
routine will pass the IRP to the next lower driver, but there are exceptions. Readers should
remember that the first two members of DeviceIoControl( ) are associated to the referred purpose:
[Figure 9] DeviceIoControl
▪ hDevice: this parameter represents a handle to a device driver, which can be easily
retrieved by using CreateFile( ) (https://learn.microsoft.com/en-
us/windows/win32/api/fileapi/nf-fileapi-createfilea).
▪ dwIoControlCode: this parameter specifies the control code for the operation. There are
multiple set of control codes organized according to the type of target device:
▪ cdrom: https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/cd-
rom-io-control-codes
▪ communication: https://learn.microsoft.com/en-
us/windows/win32/devio/communications-control-codes
▪ device management: https://learn.microsoft.com/en-
us/windows/win32/devio/device-management-control-codes
▪ directory management: https://learn.microsoft.com/en-
us/windows/win32/fileio/directory-management-control-codes
▪ disk management: https://learn.microsoft.com/en-us/windows/win32/fileio/disk-
management-control-codes
▪ file management: https://learn.microsoft.com/en-us/windows/win32/fileio/file-
management-control-codes
▪ power management: https://learn.microsoft.com/en-
us/windows/win32/power/power-management-control-codes
▪ volume management: https://learn.microsoft.com/en-
us/windows/win32/fileio/volume-management-control-codes
▪ IRP_MJ_FILE_SYSTEM_CONTROL: as readers might expect, file system drivers commonly use this
IRP major code.
11 | P a g e
https://exploitreversing.com
▪ IRP_MJ_FLUSH_BUFFERS: this IRP major code means a request to the device to flush its internal
cache, and such code is used for invoking the DispatcFlushBuffers routine.
▪ IRP_MJ_PNP: this code is used over a request for any Plug & Play operation (enumeration or
resource balancing, for example) and used for invoking the DispatchPnP routine.
▪ IRP_MJ_POWER: this IRP code is used by requests, through the Power Manager, to invoke the
power callback (DispatchPower routine).
▪ IRP_MJ_SHUTDOWN: this IRP code is handled by drivers that are responsible for mass-storage
devices with internal caches, and it is used for invoking the DispatchShutdown routine. As drivers
are organized in a stack, all intermediate drivers that are associated with mass-storage devices
need to be able to manage such requests. Of course, drivers must complete any transfer of data
that is currently in cache before finishing the shutdown request.
▪ IRP_MJ_READ: this IRP code is used for calling DispatchRead routine, which acts when application
makes requests (ReadFile( ) and ZwReadFile( )) to transfer data from the device to the application.
▪ IRP_MJ_WRITE: this IRP code is used for invoking the DispatchWrite routine, which is used by
drivers that transfer data from the system to the associated device.
Thus, so far, we have few conclusions:
▪ a driver object (_DRIVER_OBJECT) holds one or more device objects (_DEVICE_OBJECT), which are
the main interface of communication between the application and driver.
12 | P a g e
https://exploitreversing.com
13 | P a g e
https://exploitreversing.com
▪ APC LEVEL (value 1): it’ the level ued b APC (Asynchronous Procedure Calls), which is a function
that executes in the context of a thread. In few words, each thread has an own APC queue and
when an application sends an APC to a thread by invoking QueueUserAPC( ) (actually, a wrapper to
NtQueueApcThread( ) -- https://learn.microsoft.com/en-
us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc), it passes the
address of the APC function as argument and an interrupt is issued by the system. Therefore,
readers can understand that queueing an APC works as a request for the thread calls/invokes the
given APC function. The application is only able to deliver an APC to a thread when this thread is in
alertable state (it called SleepEx( ), WaitForSingleObjectEx( ), WaitForMultipleObjectsEx( ) and so
on), and this APC from the thread’ queue is executed when the thread transits from alertable state
to running state. The same concept is used when malware threats do APC injection, which is only
possible when the target thread is in alertable state. At the end of day, APC is a subtle technique
that makes it possible to execute a callback method (the function passed as argument to the APC)
in an asynchronous way. APCs can be listed by using !apc extension on WinDbg.
▪ DISPATCH LEVEL (value 2): it’ the higher IRQL aociated to oftware interruption. DPC (Deferred
Procedure Call) runs at this level as well as the thread dispatcher, and it is responsible for the post-
processing of a driver after a first, critical and short job has been performed by the ISR (Interrupt
Service Routine), which is registered (IoConnectInterrupt( ) -- https://learn.microsoft.com/en-
us/windows-hardware/drivers/ddi/wdm/nf-wdm-ioconnectinterrupt) by a device driver, runs at
DIRQL (Device Interrupt Request Level), and it is responsible for a really minimal work before
queueing (KeInsertQueueDpc( ) -- https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-keinsertqueuedpc) a DPC that will be executed when the IRQL
drops to a lower level. Furthermore, in the kernel driver’ context, routine uch a StartIo( ),
IoTimer( ), Cancel( ), DpcForIsr( ), CustomDpc( ) and so on also run at this level. Finally, it is
appropriate to mention that any thread waiting on kernel object (event, emaphore, mutex…) at
this level causes a system crash.
▪ DIRQL (value 3 and higher): these levels are related to hardware interrupts.
A kernel code, which can be interrupted by other kernel code with higher IRQL, is able to change the
current IRQL (from the current CPU) by calling functions such as KeLowerIrql( )
(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kelowerirql) and
KeRaiseIrql( ) (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-
keraiseirql). In the order side, it is not possible to raise the IRQL from a user mode application.
Although the APC topic is really attractive, the only difference between PASSIVE_LEVEL and APC_LEVEL is
that a process running at APC_LEVEL cannot get interrupted by APC interrupts. While explaining about high
level drivers (not associated to devices) that process IRP, we will be focused on PASSIVE_LEVEL and
DISPATCH_LEVEL to avoid getting distracted with other topics.
Anyway, I know that professionals usually ask about the IRQL and respective thread context when one of
commented dispatch routines (callbacks) is called, so I retrieved a list from Microsoft
(https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/dispatch-routine-irql-and-thread-
context) that could help you:
14 | P a g e
https://exploitreversing.com
15 | P a g e
https://exploitreversing.com
16 | P a g e
https://exploitreversing.com
17 | P a g e
https://exploitreversing.com
18 | P a g e
https://exploitreversing.com
We have learned that a basic kernel driver likely will have relevant routines, mechanisms and objects that
are critical for its perfect operation:
▪ DriverEntry( ) routine, which is called from IRQL == PASSIVE_LEVEL, and responsible for providing
an entry point to driver routines, initializing or even creating object, allocating non-paged or paged
memory using ExAllocatePoolWithTag( ) (for example) or retrieving a key-information from
Registry. Furthermore, it can also be used to call PsCreateSystemThread routine, which creates a
system thread to execute in kernel mode.
▪ Unload( ) routine, which is responsible for freeing resources, and that is a strong requirement for
WDM (Windows Driver Model) drivers. The I/O manager calls the Unload routine whether there is
not any reference or pending IRP request associated to device objects of the driver. Readers may
find a series of functions inside this routine such as ExFreePool( ), IoDeleteSymbolicLink( ),
PsTerminateSystemThread( ), IoDeleteDevice( ) and so on.
▪ An associated device object (remember: the device object is the actual interface of communication
with the driver).
▪ We will have kernel drivers which holds one ore more dispatch routines handling function codes
such as IRP_MJ_CLOSE, IRP_MJ_READ, IRP_MJ_CREATE or IRP_MJ_DEVICE_CONTROL,
IRP_MJ_INTERNAL_DEVICE_CONTROL, IRP_MJ_SYSTEM_CONTROL, because these routines are
usually essential to most of kernel drivers, and in different cases we will have the opportunity to
work with other ones like IRP_MJ_SET_INFORMATION, IRP_MJ_CLEANUP and
IRP_MJ_SHUTDOWN, for example. If readers are programming then system functions/macros such
as ObDereferenceObject (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-obdereferenceobject), PsLookupThreadByThreadId
(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-
pslookupthreadbythreadid), and IoCompleteRequest (explained below) will be very useful.
▪ A dispatch routine might have nothing else to do with a driver, so it would complete an IRP input
with a simple STATUS_SUCCESS, but it could be suitable in contexts and scenarios. For example,
DispatchClose routine (handles IRP_MJ_CLOSE I/O function code) could be responsible for
notifying that all references to a given file were removed. Eventually, drivers that never could be
unavailable, and the DispatchClose routine wouldn’t be called. At the same way, DispatchCleanup
routine (handles IRP_MJ_CLEANUP I/O function code) is used to perform cleaning operations after
handles of a given object have been released and, for each IRP request, this routine is composed by
operations such as setting Cancel routine’ pointer to NULL, cancelling all IRP related requests (for
example, associated to the object that has been closed) that are still in the queue and, finally,
calling the IoCompleteRequest( ) routine to complete the IRP and returning STATUS_SUCCESS.
Maybe, the most important lesson is that, although few dispatch routines will be seen in most of
software drivers, it is recommended not assuming whether one of them is more important or even
critical than other one because each driver has a particular goal and different role.
19 | P a g e
https://exploitreversing.com
Of course, the list of routines mentioned above is regarding only a basic software kernel driver, which is
part of the goal of this article, but we could explain much more about them. For sure, other routines might
be relevant for readers interested in writing a device driver such as AddDevice, StartIo, ISR, DPC routines
and so on.
As happens with userland applications, the I/O manager also manages synchronous and asynchronous
operations and as expected, over an anchronou operation the kernel driver doen’t have an obligation
to process IRP requests in a specific order. In other words, a kernel can start processing the next IRP
request without having finished the previous one. From this point, the kernel driver can pass down the IRP
to the next drivers in the stack and continue the request processing.
A concept that I have not mentioned yet is completion routine, an optional feature/function, which is
called by IoCompleteRequest( ) function, and that performs an important role over the kernel processing
because a driver can register a completion routine (IoCompletion( ) routine --
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocompleterequest) that
will be invoked by I/O manager soon a kernel driver has finished the processing an IRP.
The IoCompleteRoutine( ) makes the reverse path by sending back the IRP to the upper layer driver in the
driver stack. Thus, in a hypothetical asynchronous scenario, it is likely having a kernel driver processing the
next IRP while the I/O manager calls the completion routine from other driver that finished its IRP
processing.
Drivers provide the status of an operation within the I/O status block of IRP. Additionally, drivers can keep
the status of the operation inside the driver extension, which is really useful in the context with two or
more drivers that are part of the same stack. When a device object is created through IoCreateDevice
function (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-
iocreatedevice), the DriverExtensionSize parameter is used to prepare the driver for scenarios like
explained in this paragraph. A driver extension can be created or initialized by
IoAllocateDriverObjectExtension( ), which is invoked by DriverEntry( ) routine.
During the usage of the concept of driver stack, I am not assuming a specific number of drivers in this stack
to keep the explanation wide enough. However, it is suitable to explain that whether any driver, which
make part of the tack, doen’t receive a handle, or even pa down the IRP to next driver through the
right way, the system can (and probably will) crash. Additionally, and as a side note, so far we have mostly
explained and handled I/O operation as being IRP requests. Nonetheless, there is another type of
operation called Fast I/O that doen’t generate IRP and goes to specific drivers to complete the request,
but it is not the moment to discuss this kind of operations in this section.
Returning to outstanding points, it is time to provide a concise explanation about ISR and StartIo routines.
In general, hardware interrupts are associated with a priority (IRQL, as we learned), the device registers
(through IoConnectInterruptEx / WdmlibIoConnectInterruptEx routines ) one or more ISR (Interrupt
Service Routine) to handle interrupts. Drivers associated to physical devices, which generate interrupts,
need to have one ISR, at least. Once again, threads have an associated priority while CPUs have an
associated attribute named IRQL.
In other words, each time an interrupt is generated to that specific device, the system calls an ISR, which
could be InterruptService or InterruptMessageService routines. Anyway, it will be executed with the same
associated IRQL that the request arrived (masking interruptions at lower level) and, if the IRQL is zero (for
20 | P a g e
https://exploitreversing.com
example) before the ISR, then it will be raised to the same higher level of the interrupt (there in’t context
switch when IRQL is 2 or higher, and accessing paged memory causes system crash) and, after the ISR
completes, the IRQL will return to the previous level. Additionally, it is possible to enable or disable an ISR
by calling IoReportInterruptActive( ) or IoReportInterruptInactive( ) functions, whose references follow
below:
▪ https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-
ioreportinterruptactive
▪ https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-
ioreportinterruptinactive
ISR is short and fast. In few words, it should handle the interrupt (stop the interrupt), gather and save the
state (context), and queues a DPC (DpcForIsr or CustomDpc routines) through IoRequestDpc or
KeInsertQueueDpc routines, respectively, soon the IRQL drops below DISPATCH_LEVEL.
The DPC will be responsible for managing the I/O operation that will be conducted at a lower level than the
ISR. The ISR does only a little part of the I/O processing (the initial request), and the heavy work is left to
the DPC (Deferred Procedure Call), which has the assignment to complete the I/O operation, queue the
next IRP (ensuring the next I/O operation) and, as explained, finish the current IRP when it is possible.
The system provides a DPC object for each device object, and the first (and default) routine is DpcForIsr( ).
In case of driver to need to create additional DPC objects then CustomDpc routines are associated to these
new DPC objects. Both DpcForIsr and CustomDpc routines are called in arbitrary DPC context at
IRQL_DISPATCH_LEVEL (IRQL value 2).
The IoInitializeDpcRequest( ) routine is responsible for registering the DpcForIsr routine, receiving a
pointer to a device object represented by DEVICE_OBJECT structure (remember: a DPC object for each
device object) and also receiving a pointer to the provided DpcForIsr routine, as shown below:
21 | P a g e
https://exploitreversing.com
22 | P a g e
https://exploitreversing.com
present a big latency and, eventually, cause performance issues. Therefore, Threaded DPC, which is
enabled by default (HKLM\System\CCS\Control\SessionManager\Kernel\ThreadDpcEnable), might be
interpreted, in most cases, as a better choice than normal DPC (but it is not a rule).
Beide DPC’ uage with ISR, DPC can be alo ued with kernel timers that have a remarkably similar
behavior to other objects like semaphores, event, mutex, events and so on, as any driver can use these
objects during synchronization tasks since it happens in IRQL==PASSIVE_LEVEL and non-arbitrary context.
Independently of which of mentioned kernel objects is being taken, we can use typical waiting routines
such as:
▪ KeWaitForSingleObject (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-kewaitforsingleobject)
▪ KeWaitForMultipleObjects (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-kewaitformultipleobjects).
Getting into quite few details, kernel timer is associated and represented by a KTIMER or EX_TIMER
structure, and it is used to time out operations of kernel routines or even scheduling new operations
(other researchers and programmer might be use the term “action” or “tak”) to be executed from time
to time, so presenting well-established periodic behavior.
Kernel timers based on KTIMER structure can be set by using KeSetTimer (the timer object must have
been initialized using KeInitializeTimer/KeInitializeTimerEx routine, and its DPC also must have been
initialized by calling KeInitializeDPC routine) to set absolute or even relative interval, which after it expires
it is set to signaled state.
23 | P a g e
https://exploitreversing.com
24 | P a g e
https://exploitreversing.com
It is really crucial to underscore that I/O completion routine can be registered and configured to any
driver in the driver stack, except the lowest one because each driver stores the completion routine from the
driver immediately above in the driver stack inside its I/O stack location.
Additionally, IoCompletion routine of a driver can be executed in two different moments or conditions: in
an arbitrary thread (thus, it is not possible to know the thread in advance) or even inside a DPC context.
Thus, after a kernel driver has completed the IRP, it invokes IoCompleteRequest routine , which is usually
called from the DpcForIsr routine) to notify that everything is done. Afterwards, the I/O manager verifies
whether the upper drivers offer an IoCompletion routine (as we described) and calls one by one, from the
immediate upper driver up to the highest driver. After everything has been done (all drivers in the stack
completed their IRP processing), so the I/O manager returns a result to the caller application.
The remaining question is: how does the driver forward the IRP to the next lower driver in the stack? It
performs this task by calling IoCallDriver, which is a macro wrapping IofCallDriver routine that accepts two
parameters such as DeviceObject (a pointer to the target device object) and Irp (a pointer to IRP):
As I mentioned previously, I would comment some fields from IRP structure according to the need, and as
we are interested in understanding the data exchange between applications and drivers, so some of these
fields are relevant because, in general, applications can interact with a driver by writing (WriteFile:
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile), reading (ReadFile:
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile) or even controlling
(DeviceIoControl: https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-
deviceiocontrol) a device or another driver. However, it does not matter the operation, there will be some
transfer of information from application to device driver or vice-versa, and the buffer holding the
information must be pointed during the operation and, this time, other fields of IRP show their
importance:
▪ UserBuffer: this field contains a pointer (address) to a user buffer. Actually, this buffer is an address
of an output buffer, and is used in particular conditions of I/O control code (METHOD_BUFFERED or
METHOD_NEITHER) and respective major function code (IRP_MJ_DEVICE_CONTROL /
IRP_MJ_INTERNAL_DEVICE_CONTROL), as we will learn soon.
▪ SystemBuffer: this field holds a pointer to a system buffer (non-paged pool buffer), which it will be
useful for drivers using buffered I/O and the purpose of the given buffer is determined by the
associated IRP Major code such as IRP_MJ_READ (buffer will be used for reading from a device or
driver), IRP_MJ_WRITE (it will used for writing to a device or driver) and IRP_MJ_DEVICE_CONTROL
(buffer will be used for sending and receiving control data to/from a device or driver).
▪ MdlAddress: this field points to an MDL (Memory Descriptor List), which is defined by a MDL
structure, and followed by an array that describes physical page layout for a virtual memory buffer.
There is a series of functions to work with MDLs such as MmGetMdlVirtualAddress (gets the virtual
address of the I/O buffer described by the MDL), MmGetMdlByCount (retrieves the size of the I/O
buffer), IoAllocateMdl (this function allocates an MDL), IoFreeMdl (this function frees a MDL),
MmInitializeMld (this functions formats a non-paged memory block as an MDL),
MmBuildMdlForNonPagedPool (to initialize the mentioned array following the MDL structure) and
many other ones.
An important aspect to realize is that, regardless of the involvement of any field above, access to any
provided buffer is always controlled by system rules (including security aspects), and eventually a broken
rule will lead to a system crash. For example, accessing a user buffer can be done only from the context of
an application thread (IRQL==0) requesting this access. Nonetheless, associated functions such as DPC or
Start IO can execute from any thread (arbitrary context) where the provided address is meaningless
(different addresses spaces) and IRLQ == 2, which accessing user page is not allowed because part of the
buffer might have been paged out. Unfortunately, not even the dispatch routine might not to be reliable
due to the fact that, although it runs at the same context of the requesting thread and initially at IRQL == 0,
eventually it might run at IRQL == 2 (or higher), over an IRP activity between drivers in the stack.
Therefore, the I/O manager provides us two approaches to access the provided user buffer in a safe way:
▪ Buffered I/O
▪ Direct I/O
27 | P a g e
https://exploitreversing.com
Most of the time, the Buffered I/O method should be used for interactive services transferring a small
amount of data (likely 4 KB or less) between application and drivers. As most of operations are reading or
writing (IRP_MJ_READ and IRP_MJ_WRITE requests, respectively), so a driver selects this method of
operation when the Flag member of the Device Object (DEVICE_OBJECT structure – check the nineth field
of Figure 2), provided by the IoCreateDevice( ), is set as DO_BUFFERED_IO (actually Flag member works as
an OR operation). If the driver needs to handle or execute I/O device control operations through
DeviceIoControl function (IRP_MJ_DEVICE_CONTROL/IRP_MJ_INTERNAL_DEVICE_CONTROL requests), so
the IOCTL code’ value mut mirror thi method b uing METHOD_BUFFERED as its TransferType value.
Buffered I/O operations happen by allocating a buffer with the size of the user buffer inside for an
allocated non-paged pool (ExAllocatePoolWithTag / ExAllocatePool2) and this new address is stored as a
pointer into IRP (specifically, in SystemBuffer member from AssociatedIrp field). Afterwards, it allows
access to this new allocated buffer to the driver and there is no further concern because as the buffer is
stored in a non-paged pool, o driver doen’t run an rik of tring to acce paged-out data. Additionally,
as the address is in the kernel space, it is valid from any process and, better yet, the driver does not need
even to lock it before accessing it. Once the non-paged buffer has been created, data can be copied (by I/O
manager) from the user buffer into this new non-paged buffer for IRP_MJ_WRITE requests, or copied from
this new non-paged buffer to user buffer for IRP_MJ_READ requests.
Direct I/O operations, which is recommended for cases in which there is a bigger amount of data to be
transferred, presents a different approach from Buffered I/O. Instead of proposing a new buffer in the
non-paged pool as is done for Buffered I/O, this technique offers directly access to the buffers, so
improving the performance because there is not the overhead in first copying data to a new-created buffer
to be consumed afterwards. Apparently it would be a problem because, as we explained previously, the
meaning of an address is only valid to a given process address space, but the mechanism is different. When
the buffer is created by the user application, the I/O manager creates an MDL, which describes this buffer.
Actually, the content of the buffer might be scattered over different physical places in the memory, and
the created MDL represents this set of places as a one-piece in the virtual memory world. In another
words, MDL works as a kind of mapping of one virtual memory to one or more physical address ranges.
Soon after the MDL has been associated with the user buffer, the I/O manager checks whether such user
buffer is accessible and locks it (making it resident) on memory (non-paged memory) by calling
MmProbeAndLockPages (defined in wdm.h), which accepts the MDL as first argument, and make sure that
the content of the virtual memory pages will be not freed and relocated any time:
28 | P a g e
https://exploitreversing.com
The user memory buffer will only be unlocked whether the I/O Manager calls the MmUnlockPages
function after the driver having completed the IRP processing.
Having created the MDL, the I/O Manager fills the IRP → MdlAddress field with the pointer to the pointer
(address) of the MDL. If the device is performing a DMA operations, so it is done because device drivers
working with DMA operations require only physical addresses. However, it is not our case because we are
interested in accessing the buffer content. Thus, we have to map the provided buffer with an associated
MDL to a non-paged system address, and this address is retrieved by calling
MmGetSystemAddressForMdlSafe( ) with the MDL’ addre a first argument. This function returns a
pointer to a non-paged virtual address for the buffer represented by MDL. Therefore, we have exactly what
we need: a non-paged system address that can be accessed from any process/thread (arbitrary context)
and any IRQL because as it is locked on memory and cannot be paged out, so a system crash will never
happen even accessing it from IRQL == 2 or higher.
There is a third option named Neither I/O, which is not managed by the I/O manager, and, in this case, the
buffer management is performed (ProbeForRead and ProbleForWrite functions), and accessed from the
same context of requesting thread because the original address of the buffer is passed into the IRP, which
will be used by the driver itself. Any broken rule likely will cause a system crash. It is not easy to manage
the necessary requirements to do all these tasks without the I/O manager and, at the end of the day, the
driver itself will have to perform manually the same tasks on his own, which would be done by the I/O
manager.
In the real world, and as I explained previously, there are writing, reading and device control operations.
The first two have been covered Buffered I/O and Direct I/O operations, but while working with I/O device
control (IRP_MJ_DEVICE_CONTROL) there is the information that is provided in the control code., which is
usually defined by driver through the CTL_CODE( ), which is a macro with the following prototype:
▪ void CTL_CODE(DeviceType, Function, Method, Access);
▪ The second parameter contains the IOCTL function value, which will be used and available for user
mode applications, so it must be used with IRP_MJ_DEVICE_CONTROL requests. If it used by only
kernel-mode components, so it must be used with IRP_MJ_INTERNAL_DEVICE_CONTROL requests.
▪ The third parameter contains the method code about how the buffers are passed
(METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT and METHOD_NEITHER).
▪ The fourth and last parameter specifies the operation: FILE_ANY_ACCESS (commonly used because
works in both directions), FILE_WRITE_ACCESS (from user application to the driver) and
FILE_READ_ACCESS (from the driver to the user application).
We finished our brief review about kernel drivers, and it is time to review filter drivers.
29 | P a g e
https://exploitreversing.com
Explaining concepts about kernel drivers and file system filter drivers always demands dozens of pages, but
it’ a good opportunit to touch these themes even without including too many details.
File system filter drivers are not device drivers, and the general idea of file system filter drivers is to offer
supplemental functionality to typical file system operations such as opening files, creating files, reading
and writing file, and so on, while device drivers are usually associated a hardware device (except in case of
software kernel drivers as we learned previously in this article).
No doubt, there are many common things like IRPs (I/O Request Packets) for communication, callback
methods, IOCTLs and so on, which we can also use here and, eventually, adapt concepts to explain
minifilter driver functionality. Minifilter drivers are able to filter and intercept IRPs, fast I/O (synchronous
I/O operations, where data are transferred between given user buffer and the system cache without
suffering file system or storage driver interference ) and file system callback operations.
Filter drivers are used to customize / modify operations related to the file system and, in general, file
system filter drivers are used to intercept, monitor and even modify requests to the file system, besides
eventually extending and replacing a current functionality.
Thus, as expected, you will find file system drivers and mini-filter filesystem drivers in contexts where
intercepting and monitoring are the main objective as multiple security defense products such as
antivirus, EDR, backup programs, and so on, and such fact is not a surprise, and it is pretty cool.
On Windows there are two filter system filter models that are the minifilter model, which is supported by
the Filter Manager, and the legacy file system filter model. The minifilter model is a much better choice to
be followed because it allows to unload the minifilter driver (FilterUnload( ) on user-mode,
FltUnloadFilter( ) on kernel mode and even using fltmc command, as we will learn soon) and enables
communication between a user mode application and the own minifilter driver, for example. In addition, it
also permits to lock/stick on on a specific type of operation through of the usage of callbacks (definitions
will come on the next pages) and as shown below, there is the option to control the loading order through
a concept its respective altitude (another term that will be explained).
File system filter services are available through the Filter Manager (represented by the same fltmgr.sys file
mentioned above), which are enabled when the provided minifilter is loaded, and it makes the
programming task simpler (or less complex, at least) and, as also expected , minifilter is the model used for
creating file system minifilter drivers. As kernel drivers, minifilter is also stacked, but their order of loading
(actually, positioning in stack) is determined by its respective altitude. The concept of altitude seems to be
complex, but it is not, and readers can notice it by observing the following sequence:
a. Application requests an I/O operation
b. I/O Manager receives and forwards this request to the Filter Manager (fltmgr.sys).
c. The Filter Manages receives the request from I/O manager (that is key component) and checks all
its registered minifilter drivers (mfd1, mfd2, mfd3, mfd4...) according to the registered altitude.
d. After minifilter doing its actions, the request is forwarded to the File System Filter Driver.
e. Finally, the request reaches the Storage Driver Stack.
30 | P a g e
https://exploitreversing.com
There is a list of diverse ways to represent the flux of information involving mini-filter drivers, and one of
them is through the following image, as designed by Microsoft (from MSDN):
APPLICATION
I/O Manager
Minifilter Driver 1
Minifilter Driver 2
Filter Manager
Minifilter Driver 3
Minifilter Driver 4
File System Driver
Hardware
31 | P a g e
https://exploitreversing.com
While discussing about routines related to mini-filter drivers, there are few of them that are well-known
such as:
▪ DriverEntry( ): occurs and works as for device drivers, it is used for initialization.
▪ FltRegisterFilter( ): this function is used to register a minifilter driver (and associated callback
routines) with the filter manager.
▪ FlsStartFiltering( ): it is responsible for notifying the Filter Manager that a minifilter driver is
available and ready to attach to volumes and filter requests (IRP, fast I/O and file system callback
operations). In other words, it starts the real filtering operation.
These routines present interesting details that help to explain concepts mentioned in previous paragraphs.
The prototype of FltRegisterFilter( ), which is one the main one so far, is quite simple:
32 | P a g e
https://exploitreversing.com
Anyway, there is no doubt that the most important field of this structure is OperationRegistration, which is
part of the FLT_OPERATION_REGISTRATION structure that we just mentioned, but it is not the only one.
There are other relevant fields such as FilterUnloadCallback (it holds the address of a function that is called
when a driver is about to be unloaded), InstanceSetupCallback (it is a pointer to a callback that is called by
Filter Manager when a new volume is available), InstanceSetupCallback (it points to a callback that allows
the minifilter drivers to be notified just before the be attached to a volume),
InstanceQueryTeardownStartCallback (it contains a pointer to a function that will be called by the Filter
Manager before the teardown process, making possible for minifilter to cancel pending operations and
cancel or complete I/O requests) and so on.
About the teardown process, a minifilter driver instance is torn down in the following contexts: either the
minifilter is unloaded, or there is a specific detach request to be accomplished or the volume which the
instance is attached is dismounted.
It is also suitable to highlight that, during a tearing down operation of an instance, any routine executing
preoperation and postoperation callback routines continue executing without facing any problems, but I/O
requests waiting for these preoperation and postoperation callback routines may be cancelled.
Additionally, operations initiated by the minifilter drivers proceed until they are complete.
Other valuable members of FLT_REGISTRATION structure are:
▪ ContextRegistration: it represents a pointer to an array of FLT_CONTEXT_REGISTRATION
structures, being one for each context type (formatted data to be used by the driver if it’
necessary) that the minifilter could use.
attachment request to the given volume. As readers can realize, there are interesting practical
usages for it.
▪ InstanceTeardownStartCallback: it holds a pointer to a callback routine that will be called when the
filter manager starts tearing down a minifilter driver instance to allow it to complete any pending
operation such as closing opened files and stop queueing new work items, and save the
information. From a certain point of view, this callback routine can be interpreted as the first stage
preparing for a cleaning up routine.
▪ SetFileInformation: IRP_MJ_SET_INFORMATION
▪ QueryEa: IRP_MJ_QUERY_EA
▪ SetEa: IRP_MJ_SET_EA
▪ QueryVolumeInformation: IRP_MJ_QUERY_VOLUME_INFORMATION
▪ SetVolumeInformation: IRP_MJ_SET_VOLUME_INFORMATION
▪ DirectoryControl: IRP_MJ_DIRECTORY_CONTROL
▪ FileSystemControl: IRP_MJ_FILE_SYSTEM_CONTROL
▪ DeviceIoControl: IRP_MJ_DEVICE_CONTROL and IRP_MJ_INTERNAL_DEVICE_CONTROL
▪ LockControl: IRP_MJ_LOCK_CONTROL
▪ QuerySecurity: IRP_MJ_QUERY_SECURITY
▪ SetSecurity: IRP_MJ_SET_SECURITY
▪ QueryQuota: IRP_MJ_QUERY_QUOTA
▪ SetQuota: IRP_MJ_SET_QUOTA
▪ Pnp: IRP_MJ_PNP
▪ AcquireForSectionSynchronization: IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION
▪ AcquireForModifiedPageWriter: IRP_MJ_ACQUIRE_FOR_MOD_WRITE
▪ ReleaseForModifiedPageWriter: IRP_MJ_RELEASE_FOR_MOD_WRITE
▪ QueryOpen: IRP_MJ_QUERY_OPEN
▪ FastIoCheckIfPossible: IRP_MJ_FAST_IO_CHECK_IF_POSSIBLE
▪ NetworkQueryOpen: IRP_MJ_NETWORK_QUERY_OPEN
▪ MdlRead: IRP_MJ_MDL_READ
▪ MdlReadComplete: IRP_MJ_MDL_READ_COMPLETE
▪ PrepareMdlWrite: IRP_MJ_PREPARE_MDL_WRITE
▪ MdlWriteComplete: IRP_MJ_MDL_WRITE_COMPLETE
▪ MountVolume: IRP_MJ_VOLUME_MOUNT
The second parameter is Flags, which specifies when to call preoperation and postoperation callback
routines for cached I/O or paging I/O operations, but it is not quite relevant for us right now.
PreOperation and PostOperation are pointers to PFLT_PRE_OPERATION_CALLBACK and
PFLT_POST_OPERATION_CALLBACK routine that, obviously, are registered as preoperation and post-
operation callback routines, respectively.
In few and rough words, preoperation callback routines perform the processing tasks needed for complete
the I/O operation, and controls what should be done with IRP requests and post-operation routines. Post-
operation callback routines are invoked by the Filter Manager over an I/O operation when lower drivers
have already finished completion processing.
A PFLT_PRE_OPERATION_CALLBACK routine can return different values such as:
▪ FLT_PREOP_COMPLETE: this value means that the minifilter driver is completing the I/O operation,
and the filter driver does not call postoperation callbacks of any minifilter below the caller
(remember about the driver stack) and doen’t forward (pass down) any request to minifilter
drivers below the caller.
▪ FLT_PREOP_DISALLOW_FASTIO: this value means that the operation is a fast I/O operation, and
that the minifilter driver does not allow that the fast I/O path to be used for this operation. The
35 | P a g e
https://exploitreversing.com
remaining characteristics related to postoperation callbacks and forwarding requests are similar to
FLT_PREOP_COMPLETE.
▪ FLT_PREOP_PENDING: this value means that, for a provided minifilter driver, the operation is still
pending and only after FltCompletePendedPreOperation has been invoked is that the Filter
Manager will continue the I/O operation.
▪ FLT_PREOP_SUCCESS_NO_CALLBACK: this value means that the minifilter driver is returning the
I/O operation to the Filter Manager for further processing, but the the Filter Manager will not call
the postoperation callback of the minifilter drivers over the I/O completion.
▪ FLT_PREOP_SUCCESS_WITH_CALLBACK: this value means that the minifilter driver is returning the
I/O operation to the Filter Manager for further processing, which will invoke the post-operation
callback over of the minifilter driver over the I/O completion.
▪ FLT_PREOP_SYNCHRONIZE: this value indicates that the minifilter driver is returning the I/O
operation to the Filter Manager for further processing, but it will not complete the operation. In
addition, the Filter Manager will invoke the post-operation callback of the minifilter within of the
context of the current thread at IRQL <= DISPATCH_LEVEL.
▪ FLT_PREOP_DISALLOW_FSFILTER_IO: this value means that the minifilter driver is disallowing a fast
QueryOpen operation and forcing the operation proceed through the slow path.
Readers have realized the introduction of a new term in these last paragraphs: Fast I/O. In a few words,
Fast I/O is an additional mechanism, supported by minifilter drivers, to receive requests. Actually, a file
system driver filters I/O requests coming as an IRP (I/O Request Packet) or Fast I/O requests. At the same
way of IRP requests, Fast I/O requests also have callback methods.
It is fair to say that IRP requests have a kind of equivalence to Fast I/O requests, but they are not the same,
and IRP are able to handle much more I/O’ tpe than Fat I/O. Furthermore, the DriverEntry routine can
register IRP dispatch routines and also Fast I/O callback routines, but only a set of these routines can be
registered for a given filter driver.
By the way, what is the difference in the usage between IRPs and Fast I/O? The coverage of IRP is broader,
and it can be ued for nchronou/anchronou operation, and doen’t matter whether it i a cached or
non-cached I/O. In the case of Fast I/O, it is suitable for synchronous I/O operations on cached files.
Therefore, the general requisition and practical usage of filter drivers is focused on IRP requests, although
even in this scenarios filter driver mut define a Fat I/O routine returning ‘fale’ value.
Returning to the main topic, a PFLT_POS_OPERATION_CALLBACK routine can return different values such
as:
▪ FLT_POSTOP_FINISHED_PROCESSING: this value means that the minifilter driver already has
finished the completion processing and the Filter Manager will continue the completion processing
of the I/O operation.
36 | P a g e
https://exploitreversing.com
37 | P a g e
https://exploitreversing.com
▪ IoStatus: this member contains a pointer to an IO_STATUS_BLOCK structure, which contains status
and information for an I/O operation and as mentioned previously, its content can be changed by a
preoperation callback or even a postoperation callback.
The FLT_IO_PARAMETER_BLOCK, pointed by the Iobp parameter, has the following composition:
38 | P a g e
https://exploitreversing.com
Actually, communication ports are not buffered, so they are fast, and are used by a bidirectional
communication channel. Additionally, they are created by the minifilter drivers that keep listening for any
incoming communication and, once the user mode application tries to connect to this port, so the Filter
Manager calls the ConnectNotifyCallback routine from minifilter driver to handle the connection that is
only accepted if the user mode application has the necessary and minimum rights described by the security
descriptor. Furthermore, there are many routines offered by the Filter Manager, which are involved with
communication ports such as FltSendMessage, FltCreateCommunicationPort, FltCloseClientPort, as well
as routines available for being used by the user mode application such as
FilterConnectCommunicationPort, FilterSendMessage, FilterGetMessage, FilterSendMessage and so on.
Finally, and for completeness, it is appropriate to highlight that user mode application can interact with
minifilter drivers through an extensive series of routines for loading/unloading minifilter drivers (FltLoad,
FltUnload), enumerating filters (FilterFindFirst, FilterFindNext, …), quering information
(FilterGetInformation, FilterGetInstanceInformation,…) and o on.
Unfortunately, installing a minifilter driver is not so simple as installing a kernel driver, and it is necessary
to create an INF file, which is out of the scope of this article.
On Windows system we are able to find out a series of minifilter drivers by running the following
commands:
39 | P a g e
https://exploitreversing.com
readers are used to doing it, in this environment I am using two virtual machines (on VMware): the first
one running Windows 11 (host) and the second one running Windows 11 (target). In my case, both systems
have Windows SDK installed.
On target:
▪ bcdedit /set {default} DEBUG YES
▪ bcdedit /dbgsettings net hostip:<host ip> port:50100 key:1.2.3.4
▪ bcdedit /dbgsettings
▪ shutdown /r /t 0
On host:
▪ windbg -k net:port=50100,key=1.2.3.4
▪ Make sure that symbols are configured:
o File → Symbol File Path: srv*c:\symbols*https://msdl.microsoft.com/download/symbols
o set _NT_SYMBOL_PATH=srv*c:\symbols*https://msdl.microsoft.com/download/symbols
(personally, I prefer setting it at Advanced Windows Setting → Environment Variables and
creating the _NT_SYMBOL_PATH as explained above)
▪ Debug → Break
If everything is OK, you should see the WinDbg prompt, and can execute the following:
We can use !fltkd.filters extension command too (it is exactly the same). As in the article from Microsoft,
which is related to Windows Defender detection that was previously mentioned at beginning of this text,
the Windows Defender Filter (WdFilter.sys) is a desirable choice. We can also list its respective
communication ports by using the same fltkd extension. Picking up it object’ addre from the output
above (FLT_FILTER: ffff880f8ae9c4d0 "WdFilter" "328010") by executing the following command:
41 | P a g e
https://exploitreversing.com
42 | P a g e
https://exploitreversing.com
▪ context: it is a structure that can be associated to the filter manager object and used to save and
pass information (the context) about an object. This structure is defined by the minifilter driver
itself, and there can be contexts associated to volumes, files, instances, transactions, stream
handles (file objects) and streams. Readers could be interested in knowing that functions such as
FltAllocateContext (to create contexts), FltRegisterFilter (registering contexts), FltSetFileContext |
FltSetInstanceContext | FltSetStreamContext | FltSetVolumeContext | FltSetTransactionContext
(setting contexts) and other ones aociated to context’ manipulation. Additionall, there i an
interesting example (code) demonstrating how to do it that is available on:
https://github.com/Microsoft/Windows-driver-samples/tree/main/filesys/miniFilter/ctx.
To get a list of volumes and their respective attached filter drivers (pay attention to WdFilter driver), you
can execute the following command:
44 | P a g e
https://exploitreversing.com
Of course, we can get inside of structures and find out much more information. For example, we can get
information from the WdFilter driver by overlaying its address with the _FLT_FILTER structure:
45 | P a g e
https://exploitreversing.com
46 | P a g e
https://exploitreversing.com
At the same way we did with _FLT_FILTER structure, we can pick up one of the callback nodes and getting
information by overlaying it with _CALLBACK_NODE structure as shown below:
47 | P a g e
https://exploitreversing.com
[Figure 48] _CONTEXT_NODE structure overlay wit structure’s address from last output
An organized output containing exactly the same information is given by:
48 | P a g e
https://exploitreversing.com
Just in case readers have curiosity about the stuff, there is a PowerShell module named NtObjectManager,
written by James Forshaw (https://www.powershellgallery.com/packages/NtObjectManager/1.1.33) that
provides the communication ports easily for you:
PS C:\> Install-Module -Name NtObjectManager
PS C:\> Set-ExecutionPolicy RemoteSigned
PS C:\> Import-Module NtObjectManager
PS C:\> NtObject:\ | Where-Object TypeName -eq "FilterConnectionPort"
PS C:\> ls NtObject:\ | Where-Object TypeName -eq "FilterConnectionPort"
Name TypeName
------------------------------------------------------------------ ----------------------------
UnionfsPort FilterConnectionPort
storqosfltport FilterConnectionPort
MicrosoftMalwareProtectionRemoteIoPortWD FilterConnectionPort
MicrosoftMalwareProtectionVeryLowIoPortWD FilterConnectionPort
WcifsPort FilterConnectionPort
WinSetupMonPort FilterConnectionPort
MicrosoftMalwareProtectionControlPortWD FilterConnectionPort
BindFltPort FilterConnectionPort
MicrosoftMalwareProtectionAsyncPortWD FilterConnectionPort
CLDMSGPORT FilterConnectionPort
MicrosoftMalwareProtectionPortWD FilterConnectionPort
[Figure 51] List of registered communication ports
Returning to _FLT_PORT_OBJECT structure, the MegQ member is, as we already explained, a pointer to
the _FLT_MESSAGE_WAITER_QUEUE structure, which can be applied to the address and, executing the
following sequence of commands, we have:
49 | P a g e
https://exploitreversing.com
As a summary, while examining minifilter drivers, readers will find key routines such as:
▪ DriverEntry: it is the same routine as kernel drivers and, at the same way, it is requested for all
filter drivers. Additionally, this routine serves as a starting point for key actions, and, for example, it
is where the minifilter driver can register (through FltRegisterFilter routine) one preoperation
callback and one postoperation callback (it is not necessary to be present both ones) for each of of
different I/O types been manipulated and filtered by the minifilter.
▪ FltRegisterFilter: this routine is used by minifilter drivers to register to provide a list of callback
routines to the Filter Manager and, at the same time, to register themselves to the minifilter
driver’ lit.
▪ FltStartFiltering: this routine notifies the Filter Manager that it is ready and can start to filter
requests by attaching to volumes.
▪ FltCreateCommunicationPort: this routine opens a kernel communication server port.
▪ FltCloseCommunicationPort: this routine closes a kernel communication server port.
▪ FilterUnloadCallback: it is the routine responsible for unloading the minifilter driver. It is an
optional routine.
▪ FltUnregisterFilter: this routine unregisters the minifilter driver.
It is really important to understand the concept of preoperation callback because each minifilter driver can
have its own, and every associated preoperation callback to each registered minifilter will be called from
the minifilter driver that holds the higher altitude up to the lowest one for that specific type I/O operation.
Additionally, the Register parameter from FltRegister routine is relevant because it holds a pointer to the
FLT_REGISTRATION structure. This structure holds a field/member that is actually an array of
FLT_OPERATION_REGISTRATION structures, which each one represents a type of operation being
manipulated and filtered by the minifilter driver. Certainly, it might seem confusing because there are
three levels of redirection here, but it is not so uncommon with kernel and minifilter drivers. However, it is
not the end yet and, as there are two file system filter driver models, minifilter drivers receive the I/O
operation first, and later the legacy file system filter drivers receive it for processing. Afterwards, the
associated file system receives the I/O operation for further processing. In the order side, postoperation
routines (each minifilter drivers that has registered to process that type of I/O operation can have or not a
postoperation callback) start their work in the reverse order, finish the processing of the I/O operation,
return it to the filter managers, which passes it to the next minifilter driver at the upper layer. At this point,
it is not hard to realize that a file system minifilter likely will be using many preoperation callback routines
to manipulate and filter I/O operations, and these preoperation callbacks can return values to the Filter
Manager like FLT_PREOP_SYNCHRONIZE (for IRP based operations, which can have its type confirmed by
FLT_IS_IRP_OPERATION macro, and a postoperation routine will be invoked during the I/O completion
phase), FLT_PROP_SUCCESS_NO_CALLBACK (no postoperation callback routines will be called during the
I/O completion phase) and FLT_PREOP_SUCCESS_WITH_CALLBACK (postoperation callback routines will
be invoked during the I/O completion phase), for example, as already mentioned previously in this article.
Of course, at the same way, a minifilter driver could have more than one postoperation callback routines
that can be executed at IRQL lower or equal to DISPATCH_LEVEL and, due to this fact, data structures
must be allocated in nonpaged pool. Anyway, postoperation routines are called in arbitrary context.
Minifilter drivers also transfer information (data) between applications running in user mode and other
minifilter drivers running in lower layers, which can reach device drivers and, because these data
transferring operations, they are also use some kind of buffer.
51 | P a g e
https://exploitreversing.com
There is not any news related to data buffers, and file system minifilter drivers uses the same methods
from kernel drivers to access buffers that is Buffered I/O (mainly used over IRP operations such as
IRP_MJ_CREATE and IRP_MJ_QUERY_INFORMATION, for example), Direct I/O and Neither I/O (it can used
by operations such as IRP_MJ_SYSTEM_CONTROL and IRP_MJ_QUERY_SECURITY). Additionally, important
and usual operations such as IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_DEVICE_CONTROL and
IRP_MJ_QUERY_OPERATION (mentioned above) can be configured as Fast I/O or IRP based operations.
As readers have realized, same I/O IRP operations major codes are valid for minifilter drivers, and you can
check them by using a well-know WinDbg command:
The Windows Cloud Files filter driver (cldflt.sys) is a file system minifilter driver that is associated to the
OneDrive, for example. The GsDriverEntry( ) is a routine generated automatically when the driver is built,
which does a short initialization and, soon after having completed the initialization, it calls the real
DriverEntry( ) that was implemented.
Moving forward, I would like to comment about ECP (Extra Create Parameters) that are structures holding
information used during file creation, and that can be attached to I/O operations by using an ECP_LIST
structure. For example, a file system filter driver can manipulate ECPs (Extra Create Parameters) to
process IRP_MJ_CREATE operations, and are exactly these ECPs that are used to distinguish between
NtCreateUserProcess( ) and NtCreateProcessEx( ) calls, which were also mentioned in the Microoft’
article at beginning of this text. ECPs can be one of two available types: System-defined ECPs that are used
by the OS to attach further information to IRP_MJ_CREATE mentioned previously, and User-Defined ECPs
that are used by kernel drivers to process and add further information to the IRP_MJ_CREATE operation.
Readers likely will recognize ECPs manipulation when find routines such as
FltAllocateExtraCreateParameterList (to allocate memory to ECP_LIST structure),
FltFreeExtraCreateParameterList (to free memory used by ECP_LIST structure),
FltAllocateExtraCreateParameter (to allocate paged-memory pool for an ECP context structure, returning
a pointer to it), FltInsertExtraCreateParameter (to insert ECP context structures into the ECP_LIST
structure), IoInitializeDriverCreateContext (to initiate an IO_DRIVER_CREATE_CONTEXT_STRUCTURE) and
finally IoCreateFileEx|FltCreateFileEx2 (to attach ECPs to a given IRP_MJ_CREATE_CONTEXT).
Of course, there is an extensive list of routines to process and manipulate ECPs such as
FltGetEcpListFromCallbackData (returns a pointer to an ECP list associated with a create operation
callback-data object), FltFindExtraCreateParameter (searches a provided ECP lit for an ECP’ context
structure) and FltIsEcpFromUserMode (checks whether the ECP is originated from the user mode). A quick
sample of usage of these routines is shown below:
53 | P a g e
https://exploitreversing.com
Returning once again to the Microsoft article, the GUID_ECP_CREATE_USER_PROCESS and respective
CREATE_USER_PROCESS_ECP_CONTEXT context, which contains the token of the process to be created,
are used by kernel while it opens the process executable file. Therefore, while the NtCreateUserProcess
adds the ECP for a process creation, the NtCreateProcessEx does not do it because it uses a section handle
already created (existing). This makes it simpler to distinguish when one or the other function is used.
Certainly, ECP is not the only interesting topic because there is a new mechanism named BypassIO that has
been introduced in Windows 11, that is requested for a file handle, and it turns the I/O access for reading
files better and quicker due to a lower overhead, and this is leveraged by minifilter drivers. The big
advantage of using BypassIO is that the I/O request does not pass through the entire driver stack, but goes
directly to NTFS file system (bypassing volume and filesystem stack, and the latter can be composed by
Volume Device Object (VDO) or Control Device Object (CDO) in addition to usual minifilter device objects)
and, from there, to the underlying volumes and disks. Furthermore, calls to functions such as
FltFsControlFile routine (or native equivalents) with FSCTL_MANAGE_BYPASS_IO control code are usual
while requesting and emitting BypassIO operations.
Readers will see FSCTL_MANAGE_BYPASS_IO and IOCTL_STORAGE_MANAGE_BYPASS_IO control codes
involved with minifilter drivers using BypassIO, which demands NTFS filesystem on NVMe storage device
on Windows 11 for while. You should also pay attention to requests such as FS_BPIO_OP_ENABLE,
FS_BPIO_OP_DISABLE, FS_BPIO_OP_QUERY, FS_BPIO_OP_GET_INFO and other similar ones, mainly
because they are involved with preoperation callbacks.
We can easily check the support for BypassIO feature by executing the following command:
55 | P a g e
https://exploitreversing.com
As I had mentioned, the own framework already offers callback implementation for events, so driver needs
to implement a callback whether it needs to perform a different processing. At end of the day, readers will
realize that KMDF drivers work similarly to minifilter drivers without imposing meaningful restrictions.
One of nomenclature aspects that readers have already realized is that most (not all) objects and routines
are prefixed with “Wdf” tring (upper cae, lower cae or mixed notation). Furthermore, you will see
names of objects like WDFDEVICE (device), WDFDPC (dpc), WDFFILEOBJECT (file), WDFINTERRUPT
(interrupt), WDFSPINLOCK (spin lock), WDFQUEUE (queue) as well as routines as WdfDriverCreate,
WdfDeviceCreate, WdmDeviceCreateSymbolicLink, WdfObjectReference,
WdfDeviceCreateDeviceInterface, WdfRequestRetrieveInputBuffer, WdfRequestRetrieveOutputBuffer,
WdfRequestRetrieveInputWdmMdl, WdfRequestRetrieveOutputWdmMdl, WdfAllocateContext
(allocated in nonpaged pool and taken as part of the object, which has an equivalent meaning of WDM
device extension), WdfIoQueueCreate and so on. Such objects have properties like ParentObject, Size,
ContextTypeInfo, and so on, that are stored into WDF_OBJECT_ATTRIBUTES structure and initialized by
WDF_OBJECT_ATTRIBUTES_INIT function. By the way, there are configuration structures associated to
objects, which hold information like pointers to the event callbacks, and nomenclature of such structures is
WDF_<object>_CONFIG, and that are usually initialized by functions/macro that also follow
WDF_<object>_CONFIG_INIT as nomenclature. Therefore, while creating a KMDF driver, readers will
follow the usual order in declaring and initializing configuration structures then initializing attributes and
finally creating an object.
Similarly, we had seen for WDM, the WDF model is composed by I/O requests, queues, memory regions
and devices, of course. Through this mechanism, when the operating system sends an I/O request to a
WDF driver, the framework is responsible for handling the dispatch operation, queueing and completion of
the request. Furthermore, as most applications will interact with drivers for reading, writing or even
controlling devices, so routines like WdfIoQueueCreate routine will be used to create a queue object that
represent the respective I/O queue (as usual, everything is about managing I/O requests and memory).
Here is appropriate to highlight that the general WDF hierarch is given by a driver object → device object
→ queue object → request object. WDF drivers also handles interrupts by calling routines like
WdfInterruptCreate routine and, as you could imagine, it will create interrupt objects to each given
interrupted and register callback functions, which I do not need to repeat the same explanation. By the
way, callbacks are usually suffixed with Evt string, so there are EvtCleanupCallback, EvtDestroyCallback,
EvtDeviceAdd, EvtIoRead, EvtIoWrite, and so on.
Certainly, KMDF is an extensive topic and has its peculiarities, but it is close to the WDM development, so
these couple of pages are enough to review basics on the KMDF.
Returning to callback subject, Windows offers a series of kernel callback APIs that exported by kernel
(NtosKrnl.exe + wdm.h) and which drivers can use to register their callback routines that, eventually, will
be called for specific kernel component’ event and condition.
56 | P a g e
https://exploitreversing.com
As we are discussing kernel drivers and filter drivers, leaving a few words about this topic could be useful. If
readers are writing a kernel driver, they could use a callback object from other drivers and register a
routine (InitializeObjectAttributes( ) + ExCreateCallback( ) + ExRegisterCallback( )) to be invoked when the
specific callback is triggered (a given condition happened).
The offered kernel callback functions are used mainly by security defenses to register their own callback
routines to be able to monitor the system system according to specific events and conditions, so as
expected, kernel callback functions are available to attend different purposes and goals.
The list of kernel callbacks (sometimes called as system callbacks) is really considerable, and I only will
present the definition and concepts about few of them here:
▪ CmRegisterCallbackEx( ): this function registers a RegistryCallback routine, which is a routine used
by filter driver to monitor and modif an Regitr operation uch a ke deleting, renaming, ke’
value changing, enumeration, creation and so on. For example, malware can use this callback to
restore malicious content (for example, a malicious entry used for persistence) soon after a system
administrator has removed an entry related to persistence. As we reviewed previously, the Altitude
parameter (second parameter shown below) defines the position of the minifilter driver when
compared to other minifilters in the I/O stack. Finally, we should pay attention to the fact that the
first parameter (Function) is a pointer to the RegistryCallback routine to be registered and the third
parameter (Driver) is a pointer to a traditional DRIVER_OBJECT structure, which represents the
driver itself.
WdBoot.sys (ELAM driver) using IDA Pro + WinDbg (in a remote setup configuration) if you want to
do. As a short example to help you to start:
▪ Open the WdBoot.sys driver (from C:\Windows\system32\drivers folder) from a remote
Windows system (we will debug it later) into IDA Pro.
▪ Search for DriverEntry routine (it is called by GsDriverEntry routine)
▪ Write down the DriverEntry’s address.
▪ Examine the WdBoot.sys driver on PEBear. Write down the Image Base.
▪ Through a remote WinDbg session (I explained steps previously), set up a breakpoint on the
remote (target) to stop execution when the driver gets loaded by executing sxe ld WdBoot.sys
and reboot the system. If you want to see all messages from debugger, execute ed
nt!Kd_DEFAULT_MASK 0xFFFFFFFF
▪ Once the system rebooted and stopped on WdBoot.sys loading, setup the breakpoint on
WdBoot!DriverEntry (remember that we don’t have mbol) b executing bp WdBoot +
0x1C000B000 – 0x1C0000000 (effectively is WdBoot + 0xB000).
▪ Type g to resume the system.
59 | P a g e
https://exploitreversing.com
From this point it is possible to perform all the usual investigations using WinDbg. Anyway, the part of the
driver using IoRegisterBootDriverCallback (and respective IoUnRegisterBootDriverCallback) routines
follows:
60 | P a g e
https://exploitreversing.com
IoRegisterBootDriverCallback (remember that its pointer has been stored into SystemRoutineAddress
variable) being used to register a callback named MbEbBootDriverCallback, as shown on line 221:
called by the file system always that it registers or even unregister itself by calling functions such as
IoRegisterFileSystem( ) and IoUnregisterFileSystem( ) respectively.
▪ ExAllocateTimer( ): this function is responsible for allocating and initializing a timer object by using
an ExTimerCallback callback routine, which Windows calls when the time interval of a timer
(represented by EX_TIMER timer object) expires.
62 | P a g e
https://exploitreversing.com
As a simple example about the usage of ExAllocateTimer routine, we could check any filter driver
as WoF.sys (Windows Overlay Filter) that initializes a timer object associated with a callback
named TlgAggregateInternalFlushTimerCallbackKernelMode. The reversing job of the routine
shown below can be improved a lot, but it is enough for now because we only want to highlight the
usage of one routine:
63 | P a g e
https://exploitreversing.com
64 | P a g e
https://exploitreversing.com
▪ KeInitializeDpc( ): this routine is supplemental to the topic explained above because its role is to
initialize a DPC object and register a CustomDpc routine for such object. As expected, the second
argument is a pointer to the KDEFERRED_ROUTINE callback function that is executed after the ISR
(Interrupt Service Routine). Additionally, the CustomTimerDpc routine executes after the time
interval of a given timer object expires and, of course, readers could do an association to the
timer’ tuff mentioned previoul in this article.
65 | P a g e
https://exploitreversing.com
▪ KeInitializeApc( ): this routine is used to initialize an APC (Asynchronous Procedure Calls) object. As
readers could already know, APC is a kind of kernel mechanism that is used to queue a task that will
be performed in a context of a given thread. Additionally, APCs have been used to inject code into a
user process (in alertable state) from a kernel driver, for example. There are distinct types of APC
(UserAPC, Special User APC and Kernel APC), which the first two cases are associated with APIs
such as QueueUserAPC( ) and NtQueueApcThreadEx2( ) respectively. Kernel APC is a bit different,
runs in kernel mode at IRQL = PASSIVE_LEVEL (Special Kernel APC run at IRQL = APC_LEVEL), it is
able to prompt any user mode code running at IRQL = PASSIVE_LEVEL and one of its main
structures is the _KAPC (actually, this structure makes part of a doubly-linked structure within the
_KAPC_STATE structure, which makes part of the KTHREAD structure in the kernel) that must be
allocated from a NonPagedPool memory. At end, Kernel APC works as an interruption because it
can happen at almost any time.
66 | P a g e
https://exploitreversing.com
Many years ago, I could find malware threats using this callback to prevent digital forensic tools to
dump the memory image, so also preventing researchers of analyzing memory.
▪ ObRegisterCallbacks( ): this routine is one of most interesting ones because it registers a list (given
by OB_CALLBACK_REGISTRATION structure) of callback routines to thread, process and desktop
handle operation. Additionally, there is also the ObUnregisterCallbacks routine to revert all
callback’ regitration. Besides the obvious usage by malware threats (including rootkits), I have
seen it being used in anti-cheats too and, of course, Microsoft drivers also use it, of course. For
example, in the piece of code below that also comes from mssecflt.sys (it is the SecObAddCallback
function) , readers can clearly see the call for ObRegisterCallbacks routine, its parameters being
setup and even a a reference to a PreOperationCallback being setup few lines above:
extension command. Anyway, you should make sure that you are using the right WinDbg version (x64) with
the correct extension. A simple execution retrieving callbacks using SwishDbgExt follows:
68 | P a g e
https://exploitreversing.com
Of course, readers could retrieve a specified list manually. For example, get a list of
PsCreateProcessNotifyRoutines by executing the following command:
0: kd> .for (r $t0=0; $t0 < 9; r $t0=$t0+1) { r $t1=poi($t0 * 8 + nt!PspCreateProcessNotifyRoutine); .if ($t1
== 0) { .continue }; r $t1 = $t1 & 0xFFFFFFFFFFFFFFF0; dps $t1+8 L1;}
ffffb788`6fedff98 fffff804`5a5d2840
ffffb788`705fe2b8 fffff804`6115f6b0
ffffb788`705fea68 fffff804`5a74d470
ffffb788`705fea08 fffff804`6189c480
ffffb788`70c30b68 fffff804`61e00750
ffffb788`70c31a38 fffff804`605d9060
ffffb788`70c31ac8 fffff804`66bba740
ffffb788`728ac4a8 fffff804`67b90a60
ffffb788`7208b488 fffff804`695b7d00
We noticed that all addresses above do not have symbols associated, but the reason is that I tested the
command in Windows Inside Preview, and I didn’t have time to download its respective symbols.
Repeating the same procedure on a daily Windows 11 we have:
0: kd> dd nt!PspCreateProcessNotifyRoutineCount L1
fffff800`16b5377c 00000006
0: kd> .for (r $t0=0; $t0 < 6; r $t0=$t0+1) { r $t1=poi($t0 * 8 + nt!PspCreateProcessNotifyRoutine); .if ($t1
== 0) { .continue }; r $t1 = $t1 & 0xFFFFFFFFFFFFFFF0; dps $t1+8 L1;}
Another way to get the same result would be executing the following sequence of commands:
69 | P a g e
https://exploitreversing.com
As you can see, first I got the number of callback functions then I made a simple loop to retrieve the
response. Certainly, readers might ask the reason I am using PspCreateProcessNotifyRoutine (with an
extra “p” in t e name) and not PsCreateProcessNotifyRoutine (the name of the function responsible for
registering callback routines). It happens that PspCreateProcessNotifyRoutine (wit an extra “p” in t e
name) is an array the stores up to 64 callback routines.
If readers want to repeat the procedure using wdbgark, so I suggest the following commands:
▪ !load C:\Users\Administrator\Desktop\remote\wdbgark.dll (example)
▪ !wdbgark.help
▪ !wa_systemcb
The output is extensive, so I will not include it here, but readers will like it because it is very complete.
Finally, if you want to test, you can use Volatility to retrieve callbacks from Windows. To install Volatility 3
on Linux (my environment is an Ubuntu 22.10), execute the following steps:
▪ git clone https://github.com/volatilityfoundation/volatility3.git
▪ pip install -r volatility3/requirements.txt
▪ wget https://downloads.volatilityfoundation.org/volatility3/symbols/windows.zip
▪ mv windows.zip volatility3/volatility3/symbols/
Acquire the target tem’ memor b uing one of available:
▪ Surge (commercial tool): https://www.volexity.com/products-overview/surge/
▪ WinPmem: https://github.com/Velocidex/WinPmem/releases
▪ Magnet RAM Capture: https://www.magnetforensics.com/resources/magnet-ram-capture/
▪ Belkasoft RAM Capturer: https://belkasoft.com/ram-capturer
▪ Magnet DumpIt for Windows: https://www.magnetforensics.com/resources/magnet-dumpit-for-
windows/
You can list all enabled callbacks. As the output is long, so I used grep command to filter only one callback
type and I also run the command on another Windows 11 with 4 GB (and not 64 GB) to speed up the test:
70 | P a g e
https://exploitreversing.com
As I already described, programming and handling kernel events is a different approach and, as expected,
the nature of these mechanisms is also different, starting by the memory organization, where the heap is
referred by kernel pools, and these ones are presented with distinct characteristics. Actually, in recent
versions of Windows 10 and 11, the kernel is using the Segment Heap instead of being’ using the old pool
scheme, but concepts are the same. Check for the following structures:
a. _EX_POOL_HEAP_MANAGER_STATE:
https://www.vergiliusproject.com/kernels/x64/Windows%2011/22H2%20(2022%20Update)/_EX_P
OOL_HEAP_MANAGER_STATE
b. _EX_HEAP_POOL_NODE:
https://www.vergiliusproject.com/kernels/x64/Windows%2011/22H2%20(2022%20Update)/_EX_H
EAP_POOL_NODE).
The heap can be NonPagedEx (non-paged and non executable), NonPaged (non-paged), Paged, Session
and Special, although we will be using the first three types here. The non-paged heap (or pool) refers to
memory pages that can not be sent (paged out) to the disk and, of course, in the case of paged heap (or
pool) such memory pages can be sent to the disk. Modern mechanisms as Segment Heap also bring other
different concepts in terms of its organization like Low Fragmentation Heap (used for allocations lower
than 512 bytes, and now an allocation there i completel randomied in term of location’ addre),
Variable Size (for allocations between 512 bytes and 128 KB), Backend (for allocations between 128 KB
and 512 KB) and, finally, Large Block (for allocations greater than 512 KB).
Unfortunately (for researchers), many protections have been introduced or improved, and the main
protections are Kernel Mode Code Signing (KMCS), which is enforced by ci.dll and that demands that any
loaded driver to be signed, kASRL (kernel address space randomization), Hypervisor Code Integrity
(HVCI), which is VBS-based and protects the kernel against exploitation by preventing executable and
writable (W^X) privileges at same time for a page allocation on the kernel, so preventing any malware and
shellcode execution there. Additionally, any allocation must come from a signed driver and helped by the
Secure Kernel (running on VTL 1). Exploiting kernel driver’ vulnerabilitie have become harder in the lat
years. No doubt, this topic is incredibly attractive and could fill up dozens of pages, but these introductory
paragraphs are enough for us, and I recommend readers search for details on books, articles and MSDN
pages from Microsoft.
Returning to kernel drivers themselves, it could be quite complicated to know the starting point to initiate
an analysis because most drivers have dozens or hundreds of routines to examine and, of course, having
reference points are useful. Eventually an exception to this rule are malicious drivers, which might be large,
but usually are not, and sometimes it could make tasks simpler.
No doubt, all concepts I have mentioned along of this article are essential as well as all referred routines
that, almost certainly, readers will find when opening it on IDA Pro. For example, DriverEntry( ) is the first
and obvious choice because it works as a routine to invoke other important routines under certain
conditions. However, I want to comment about other aspects of the subject that will be useful for you.
71 | P a g e
https://exploitreversing.com
As we learned, applications submit requests to other drivers by calling routines like DeviceIoControl using
device I/O controls (which are also known as IOCTL), which forces the I/O Manager to create and submit
an IRP. At the same way, even other drivers can submit requests to the target driver by using well-known
functions such as IoCallDriver (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-iocalldriver) and IoBuildDeviceIoControlRequest
(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-
iobuilddeviceiocontrolrequest), whose macro and routine are associated with the
IRP_MJ_INTERNAL_DEVICE_CONTROL major code. As drivers has a device object by the IoCreateDevice
routine (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice),
and a link for such device object and the respective device name are given by a symbolic link created by
the IoCreateSymbolicLink routine (https://learn.microsoft.com/en-us/windows-
hardware/drivers/ddi/wdm/nf-wdm-iocreatesymboliclink).
Probably readers already noticed that, at this point, the next most important piece of code is the
initialization of the dispatch routines and, in special, the array of the function pointers that is contained by
MajorFunction member field that makes part of the _DRIVER_OBJECT structure. As expected, there are
multiple dispatch routines and, sometimes, it is hard to examine all of them, so maybe a good approach
would be starting by the most used one such as DispatchRead (IRP_MJ_READ code),
DispatchWrite(IRP_MJ_WRITE code), DispatchCreate (IRP_MJ_CREATE code) and DeviceIoControl|
IoBuildDeviceIoControlRequest (IRP_MJ_DEVICE_CONTROL | IRP_MJ_INTERNAL_DEVICE_CONTROL
codes) routines. This last one is a consequence of calling DeviceIoControl |
IoBuildDeviceIoControlRequest | IoCallDriver routines (mentioned above), and it is responsible for
sending a control code (IOCTL) to a target driver. Thus, it becomes the most important for us because it
shows the meage’ flow between application and driver, or even between the current driver and other
supportive ones. While there is a list of I/O control codes defined in the SDK header files, most of these
IOCTL codes are private and defined by drivers, and it might turn analysis a bit harder. No doubt, learning
about these I/O control codes through an eventual reverse engineering task is really useful for getting a
better understanding of the kernel driver.
If readers need to a list of standard and well-known I/O control codes, so eventually some of them are
available on Internet: http://www.ioctls.net/
So far we have the following key points to be regarded at first moment of a driver analysis:
▪ Finding the DriverEntry routine.
▪ Take an initial note about key routines being invoked from DriverEntry routine as callback routines
for reading, writing and sending control codes to a device driver.
▪ Searching for the symbolic link associated with the device object.
▪ Finding the device name (DeviceName).
▪ Analyzing I/O control codes, device object and buffers used by routines such as DeviceIoControl
and IoBuildDeviceIoControlRequest.
Sure, these items are only a starting point. If readers are wondering how the IOCTL codes, which are used
with IRP_MJ_DEVICE_CONTROL requests (created by invoking DeviceIoControl( ) for communication
between user-mode application and kernel driver) or IRP_MJ_INTERNAL_DEVICE_CONTROL requests
(created by invoking IoBuildDeviceIoControlRequest for communication between two kernel drivers),
there is a macro as shown below:
72 | P a g e
https://exploitreversing.com
[Figure 76] Listing malicious drivers from Malware Bazaar using Malwoverview (truncated output)
The next step is to open it on IDA Pro and observe a few facts.
73 | P a g e
https://exploitreversing.com
After launching IDA Pro and before jumping to DriverEntry routine, do not forget few basic steps:
▪ Force decompilation of the entire driver by going to File → Produce file → Create C File.
▪ Go to Edit → Plugins → Hex-Rays Decompiler → Options and change Default radix value to 16.
▪ As we are handling an x64 driver, open Type Libraries View (SHIFT+F11) and add (INSERT key) two
libraries: ntddk64_win10 and netapi64_win10.
▪ Open the Signatures View (SHIFT+F5) and check whether the following signatures are present:
ms64wdk, v64seh and vc64ucrt. If they are not, add them.
▪ Type CTRL+E to go to the Entry Point (DriverEntry).
Likely readers will find common structures and routines that we have commented on in this article and,
hopefully, it will not be hard. Actually, there are references that are familiar for us:
▪ DriverEntry: driver’ entr point.
▪ DriverObject: a variable of type DRIVER_OBJECT, which represents the image of a loaded driver.
▪ DriverUnload: routine used to unload the driver.
However, there are two routine that we don’t comment about yet:
▪ RtlCopyUnicodeString: as you already realized, this routine copies a string to a destination buffer.
Remember that Rtl means Real Time Library.
▪ WdfVersionBind: this routine binds the driver to a specific WDF library version.
I could find definition of this function (and also WdfVersionUnbind) on
https://github.com/microsoft/Windows-Driver-
Frameworks/blob/main/src/framework/shared/inc/private/common/fxldr.h , which have the following
prototypes:
NTSTATUS
WdfVersionBind(
__in PDRIVER_OBJECT DriverObject,
__in PUNICODE_STRING RegistryPath,
__inout PWDF_BIND_INFO BindInfo,
__out PWDF_COMPONENT_GLOBALS* ComponentGlobals
);
NTSTATUS
WdfVersionUnbind(
__in PUNICODE_STRING RegistryPath,
__in PWDF_BIND_INFO BindInfo,
__in PWDF_COMPONENT_GLOBALS ComponentGlobals
);
Reader alread noticed that there are two tpe that we don’t do not know anthing about uch a
PWDF_BIND_INFO and PWDF_COMPONENT_GLOBALS. Usually, I have used two approaches find this
information:
▪ Cloning the repository (git clone https://github.com/microsoft/Windows-Driver-Framework) and
search recursively for the structures by using: findstr /S <string> *.
75 | P a g e
https://exploitreversing.com
[Figure 78] Local types being declared and added into idb
Multiple entries will be created separately in the Local Types View, so right-click all of them and choose
Synchronize to idb option.
[Figure 79] Local types being declared and added into idb
There will not be an amazing effect in the code for this specific case, but this procedure is still valuable to
explain to readers how to proceed in similar cases. Anyway, by going to sub_140003C20 →
sub_14000395C readers will easily identify the device name associated with the driver:
76 | P a g e
https://exploitreversing.com
Moving into sub_140005284 routine (not shown in the last image, but only three instructions below), we
will find the following content:
The WFP (Windows Filtering Platform) is composed by the following large components:
▪ Filter Engine: the component is responsible for performing the filtering task, calling callouts based
on the classification and, at end, allow or not a determined traffic.
▪ Base Filtering Engine: this component is a macro component in the WFP, and it ties filters, reports,
statistics, security model and configuration together.
▪ Shims: this component represents kernel mode components that actually make the filtering
decision based on the classification.
▪ Callout: this component, as we learned so far, is a function that effectively permit, block, modify
and even reinject a network traffic. As expected, they must be registered to WFP layers.
In few words, we can directly or indirectly interact with multiple components and subcomponent of the
WFP such as:
▪ Filters: they are involved in the classification then they can be interpreted as rules to accept or
block network traffic. Filters are organized within sublayers, and the order is given by the weight,
which is similar to altitude for minifilter drivers.
▪ Layers: the work a the filter’ organiation inide the filter engine, and cannot be removed.
▪ Sublayers: they make part of layers, and generally handle exceptions in rules or a particular
scenario. They can be added or removed, and there is a set of sublayers that are inherited by
layers.
▪ Callout: they are a set of functions actively involved in the classification process as permitting or
blocking network data. Callouts can be added or removed.
▪ Shims: it is the kernel-mode component that is responsible for making classifying decisions on
filters of a specific layer. In other words, the shim component starts the classification, which is
composed by applying the filters to, at the end, decide if a network traffic should be blocked or
allowed.
The sequence of components involved in the processing is network packet → network stack → shim →
filters (from a layer) → callouts → shims (actually performing and following the filtering decision).
Decisions can be simplified as permitting (FWPM_ACTION0.type = permit) or blocking
(FWPM_ACTION0.type = block), but there are few nuances:
79 | P a g e
https://exploitreversing.com
▪ the fourth member (notifyFn) is a pointer to a function that will be called when any filter
using this callout is added or deleted, as well associated events with callout happen.
▪ the fifth parameter (flowDeleteFn) holds a pointer to a function that will be invoked
when the data flow being processed by the callout is finished.
The sub_140004FB8 is the most important routine so far:
80 | P a g e
https://exploitreversing.com
As highlighted in the code, there are three key subroutines being called:
▪ FwpmCalloutAdd0: this routine is responsible for adding a new callout to the system and its
prototype is DWORD FwpmCalloutAdd0([in] HANDLE engineHandle, const FWPM_CALLOUT0
*callout, PSECURITY_DESCRIPTOR sd,[out, optional] UINT32 *id). The first parameter is a handle
to the open session to the filter engine, the second parameter is a pointer to the callout object
(FWPM_CALLOUT0 structure) and the last parameter represents the output, which is a runtime
identifier.
▪ FwpmSubLayerAdd0: this routine adds a sublayer to the system, and its prototype is given is
DWORD FwpmSubLayerAdd0([in] HANDLE engineHandle, [in] const FWPM_SUBLAYER0
*subLayer, [in, optional] PSECURITY_DESCRIPTOR sd). The second argument represents the
sublayer to be added.
▪ FwpmFilterAdd0: this routine adds a new filter object to the system, and its prototype is DWORD
FwpmFilterAdd0([in] HANDLEengineHandle, [in] const FWPM_FILTER0 *filter, [in, optional]
PSECURITY_DESCRIPTOR sd, [out, optional] UINT64 *id), whose second parameter is a pointer to
the filter object to be added and the fourth parameter, similar to the FwpmCalloutAdd0,
represents the output as a runtime identifier.
Line 7 from the last figure has a reference to xmmword_140007680. Actually, if we follow this data
reference, we will ee a big hexadecimal number. Preing “U hotkey” (or even “A hotkey”), we will ee a
Unicode string, but without an appropriate representation (actually, it is not necessary to press U or A hot
keys, and I show it to prove that is a Unicode string). Selecting all lines containing characters and going to
Edit → Strings → Unicode, and the “redirectCalloutV4” tring will pop up. There are other Unicode strings
being used by the pseudo code within this routine, so readers can repeat the same approach for them.
After handling strings and renaming variables, we have the following pseudo code:
81 | P a g e
https://exploitreversing.com
82 | P a g e
https://exploitreversing.com
83 | P a g e
https://exploitreversing.com
▪ On the IDA Pro command line, run this macro proving the address of the start of the GUID:
Guid(0x00000001400084F8) == {AE1E820A-C60A-42A8-B4A2-9ACFB050387F}.
▪ The weight of the sublayer is 0xFFFF (line 29), which means that it is the first to be invoked.
▪ The number of filter conditions (numFilterConditions) is zero. Thus, there is not any established
condition to invoke the filter.
▪ The dipla’ name of the filter is redirectFilterV4 and its respective description is “IPv4 fil er for
re irec ” (lines 42 and 45).
▪ The filter’ action tpe i FWP_ACTION_CALLOUT_TERMINATING, which basically forces
invoking a callout that always returns block or permit. To show this string representation, I
searched for a macro (M hotkey).
▪ The FWPM_FILTER0_.weight.type equal to FWP_UINT64 (line 48) means that the Base Filtering
Engine will use the provided value as weight, which is 0xFFFFFFFFFFFFFFFF (lines 37 and 50).
▪ On line 53, calloutKey is the GUID for a callout that is valid in the layer (line 16) and layerKey
(line 64) holds the GUID which the filter is hosted, and it matches against the line 17.
▪ On line 55, finally the code adds a filter object into the system by calling FwpmFilterAdd0
routine, which used the filter object constructed in previous lines.
Readers already noticed that WFP is basically a set of hooks inside the network stack and also filtering
engine, which allow us interacting, monitoring and eventually controlling the network data information. By
the way if you are wondering about the meaning of FWPM, it is Filtering Windows Platform Management,
which is an appropriate name for the framework. Therefore, apparently the malware is adding a new
sublayer, filter and associated callout to handle the IPv4 communication that, in this case, it is working as
an IPv4 redirector to another IP address, but it early to conclusions. We alo have mentioned an “arbitrar
GUID” and there i nothing new here becaue a a callout i a common kernel driver, any GUID can be
generated by Visual Studio and likel the malware’ author did it.
84 | P a g e
https://exploitreversing.com
On purpose I quickly commented about the the sub_140004F2C routine (Figure 82), but we must
remember that is this routine which is responsible for registering the callout with the filter engine.
Additionally, its members like classifyFn (points to a function that will be called whenever there is data to
be processed) and notifyFn (points to a function that is called whenever data flow that is being processed
is terminated) from the FWPS_CALLOUT1_ structure are relevant.
The classifyFn is actually a callout of the callout, and its prototype is given the following:
85 | P a g e
https://exploitreversing.com
86 | P a g e
https://exploitreversing.com
88 | P a g e
https://exploitreversing.com
92 | P a g e
https://exploitreversing.com
The WSK_CLIENT_NPI structure is used when Network Programming Interface (NPI) is being
implemented. In a few words, NPI defines an interface between network modules, which implements a
function in the network stack, which can be attached and integrated one with other. Thus, the
WSK_CLIENT_NPI structure is described and defined as shown below:
WskRegister routine registers a WSK application that is provided and implemented by WSK application
(WskClientNpi) and a pointer to a memory location identifying the registration instance of the WSK
Application (WskRegistration), which is actually initialized by WskRegister routine as the result from its
processing. Once the return is success then the WskCaptureProviderNPI routine, which is running at IRQL
<= DISPATCH LEVEL in this case because its second argument is 0xFFFFFFFF (WSK_INFINITE_WAIT), is
invoked and it captures a provider NPI when it becomes available. The first parameter (WskRegistration)
has been initialized by WskRegister routine and the third parameter contains a pointer to the WSK
provider dispatch table, which provides callbacks that the WSK application will be able to call.
Return to the sub_14000395C routine, it is time to quickly examine the sub_140004A10 routine, as shown
below:
94 | P a g e
https://exploitreversing.com
95 | P a g e
https://exploitreversing.com
The code starts calling KeEnterCriticalRegion routine on line 05, which disables the execution of normal
kernel APCs. This is a usual action when is expected that the threat performs an I/O operation. The kernel
APCs will only be re-enabled again when the code call KeLeaveCriticalRegion( ) on line 34.
On line 06, the KeSetBasePriorityThread routine is called to set the run-time priority of the current threat
by adding 5 to the base priority of the process holding the thread.
From this point at the code, the number of functions explodes, and there are too many to analyze in this
article, so I will offer only a few insights and readers can investigate by themselves if it is necessary.
The routine sub_140005678, which is called five times using different arguments, has as its main content
non-paged pool allocation using ExAllocatePoolWithTag routine (go to sub_140005678 →
sub_1400044FC). The tag used by ExAllocatePoolWithTag routine is “TLXE”. Of course, we already know
that this routine has been deprecated and replaced by ExAllocatePool2( ), but malware’ author continue
using it. Additionally, sub_140005678 routine receive a function’ pointer a firt argument, and a
mentioned, it is provided one different function by each call.
The sub_1400069A4 routine (sub_140003BF0 → sub_140004A7C → sub_140004B5C → sub_1400069A4)
has intereting function’ invocation as shown below:
96 | P a g e
https://exploitreversing.com
We should do an analysis in reverse order to get an overview of the code. The sub_140006B2C routine
(Figure 107) is being called with ProcessID == 4 (check line 9 in sub_140006B74 routine), which know that
is the System process. Inside sub_140006B2C routine, this processes are searched by
PsLookProcessByProcessId function, and a handle to the EPROCESS structure of the provided process is
returned. Using this handle, the PsGetProcessImageFileName function is called, and a pointer to the image
file (executable file) backing up the process in the disk is returned. Finally, the ObDereferenceObject
function is called to decrease the reference count to the EPROCESS structure and, at end of the routine,
the same pointer to the image file is returned to sub_140006B74 routine.
Returning to sub_140006B74 routine, there is a while(true) condition parsing each process until a
provided PID limit (0x10000) and searching for the first occurrence of the tring “explorer.exe”. Once it i
found, it returned through by invoking PsLookProcessByProcessId function the pointer to its respective
EPROCESS structure.
Now going up to sub_1400069A4 routine (Figure 105), which is the caller of sub_140006B74 routine, we
know that ObOpenObjectByPointer function opens an object referenced by the returned pointer from
sub_140006B74 routine, and returns a pointer to the object. In other words, it is returning a pointer to the
process represented by the EPROCESS structure that, in this case, it is the explorer.exe. Pay attention to
line 20, which confirms our interpretation that it is a pointer to a process because the fifth parameter
(ObjectType) is exactly PsProcessType, and the AccessMode given by the sixth parameter is KernelMode
(zero).
Having thi proce’ handle, it is opened by ZwOpenProcessTokenEx function, which returns the
respective TokenHandle into its fifth parameter. On the next line ExAllocatePoolWithTag is called to
allocating a PagedPool (so its content can be paged out) with the tag “WENE” and size 0x1000 bytes, and
the validity of this allocated pool is checked by invoking MmIsAddressValid function (although Microsoft
doen’t recommend using this function).
On line 41, the NtQueryInformationToken is invoked to retrieve information about the provided access
token (first parameter: TokenHandle), with second parameter equal to TokenUser which is a
TOKEN_INFORMATION_CLASS value that determines that the allocated buffer receives a TOKEN_USER
structure with the user account of the token , the third parameter is a pointer to the allocated paged pool,
the fourth parameter indicating the size of the TokenInformationBuffer (0x1000) and finally the last
parameter (ReturnLength) as being the length of the returned information.
At the end, the SID_AND_ATTRIBUTES structure, which is the only member of TOKEN_USER structure and
represents the user related to the access token, is used as argument of RtlConvertSidToUnicodeString
function (line 53) to convert it to a Unicode string representation of the SID. In other words, we have the
SID of the account associated with the explorer.exe process, which is returned within a UNICODE_STRING
structure:
99 | P a g e
https://exploitreversing.com
Analyzing drivers demands a good effort because they can contain multiple routines and, as expected, it
demands time. No doubts, when analyzing a system driver on Windows we have the offered public symbol
b Microoft and the function’ name are alread provided. The goal here is not analyze a driver, but only
interact with the first routines to show that everything we learned so far in this article is present and
readers can move forward by themselves without any serious issues.
I picked up the srv2.sys driver, which is the Smb2.0 Server driver (a network driver), which has been
updated very often in the last months, and a few of them due to security issues. Opening it on IDA Pro and
making a complete decompilation (File → Produce File → Create C File), the routine shown as entry point
will be GsDriverEntry, which is automatically generated when the driver was compiled and initialize the
security cookie, calls the DriverEntry at its end:
100 | P a g e
https://exploitreversing.com
101 | P a g e
https://exploitreversing.com
▪ Macros (M hotkey) have been applied to IoCreateDevice routine and also to major functions from
lines 65 to 69.
▪ A device object (network device) has been created by IoCreateDevice routine, and its name is
\Device\Srv2.
▪ The IoGetCurrentProcess function is called, and it returns a pointer to the current process.
▪ The DriverObject’s dispatc table contains pointers to four dispatch routines: cleanup
(Srv2Cleanup), close (Srv2Close), create (Srv2Create) and device control (Srv2DeviceControl).
▪ As usual and recommended, there is a DriverUnload routine to unload the driver.
We could examine the drivers and, as usual, the DispatchDeviceControl dispatch routine
(Srv2DeviceControl) is always a good starting point. I will not do it here because it is not the purpose of the
article analyze any kernel or filesystem driver in particular, but helping readers to learn about them and
respective techniques involved in the procedure.
Unfortunately, when reversing drivers that we do not have their symbols in hands, the task is harder and,
as a consequence, it might take an extended time to be finished. Readers can pick up any non-Microsoft
driver from their system during this example exercise. There are multiple applications to list drivers and
respective details from a running system, and readers could use applications such as driverquery (from
Windows: https://learn.microsoft.com/en-us/windows-server/administration/windows-
commands/driverquery) and DriverView (from Nirsoft: https://www.nirsoft.net/utils/driverview.html) that
are very simple. In my case I picked up the veracrypt.sys driver just to show the meaningful difference
between both examples (with and without debugging symbols):
102 | P a g e
https://exploitreversing.com
103 | P a g e
https://exploitreversing.com
104 | P a g e
https://exploitreversing.com
To avoid extending this article, I will be using an IDA Pro plugin named DriverBuddyReloaded
(https://github.com/VoidSec/DriverBuddyReloaded) to decode the IOCTL:
105 | P a g e
https://exploitreversing.com
106 | P a g e
https://exploitreversing.com
107 | P a g e
https://exploitreversing.com
108 | P a g e
https://exploitreversing.com
There are excellent cyber security researchers and companies keeping blogs and writing really good articles
about operating system internals, reverse engineering, vulnerability research and exploit development. A
list of interesting websites and respective Twitter handles, in alphabetical order, follows below:
11. Conclusion
This article, as I said at its beginning, is really an introduction to a complex topic that are kernel drivers and
minifilter drivers. The objective is to help professionals to get a minimal knowledge about involved
concepts, and provide the necessary foundation for the next articles.
Nowadays I have been working in a different area today (reversing + exploit development), but I always like
to remember closer researchers that each person has a unique perspective of the information ecurit’
world, and none of them are wrong. Follow your heart. :)
Just in case you want to stay connected:
▪ Twitter: @ale_sp_brazil
▪ Blog: https://exploitreversing.com
Keep reversing and I see you at next time!
Alexandre Borges
109 | P a g e