Dissecting the Infamous Capcom Driver

and taking advantage of it…hehe

Howdy peeps!

A couple years ago, a pretty sick driver came out for Street Fighter 5 that did something pretty interesting. I had no idea how all this worked at the time this was discovered and I always told myself I would come back to this once I understood this area better and here I am now!

To follow along this post, you will need the following:

  1. The driver itself, Capcom.sys
  2. Device Tree – to view the device name associated with the drivers
  3. IDA 7.0 Freeware
  4. WinDBG

The awesome thing about this driver is how small it is coming in with a whopping 8 functions! In my opinion, this would be an ideal driver to practice on (along side of Hacksys’ driver).

The Driverrrr

Before I really do anything, I always check if the driver is picked up by Device Tree to see if the device name associated with the driver is there. In this case, we are able to see the Capcom driver having a device name of:

We can start forming our PoC by grabbing a handle to this driver using CreateFile with the following parameters:

One of two things will happen: you will get a handle to it or you won’t. If you can get a handle, great! If not, cue sad trombone.

Once we run it, we have our handle!

When throwing this driver in IDA, we can see we have seven functions and the driver’s entry.

Due to the size of these functions making these picture stupid huge, you can view the DriverEntry in its entirety here.

You normally see the device names when you are reversing the driver; however, this driver doesn’t do that. What this driver does is creates the device name during run-time by deobfuscating a set of values. This makes it a tad more “difficult” because you can’t just look at it and see the device name it uses. If you really want it though, you’ll find out or figure out how it’s done by going balls deep in this mofo! Device Tree makes this mad easy because if the driver is activated, you can easily see the device name listed under the driver name. For practice, let’s do it anyways because why the eff not. We hate easy…

Let’s take a look at loc_10656:

This small function is taking some byte in an array at offset 0x774. We have absolutely no idea what is at that offset and we really can’t view it in IDA so the only way to find out is by placing a breakpoint and seeing what is being pulled out.

The first byte that we receive is 5C, which gets thrown into r11 and then our counter of 2 kicks in. This keeps happening until we reach a byte of 0.

After we reach our 0 and get past this, we have the following values: 0x5c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5c. Notice how they’re all alpha characters?

So this function brings out our \Device\ string during runtime. Sick.

After this function is finished running, we head on over to our next function:

We have our first call to sub_103AC. We aren’t sure what this call is to but what we do know is that it takes two parameters: the address of unk_10980 and our string. But wait…what the heck is unk_10980 and how would you find it if it’s not in IDA???

This question can be answered when we turn to our debugger. This will most definitely tell us what unk_10980 holds for us!

Interesting. It’s an array of some sort. This can be seen as:

Let’s dive into this function…you can view this function in its entirety here.

If we take a look at sub_103AC, there is a function that is almost the same function that pulled our string. I can only assume this will hold what we need to have the actual device name. There are various types of bitwise operations and arithmetic. It only makes sense that this function can be renamed to deobfuscateFunction. Let’s break this bad boy down and see what the h*ck is going on….

So we run into this same loop as we did before only we get the following values: 0x87, 0xea, 0xfd, 0x9a, 0x4b, 0x73, 0x54, 0xa4, 0x5c, 0x8f, 0x00. Notice how these values aren’t alphanumeric. These values are held in rdx. Once we retrieve our values, we head on over to our next block:

This section is doing the following:

  1. Setting edi to 0
  2. Placing a pointer of our array into rdx
  3. Moving 0x5555 into r9w
  4. Checking if our value is 0
  5. Jumping depending on the outcome of this condition

For now, this code block can be seen as:

If you refer back to this entire function, you’ll see this is some type of loop. While I went through it, I decided it goes as a while loop (a for loop will also work).

If it is 0, it will break; otherwise, it will continue to loc_103DD.

Here is where it starts pulling from our array. We will walk this entire function with our first byte, 0x87, so you can get the idea of what’s going on.

