Exploiting Capcom.sys On x86_64 Windows 1607 RS1

Capcom.sys is a signed Windows kernel driver that was included in a release of Street Fighter 5 in 2016. Capcom.sys include a security vulnerability within its I/O functionality in which it accepts a user-mode provided memory pointer and executes it within kernel-mode, but not before disabling Supervisor Mode Execution Prevention (SMEP) on the system, disabling SMEP allows the kernel to perform callbacks to user-mode regions of code (the user-provided pointer in this case).

Motive For Exploitation

Why is Capcom.sys a good target for exploitation? The driver is vulnerable (as the analysis here shows) and it’s also digitally signed. With Microsoft’s introduction of Kernel Mode Code Signing (KMCS) on Windows Vista, loading kernel modules (drivers) requires having the driver be digitally signed. When conducting post-exploitation during an offensive operation, actors typically want to load additional kernel code (drivers) and or rootkits/bootkits. In order to do so, they need to disable KMCS. Throughout the past few years, many techniques for doing so have been publicized. One of the easier approaches is using Microsoft’s BCDEDIT.EXE utility to disable the driving signing requirement by putting the system into test signing mode (used for developers if they are writing drivers, so they can test their code without purchasing an entire certificate), while this is an effective technique, it does require a system reboot and it causes a watermark to persist on the system’s display stating it’s in test mode.

ab8f3644fe88450c856f6dd97704ba2e

