
(1) IQVW32.sys before 1.3.1.0 and (2) IQVW64.sys before 1.3.1.0 in the Intel Ethernet diagnostics driver for Windows allows local users to cause a denial of service or possibly execute arbitrary code with kernel privileges via a crafted (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, or (d) 0x80862007 IOCTL call.
(1) IQVW32.sys before 1.3.1.0 and (2) IQVW64.sys before 1.3.1.0 in the Intel Ethernet diagnostics driver for Windows allows local users to cause a denial of service or possibly execute arbitrary code with kernel privileges via a crafted (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, or (d) 0x80862007 IOCTL call.
This repository contains a write-up of the vulnerability in question, along with proof-of-concept exploits functional on 64-bit Windows 7 SP1 and Windows 10 20H2. The driver file can be located in the Driver Files directory. If you discover any typos throughout the write-up/paper, or if you would like to see certain details with a more elaborate description, please create an issue on the repository! I will fix them as soon as possible.
The motivation behind writing an exploit for this device driver in particular is solely because it is currently being abused in the wild to load an attacker's unsigned root-kit. Using the BYOVD (Bring Your Own Vulnerable Driver) method, malware can check if it is running with elevated privileges, drop a copy of the vulnerable device driver, load the driver, and subsequently exploit it to gain kernel code execution to load the root-kit. I was unable to successfully reverse engineer the malware sample, so I took it upon myself to create the exploit.
Samples spotted in the wild: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
The device driver, namely iqvw64e.sys, is a driver designed to perform network adapter diagnostics. It allows the user-mode component to interact with the device driver to perform a plethora of kernel routines by exposing a few IO control codes (also known as IOCTLs), with a "sub" IO control code provided in the user's input buffer during the interaction. The IO control code that will be used to hit the vulnerable code-path is 0x80862007. In addition the primary control code, the aforementioned "sub" IO control codes that will be covered in this analysis will be the 0x33 code to hit the memmove function call, and the 0x30 code to hit the memset function call code-paths. This write-up will not be covering any details regarding the DriverEntry routine, as there is enough documentation on Microsoft's Documentation page to give you a thorough explanation.
To start, we want to know how we can interact with this particular device driver in the first place. The most common means of communicating with a device driver is through the usage of a function named DeviceIoControl. The general idea behind this function is that we can pass a valid driver handle created by CreateFileA, pass in an IO control code that corresponds to the kernel routine we want, pass in a structure (or buffer) that it is expecting, and it will return data in our output buffer. While routines like these can be at times necessary (e.g. accessing model-specific registers for overclocking purposes), they also pose a serious risk to security. But... how?
In the case of CVE-2015-2291, the vulnerability can be triggered by an unprivileged user. Because there are no santization checks present, and administrator privileges are not required to exploit the vulnerability, this poses a security risk. What lies underneath these two flaws, is the ability to fully control the memset and the memmove function calls exposed by the IO control code interface. Remember the aforementioned function DeviceIoControl from before, how we're able to pass in a structure that will be used in a kernel routine? This is how it all comes together.
Let's take a step back. We first want to obtain the driver handle that is related to the vulnerable device driver. Even before this though, we need to locate the correlating named device object. These are exposed to user-space by a symbolic link (commonly hard-coded), which can be found using WinObj, part of the [SysInternals suite]. While we could use a string dumping utility to dump the symbolic link, or alternatively reverse engineer the device driver, I simply loaded the device driver and located it using WinObj. The symbolic link found to be in relation to the device driver is \\.\GLOBALROOT\Device\Nal. To obtain the driver handle, we need to call the CreateFileA function and have it return a valid driver handle for us to use later in the process. The code for this process is as follows:
if (h_nal == (HANDLE)-1)
{
printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);
We will be using the driver handle later in the exploitation process. For now, we will begin the preparation of our exploit. The next step would be to load the ntdll.dll library using the LoadLibraryA function to return a module handle, so we can dynamically locate the functions we need. While the ntdll.dll library may already be loaded into our process, we nonetheless need to obtain a handle to the library that we can use. The functions that we need for exploitation are NtQuerySystemInformation to leak the base address of the NT Kernel (with medium process integrity) for later in the exploitation process, and the NtQueryIntervalProfile function to trigger the vulnerability. As for the code to load the ntdll.dll library, it is as follows:
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);
Now that we have obtained a handle to the library, we will start off by locating the NtQueryIntervalProfile function. To start, we will need a type definition for this function, as it is undocumented. While you can find the type definition online, I have provided it here for easier access:
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);