If you haven’t read the previous post you can find it here: Part 2 – Analyzing init functions. In this post, we will finally analyze the Dispatcher function. Remember that we identified this function in the first post. This function has the main goal of processing any I/O request packet (IRPs), and in this case, to handle any request from the major function code IRP_MJ_WRITE.
What we will go through?
- Learn how Dispatcher routines are implemented.
- To reverse the parsing method that handles the IRP_MJ_WRITE requests.
- Identify custom structures used by the Driver and create new Local Types on IDA.
- Understand how the Driver dispatches the different hard-coded handles by traversing an array with custom structures.
When reversing Windows related things, reading the MS documentation should be mandatory. They usually provide a lot of useful information that will save you a lot of time, such as structures they use, parameters and code examples. Something that I like to do, is to browse through the examples available on their repositories and look for similar pieces of code. Driver developers usually reuse a lot of code from those examples. Looking for similar patterns in our binary could provide us a lot of information about the context and the functions that are being called inside of a similar snippet of code. At the end of this post, you will find a list of useful links
First, take a few minutes to analyze the initial decompiled code here and the assembly. A driver can provide multiple dispatch routines, in this case, they implemented only one major function code, as we saw before.
If you want to cheat a little bit, here is the final code.
We can see that it receives two parameters, but do we know what those parameters are? Yes, MS documentation explains that the IRP_MJ_WRITE dispatcher functions receive as first parameter a PDEVICE_OBJECT and as second a pointer to an IRP structure (IDA will identify this one by its own).
On line 16 the driver controls that the length of the input buffer is equal to 0x270, it seems that it is the only possible length.
Be careful when analyzing the following line:
As you can see below the MasterIrp shares the same offset with the IrpCount and the SystemBuffer inside of a union (struct _IRP):
In this case the Dispatcher is trying to retrieve the SystemBuffer from the IRP request; it is not making reference to the MasterIrp pointer. We need to properly fix that. In IDA PRO this can be done easily by doing “Right Click->Select union field”:
Now that we have identified where the input buffer is being stored, let’s see how it is parsed:
We can see that the first DWORD is expected to be equal to 0x270 (same value of the length); Then, the next DWORD needs to have some sort of magic value that matches 0x345821AB. If both requirements are fulfilled, the function sub_140001E00 is called sending the buffer in the first parameter, and a reference to a yet unknown structure on the second parameter. I decided to call this function fn_DispatchIOCTLMethod and we are going to analyze it in the next section.
After some renaming to the variables, we can conclude that the structure of the input buffer would be something like this:
Since we are not analyzing all the dispatch methods yet but I want to provide you a full analysis of the input buffer, I will show you the complete structure the Driver is using. The final structure would be like this:
This function does a few more things, but let’s ignore them for now.
This function is small, and we will see that after setting the types properly and doing some renaming, everything becomes much clearer.
The first thing we would need to do is to add the previously defined structure DrvInputBuffer to IDA Pro. On the “Local Types” sub-view, it is possible to do “Right Click->Insert”, there you can copy-paste the structure definition:
Next step, rename the first parameter to be a pointer to that structure by doing “Right Click on the variable -> Convert to struct *”.
If we hide the casts, we will get something like this:
If you have read the previous posts, you may recognize dword_14000A240. We renamed this variable on the second post, while we were analyzing fn_InitDispatchMethodArray: this was the variable FunctionsCount.
The same happens with dword_140009E40, which was renamed to IOCTLFunctionArray on the same post, and it is an array containing multiple DispatcherStruct structures:
Based on that, we can identify the following behavior: First, the application validates that the IOCTLFunctionArray has been initialized by comparing FunctionsCount with NULL. Then, it iterates on a while loop, comparing the Index value of each element with the DWORD at the offset 0xC of the input buffer (defined on the structure as FnIndex). They increase the counter until it reaches the maximum value stored at FunctionsCount. If there is a match between the indexes, the function stored at DispatcherStruct->FnPtr is call; sending the same two parameters of fn_DispatchIOCTLMethod: the SystemBuffer and the reference to a yet unknown structure.
This would be the final function:
- Analyzing NotifyRoutines (fn_InitRegistrationNotifyAndCallbackRoutines and fn_RegisterCreateProcessNotifyRoutine)
- Load kernel structures with .h files and local types