RDX contains our first value, 0x0087. This value gets moved into ecx. It is then shifting our key 2 bits to the left. It is making a copy of 0x87 into r10d. The key is getting the counter added to it.  Our copy is getting shifted 6 bits to the right. It’s loading the result of r10 – 1 into eax and then being compared to 2. If the result is above 2, it will break; if it isn’t, it’ll lead to our next code block. This can be seen as:

CL, or 0x87, gets xor’ed with the last byte of our key and ax gets cleared. CL is then subtracted from our counter and gets subtracted again with our modified value from the previous code block. The bitwise AND operation is performed. At this point, our value is 0x11 and no longer 0x87. It then checks if it is above or equal to 0xa. The interesting thing is it is seen in IDA as jnb, but jae in the debugger. This part can be seen as:

Now here is where the comparisons start coming in…

If our value is smaller than 0xa, we veer off into the right and add 0x30. If it isn’t, we go to loc_10414 and see if it is above or below 0x24. Again, this jump is seen as a jae rather than jnb. If it is above 0x24, it heads over to loc_10423; otherwise, we head on over to adding 0x37 to our value. What happens when we do this??? Check it:

0x48 is the ascii value of H. Sick. This is the first letter or our device name!

It will follow this flow until all the values in the array get ran through. This can be seen as:

After this insanity, we retrieve our deobfuscated values 🙂

Lastly, we put everything together and we get the following decrypting function. You can view the full code block here!

After the array gets ran through this decryption function, we get our device name! To verify this, I compiled this function as a standalone and execute it. What do we get after we run it???

H*ck to the yea!!

On to the next!

Next thing that happens is it turns this string into unicode with RtlInitUnicodeString. This function takes two parameters: the destination string and the source string. This can be seen as:

The parameters for IoCreateDevice are being loaded to fill the parameters needed by IoCreateDevice. If eax is 0, meaning it was successful, it’ll move to our next code block that you should be familiar with by now. If it isn’t successful, it will simply break and die to death. This can be seen as:

Next, we come here:

We run into our loop again only this time, we get a different array. We get \DosDevices\:

As before, it is taking the array and string into the deobfuscating function. It turns that into unicode string and then creates a symbolic link with the new string. This can be seen as:

If this fails, it will simply just delete the device and move on with life!

This can be seen as:

If it doesn’t fail, it will lead to loc_10723. What are these next functions? Let’s take a look…

LOC_10723

There are three different functions being loaded into four different offsets. SUB_10590 and sub_1047C are being loaded into some offset in some structure. Notice how sub_104E4 is being loaded twice at some offset in some structure? This is interesting. We have no idea what these offsets are, but we can easily find them if we turn to our debugger!

If we query the driver object that belongs to Capcom, we can get an idea of what these offsets are. For example, check out the following output:

With this information, we know that sub_1047C is our DriverUnload function because of it being at offset 0x68. We can rename this to DriverUnload and from this point on, that’s what I will be referring to this function as. I can assume that the other two functions are being loaded in the MajorFunction at some offset belonging in that structure. You can find exactly what functions they’re being loaded into by querying Capcom’s driver object again using drvobj with a mask of 2. Take a look at the following output:

This just confirms our DriverUnload function. We can see sub_104E4 getting loaded twice into the following values: IRP_MJ_CREATE and IRP_MJ_CLOSE. We can see sub_10590 being loaded into IRP_MJ_DEVICE_CONTROL. So loc_10723 is the section where our IRP_MJ_* functions are held. Fuck yea! This can bee seen as:

What does sub_104E4 have in store for us today??? Let’s take a peek…

SUB_104E4

So we know this function is for IRP_MJ_CREATE and IRP_MJ_CLOSE so we should be working with an IRP object. This function is doing a couple things:

  1. It’s moving some type of value from an array held in rdx at offset 0xB8 into rax.
  2. It is zeroing out ecx
  3. moving rdx into rbx

As before, it’s loading the value of 0 into offset 0x30 and 0x38 in some array. We aren’t sure what these offsets are and again…we can depend on our debugger to find out what these values are by querying the IRP object using dt. Look at the following output:

