Introduction
This time we are dealing with kernel drivers.
I’ll be using Ghidra and x64dbg for static and dynamic analysis for this challenge.
Note: It is advised to use kernel debugging, but I had some problems in my windows host so this solution is using static analysis and a small dynamic analysis (not kernel).
Analysis
kmum.sys is the driver and app.exe mostly will load the driver and call into it.
app.exe analysis
The main function is at FUN_140001940. And this time it looks like C++ binary, nice.
The main starts by printing some strings and taking the input from the user, then
FUN_140001070 is called.
FUN_140001070 analysis
The function starts by opening the driver file "\\\\.\kmum", if it fails
it will first get the .sys file by calling FUN_140001780 and installing
the driver from the service manager in the function FUN_140001270.
For FUN_140001270 it calls it with 0 in the 3rd argument to stop it and with 1
to start it again.
I didn’t talk much about
FUN_140001780andFUN_140001270because they are just bunch of API calls and have some logging, so they are easy to understand from the code.
Going back to main, the most important part is:
Looking at the documentation for DeviceIoControl:
DAT_1400067d8is a global reference to the driver file.0x222000is theioctlcode.local_118input buffer.lVar6length of input buffer.local_128output buffer.1max length of output buffer.local_124length returned for output.
Lastly, the output buffer should have the result of the flag check. Now let’s go into the driver and see how the flag check is made.
kmum.sys analysis
It’s good to look at simple kernel driver examples, to see the similarities and to ease analysis. here is a good simple driver we will be referencing.
Drivers does not have main but have DriverEntry which should setup the driver.
When looking at entry in Ghidra, two functions are being called FUN_14000602c
(which seems not important for analysis), and FUN_140001000, which takes the first argument from entry.
Ghidra didn’t detect the second argument from DriverEntry because it is not used.
In FUN_140001000 let’s change the parameter type to DRIVER_OBJECT.
Note: if you don’t see
DRIVER_OBJECTin Ghidra you can download data types archives from here.
Here is the function:
| |
First it creates the device using IoCreateDevice, then it sets the handler functions.
Looks like FUN_140001190 is being used as handler to all major functions except for
DriverUnload to FUN_140001260, and MajorFunction[0xe] to FUN_1400011d0.
The function for 0xe is IRP_MJ_DEVICE_CONTROL, which matches what app.exe calls. (DeviceIoControl).
| |
FUN_1400011d0 analysis
The function first calls FUN_1400012b0, which looks like IoGetCurrentIrpStackLocation.
Then it checks the ioctl code:
Ghidra didn’t manage to correctly figure out offsets because IO_STACK_LOCATION contains unions, and its hard to analyse.
So stack_io->Parameters + 0x14 is stack_io->Parameters.DeviceIoControl.IoControlCode.
If the code is good it goes to FUN_140001810 next:
FUN_140001810 analysis
Here is finally where the flag is checked.
First it checks input and output buffer sizes:
| |
Which are Parameters.DeviceIoControl.OutputBufferLength and Parameters.DeviceIoControl.InputBufferLength respectively.
First stage in checking is XORing and shifting the whole flag and checking for a result:
If that passes, there is 3 more stages to go in functions:
FUN_140001a90, FUN_1400015b0 and FUN_1400019b0.
FUN_140001a90 analysis (stage 2)
The simplified function code:
| |
We can see that local_18 is state and we should go through all checks and reach state 0x17.
These are all checks:
| |
FUN_1400015b0 analysis (stage 3)
This is very similar to FUN_140001a90 in terms of state logic, but its much simpler and straight forward,
these are the checks performed:
Which is "wgmy{}" part. so this is not very interesting.
FUN_1400019b0 analysis (stage 4)
| |
FUN_1400020e0, FUN_1400012d0, FUN_140001370, and FUN_140002010 all are complex but independant of the input, and that means
we can run debug this function and checks uVar2 at the end for every loop.
Now, this could be debugged using windbg in kernel mode, but I got into problems with my windows host, so I struggled in the beginning of how to debug this, but then I found the solution.
Debugging FUN_1400019b0
The good thing about this function and its internal functions is that it does not depend on the input or anything else in fact,
it just uses global variables to store data and do its computation. Which means we can just start execution from here by editing
the RIP (64bit Instruction Pointer) register.
When trying to start kmum.sys in x64dbg it does not work as it is a kernel driver and not a normal executable.
We can fix this issue by editing the PE header and fooling x64dbg into believing this is a normal exe.
Converting sys into exe
Looking in the PE format, there is a byte to control the subsystem that this binary represent. The driver has subsystem value of 1, and the cli executable has value of 3.
In our file here, the offset is at 0x134, so we change that and we can run the driver now.
When running the driver, first we continue until we reach the entry breakpoint and immediately change
$rip to xxxxxxxxxxxx19B0.
After that since this function expects a buffer as argument in rcx. We can allocate memory from x64dbg, put a
flag in there (example: wgmy{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}) and put the address in rcx.
Next, we continue until xxxxxxxxxxxx1A70:
| |
eax should have the correct character, for easier analysis we can patch the jump instruction after it to force
it to jump. It now jumps if they are equal. all we need is convert jz into jmp.
| |
Then we just continue execution and record all 16 characters:
| |
Solution
Now that we have all the pieces, stage 2, stage 4 and the xor thing. We can solve them using z3-solver, which is a theorem prover from Microsoft Research.
I started by grouping all rules we collected and got this:
| |
Note: the reason we used
BitVec(f'x{i}', 32)for each character instead of 8, is thatBitVecof 8 bits in size cannot be shifted and converted into 32 bit.
BUT, this gave us:
| |
And that means the constrains are not enough, so I thought of making the inside only in 0-9a-f.
This replaces:
And we got the flag:
| |