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_140001780
andFUN_140001270
because 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_1400067d8
is a global reference to the driver file.0x222000
is theioctl
code.local_118
input buffer.lVar6
length of input buffer.local_128
output buffer.1
max length of output buffer.local_124
length 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_OBJECT
in 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 thatBitVec
of 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:
|
|