Kernel operation: CVE-2020-17382 MSI Ambient Link Driver is being armed

Kernel operation: CVE-2020-17382 MSI Ambient Link Driver is being armed

Preamble – Why are drivers still an important goal?

The kernel is, without any euphemism, complex software, and Windows is no exception. Since it is one of the most difficult to learn due to the lack of source code and undocumented APIs, it is now documented to a greater extent thanks to the enormous efforts of the research community. Unfortunately, the kernel has also recently become more complex and improved the way it is protected. So why attack drivers? Besides the drivers provided by Microsoft, third-party drivers are the only and more available tool for third parties to force code execution in the zero ring. Starting with the anniversary update of Windows 1607, you can now only download signed drivers that have passed WHQL certification, and as a result, creating and delivering your own bug door is no longer an easy task. For more information on the entire driver signing process, click here (

In this post I would like to talk about “low-hanging fruit”; these are hidden and still undetected vulnerabilities in signed and trusted production drivers that are too widely used on consumers and corporate endpoints.

Exploitable kernel driver vulnerability can lead unprivileged user to SYSTEM privilege only because vulnerable driver is locally available to anyone. (Well, sometimes vulnerable driver or kernel component can catch even remotely – EternalBlue?



My goal here is to illustrate the general approach to performing initial error analysis using IDA and WinDbg, run a simple fuzzing test and then construct a bottom-up exploit.
The exploit I create is based on this vulnerability discovered by Lucas Dominikow from CoreSecurity that affects the MSI Ambient Link driver. Even though I was able to create a robust but not so elegant exploit for Windows 7 with Service Pack 1 (SP1), we will only focus on Windows 10.
This is the two builds of Windows 10 that I tested for both 1709 and 2004:



I decided not to publish the 2004 version because it is almost identical to the 1709 version, except for the offsets of ROP gadgets. PoC can be found on my Github.

Driver Anatomy .

In other words, a driver is nothing more than a loadable kernel module, which means that if it is not written as fully independent, it will require some kind of custom analogue to interact with it. Before a real funk can start, a user mode application must have a valid driver descriptor, which is just a secure way to communicate with ring0.

Device objects.

As we will soon see, a custom application calls the CreateFile function to get a valid descriptor. This function accepts a symbolic link, which in our case is an element of DEVICE_OBJECT. This device object is created by the driver itself as a communication channel available for any user application and, not surprisingly, it can increase the attack surface if access is not controlled properly.

The actual device object is controlled by Windows I/O manager, and once a correct request is made, it will return the actual device descriptor to the user mode application.




DriverEntry and Driver Object

DriverEntry, which is present in each driver, is the entry point for the driver. It can be considered as the “main” driver function, similar to the classic main function of a user mode application.

The first argument accepted by the DriverEntry function is the DRIVER_OBJECT structure. This structure, created by the kernel and not fully initialized, is then passed to the DriverEntry subroutine. After loading, the driver can fill the structure with its own functions. The second argument of RegistryPath is simply a pointer to a string in the registry, where we can pass the configuration parameter key to the driver.


NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {.
    NTSTATUS status;

    // device object
    UNICODE_STRING devName = RTL_CONSTANT_STRING (L"\\Device\\uf0DeviceObject");
    PDEVICE_OBJECT DeviceObject;
    NTSTATUS status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    // symlink
    UNICODE_STRING symLink = RTL_CONSTANT_STRING (L"\\??\\uf0SymLink");
    status = IoCreateSymbolicLink(&symLink, &devName);

From the above fragment we can also notice that the DriverEntry procedure is responsible for creating the DeviceObject and a symbolic link that is ultimately provided to a user application to communicate with the driver. Note the Io prefix of both API interfaces used by the driver, IoCreateDevice and IoCreateSymbolicLink, which indicate membership in the I/O Manager area.

Core functions .

The functions to be initialized by the driver are actually called Dispatch Routines, which are an array of function pointers within a MajorFunction member of the DRIVER_OBJECT structure. This array shows which functions the driver supports; a brief list of the most common ones is given below.









We can consider each of them as an analogue of standard user mode functions in kernel mode. For example, IRP_MJ_CREATE is equivalent to the CreateFile, IRP_MJ_CLOSE CloseFile and so on.

For the sake of our operation goals we will now move our attention to IRP_MJ_DEVICE_CONTROL as it is used as a “function manager” for most of the internal driver functions. We may notice that each major function has an IRP prefix in its name: this is because these functions are designed to handle different types of IRP. Cool, isn’t it? but we haven’t mentioned anything about IRP yet, right?


As we have seen before, interaction with the driver from the user code is carried out with the help of exported DLL functions and I/O manager, which is the highest control of driver requests and from drivers. For each request, the I/O Manager creates an IRP packer, which is an opaque structure partially documented in MSDN. The driver will receive an IRP packet addressed to itself and send it back to IoManager after the procedure is complete.

As we have seen before, once a user mode application receives a valid driver descriptor, it can start communicating with the driver by sending IRP packets which eventually contain the IOCTL code which will invoke a specific driver procedure. The whole process can be simplified in the following way:




Now that we have some knowledge about the driver back-end, let us try to analyze the vulnerable driver and see if we can use it as a weapon using a full exploit.

MSIO64.sys under the microscope

Using our new toolset, we can create a common approach to what to look for when looking for vulnerabilities in drivers. Here are some things I recommend to check when working with them:

– The driver allows users with low privileges to interact directly with it.

– The import address table (IAT) contains MmMapIoSpace or ZwMapViewOfSection.

– It has some customizable memmove or a known insecure function

Note: this is just a basic starting point, which can certainly be extended.

To check the first point, we need to check the driver’s DACL (discretionary access control) list. We can use this very handy tool called DeviceTree from OSR.

We just need to find the right symbolic link and check the DEV tab nested under DRV. From there we should open the “Security Attributes” window and check which privileges the group “All” has. The good news is that the object of MsIo device provides any access between itself and any user, which means that the driver can be accessed even from a low integrity process. This is a very good pre-requisite to start with.





I would like to talk about the two critical functions of MmMapIoSpace and ZwMapViewOfSection, as they are absent in our target driver. At this point, suffice it to say that they can be used to map kernel memory to the user process (and yes, this can be extremely dangerous).

Then, any function that deals with the user mode buffer must perform an edge check, otherwise we will encounter a classic buffer overflow, which is the same error that affects MSIO64.sys.

Reverse and debug driver

Let’s imagine that we have no idea what the message says except that we are dealing with a normal buffer overflow that will trigger at some point after a particular function matches the specified IOCTL code.

Before we start searching for the IOCTL, we have to start analyzing the driver from the beginning, or in other words, with DriverEntry.




We immediately notice that this pre-function is just a plug pointing to RealDriverEntry, which I successfully renamed. Let’s move on to that:


From RealDriverEntry we can take the right line of the symbolic link, which in our case is \\\Device\\MsIo, and write it down. We can also look at the fifth line, where the case offset rdi points to DriverObject.




Moving to the next code block, we see that the address of the main handler of the function sub_113F0 is copied to the register rax, which is then referred to by rdi/DriverObject several times with different offsets, for example 0x68,0x70,0x80,0xE0. We can get an accurate reference by checking these offsets against the DRIVER OBJECT symbol in WinDBG.

0: kd> dt _DRIVER_OBJECT


+0x068 DriverUnload : Ptr64 void

+0x070 MajorFunction : [28] Ptr64 long

We can already guess that the missing values 0x80 and 0xE0 are offsets within the MajorFunction procedure itself, along with 0x70, which is the first argument. Since 0x80 is 16 bytes from the first argument, we can derive all the dispatch procedures under consideration:

1: kd> !drvobj MSIO64 2


Dispatch routines:

[00] IRP_MJ_CREATE fffff880055b63f0 MSIO64+0x13f0


[02] IRP_MJ_CLOSE fffff880055b63f0 MSIO64+0x13f0


[0e] IRP_MJ_DEVICE_CONTROL fffff880055b63f0 MSIO64+0x13f0

Now let’s look at the real nature of the driver, that is, the sub_113F0 procedure, also known as the main function handler, which I renamed MsIoDispatch.


It should be noted that the register rdi indicates the structure of IRP, which is accessed at offsets 0x0b8 (CurrentStackLocation) and 0x38 (IoStatus.Information).

We can also double check this information dynamically with WinDbg. Let’s place a breakpoint at the very beginning of MajorFunctionHandler.

1: kd> u MSIO64+13f0


fffff802`4b4313f0 488bc4 mov rax,rsp

1: kd> bp MSIO64+13f0

And make sure that we have the right information:

0: kd> dt nt!_IRP @rdx Tail.Overlay.CurrentStackLocation->*

+0x078 Tail :

+0x000 Overlay :

+0x040 CurrentStackLocation :

+0x000 MajorFunction : 0xe ”.

+0x001 MinorFunction : 0 ”.

+0x002 Flags : 0x5 ”.

+0x003 Control : 0 ”.

+0x008 Parameters : <anonymous-tag>

+0x028 DeviceObject : 0xffff8083`f7d97a70 _DEVICE_OBJECT

+0x030 FileObject : 0xffff8083`fe9a2ba0 _FILE_OBJECT

+0x038 CompletionRoutine : (null)

+0x040 Context : (null)

0: kd> dt nt!_IRP @rdx Tail.Overlay.CurrentStackLocation->Parameters.DeviceIoControl.

+0x078 Tail :

+0x000 Overlay :

+0x040 CurrentStackLocation :

+0x008 Parameters :

+0x000 DeviceIoControl :

+0x000 OutputBufferLength : 0

+0x008 InputBufferLength : 0x80

+0x010 IoControlCode : 0x80102040

+0x018 Type3InputBuffer :(null

From above we can detect IoControlCode 0x80102040: taking into account the IOCTL value is useful when debugging / fusing a new driver for vulnerabilities, because we can quickly define a target internal function.

Okay, I think we have completely rebuilt and debugged the pre-driver part, so we can take a look at the one we like most: the vulnerable function. But we don’t know yet (or pretend not to know) which sub-program is affected, right? Well, let’s find out. How? First, we need to find all the IOCTLs used by MajorFunction, and then we can either check the functions in the IDA, or we can do a phasing.

Getting the IOCTL list is pretty easy and we can use this plugin which I recently moved to Python3 and IDA Pro. The method of calculating the IOCTL is not flawless, but we may have some idea about them:




Apart from the initial value 0x2, which completes the main function via IRP_MJ_CLOSE, the remaining four seem to be valid IOCTL codes displaying internal functions.

Now that we have taken all the IOCTL, we can pass these values into this very minimal phase inspired by Jaime Geiger’s idea.

Mandatory parameters for the phaser are the device symbolic reference name, comma-separated IOCTL values, and the input buffer length.

To simplify the task, we can stick to one IOCTL and a buffer size of 1000 bytes.

C:\> python3 -d \\.\MsIo -i 0x80102040 -l 1000

This simple test was enough to start the error checking immediately, and after analyzing the call stack frame we see that the return address of the main function was overwritten by the letters A of our phaser.


2: kd> k

# Child-SP RetAddr Call Site

00 ffffa687`18b16608 fffff802`23c12802 nt!DbgBreakPointWithStatus

01 ffffa687`18b16610 fffff802`23c12087 nt!KiBugCheckDebugBreak+0x12

02 ffffa687`18b16670 fffff802`23b768d7 nt!KeBugCheck2+0x937

03 ffffa687`18b16d90 fffff802`23b907db nt!KeBugCheckEx+0x107

04 ffffa687`18b16dd0 fffff802`23b821ce nt!KiDispatchException+0x16202b

05 ffffa687`18b17480 fffff802`23b80234 nt!KiExceptionDispatch+0xce

06 ffffa687`18b17660 fffff802`231816b9 nt!KiGeneralProtectionFault+0xf4

07 ffffa687`18b177f8 41414141`41414141 MSIO64+0x16b9

Armed with knowledge of the vulnerable IOCTL, we can analyze the relevant procedure in the IDA, where the actual comparison is made.


Which will eventually lead to this branch, which calls the custom version of the memmove function.



As shown in the Core Security recommendations, we also proved that there is no limit check for the size of the source buffer stored in the rdx register, which is 72 bytes away from the function return pointer. Without any doubt, we can say that this will be our landing zone for exploits.


Executing MSIO64.sys

Now that we know how to get control over the instruction pointer in Ring0, we can start developing POC. The shell code we are going to use is a standard token stealing code which aims to increase the privileges of the current (or other) process by stealing a SYSTEM process token. As I said, I’m not going to go into detail about Windows 7, but it did give me some good ideas about how important it is to restore a process in the kernel world. I will then focus on that topic before upgrading to Windows 10.

Windows 7

After playing with this exploit for some time in Windows 7, I finally managed to get the whole shell code to work, but not without a huge loss: I couldn’t find a stable way to restore the original thread and spin the rest of the code in the call stack. Each attempt resulted in a hard Bug Check.

Then I put on some stronger glasses and came up with a few “what if”. What if we could find a way to freeze the kernel thread with the ability to raise the level of another process? The problem is that we can’t let that process jump to the kernel area without stopping the whole system. We must find a way to either restore execution or make the thread safe.

Then I started to configure the shell code / exploit to copy the stolen SYSTEM token to an arbitrary process, e.g. to another open cmd prompt that can be passed as a command line argument. This way, we could care a bit less about the fate of the startup process, as we have already raised the privilege of the target process when error checking occurs. Yes, but how can we prevent BSOD? Suddenly, I remembered that Matt Miller had written a cool article about Ring0 payload, so I trusted her, which was soon rewarded. Among many other interesting payloads, there was a rather unexpected discovery: the solution I found consisted of only two bytes, namely \xEB\xFE or JMP 0x0. This trick is quite old and is not limited only to kernel shell codes. After increasing the privileges of the target process, by adding these instructions to the end of the shellcode, we will allow the source thread to rotate endlessly, preventing the system from crashing. This is by no means an elegant solution, but it works at least in Windows 7 (Windows 10, which probably has a better watchdog, will eventually detect a wasteful kernel thread and cause an error check).

To exploit this vulnerability as a weapon, we just need to place the shell code to steal tokens in the user space buffer, and since Windows 7 has almost no protection against threats, our exploit will simply have to overwrite the return address with a shell code pointer.

Here is the exploit in action:



Windows 10 1709


Bypassing SMEP and other protection against kernel exploits


Unlike Windows7, Windows 10 uses several kernel-level exploits, for example:


– Kernel Mode Code Signing (KMCS)


– Supervisor Mode Execution Prevention (SMEP)

– Kernel Address Space Layout Randomization (KASLR)

– Kernel Patch Protection (KPP, also known as Patch Guard)

– Control Flow Guard (CFG)

– Virtualization Based Security (VBS)

Apart from the latter two, CFG and VBS, we are going to study how we can bypass or avoid the other four protection methods, of which SMEP seems to be the least easy to overcome.

Before testing the latest and best version of Windows 10, I first decided to try everything on 1709, also known as Redstone 3 because it is well documented in terms of protection and circumvention techniques.

Returning to the protection, let’s take a look at it individually along with a specific circumvention.


Well, the driver is already signed, so there really is no bypass, however we can claim that stopping the download of an unsigned driver may be the initial obstacle to blocking simple evil drivers, but does not provide an additional guarantee to prevent a missigned driver from downloading.

Much has already been written and documented about SMEP, so we can summarize this as protection aimed at stopping the execution of any user shell code from the kernel mode context. In a typical kernel operation scenario, after getting control over the instruction pointer through the Ring0 vulnerability (i.e. in the driver), it is convenient to switch to the user shellcode, e.g. token theft, instead of creating a more complex kernel payload.

SMEP is implemented in the 20th bit of the CR4 control register and is checked whenever code that is in user mode is executed from the kernel context.



If this is the case, a 0x000000fc error is displayed along with the “ATTEMPTED TO EXECUTE NOEXECUTE MEMORY” message blocking any attempted operation. Despite the protection, this popular bypass method is described in Economy/Nissim and has been widely used since then, and we will not make any exceptions here.

It is planned to use some ROP gadgets from ntoskrnl to disable the SMEP bit before switching to our shellcode. In fact, we will only need two gadgets to achieve our goal: one to load the desired CR4 value into the general-purpose register, and one to load the desired value into the cr4 itself.

pop rcx ; ret # store the desired value into rcx

[target cr4 value]

mov cr4, ecx ; ret # load the new value into cr4

The original cr4 value may vary depending on the host system / hypervisor and supported CPU functions. In our case we got 1506f8:

0: kd> .formats cr4


Hex: 00000000`001506f8

Decimal: 1378040

Octal: 00000000000005203370

Binary: 0000000000 0000000000 0000000000 000101 00000110 11111000

Chars: ……..

Time: Fri Jan 16 23:47:20 1970

Float: low 1.93105e-039 high 0

Double: 6.80842e-318

To get the SMEP bypass value, we just need to flip the 20 bit, which is the leftmost unit set in the above fragment. The result is a value of 506f8. Now we can look for ROP gadgets and use rp++ to find the ones we need from the ntoskrnl.exe version we are testing, and we’re done. That’s all – and yes, SMEP is so easy to get around, at least on Redstone 3.


Based on a medium integrity process such as an unprivileged user shell, we can use the handy Psapi EnumDeviceDrivers API to get the base address of the nt core. We will need the base address to calculate the ROP devices needed to bypass SMEP later. This is not a real traversal because KASLR is designed to protect mainly processes with low integrity such as browsers: in this case it will be impossible to request Psapi or NtQuerySystemInformation from such integrity level. However, in a standard local EoP script such as ours, KASLR can be ignored.


Kernel Patch Protection AKA Patch Guard will bark and show a blue screen if it detects a fake critical core structure. So yes, since we are dealing with a CR4 register, there is a chance to provoke KPP. However, we know that KPP is triggered at random intervals, so if we recover CR4 quickly enough, we can avoid triggering any blue screen at all.

Windows 10 2004 – new obstacles ahead?

As the last exercise, I thought about moving the exploit to the latest official version of Windows 10 2004 as of September 2020. At first I thought it was just a matter of recalculating the gadget shift, and then I will get what I need. On the contrary: without VBS enabled I got the 0xfc boot code, although the 20 bit CR4 was turned off. What’s interesting is that I saw how it works properly on another CPU/HyperVisor. I decided not to spend more time on this issue, but it looks like this exploit may or may not work, depending on the hypervisor or hardware installed below.

Update 29.09.2020

Suddenly, it made more sense after Alex Ionescu pointed to the right path, which indicated that the behavior I’ve observed so far on the build 2004 is due to the Meltdown KVA Shadow protection that Microsoft introduced in March 2018. Then I quickly found this great blog post from Blue Frost Security, which confirmed that SMEP was checking me from the beginning. Microsoft took advantage of two PML4s, and set the PML4 user mode as “Unexecutable” when called from the kernel context. This protection is also known as SMEP in software because it is provided by the OS memory swap structure (PML4) rather than by a CPU component (CR4 register). Strangely enough, I managed to avoid this protection in WinDBG without knowing about technical implementation: comparing PTE record from 1709 and 2004 versions, I noticed that they are identical, except for PML4 flags (PXE in WinDBG terminology). Here is the 2004 custom build mode shellcode page:

1: kd> !pte 000001f3fe2a000000

VA 000001f3fe2a0000

PXE at FFFF57ABD5EA018 PPE at FFFF57ABD403E78 PDE at FFFF57A807CFF88 PTE at FFFF500F9FF1500

8A000012F1B867 contains 0A000057D9C867 contains 0A0000000E01D867 contains 01000017504847

pfn 12f1b —DA–UW-V pfn 57d9c —DA–UWEV pfn e01d —DA–UWEV pfn 17504

And we can see the missing executable flag in PXE/PML4E, which means that the NX bit is set. Meltdown protection aims to protect any process with low or medium integrity, which explains why the exploit is successful from the administrator shell. Then, I managed to write a fast PoC based on Blue Frost Security, which bypasses both hardware and software SMEP, using the Meltdown error to leak PML4 virtual addresses and clearing the NX flag on PML4E. PoC will be online soon (thanks to Rui for being a great sparring partner).

In the meantime, we can outline the steps necessary to bypass the SMEP software, which consist of disabling the NX bit set in user shell mode PML4E:



– Leak VA PML4 via Meltdown, as shown in Blue Frost Security.


– When we have PML4, we can get the VA offset of our PML4E shellcode by the following formula:

INT64 getPML4EfromVA(INT64 ua_va) {

int pml4e_offset = ((ua_va >> 39) & 0x1ff) * 8;

return pml4e_offset;

This offset value should be added to the base PML4 address obtained earlier through a leak.


– The last step is to disable the NX bit in the PML4 shellcode record by extending the ROP chain with gadgets like this:

pop rcx; ret;


pop rdx ; ret


and qword [rcx], rdx ; ret

Performing the AND operation between the PML4 record loaded in RCX and the 0x0FFFFFFFFFFFFFF mask, we end up clearing four high bits, including the NX bit.

It is also worth mentioning that the protection for Meltdown is temporary, because the error will not affect the fresher processors. As a quick check using the SpecuCheck Alex Ionescu tool, we can compare two identical Windows 10 releases running on different processors.









We can notice on the right machine itself that Kernel VA Shadowing is marked as unnecessary: once the CPU is detected and marked as invulnerable at boot time, KVA protection will not be enabled and the SMEP software will not run, which means that once the latest and best CPU versions are run at scale, only a hardware-based SMEP traversal is required.

Related thoughts

The WHQL process does not confirm that this driver is error-free: as documented by Eclypsium, a huge number of unpatched signed drivers are amazing. On the other hand, HVCI and VBS stop all these types of attacks because they detect any register-level intervention, such as a change in the CR4 control register. And since the latest version of Windows 10 20H1, VMWare has started offering virtualization-based security support: then we can predict that the attack surface will soon be smaller because VBS will stop any SMEP shutdown attempt by detecting any real-time changes to the CR4 bit.

More information can be found here:…vilege-exploit-for-cve-2017-0005/?source=mmpc. SMEP bypass U=S.pdf…eering-of-windows-kernel-drivers-3115b2efed83.



WARNING! All links in the articles may lead to malicious sites or contain viruses. Follow them at your own risk. Those who purposely visit the article know what they are doing. Do not click on everything thoughtlessly.


0 0 vote
Article Rating
Notify of
Inline Feedbacks
View all comments

Do NOT follow this link or you will be banned from the site!
Would love your thoughts, please comment.x

Spelling error report

The following text will be sent to our editors: