In information security, even seemingly insignificant issues could pose a significant threat. One notable vector of attack is through device drivers used by legitimate software developers. There are numerous available drivers to support legacy hardware in every industry, some of which are from businesses that have long stopped supporting the device. To continue operations, organizations rely upon these deprecated device drivers.
This creates a unique attack vector, as Microsoft Windows allows loading kernel drivers with signatures whose certificates are expired or revoked. This policy facilitates threat actors to disable security software functions or install bootkits using known vulnerable drivers. Since the Windows 11 2022 update, the vulnerable drivers are blocked by default using Hypervisor-Protected Code Integrity (HVCI). However, this banned-list approach is only effective if the vulnerable driver is known in advance.
The Carbon Black Threat Analysis Unit (TAU) discovered 34 unique vulnerable drivers (237 file hashes) accepting firmware access. Six allow kernel memory access. All give full control of the devices to non-admin users. By exploiting the vulnerable drivers, an attacker without the system privilege may erase/alter firmware, and/or elevate privileges. As of the time of writing in October 2023, the filenames of the vulnerable drivers have not been made public until now.
In this blog post, TAU will describe how to identify unknown vulnerable drivers. This is a long one so here is a quick table of contents if you want to find a specific section:
- Previous Research
- Our Approach
- Implementation
- Triage Function
- Analysis Function
- Hunting Vulnerable Drivers
- Triage
- Analysis
- Result
- Exploit Development
- Reporting
- Wrap-up
- Tool and PoCs
- Customer Protection
1. Previous Research
Previous research such as ScrewedDrivers and POPKORN utilized symbolic execution for automating the discovery of vulnerable drivers. As far as TAU researched, symbolic execution (or the specific implementations based on angr) fails at an unignorable rate by causing path explosions, false negatives and other unknown errors.
Even if symbolic execution works, we need to verify the result on a disassembler manually before reporting the vulnerability. For example, the previous research implementations do not check if the output buffer of the IOCTL request contains the result value in read primitives. In this context, a primitive refers to a basic operation supported by the operating system. Specifically, a read primitive reads data from a driver’s internal buffer, though normally the results would be validated. Another example is that the constraint of the input buffer is sometimes too chaotic to understand. In both cases, there is no way to avoid manual analysis.
Additionally, previous research focused primarily on Windows Driver Model (WDM) drivers. Windows Driver Framework (WDF) drivers, whose code and data structures are different by versions and compiler settings, have been unexplored sufficiently. Even if code handling WDF drivers is newly added to the existing implementations, it seemed unlikely to work due to the WDF’s complexity. Therefore, TAU needed to take another approach for discovering both WDM/WDF vulnerable drivers.
2. Our Approach
TAU automated the hunting process of vulnerable WDM/WDF drivers by using an IDAPython script. IDAPython is the Python programming language to access APIs of IDA Pro (hereinafter called IDA), which is a commercial disassembler widely used by reverse engineers. The script implementation is based on the Hex-Rays Decompiler SDK and will be detailed in the next section below.
In this research, TAU focuses on drivers that contain firmware access through port I/O and memory mapped I/O for the following reasons.
- Detecting the drivers is almost always the only way for OS-level security software to catch firmware implant installation behavior that modifies SPI flash memory. The SPI flash modification can be done through the low-level APIs and assembly instructions shown in Table 1. It is almost impossible for AV/EDR to detect potential firmware modifications solely on these low-level activities. Once a firmware implant is installed, it can also disable firmware scanners used by security vendors.
- The drivers handling such low-level I/O often contain other vulnerabilities like virtual memory access in kernel space (a.k.a. data-only attack). For example, among the discovered vulnerable drivers, the AMD driver PDFWKRNL.sys allows arbitrary virtual memory access. Another Intel driver stdcdrvws64.sys did not provide the read/write primitives directly, but attackers can obtain the same effect by combining memory mapped I/O and the MmGetPhysicalAddress API through the multiple IOCTL requests.
user space | kernel space | |
APIs | DeviceIoControl | MmMapIoSpace/MmMapIoSpaceEx (memory mapped I/O) |
Instructions | – | IN/OUT (port I/O) |
Table 1: Indicators modifying a SPI flash memory
3. Implementation
The IDAPython script has two functions: triage and analysis. The triage function robustly detects potentially vulnerable drivers from large sets of samples in IDA batch mode (command-line interface) execution. After the triage, we need to confirm that the detected drivers are truly vulnerable on IDA GUI. The analysis function substantially assists the tedious manual validation.
3.1. Triage Function
The triage function identifies IOCTL handlers then finds execution paths from the handlers to the target APIs/instructions. As shown in Table 1 already, the targets are memory-mapped I/O APIs and port I/O assembly instructions. Within the IDA GUI, we can easily find an execution path by using “Add node by name” and “Find path” menus in the proximity view. However, IDA does not provide this functionality through its API. Therefore, TAU implemented the path finder.
The IOCTL handler identification method depends on the driver type. In WDM drivers, the triage function code simply detects an assignment to the MajorFunction array member of the DRIVER_OBJECT structure then applies the function type. On the other hand, the method for WDF drivers requires a multiple-step procedure.
First, the script detects a call instruction of WdfVersionBind API then imports a header file including WDF type information provided by kmdf_re as IDA itself does not support the information. Unlike WDM, WDF APIs are basically called through a function table defined as WDFFUNCTIONS, that is pointed by the member of the third argument’s structure (WDF_BIND_INFO.FuncTable). Next, the script tracks cross-references to WdfIoQueueCreate API in the table. If a function address is assigned to the member of the second argument’s structure (WDF_IO_QUEUE_CONFIG.EvtIoDeviceControl), that must be the WDF IOCTL handler.
While this approach is straightforward, the devil is in the details. As shown in the examples of Figure 1 and 2, sometimes WDF_BIND_INFO.FuncTable points the pointer to WDFFUNCTIONS (PWDFFUNCTIONS), and sometimes it points WDFFUNCTIONS directly. TAU found that it depends on the WDF version (WDF_BIND_INFO.Version). Specifically, version 1.15.0 and later is true of the former. Version 1.13.0 and below is applicable to the latter.
Figure 1: FuncTable pointing the pointer to WDFFUNCTIONS (PWDFFUNCTIONS)
Figure 2: FuncTable directly pointing WDFFUNCTIONS
Even in the old WDF versions, the script had to consider the variations of how to call WDF APIs. For instance, one driver calls WDFFUNCTIONS.WdfIoQueueCreate directly (Figure 3), and another calculates the offset from WDFFUNCTIONS to call the API (Figure 4). We need to track the cross-references from the head of the table in the latter case.
Figure 3: Direct call of WDFFUNCTIONS.WdfIoQueueCreate
Figure 4: Offset calculation for WdfIoQueueCreate
Moreover, WDF drivers built with debug information, or some older drivers, create function wrappers when calling WDF APIs. In that case, the script detects the wrappers and sets their function types then traces back assignments to the arguments in the parent functions.
Figure 5: Function type applied to the WdfIoQueueCreate wrapper
Thus, WDF code and data structures are different by WDF versions and compiler settings.
3.2. Analysis Function
The analysis function fixes union fields in IOCTL-related structures and propagates function argument names/types in subroutines recursively to quickly decide if input/output can be controlled. TAU will describe the automation in both WDM/WDF cases.
In one WDM driver example (TdkLib64.sys), TAU had the following code when starting to analyze the IOCTL handler in IDA pseudocode view (sub_1203C is the handler).
Figure 6: Typical WDM IOCTL handler code
If we manually validate that this sample contain vulnerabilities, we need to:
- Set PDRIVER_DISPATCH to the function type of sub_1203C
- Fix the union field numbers in the structures (IO_STACK_LOCATION.Parameters and IRP.AssociatedIrp) for IRP_MJ_DEVICE_CONTROL requests
- Rename local variables and change the types according to the assignments
- Repeat Step 2 and 3 in the called functions recursively until the execution reaches to the targeted APIs and instructions
- Check if users can control the input for the APIs and instructions then receive the result
The analysis function automates all steps except Step 5. After running the script, IDA displays the improved code.
Figure 7: Handler code improved by script execution
Additionally, the script execution tells us that one of the targeted APIs (MmMapIoSpace) is called in the grandchild routine. By the name propagation, we can judge immediately that the API arguments are controllable and the result data is acquirable, as SystemBuffer is utilized for user data input/output and qmemcpy right after MmMapIoSpace plays a role in arbitrary read/write for the mapped memory.
Figure 8: Code calling MmMapIoSpace
Figure 9: Automatically modified code with script execution log
The script handles WDF drivers in the same way, but it additionally sets argument names and types of the following WDF APIs handling user data I/O since IDA does not support WDF type information by default.
- WdfRequestRetrieveInputBuffer
- WdfRequestRetrieveOutputBuffer
- WdfRequestRetrieveInputWdmMdl
- WdfRequestRetrieveOutputWdmMdl
- WdfRequestRetrieveInputMemory
- WdfRequestRetrieveOutputMemory
- WdfRequestGetParameters
In one WDF example (stdcdrv64.sys) below, the code modified by the script shows that we can gain full control of the MmMapIoSpace’s arguments.
Figure 10: WDF code before script execution
Figure 11: WDF code after script execution
It should be noted that the name propagation is not perfect. Renaming local variables uncommonly fails due to the following reasons. In these cases, TAU needed to change the names manually.
- The function argument’s item type is cot_call when traversing the ctree (e.g., 0x11286 in SysInfoDriverAMD).
- Renaming information looks to be lost as the local variable name is changed (e.g., 0x140003657 in IoAccess.sys).
- Renaming fails multiple times even if a new name is unique in the function (e.g., 0x11ef1 in cpuz.sys).
They are rare cases, and any issue is likely caused by internal changes of the ctrees in IDA. Therefore, TAU hasn’t investigated them any further.
4. Hunting Vulnerable Drivers
In this section, TAU will describe how to collect and narrow down the driver samples then identify the vulnerabilities.
4.1. Triage
TAU collected about 18K Windows driver samples by VirusTotal retrohunts. The YARA rule is simple.
import "pe" rule hardware_io_wdf { meta: description = "Designed to catch x64 kernel drivers importing a memory-mapped I/O API (MmMapIoSpace)" strings: $wdf_api_name = "WdfVersionBind" condition: filesize < 1MB and uint16(0) == 0x5a4d and pe.machine == pe.MACHINE_AMD64 and (pe.imports("ntoskrnl.exe", "MmMapIoSpace") or pe.imports("ntoskrnl.exe", "MmMapIoSpaceEx")) and $wdf_api_name and // WDF //not $wdf_api_name and // WDM for all signature in pe.signatures: ( not signature.subject contains "WDKTestCert" ) } |
Next, TAU de-duplicated the samples based on imphash then executed the IDAPython script for the imphash-unique samples in batch mode. The extracted samples were about 300 WDM drivers and 50 WDF. Among them, TAU excluded drivers with the following conditions.
- Already-known vulnerable drivers (source: CVE List, MS block rules, loldrivers.io, popkorn-artifact, etc.)
- Drivers whose signature caused verification errors
- Old version of the same drivers validated by bindiff wrapper
- Drivers setting their device’s access control
- In source code: IoCreateDeviceSecure (WdmlibIoCreateDeviceSecure) and WdfControlDeviceInitAllocate
- In configuration file: INF AddReg directive
- By other minor methods
The device access control is normally set using Security Descriptor Definition Language (SDDL) strings defined in either source code or configuration file. For instance, the following WDM/WDF drivers set access control by calling APIs in code. The SDDL string “D:P(A;;GA;;;SY)(A;;GA;;;BA)” shows that users except kernel/system/administrator are unable to access the devices.
Figure 12: Access control using WdmlibIoCreateDeviceSecure
Figure 13: Access control using WdfControlDeviceInitAllocate
TAU also found that some WDF drivers (e.g. WDTKernel.sys and H2OFFT64.sys) set access control in their INF files like an example below.
[WDTInstall.AddReg] HKR,,DeviceCharacteristics,0x10001,0x0100 ; Use same security checks on relative opens HKR,,Security,,”D:P(A;;GA;;;BA)(A;;GA;;;SY)” ; Allow generic-all access to Built-in administrators and Local system |
Those drivers are not vulnerable in terms of access control, though privileged attackers can still abuse them as the Bring Your Own Vulnerable Driver (BYOVD) techniques by loading and exploiting the drivers for their purposes like disabling security software.
Other minor methods for device access control are further detailed in the next section.
4.2. Analysis
After the triage, TAU analyzed the extracted samples on IDA to verify the vulnerabilities. Thanks to the script’s analysis function explained in the previous section, the validation was basically straightforward, but TAU had to take care of specific issues caused by the drivers. TAU introduces two examples.
cpuz.sys implemented its own device access control method. The driver code checks if processes trying to open the device have the specified privilege (SE_LOAD_DRIVER_PRIVILEGE).If not, the driver returns an error whose status is STATUS_ACCESS_DENIED. Even administrators do not have the privilege unless the user-mode process requests it programmatically. Hence, TAU excluded the driver from the list of the discovered vulnerable ones.
Figure 14: SE_LOAD_DRIVER_PRIVILEGE check by cpuz.sys
The next unique method was embedded in TdkLib64.sys. The driver decodes the IOCTL request data using the unique byte map table and validates the header values. After the actual IOCTL handler is executed, the result is also encoded by the table. In this case, non-privileged users can send the IOCTL requests if the data format is correct. Therefore, TAU concluded that this driver was still vulnerable.
Figure 15: IOCTL data decoding in TdkLib64.sys
As seen above, we need to confirm manually that the drivers triaged by the batch execution are truly exploitable, so it’s difficult to completely automate the vulnerability discovery. That’s why the script’s analysis function is handy in the manual validation work.
4.3. Result
Finally, TAU discovered 34 vulnerable drivers (30 WDM, 4 WDF) with firmware access, including ones made by major chip/BIOS/PC makers. This is the number based on the unique filenames. Practically, there are 237 file hashes in the wild. All discovered drivers give full control of the devices to non-admin users. TAU could load them all on HVCI-enabled Windows 11 except five drivers.
Filename | Number of Hashes | Type | Signature Status (when discovered) | Other R/W Vulnerabilities | PoC | Note |
stdcdrv64.sys | 1 | WDF | Valid | MSR/CR | Firmware erase | – |
IoAccess.sys | 2 | WDF | Expired | – | Firmware erase | – |
GEDevDrv.SYS | 4 | WDF | Expired | – | – | – |
GtcKmdfBs.sys | 5 | WDF | Expired | MSR | – | – |
PDFWKRNL.sys | 3 | WDM | Valid | Virtual memory | Firmware erase, EoP | CVE-2023-20598 |
TdkLib64.sys | 18 | WDM | Valid | MSR | Firmware erase | CVE-2023-35841,
unique buffer encoding |
phymem_ext64.sys | 4 | WDM | Valid | Registry | Firmware erase | Loading failure on HVCI-enabled Win11 |
rtif.sys | 7 | WDM | Expired | – | – | KeBugCheckEx |
cg6kwin2k.sys | 1 | WDM | Valid | – | Firmware erase | – |
RadHwMgr.sys | 6 | WDM | Valid | Virtual memory, registry | – | Error in device creation |
FPCIE2COM.sys | 5 | WDM | Valid | – | – | – |
ecsiodriverx64.sys | 2 | WDM | Expired | MSR | – | – |
sysconp.sys | 2 | WDM | Expired | MSR | – | – |
ngiodriver.sys | 12 | WDM | Expired | MSR (read-only) | – | – |
avalueio.sys | 2 | WDM | Expired | – | – | – |
tdeio64.sys | 2 | WDM | Expired | – | – | – |
WiRwaDrv.sys | 1 | WDM | Expired | – | – | Loading failure on HVCI-enabled Win11 |
CP2X72C.SYS | 5 | WDM | Expired | – | – | Loading failure on HVCI-enabled Win11 |
SMARTEIO64.SYS | 1 | WDM | Expired | – | – | – |
AODDriver.sys | 9 | WDM | Certificate revoked | MSR | – | – |
dellbios.sys | 11 | WDM | Certificate revoked | – | – | – |
stdcdrvws64.sys | 1 | WDM | Certificate revoked | Virtual memory, MSR/CR | EoP | Old WDM version of stdcdrv64.sys |
sepdrv3_1.sys | 1 | WDM | Certificate revoked | – | – | – |
kerneld.amd64 | 36 | WDM | Certificate revoked | – | – | – |
hwdetectng.sys | 3 | WDM | Certificate revoked | MSR | – | – |
VdBSv64.sys | 1 | WDM | Certificate revoked | MSR | – | – |
nvoclock.sys | 25 | WDM | Certificate revoked | Virtual memory | – | – |
rtport.sys | 8 | WDM | Certificate revoked | Virtual memory | EoP | – |
ComputerZ.Sys | 50 | WDM | Certificate revoked | MSR | – | – |
SBIOSIO64.sys | 4 | WDM | Certificate revoked | – | – | – |
SysInfoDetectorX64.sys | 1 | WDM | Expired | MSR (read-only) | – | – |
nvaudio.sys | 1 | WDM | Certificate revoked | Virtual memory, MSR/CR
|
– | Buffer encryption, 72% code similarity with nvoclock.sys |
FH-EtherCAT_DIO.sys | 2 | WDM | Expired | MSR | – | Loading failure on HVCI-enabled Win11 |
atlAccess.sys | 1 | WDM | Expired | – | – | Loading failure on HVCI-enabled Win11 |
Table 2: Research result
As shown in Table 2, TAU additionally found other arbitrary read/write vulnerabilities outside of firmware access (arbitrary port I/O and memory mapped I/O). For more details of each vulnerability, check the previous blog post written by TAU and the ESET’s write-up.
- Six drivers allow kernel memory access (arbitrary virtual memory R/W). This can be exploited for elevation of OS privilege (EoP) or defeating security software functions like AV/EDR.
- Twelve drivers accept model-specific register (MSR) access. Attackers can patch system call entry addresses or disable KASLR if the process integrity level is low.
- Three drivers provide control register (CR) access like disabling SMEP/SMAP.
- Two drivers offer registry key/value access.
4.4. Exploit Development
TAU developed firmware erasing and EoP PoCs for a subset of the drivers.
The firmware erasing PoC (rwf.py, available on GitHub) targets the Intel Apollo SoC platforms by exploiting the six drivers marked as vulnerable in Table 2. This PoC takes one argument for the target device then erases the first 4KB data of firmware in the SPI flash memory. The PoC behavior is the same as the CHIPSEC framework’s “spi erase” command as described below, but it exploits port I/O and memory mapped I/O provided by each vulnerable driver with unique IOCTL request data structures, instead of the CHIPSEC driver.
- Get the SPI Base Address Register (SPIBAR) value through arbitrary port I/O
- Access SPI registers using the SPIBAR through arbitrary memory mapped I/O
- Clear FDONE/FCERR/AEL bits in the Hardware Sequencing Flash Status (HSFS) register for initialization
- Set the address value (0) to the Flash Address (FADDR) register
- Set FCYCLE and FGO bits in HSFS to start the SPI erase command
- Check FDONE & SCIP bits in HSFS to make sure that the command execution is finished
As shown in Figure 16, four drivers require to send multiple IOCTL requests for the memory mapped I/O operations above. On the other hand, two drivers (IoAccess.sys and phymem_ext64.sys) return a user-mode address pointer mapping the SPI registers in the output buffer, so a single IOCTL request is enough to erase firmware.
Figure 16: Firmware erase by exploiting Intel driver (stdcdrv64.sys)
It should be noted that the tested platform enabled the SPI flash protection settings like BIOS Lock Enable (BLE) and SMM BIOS Write Protection (SMM_BWP) described in the Eclypsium’s blog. Normally firmware modification by the SPI write command on modern systems requires another firmware-level vulnerability/flaw defeating the protection settings and Intel Boot Guard.
Figure 17: BIOS write protection settings of the tested hardware
However, the settings were not effective in preventing the erase command. After erasing, the system became unbootable since the firmware’s header was eliminated as displayed in Figure 18.
Figure 18: firmware headers before/after erasing
The EoP PoCs (eop_*.py) were implemented for the three drivers included in Table 2. They are classic token stealing exploits that read a token value of the System process in the _EPROCESS structure and write the value into the field of the Python exploit process. In Figure 19 below, a non-privileged user could run cmd.exe with system integrity level by the exploit on HVCI-enabled Windows 11.
Figure 19: EoP exploit for AMD driver (PDFWKRNL.sys) on HVCI-enabled Windows 11
Two drivers allow arbitrary virtual memory access directly for EoP. Another driver (stdcdrvws64.sys) demands two IOCTL requests per access to translate a virtual address to a physical one by MmGetPhysicalAddress then read/write data at the physical address through arbitrary memory mapped I/O.
5. Reporting
In April and May 2023, TAU reported the vulnerabilities to the vendors whose drivers had valid signatures at the time of discovery. Only two vendors fixed the vulnerabilities and the following CVEs were assigned.
Driver Name | Vendor | CVE | JVN (Japan Vulnerability Notes) | Vendor Advisory |
TdkLib64.sys | Phoenix Technologies Ltd. | CVE-2023-35841 | JVNVU#93886750 | N/A |
PDFWKRNL.sys | Advanced Micro Devices, Inc. | CVE-2023-20598 | JVNVU#97149791 | AMD-SB-6009 |
Table 3: Fixed vulnerabilities
TAU appreciates each vendor’s effort to remediate the vulnerabilities. TAU also thanks JPCERT/CC for coordinating the fixes with the vendors patiently.
6. Wrap-up
By implementing the static analysis automation script, TAU discovered 34 unique vulnerable drivers (237 file hashes) that were not recognized previously. WDM drivers are still widely used, but we can also discover and exploit vulnerable WDF drivers in a similar fashion.
While a lot of vulnerable drivers have been reported by researchers, TAU found not only old vulnerable drivers but also new ones with valid signatures. It seems likely that we need more comprehensive approaches in the future than the current banned-list method used by Microsoft. For example, a simple prevention of loading drivers signed by revoked certificates will block about one-third of the vulnerable drivers disclosed in this research.
Finally, note that the typical vendor’s fixes for vulnerable drivers are to just set the device access control, rejecting non-privileged user’s requests. It can prevent EoP, but leaves the BYOVD techniques unresolved as attackers already have administrator privilege to load kernel drivers. Therefore, TAU expects that threat actors will continue to utilize the techniques by exploiting the “not vulnerable” drivers. TAU will continue to monitor this issue.
7. Tool and PoCs
The IDAPython script and PoCs are available here. The current scope of the APIs/instructions targeted by the script is narrow and only limited to firmware access. However, it is easy to extend the code to cover other attack vectors (e.g. terminating arbitrary processes). TAU hopes that more people in the cybersecurity industry will recognize the vulnerable driver issue by utilizing this tool and hunting zero-day vulnerabilities.
8. Customer Protection
For protecting our customers, the IOCs (Indicators of Compromise) of vulnerable drivers discovered by this research are available in the “Living Off The Land Drivers” watchlist whose information is created from the website LOLDrivers. TAU provided the result with the creator Michael Haag. TAU appreciates his contributions to the security community.
The provided IOCs contain not only the vulnerable drivers but also “not vulnerable” ones that TAU identified during the research (e.g., WDTKernel.sys, H2OFFT64.sys, the fixed version of TdkLib64.sys and so on) as both can be exploited easily for the BYOVD techniques. The number of added hashes was 272 in total.