So we are working with _IO_STATUS_BLOCK. There are more options available for _IO_STATUS_BLOCK. If we dump that, we can get what is at offset 0x38.

Very nice. So offset 0x30 is the Status parameter and offset 0x38 is the Information parameter. These values are being assigned 0. This can be seen as:

As a side note, STATUS_SUCCESS is the variable name for 0!

Next up is the value held in rax is being compared to cl. Although IDA says it’s a jz jump, it is a je jump in the debugger. So if the value of rax is equal to cl, then we head on over to loc_1050D; otherwise, we go to our next comparison. Again, the jump is a je so if the value of rax is 2, then we jump on over to loc_1050D. If it isn’t the same, than we get 0xC0000002 loaded into IoStatus.Status. This value is STATUS_NOT_IMPLEMENTED.

Lastly, it makes a call to IofCompleteRequest and this takes two parameters: a pointer to the IRP and a system defined constant. We have our two values of 0 (xor eax, eax) and our IRP object (mov rcx, rbx). 0 is the numerical value for IO_NO_INCREMENT. This can be seen as:

Using what we have, we can make a “CreateClose” handler for this function since it’s using the same function for both IRP_MJ_CREATE and IRP_MJ_CLOSE. We can rename this function to CreateCloseHandler. Putting this all together, we get the following function:

Our next function to look at is sub_10590. We can rename this function to DispatchTable. We will come back to this function later for the juicy tidbits. Let’s knock out this last function…

DriverUnload

Here is our DriverUnload function. The first two code blocks we had already gone over already. Just to remind you, all this is doing is pulling out an array of obfuscated values and then running them through the deobfuscating function to get the full device name. It’s converting the string into unicode and then deleting both the symbolic link and device. This can be seen as:

Putting all the pieces of this function together, we probably get something that looks like this or similar:

Now that we have those knocked out, let’s move on to the juicy tidbits! I can hardly contain myself I can punch you in the face!!

The Dispatch Table

The Dispatch Table! You can view it in its entirety here.

When we take a look at this function, we could see that this uses 2 IOCTL codes: 0xAA012044 and 0xAA013044. We interact with this area by sending a request with the DeviceIoControl function. I will just throw random values as parameters just to see how it reacts. For now, our DeviceIoControl will look like this:

In this first code block, it is moving several variables into different registers. The important ones to note are the following:

  1. [rdx + 0B8h] is being moved into rax. This contains the parameters that DeviceIoControl contains
  2. [rdx + 18h] is being moved into rdi. This is where the pointer to our user buffer lies
  3. [rax + 10h] is the size of lpInBuffer
  4. [rax + 8] is the size of lpOutBuffer
  5. Lastly, [rax + 18h] contains our IOCTL code and gets moved into edx

Looks like this part of the code block handles all the parameters from DeviceIoControl. Knowing these will make the later part of this function makes sense.

It will do a comparison on rax against 0x0e. If they are equal, we continue on to our next code block. The jz is a je instruction in the debugger, which is jump if equal. If it isn’t equal to 0x0e, it will error out with STATUS_NOT_IMPLEMENTED. It will then call on IofCompleteRequest and is dieeee.

Here’s where our IOCTL codes come into play. This looks a bit different than what I usually see as these are normally done as switch statements. This appears to be a couple if statements. In loc_105C8, it moves one code into r11d. If we refer back to our previous code block, we know ecx is 0 so it is zeroing out eax and esi. EDX holds the IOCTL code in our DeviceIoControl function. If our IOCTL code is 0xAA012044, we jump over to loc_105EC. 4 will be moved into esi and eax and then jump to loc_105F3.

If our IOCTL code is 0xAA013044, we head over to our next code block. It will check if the IOCTL code we use (edx) is the same as 0xAA013044 (r10d). 8 gets moved into eax and the result of 8 – 4 goes into esi and we head on over to loc_105F3. Any other IOCTL will jump directly to loc_105F3. One more thing to note is that we will be getting STATUS_INVALID_PARAMETER errors if the size of the input buffer and output buffer does not match with 8 or 4. Reversing this function will help troubleshoot why and how the parameters of DeviceIoControl will be. Here is where we finally start getting to the good stuff!