A more powerful technique for bypassing the certificate requirement is by exploiting a digitally signed but vulnerable driver. A driver that can be loaded onto the system since it’s signed, but also includes a security vulnerability that allows for exploitation from user-mode. Capcom.sys is a perfect example of this, the driver is digitally signed, but it also includes a vulnerability in which it executes user-mode provided pointers directly into kernel-mode. This type of vulnerability is commonly abused for privilege escalation, but in the case of post-exploitation and loading rootkits, it can be used to disable KMCS on the system directly (using older techniques like the Turla technique by setting ntoskrnl!g_CiEnable to false or by mapping a driver (rootkit) directly into kernel space.

Popular tools such as https://github.com/hfiref0x/DSEFix do just this, it abuses a vulnerable VirtualBox driver to overwrite the global system variable ntoskrnl!g_CiEnable which disables DSE, in this specific instance, this technique is now obsolete, but the concept remains.

Capcom.sys Driver Internals Reverse Engineering

After loading the Capcom.sys driver onto a system (using a tool like OSR’s DriverLoader or using the SCM) it creates a device name called \Device\Htsysm72FB which is accessible by anyone on the system, permissions wise, should be restricted to only members of the Administrators group. Luckily, from an exploitation perspective, the driver is accessible to anyone with full read/write/delete/all access permissions.

5f993f2656fb4189ab9f190dc56dde6d

Using WinDBG to view the loaded modules on the system shows how Capcom.sys was loaded from a non-standard directory, this is typically a good indicator to look out for when performing system forensics during an incidence response session, in this case, the driver was loaded for vulnerability research purposes, but in a non-research context, looking for drivers that load from strange or temporary directories is something to look out for.

ad76c0ed90004be5ab11569799fc3ffe

Looking at the exports for the driver show fairly standard functions, the functions being exported are mainly related to driver device creation and creating symbolic links to expose to user-mode, this is a good sign when analyzing potentially vulnerable drivers, you want to ensure it includes some aspect of IO functionality so you can actually communicate and exploit it.

23f89b3f72114e0c89fe42e4c25339b7

DriverEntry

Drivers typically include a function called DriverEntry as their main, first function of execution. In Capcoms case, RtlInitUnicodeString is called to initialize the driver name, v5 is the same device name seen earlier in DeviceTree Htsysm72FB, this comes from a deobfuscation function which the driver uses to deobfuscate an obfuscated instance of Htsysm72FB, for a driver, this behavior is slightly suspicious. The, IoCreateDevice is called to create the device object.

9438bf02668b45f9a9ad70eb810f4b95

This is followed up with a second call to RtlInitUnicodeString to initialize \\DosDevices\Htsysm72FB. And finally, IoCreateSymbolicLink is called to create a symbolic link between the two names, this symbolic link is then exposed to user-mode so a user application can open a handle to the driver. This symbolic link creation is followed up by the established MajorFunction table, within this table are the various IRP routines that the driver supports (IRP_MJ_DEVICE_CONTROL, etc). First, The IRP routines of IRP_MJ_CREATE and IRP_MJ_CLOSE are set with the function DispatchCreateClose, this function is responsible for allowing a handle to be opened and closed with the driver. After this, the IRP routine IRP_MJ_DEVICE_CONTROL is set with the function DispatchDeviceConto, this is one of the most important functions since it’s responsible for handling the inbound IO communications with the driver, including IRP’s/IOCTLs that are sent from user-mode. Finally, the driver’s unload function is defined, so if the driver is prompted to be unloaded from the system, it can do so.

7387efda9bc84cc0ae7f6d9500045e44

DispatchCreateClose

Drivers have the ability to merge their DispatchCreate DispatchClose routines into a single routine, typically this is referred to as DispatchCreateClose. To establish their existence on a system when a create request is issued from user-mode CreateFileA for example when opening a handle to the driver, the driver will include a create routine. The same logic applies to a close request.

For Capcom.sys, it includes a combined create and close routine called DispatchCreateClose. Within the function, first, a pointer to the current I/O stack location in the IRP is obtained through Irp->Tail.Overlay.CurrentStackLocation. Then the Status member of the I/O status block is set to STATUS_SUCESS. A check is performed to see if the MajorFunction member from _IO_STACK_LOCATION is equal to 2, if the check fails, the Status member is set to 0xC0000002 which indicates a STATUS_NOT_IMPLEMENTED code.

Otherwise, the function calls IofCompleteRequest to complete the IRP request and return the IRP to the I/O Manager. And finally, the Status member which was previously set to STATUS_SUCCESS is returned from the function.

DispatchCreateClose

DispatchDeviceControl

The IRP_MJ_DEVICE_CONTROL routine points to the DispatchDeviceControl function, this is the function that is responsible for handling inbound requests from a user-mode application.

The function starts by setting the IRP’s current stack location to a variable, setting the Status member, and defining the input and output buffer variables along with the IOCTL control code variable, these are all set using members from IrpStackCurrentLocation->Parameters. Now, the function defines a set of requirements for input and output buffers if the user-mode client wants to successfully communicate with the driver.

  1. For 32-bit requests, the IOCTL 0xAA012044 should be used within DeviceIoControl along with a required input and output buffer size of 4
  2. For 64-bit requests, the IOCTL 0xAA013044 should be used within DeviceIoControl along with a required input buffer size of 8 and an output buffer size of 4
  3. There is a check against the received buffer sizes and the required sizes that were previously defined

DispatchDeviceControl1

Now the user provided input buffer is passed through the ExecuteUserSpaceCode function, which leads to the “vulnerability” within this driver.

DispatchDeviceControl2

Vulnerability Analysis

Looking at ExecuteUserSpaceCode provides more information about what happens with the inbound data buffers passed by the user.

  1. There is first a check against the provided buffer, in which the first 8 bytes of the provided input buffer must be equal to the address of the function that is located at the input buffer + 0x8.
  2. The parameter passed to this function inputBuffer is reassigned to a new variable (user_space_passed_data.
  3. The address of MmGetSystemRoutineAddress is assigned to a variable
  4. A function called DisableSmep is called with the value of old_cr4_value being passed to it, this function is responsible for disabling SMEP temporarily
  5. Whatever was passed from user-mode in the input buffer is now executed
  6. SMEP is re-enabled using the old_cr4_value again

ExecuteUserCode

Now, we can see the execution flow we need to hit when writing our exploit, to have the provided input buffer executed in kernel-mode (with the function call at capcom.sys+573) we need to go through passing our input buffer to the driver via a call to DeviceIoControl, which will be handled by the DispatchDeviceControl function, if the proper IOCTL is passed (0xAA013044 since we are exploiting this driver on a 64-bit version of Windows), the user-provided input buffer will eventually reach capcom.sys+613 which passes the input buffer to be executed (as seen above in the ExecuteUserSpaceCode pseudocode).

ExecutionFlow

DOS BSOD Proof-of-Concept Exploit

  1. The two Windows API functions calls required to communicate with this driver are CreateFileA for establishing a handle to the driver’s file object that it creates when calling .IoCreateDevice

CreateFileA("\\\\.\\Htsysm72FB", 0, 0, NULL, OPEN_EXISTING, 0, NULL)

  1. DeviceIoControl is used by providing the handle returned from CreateFileA to send the driver an IOCTL, input, and output buffer, the buffer sizes
  2. In this case, as seen from reverse-engineering the DispatchIoControl function, for 64-bit communication, we need to specify 0xAA013044 as the IOCTL code.

DeviceIoControl(hDevice, 0xAA013044, &inputBuffer, 8, &outputBuffer, 4, &sizeReturn, NULL)

0fb18ff9be2349d5b5b73ef4c642751d