This is a guest entry written by Arnaud Gatignol and Julien Staszewski from the THALIUM team. The views and opinions expressed in this blog post are solely those of the authors and do not necessarily reflect the views or opinions of Hex-Rays. Any technical or maintenance issues regarding the code herein should be directed to the authors.
Helping KMDF driver analysis with an IDA plugin
Drivers are software components that serve as intermediaries between hardware devices and the operating system (OS) on a computer. They play a crucial role in the hardware/software interactions. There are also drivers that only serve software architecture purposes. In every case, they need to abstract interfaces to provide a generic way to communicate with the computer low-level layers.
On Windows, Microsoft has provided, over the time, multiple models and frameworks to implement drivers. The two most famous are:
- Windows Driver Model (WDM)
- Windows Driver Framework (WDF)
The introduction of WDF, and more specifically, KMDF (Kernel Mode Driver Framework) has added some complexity to the reverse engineering process.
Here comes the plugin we developed in order to facilitate the analysis of KMDF drivers within IDA by applying specific types and be as transparent as possible for the end user.
Before
After
With our plugin installed, these changes occur without a single click.
In this blog post, we will introduce some concepts regarding KMDF and its impact on analysis. Then, we will introduce the plugin and what it can do to help you. Finally, some insights on the plugin implementation will be given.
If you are already familiar with basic KMDF concepts, we do invite you to jump to the use case section. In the other case and if you want to go deeper, the Microsoft documentation might be helpful.
What's KMDF ?
The Kernel-Mode Driver Framework aims to simplify and streamline the process of developing kernel-mode device drivers for the Windows operating system. KMDF offers a higher-level abstraction and a set of libraries that help driver developers create reliable and efficient drivers with reduced complexity, allowing developers to focus on device-specific functionality rather than dealing with complex kernel and hardware intricacies.
KMDF introduces many new types and functions and makes the preliminary analysis steps completely different from a WDM driver analysis.
Going through the example of looking for the MajorFunction table, and more specifically for the IOCTL dispatcher function, we will see that KMDF-based drivers analysis can be slow at first.
KMDF: Close Encounters of the Third Kind
The first steps when analyzing a Windows driver might include looking for what it exposes. The most known driver interface is certainly IRP_MJ_DEVICE_CONTROL
which gives IOCTL support. This MajorFunction
handler is registered in a table in the DriverEntry
of the driver.
Two ways to identify the IOCTL dispatcher are:
- Analyze the
DriverEntry
or the sub-function that registers theMajorFunction
. - Dump dynamically the
nt!_DRIVER_OBJECT.MajorFunction
related to the given driver.
Obviously, sometimes looking at the debug symbol is enough.
For a WDM driver, the output of these two methods is given below.
The registration of the IOCTL dispatcher (14
) is clearly and quickly identified (srv2!Srv2DeviceControl
).
The dump of nt!_DRIVER_OBJECT.MajorFunction
is also very straightforward:
However, it is not this simple for KMDF drivers, and these two methods do not yield significant results.
In fact, the driver entry does not expose the MajorFunction
registration, as KMDF uses a different method to set these function pointers.
Indeed, as the KMDF types are not being used, the EvtIoDeviceControl
callback (which handles the I/O control requests in a KMDF driver) cannot be easily found. Moreover, the driver object is full of KMDF stubs (Wdf01000!FxDevice::DispatchWithLock
) that prevent us from clearly identifying driver-specific code.
From this, it appears that we need to develop a tool that is be able to quickly reverse engineer a KMDF driver. This tool should aid in identifying standard KMDF structures and functions in order to achieve the same ease of reverse engineering as WDM drivers.
Therefore, the tool is expected to be able to apply and propagate KMDF types in a transparent way with TIL files. TIL stands for Type Information Library
, it is a file format containing type descriptions that can be used in IDA. Hex-Rays has published two related Igor
tips
blog posts.
Use case: apply KMDF types to an IDB
Before applying KMDF types, we need to add them to our IDA base. To add new types in an IDB, you need to import a type library (TIL files).
Thus, our tool has three different usages:
- TIL file generation: generate new TIL files containing SDK/WDK/KMDF types
- TIL files application: apply the new types to an already existing collection of IDB
- IDA plugin: apply transparently your types to the IDB you've just opened
The two first usages are discussed below.
TIL file generation
The generation of the TIL files has been made very simple:
python wdfalyzer.py make_til \
--wdf="C:\Users\admin\Desktop\Windows-Driver-Frameworks" \
--wdk="C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\"
--til="C:\Users\admin\AppData\Roaming\Hex-Rays\IDA Pro\til"
The wdfalyzer
python script is the main interface to this tool:
make_til
: command to generate a TIL file--wdf
: the path to the Microsoft WDF--wdk
: the path to the WDK--til
: the path to your TIL folder
As an output you should have two TIL files (x86
and x86_64
) per WDF version:
Created 32 bit til for version 1.15
Created 64 bit til for version 1.15
Created 32 bit til for version 1.17
Created 64 bit til for version 1.17
Created 32 bit til for version 1.19
Created 64 bit til for version 1.19
Created 32 bit til for version 1.21
Created 64 bit til for version 1.21
Created 32 bit til for version 1.23
Created 64 bit til for version 1.23
Created 32 bit til for version 1.25
Created 64 bit til for version 1.25
Created 32 bit til for version 1.27
Created 64 bit til for version 1.27
Created 32 bit til for version 1.31
Created 64 bit til for version 1.31
Created 32 bit til for version 1.33
Created 64 bit til for version 1.33
TIL files application
Now that the TIL files have been generated, let's apply them to the IDB:
python wdfalyzer.py analyze \ "C:\Users\admin\Desktop\acpiex.sys.i64"
The output is straightforward:
* idat64: C:\Program Files\IDA Pro 8.3\idat64.exe
* log: C:\Users\admin\AppData\Local\Temp\acpiex_xwe83q0_.log
* running: "C:\Program Files\IDA Pro 8.3\idat64.exe" -A
-L"C:\Users\admin\AppData\Local\Temp\acpiex_xwe83q0_.log" -S"\"C:\Users\admin\Desktop\ida_kmdf-main\wdf_plugin\wdf_analysis.py\"
\"--quit\" \"--prefix\" \"cpncdp\""
"C:\Users\admin\Desktop\acpiex.sys.i64"
* IDA script success
The types are applied, and we are now able to look for cross-references on WdfFunctions->pfnWdfIoQueueCreate
, which is used to register the callbacks that handle, among others, I/O control.
Go straight to the Structures view
and look for WDFFUNCTIONS.pfnWdfIoQueueCreate
. The cross-references are automatically populated by the script.
A _WDF_IO_QUEUE_CONFIG
is passed to the pfnWdfIoQueueCreate
as argument. A _WDF_IO_QUEUE_CONFIG
is a structure which contains the pointer to the I/O control dispatcher function.
typedef struct _WDF_IO_QUEUE_CONFIG {
ULONG Size;
WDF_IO_QUEUE_DISPATCH_TYPE DispatchType;
WDF_TRI_STATE PowerManaged;
BOOLEAN AllowZeroLengthRequests;
BOOLEAN DefaultQueue;
PFN_WDF_IO_QUEUE_IO_DEFAULT EvtIoDefault;
PFN_WDF_IO_QUEUE_IO_READ EvtIoRead;
PFN_WDF_IO_QUEUE_IO_WRITE EvtIoWrite;
PFN_WDF_IO_QUEUE_IO_DEVICE_CONTROL EvtIoDeviceControl; // the use case goal
PFN_WDF_IO_QUEUE_IO_INTERNAL_DEVICE_CONTROL EvtIoInternalDeviceControl;
PFN_WDF_IO_QUEUE_IO_STOP EvtIoStop;
PFN_WDF_IO_QUEUE_IO_RESUME EvtIoResume;
PFN_WDF_IO_QUEUE_IO_CANCELED_ON_QUEUE EvtIoCanceledOnQueue;
union {
struct {
ULONG NumberOfPresentedRequests;
} Parallel;
} Settings;
WDFDRIVER Driver;
} WDF_IO_QUEUE_CONFIG, *PWDF_IO_QUEUE_CONFIG;
At first, the tool did not apply the type on this argument:
But if we manually apply the type, already present in the local types thanks to the tool, and add some naming:
The IOCTL dispatcher acpiex!EvtIoDeviceControl
has been identified almost automatically. The job is now done and the analysis can now begin!
NB: this is also possible with the !wdfqueue
WinDbg command.
KMDF IDA plugin
We also implemented a plugin in order to make the previous type application transparent for your use.
Once the plugin files have been copied to %IDA_DIR%/plugins
and the TIL files to %IDA_DIR%/til
, no further action is required. The plugin will be executed on IDA startup to apply the identified types.
How does it work?
TIL files creation from headers
In order to load new types in IDA, we need a type library (TIL file).
These can be created using the tilib
utility. Tilib extracts types and structures from a C header file and gathers them in a type library for IDA.
Creating the TIL file may seem straightforward – locate the WDF headers and call tilib
on them. However, this is not quite the case as this process can be more complex than it appears. The WDF headers are numerous and contain a lot of generated code, and the dependency graph of the various headers is quite complex.
This complexity makes it very hard for tilib
(which is not a C compiler) to parse everything and generate the type library we want. Moreover, the WDF headers include a lot of types and definitions from the Windows SDK without including the proper headers, or from other WDF headers but either without including them or in a bad order.
To address this issue, we decided to hand-craft some headers and create a wrapper header including all files required to generate a complete enough WDF type library.
Even with all of that, some errors still remain, and the generated TIL files are not complete and do not contain every WDF types and structures, but we have the most important ones for reverse-engineering most of the drivers:
- The
WDF_DRIVER_GLOBALS
structure, which contains a handle to the driver with some additional information (flags, tag, name…). - The
WDFFUNCTIONS
structure, which contains pointers to all WDF functions. Applying this type at the proper address is the core of our plugin. - The
WDF_BIND_INFO
structure, which contains the version of WDF used by the driver, and pointers to the two structures above.
Type propagation
Once the TIL files are generated and placed in the %IDA_DIR%/til
directory, we need to apply the three types mentioned above to our IDA base.
The first step is to know which version of WDF is used by the driver. To do so, we use two approaches.
First, we search for the imported function WdfVersionBind
, which is called by the driver to bind itself to the kernel. Once we get the address of this function, we can apply the dummy base type int WdfVersionBind(int, int, int, int);
to it and check all calls to it to get the major and minor versions passed as arguments, along with the address of the global WDF bind info structure.
If this approach fails, we search for the UTF-16 encoded string "KmdfLibrary"
. This string is present in every WDF driver and is referenced in the bind info structure. By looking at cross-references to this string, we can identify the one in the bind info structure, and get the version by checking the next field of the structure, which contains the major and minor version, alongside the build number.
Once the version is known, we can load the proper TIL. Upon loading the TIL, IDA will automatically apply some types for us. To be sure everything is loaded properly, we apply the name BindInfo
to the bind info structure, and apply its type with the Python bindings of IDA's API.
From the bind info structure, we can apply the two other types of interest: WDF_DRIVER_GLOBALS
and WDFFUNCTIONS
.
Once these types are applied, IDA's magic will propagate the WDFFUNCTIONS
type, and all accesses to a function pointer will be translated to a human-readable form thanks to the data in the TIL.
Cross-references
Having all WDF function calls in a human-readable form is a good thing, but having all the cross-references to the function table members would be even better. Luckily, with all the types already set, this is not too hard to obtain.
Cross-references to structure members are only tracked if the structure is applied in the disassembly, not in the decompiled code, which is why merely applying a type to its address is not sufficient.
To apply a structure offset to an operand (t
in the GUI), we can use the op_stroff
function from the API. This function requires the structure type to be imported. Local types won't be sufficient.
It is important to note that when loading a TIL file, its types are loaded in the local types group, the structures are not imported to the structure view. Hence, the first step to get our cross-references is to import the WDFFUNCTIONS
structure via the API.
Once this is done, we can walk through all the cross-references to the WDF function table. These cross-references will follow the pattern mov <reg>, <wdf_func_table_ea>
. For each of those, we check inside the function for the instruction pattern mov <reg>, [<reg>+n]
with the second operand being the same register containing the function's table address.
This pattern matches a WDF function pointer being loaded into a register in order to be called. We simply apply the structure offset to the second operand with op_stroff
, and the cross-references are now added to the structure member.
- Before
- After
After walking through all the references to the function table, we get all references to every WDF function in the structure view.
Limitations
The heuristic we use when applying the structure offset is very basic and has limitations.
We are conservative. For each function cross-referencing the function table address, we stop after the first call to op_stroff
in case the register changes: we do not want to mistype something.
This does not seem to be a problem since we have observed that IDA is clever enough to propagate all cross-references as long as the register does not change anyway.
Currently, we don't handle the following case correctly when the register containing the address of the function table changes before accessing the offset:
mov rax, <wdf_func_table_ea>
mov rbx, rax
mov rbx, [rbx+<offset>]
[...]
call rbx
We would miss this cross-reference because we would be looking for the pattern mov <reg>, [rax+<offset>]
.
In other words, our plugin does not propagate the types among the codebase, which could lead to missed cross-references in some edge cases. Fortunately, those edge cases seem to be rare enough that we did not encounter one during our tests.
Anyway, being able to propagate types among the code base would be great. Perhaps some other plugins, such as Symless (also made by Thalium), could come in handy.
Conclusion
Thank you for reading this blog post. We hope you enjoyed it and that it can be useful to you.
The tools are maintained on the Thalium github repository.
Besides the code, the pre-made TIL files are included in the repository, so feel free to use them if you don't want to rebuild them yourself.
Feel free to open PR! May the green shard be with you!