This is another series of blog posts where I will be explaining some interesting facts that I implemented for “AntiCheat-Testing-Framework”, which can be found on my Github. In this case, I will show you how much we can learn from a <10 lines exploit 🙂

What we will go through?

Learn how to write an exploit for a vulnerable driver.

Learn to identify the vulnerable IOCTL by reversing the .sys file

Learn about the technical background of the vulnerability

Identify the Device Name of the Driver

Identify input/output buffer format and size

Successfully exploit the vulnerability

AntiCheat-Testing-Framework

Being this the first post of the series, I need to introduce what AntiCheat-Testing-Framework is:

It is a compilation of multiple projects about different techniques used to bypass Anti-Cheats (AC) protections on the game. Those projects can be compiled together or by separate, and all the code I wrote is the results of some talks I presented at Recon and BlackHat. I started programing some basic bypasses and then it ended up growing until it became a compilation of multiple user-mode and kernel techniques.

The main idea is to use this project as a template or code base to test any Anti-Cheat implementation on the market and learn along the way. This can be used by game developers or security enthusiasts to get into this topic and learn how the different techniques, implemented on these projects, can be developed and used to bypass the most used Anti-Cheats on the market.

Technique Explained

Getting a HANDLE to a process could be one of the main goals while developing a cheat. However, ACs on the market control which processes are able to obtain a HANDLE to the process of the game, and most of them don’t even allow other external processes to get a HANDLE successfully. But what would happen if we could create that HANDLE from kernel using a low privilege process?

This technique is far from being perfect and it does not bypass all the available ACs. However, it provides a good way to attempt a decent bypass and learn while you develop the exploit.

I tried this technique against multiple ACs, and sadly most of them block this attack, however, I decided the include it on the framework due to the clever way of getting a handle.

Exploit

If you want to spoil yourself, you can just go to my Github and check this particular module: DriverTester (not the best name, I have to be honest, I decided this name at the very beginning).

This module exploits Razer Synapse rzpnk.sys (2.20.15.1104) – CVE-2017-9769 to open a new HANDLE to the game from kernel mode. Then, it attempts to access the memory of the game by using this handle.

CVE-2017-9769

A specially crafted IOCTL can be issued to the rzpnk.sys driver in Razer Synapse 2.20.15.1104 that is forwarded to ZwOpenProcess allowing a handle to be opened to an arbitrary process.

In practice, this CVE can be used in many different ways. For example, can be used to open a handle with full privileges to a NT_AUTHORITY\SYSTEM process (like winlogon) and control the process execution in order to execute your own shellcode as @FuzzySec explains here.

The attack can be divided into the following steps:

Open a HANDLE to the Driver (we need this in order to use the available IOCTLs) Prepare the input/output buffers Send the IOCTL request to the Driver Attempt to use the new HANDLE to access the process of the game

Opening a HANDLE to the Driver

The first thing we need to do in order to interact with the Driver is to obtain a HANDLE and make sure that it is valid. In order to do that, we are going to use CreateFile, which requires the device name of the Driver as the first parameter. The device name that belongs to rzpnk.sys Driver is “\\.\47CD78C9-64C3-47C2-B80F-677B887CF095”. We can identify it by opening the .sys file with IDA Pro or Ghidra:

We can see that the driver is sending that string as the third parameter when calling IoCreateDevice.

We can see in the following lines (extracted from the exploit) how we call to CreateFile providing the desired access and the rest of the parameters:

HANDLE hDevice = CreateFile("\\\\.\\47CD78C9-64C3-47C2-B80F-677B887CF095", FILE_SHARE_WRITE | FILE_SHARE_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDevice == INVALID_HANDLE_VALUE) { std::cout << "INVALID_HANDLE_VALUE: " << GetLastError() << std::endl; return 1; }

We can make sure that we have received a valid handle by comparing it with the constant INVALID_HANDLE_VALUE, as it is possible to see in the previous lines.

Preparing the input/output buffers and the IOCTL request

In this case, we have a CVE that let us know which IOCTL we have to invoke, however, when we are exploiting our own bugs, we need to be able to reverse and identify the available IOCTLs.

This Driver implements the IRP_MJ_DEVICE_CONTROL (0xE) major function code (0xE):

We talked about Major Function Codes when reversing XignCode3 Driver.

In the next image, we can see how the IOCTL value is retrieved from CurrentStackLocation.Parameters.DeviceIoControl.IoControlCode as the documentation explains here:

The vulnerable IOCTL is 0x22a050. It is possible to see the different validations/calculations that are performed against this variable in order to determine which IOCTL is being invoked (on the comments you have the calcs for the value 0x22a050).

On the last “ELSE IF”, we can see how the SystemBuffer is retrieved from IRP->AssociatedIrp.SystemBuffer after that the Parameters.DeviceIoControl.InputBufferLength and Parameters.DeviceIoControl.OutputBufferLength have been validated against the value 16. As we can see, the input and output buffer must be 16 bytes long. For more information about the buffer descriptions for I/O control codes, I leave you this link.

Since the SystemBuffer is being sent directly as a parameter to the IOCTL dispatcher for this particular code, we need to know what we have to send inside of the buffer. We can discover this by analyzing Dispatch_0x22a050_ZwOpenProcess, and identifying what is this function doing with the buffer:

ZwOpenProcess receives a PCLIENT_ID as the 4rd parameter. This is how the PCLIENT_ID structure looks like:

typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, *PCLIENT_ID;

Based on that, I defined the buffer with the following structure:

struct buffer { INT64 pid1; INT64 pid2; } inB, outB;

After doing memset to the input and output buffer, we can finally call DeviceIoControl sending the expected parameters:

DWORD returnedBytes = 0; memset(&inB, 0, sizeof(buffer)); memset(&outB, 0, sizeof(buffer)); inB.pid1 = targetPid; DeviceIoControl(hDevice, 0x22a050, &inB, sizeof(buffer), &outB, sizeof(buffer), &returnedBytes, NULL);

The end

The new HANDLE will be returned inside of outB.pid2. I recommend you to take a look at the function handleTests on my Github. There, you can see a good example of how this handle can be used to invoke different windows API functions: ReadProcesMemory, WriteProcessMemory, NtReadVirtualMemory, NtWriteVirtualMemory, ZwReadVirtualMemory and ZwWriteVirtualMemory.