It will check if the size of our input buffer (r9d) is set to 8 (eax). If it is, it will check if the size of our output buffer (r8d) against 4 (esi). In the debugger, the jnz jump is actually a jne jump. Next, it will check to see if the user supplied IOCTL (edx) is equal to 0xAA012044 (r11d). If it is, we head over to loc_1060C and put our user buffer into ecx and then we hit loc_1060E; otherwise, we check if the IOCTL is 0xAA013044 (r10d). If it is, we move our user buffer into rcx and then we hit loc_1060E. If it doesn’t match any of those, we get our STATUS_INVALID_PARAMETER error. Lastly, we make a call to sub_10524. We are almost done with all these! Hang in there 🙂

SUB_10524

You can view this function in its entirety here.

So the first thing it’s doing is putting rcx into rsp+arg_0. RCX contains the address of our user input and you can verify that with windbg.

So here is where it gets just a little bit strange…

It is moving the address of our buffer into rax and rcx. It does a comparison where it compares the value of rax – 8 against rcx. In the debugger, the jz instruction is a je. So if the value of rax – 8 is equal to the address of rcx, then we head on over to loc_10541; otherwise, nothing happens. That’s a strange check, but whatevs! We gotta do what we gotta do to fuck. Shit. Up!

First thing being done is the address of our buffer is being put into rax and then gets thrown into [rsp + 48h + var_20]. So not sure what’s going on with MmGetSystemRoutineAddress. All I know is it’s placing a pointer into var_18. The most interesting part is it’s calling rax, which is the address of sub_10788. After this, it makes two more calls to [rsp + 48h + var_20] and sub_107a0. Let’s take a closer look at the first call that is being made in this function…

SUB_10788

Here is what we have been waiting for this entire time…check out this snippet of code:

If this doesn’t stand out to you, it definitely should because this specific set of instructions is disabling SMEP!! Facking wat?!

Yes, it’s disabling SMEP….SMEP is being disabled 🙂

So in case you’re wondering what the fook SMEP is, SMEP stands for Supervisor Mode Execution Prevention. This is a kernel exploit mitigation that detects ring 0 code in user space. This was introduced in Windows 8, but what does this mean for you??? It means that local privilege escalation is “harder” now. If it detects it, your machine will BSoD! This code snippet, however, bypasses SMEP. How freaking awesome is this?!?! We aren’t exploiting anything! Just taking advantage of the driver’s features 🙂

So a quick run down of what it’s doing…

  1. It’s moving the CR4 register into eax
  2. It’s saving a copy into rcx
  3. It’s ANDing with 0xFFFFFFFFFFEFFFFF
  4. It’s moving the modified value into CR4

And voila! SMEP is disabled for the meantime with a copy of the original value in rcx waiting for us. SUB_10788 can be renamed to SMEPdisable.

After this is done, we move on to our next call of [rsp + 48h + var_20]. This is where our user buffer comes into play. The exciting part of this is it’s executing anything you want to with System rights! Fuck yea!! It does no types of checks whatsoever. Lastly, we move on to our last call of sub_107a0.

SUB_107A0

Knowing what we know with our previous function, we know this is restoring SMEP. How do we know??? Well if we take a look at it, we know it’s moving the original value of CR4 back into the CR4 register and all is well as if nothing ever happened. SUB_107A0 can be renamed as SMEPrestore.

AAAAAND that’s it! In my eyes, all this driver does is disables SMEP to run whatever you want and then re-enables it as if nothing ever happens. What does this mean for us?? An easy privilege escalation!!! We can use this feature to do any type of anything. Pretty much anything our little evil heart desires 🙂

With the following code, we can elevate our privileges and become NT System!

As a side note, this shellcode will only work for Windows 10. It will not work on all version of Windows due to offsets being static and being different across platforms. What you can do is use a few different API’s to get what you need dynamically and make it work across different platforms 🙂

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *