A Look at CVE-2020-17087

Or how I failed at exploitation but mitigated it instead…

On Thursday, October 22, 2020, an interesting issue was posted on bugs.chromium.org titled “Issue 2104: Windows Kernel cng.sys pool-based buffer overflow”. There was an issue in CNG.sys, the Windows driver for Cryptography API: Next Generation, where IOCTL 0x390400 led to a function that was vulnerable to a 16-bit integer overflow. The function in question is cng!CfgAdtpFormatPropertyBlock, a function that is not exported. This particularly piqued my interest due to the following reasons:

  1. Vulnerability was in a Microsoft driver called CNG.sys
  2. Present in all versions of Windows
  3. Has the WHQL digital signature
  4. The note at the bottom of the description “We have evidence that this bug is being used in the wild.”

The last point told me that this can, and most definitely was, used to escape the sandbox and escalate the privileges to System. Let’s dive into what’s happening here.

Deep Dive

The vulnerability was published by j00ru and he gave us some pretty good information to be able to chase this down. He dropped the callstack, the code snippet responsible, and part of the !analyze command. This is the callstack we are working with so it is pretty simple to immediately hit the ground running:


He also released a PoC that we can work with which you can find here. My plan is to do dynamic analysis so I can get an idea of what the values in the PoC mean and better understand it, that way when I start modifying something or adding onto it, I don’t break anything or change the path to the vulnerable function. Special pools is needed for CNG to easily debug it otherwise you will get unique crashes from different images/processes so make sure to do the following command if you plan on using this as a walkthrough:

verifier /flags 1 /driver cng.sys

My first step will be to place a breakpoint on cng!CngDispatch, that way when I reach cng!CfgAdtpFormatPropertyBlock, I know what the parameters are that it takes. This will be especially useful for the later part of this blog post when I start talking about defense.

One thing to note is this is Windows’ Next Generation Cryptography driver so everything in the system will use it. If you place a breakpoint on it, you will hit that breakpoint a lot so make sure to set a breakpoint only when it’s under the context of your PoC otherwise you will be playing a race with your PoC and other System threads:

ba e1 /p eprocess cng!CngDispatch

Once the breakpoint is hit and I am inside CngDispatch, I tc, or trace call, and land on cng!CngDeviceControl, the next item on the callstack. CngDispatch, itself, is nothing crazy. This is where it is parsing the IOCTL information based on the IRP_MAJOR_FUNCTION’s. Since we’re using an IRP_MJ_DEVICE_CONTROL, I focused solely on that particular part of the code. Below is the relevant code, as shown in IDA, after cleaning it up to something more understandable and easier to follow:

    ULONG ulIoControlCode = currentStackLocation->Parameters.Read.ByteOffset.LowPart;
    if (METHOD_FROM_CTL_CODE(ulIoControlCode) == METHOD_OUT_DIRECT && 
        --snipped because not important--
        MappedSystemVa = Irp->AssociatedIrp.MasterIrp;
        lpInBuffer = MappedSystemVa;

        NumberOfBytes = currentStackLocation->Parameters.Read.Length;

    Irp->IoStatus.Status = CngDeviceControl(
    Irp->IoStatus.Information = nOutBufferSize;

Basically what this does is check to see if the method from the control code is of type METHOD_OUT_DIRECT and will verify that the size being used from user mode is not NULL. The vulnerable IOCTL is 0x390400, which uses the METHOD_BUFFERED method so everything inside that IF statement does not apply to us. I can skip to the code inside the ELSE statement with no worry.

Now this is where we need to start keeping track of what goes where from the PoC. The PoC becomes relevant at line 159:

if ( dwIoControlCode == 0x390400 ) 
    return ConfigIoHandler_Safeguarded(a1, a2, a3); 
goto LABEL_58;

I need to see exactly what values a1, a2 and a3 are so I check in the debugger:

kd> tc
fffff801`4dc361da e85d0a0000      call    cng!ConfigIoHandler_Safeguarded (fffff801`4dc36c3c)
kd> r rcx, rdx, r8
rcx=ffff8909a52d2000 rdx=0000000000003aab r8=ffff8909a52d2000
kd> db ffff8909a52d2000
ffff8909`a52d2000  4d 3c 2b 1a 00 04 01 00-01 00 00 00 00 00 00 00  M<+.............
ffff8909`a52d2010  00 01 00 00 00 00 00 00-03 00 00 00 00 00 00 00  ................
ffff8909`a52d2020  00 02 00 00 00 00 00 00-00 03 00 00 00 00 00 00  ................
ffff8909`a52d2030  00 04 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffff8909`a52d2040  00 05 00 00 00 00 00 00-00 06 00 00 00 00 00 00  ................
ffff8909`a52d2050  ab 2a 00 00 00 00 00 00-00 10 00 00 00 00 00 00  .*..............

Now I know a1 and a3 are pointers to the input buffer and a2 seems to be the size of 0x2aab + 0x1000. Perfect.

cng!ConfigIoHandler_Safeguarded does a series of checks and creates two allocations with BCryptAlloc and copies the contents of the input buffer into one of the newly created allocations.

BCryptAlloc is a function inside CNG that is a wrapper for ExAllocatePoolWithTag/SkAllocatePool that uses tag ‘bgnC.’

It then goes on to initialize the second allocation to zeroes but the interesting part that stood out to me is the function called after, IoUnpack_SG_ParamBlock_Header:

__int64 __fastcall IoUnpack_SG_ParamBlock_Header(PDWORD a1, unsigned int a2, PDWORD a3, _QWORD *a4)
  if ( a1 + 2 > (a1 + a2) || *a1 != 0x1A2B3C4D )
    return 1i64;

The important part I got out of it is that the value 0x1a2b3c4d, which must be some sort of magic value. If it’s not this value, it will fail and I will not hit the vulnerable code path.

Next up on the list is cng!ConfigFunctionIoHandler which takes in six parameters that I can identify inside the debugger:

kd> tc
fffff801`4dc36d10 e81b010000      call    cng!ConfigFunctionIoHandler (fffff801`4dc36e30)
kd> r rcx, rdx, r8, r9
rcx=0000000000010400 rdx=ffff8909a5919000 r8=0000000000003aab r9=ffff8909a52d2000
kd> db ffff8909a5919000
ffff8909`a5919000  4d 3c 2b 1a 00 04 01 00-01 00 00 00 00 00 00 00  M<+.............
kd> db ffff8909a52d2000
ffff8909`a52d2000  4d 3c 2b 1a 00 04 01 00-01 00 00 00 00 00 00 00  M<+.............
kd> dqs rsp + 20 l2
ffffd904`f7bbf0f0  ffffd904`f7bbf150
ffffd904`f7bbf0f8  ffff8909`a5846000

RCX is set to the second DWORD of that PoC, R8 is set to size + 0x1000, and RDX and R9 are the allocated buffers with our data in it. I’m not too worried about the other two values but let’s move on to the next step, the switch statement.

The important thing here to note is I have to make sure that we take the path to cng!_ConfigurationFunctionIoHandler. You’ll notice that it takes the high word of RCX and will go to case x based on that. RCX is based off the second DWORD of the PoC, 0x10400. If this is anything other than 0x10400, the path gets altered and I do not hit that code path. The other functions in the switch statement don’t really do anything interesting, at least from an exploitation point of view. At this point, we get out of the configuration phase of things and begin to enter the BCrypt* functions.

There are actually a lot of things going on in ConfigurationFunctionIoHandler for setting cryptography settings such as creating contexts, deleting contexts, setting contexts, configuring, etc. Lots of things going on but the loword of 0x10400, or 0x400, takes us where we need to be:

if ( a1 == 0x400 )
    return BCryptSetContextFunctionProperty(

I’m keeping this function in mind as I will be coming back to this a little later. The next function does cryptography things, but we hit cng!CfgAdtReportFunctionPropertyOperation at line 192. Now here is where all the “fun” begins now that the boring stuff is out of the way.

if ( a1 && u16Length && usDestination )
    v7 = 6 * u16Length;
    v8 = BCryptAlloc((6 * u16Length));
    v9 = v8;
    if ( v8 )

Here is where j00ru was talking about where the bug lies. There isn’t any type of input validation on the uint16 verifying that the variable will not get overflown. It’s usually good practice to do “defensive programming” when it comes to taking in any input from the user; however, Microsoft probably didn’t expect for user input? Not sure what the case is here. Anyhow, your user-defined value is multiplied by six which makes the possibility of overflowing the datatype 100%. For instance, 0x2aab * 6 is 0x10002 which is fine but is seen as “2” in memory.

The entire function is in the advisory, so now all there is to do is play with the PoC and try to get something out of it. The PoC provided will trigger a PAGE_FAULT_IN_NONPAGED_AREA. All I know is the first two parameters of the input buffer in the PoC cannot be modified otherwise I will hit different code paths. I also found modifying some of the other values made me hit different code paths as well or straight up failed. Somewhere in that buffer takes input and I’m not quite sure where at but eventually, I was able to trigger a buffer overrun after playing with sizes. I was still unable to crash with my controlled data so the next question I asked myself was, “Do I really need to use the PoC? Is there a different way to trigger this vulnerability??”.


I took a look at CNG’s exports to see if there was anything that I can use that will be beneficial and came across BCryptSetProperty; however, I did not see BCryptSetContextFunctionProperty in the exports. I checked to see if BCryptSetProperty is in the MSDN and it was, but I saw something even better…I came across the function that is in this very same call stack. The heavens opened up and shone a bright ass light directly on BCryptSetContextFunctionProperty. The prototype is shown below:

NTSTATUS BCryptSetContextFunctionProperty(
  ULONG   dwTable,
  LPCWSTR pszContext,
  ULONG   dwInterface,
  LPCWSTR pszFunction,
  LPCWSTR pszProperty,
  ULONG   cbValue,
  PUCHAR  pbValue

The values that most interested me are cbValue and pbValue because:

Contains the size, in bytes, of the pbValue buffer. This is the exact number of bytes that will be stored. If the property value is a string, you should add the size of one character to also store the terminating null character, if needed.     
The address of a buffer that contains the new property value.

All I needed to do was plug in values required by the API and hope by setting the cbValue to a malicious size will crash my system. The theory checks out, but as many of you know it doesn’t always check out practically until…

*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *

An exception happened while executing a system service routine.
Arg1: 00000000c0000005, Exception code that caused the bugcheck
Arg2: fffff8041d2d17c0, Address of the instruction which caused the bugcheck
Arg3: fffff80422182920, Address of the context record for the exception that caused the bugcheck
Arg4: 0000000000000000, zero.

Debugging Details:

READ_ADDRESS:  ffff810b6dcf5000 Special pool 


IMAGE_NAME:  cng.sys 


FAULTING_MODULE: fffff8035ea30000 cng 

PROCESS_NAME:  CVE-2020-17087.exe 

TRAP_FRAME:  ffffbb8cdd49e770 -- (.trap 0xffffbb8cdd49e770) 
NOTE: The trap frame does not contain all registers. 
Some register values may be zeroed or incorrect. 
rax=0000000000000020 rbx=0000000000000000 rcx=ffff810b6dcf5000 
rdx=ffff810b6dcf4ff0 rsi=0000000000000000 rdi=0000000000000000 
rip=fffff8035ea92503 rsp=ffffbb8cdd49e900 rbp=0000000000002aab 
 r8=0000000000002aa9  r9=0000000000000002 r10=fffff8035eac7e70 
r11=ffffbb8cdd49e850 r12=0000000000000000 r13=0000000000000000 
r14=0000000000000000 r15=0000000000000000 
iopl=0         nv up ei ng nz ac po nc 
fffff803`5ea92503 668901          mov     word ptr [rcx],ax ds:ffff810b`6dcf5000=???? 
Resetting default scope

Well that’s nice!

I bluescreened the machine but I soon realized that the call stack is different from what I was expecting:


It goes from BCryptSetContextFunctionsProperty, transfers to the kernel in NtDeviceIoControlFile, hops into KsecDispatch, then transfers to Cng and then crashes at the vulnerable function, only this time, crashing with my data:

0: kd> db ffff810b`6dcf5000 - 0n16
ffff810b`6dcf4ff0  34 00 31 00 20 00 34 00-31 00 20 00 34 00 31 00  4.1. .4.1. .4.1.

Or crashing trying to read from an address that doesn’t exist but points to my data:

CONTEXT:  fffff80422182920 -- (.cxr 0xfffff80422182920)
rax=0031003400200031 rbx=0000000000000001 rcx=00000000f27e7ffd
rdx=0031003400200031 rsi=ffffae0feebecfe0 rdi=ffffae0feebe7000
rip=fffff8041d2d17c0 rsp=ffff85045597ee70 rbp=ffffae0fe6602280
 r8=0000000004060002  r9=412fae0e1b5fe029 r10=0000000000000000
r11=ffffae0fe6602290 r12=0000000000000000 r13=0000000000000000
r14=ffffae0feebece10 r15=0000000000000003
iopl=0         nv up ei pl nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00050202
fffff804`1d2d17c0 8b4af8          mov     ecx,dword ptr [rdx-8] ds:002b:00310034`00200029=????????
Resetting default scope

PROCESS_NAME:  CVE-2020-17087.exe

Either way, I control it 🙂

Finally after playing with sizes, I was greeted with the following error:

A kernel component has corrupted a critical data structure.  The corruption
could potentially allow a malicious user to gain control of this machine.
Arg1: 000000000000001d, Type of memory safety violation
Arg2: fffffe8e45e3ec30, Address of the trap frame for the exception that caused the bugcheck
Arg3: fffffe8e45e3eb88, Address of the exception record for the exception that caused the bugcheck
Arg4: 0000000000000000, Reserved
rax=0031003400200030 rbx=0000000000000000 rcx=000000000000001d
PROCESS_NAME:  CVE-2020-17087.exe
ERROR_CODE: (NTSTATUS) 0xc0000409 - The system detected an overrun of a stack-based buffer in this application. This overrun could potentially allow a malicious user to gain control of this application.


Unfortunately, I was not able to get anywhere outside of this. This particular vulnerability stumped me if I were perfectly honest, but the fun didn’t stop here for me. If I can’t exploit it, would I be able to at least protect from it??? Let’s continue…


This bug was disclosed October 22 and one of the comments mentioned there will not be a fix until November 10th, which lies on the second Tuesday of that month, Patch Tuesday, but what happens if you’re a target of X actor? What’s stopping them from exploiting that issue if one of your users likes to click on weird links despite all the awesome, fun Security Awareness Training?? I may be reaching, I may not be reaching but you never know. Anyways, that’s 19 days of possible exploitation and hoping that nothing happens. Here is a solution I would do…

The Game Plan

What I do know is the root cause of this vulnerability is an integer overflow so what is there to do? Here are a couple thoughts that immediately came to mind:

  • Inline hook the vulnerable function itself

If I hook the dispatch routine function, I would need to re-implement the entire function. This would be the most simple route in terms of hooking, but can end very badly if I screw up even once as I am totally messing with the System’s cryptographic functions. All I need is one tiny mistake and my ass gets Straight Up Clapped™. Totally possible, but extremely risky and I’m pretty prone to messing up. Another interesting point is the fact that hooking CNG’s dispatch table will only be specific to the PoC provided by Project Zero and here’s why: I would need to parse the DeviceIoControl parameters and extract the size. This works perfectly with the PoC, but not so much when you trigger it using the BCrypt functions specifically because of the different code path it takes as you seen a few paragraphs up. This will 100% get bypassed and I would get laughed at and mocked on Twitter because that’s what InfoSec Twitter likes to do now.

If I place an inline hook on the vulnerable function, it significantly lowers my chances of screwing up because I am patching the function itself by adding a check to size. The downside to this is CfgAdtpFormatPropertyBlock is not an exported function so creating a signature for it is a must if I would like to resolve it dynamically. You can add the offset to the base address but I personally don’t like static anything.

Ultimately I went with inline hooking CfgAdtpFormatPropertyBlock because it only makes sense to go the easier, more efficient route that would stop everything in it’s tracks regardless of what code path your PoC takes and mostly because I have a lesser chance of screwing things up! 🙂

The game plan now is to create a driver that will basically do the following:

  1. Hijack the device object of CNG.sys to easily resolve its base address
  2. Create a signature for CfgAdtpFormatPropertyBlock
  3. Create the hooking stub
  4. Dynamically resolve CfgAdtpFormatPropertyBlock
  5. Patch it
  6. Unhook the function if the driver gets unloaded

The signature I created was based off of CNG in 20h2:

This was more than enough bytes to uniquely identify cng!CfgAdtpFormatPropertyBlock.

The stub for my hook is shown below:

UINT8 __stub_detour[] =
    0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	/// mov rax, address
    0xff, 0xd0,							/// call rax
    0x0f, 0x1f, 0x00						/// filler

The stub is pretty self explanatory. The address of my hook will get copied into the empty eight bytes during runtime and placed into RAX and then get called. I chose to use a CALL simply for the fact that I can easily go back to the original function using a RET. Totally makes life easier for me in my opinion. Anyways, I end up with 12 bytes but end up having to pad with three bytes due to it partially overwriting an instruction.

Next thing I had to plan out was how I was going to patch this function in a safe manner. CNG.sys is only going to have read permissions, not read-write so how am I going to write to something without causing a bluescreen? One way to do so is by getting the value of Control Register 0 and disabling the Write Protection bit (CR0[WP]). The downside to this is this is per CPU state, meaning if I have CR0[WP] disabled on processor 0 and a thread migrates to processor 1 and tries to patch cng!CfgAdtpFormatPropertyBlock, I will bluescreen because CR0[WP] is not disabled on processor 1. I would need to prevent thread migration to another CPU for this to work, patch CfgAdtpFormatPropertyBlock, and then re-enable CR0[WP]; however, this involves modifying kernel components which is what I am trying to avoid.

I can also try disabling the Read Only bit in the Page Table Entry, but that will also require modifying kernel components. One thing I can do is double map memory using Memory Descriptor Lists, or MDL’s. This seems to fit the bill perfectly for the goals I set for myself so that’s what I opted for. Plus it’s safer! I came up with the following:

PMDL pmdl = IoAllocateMdl(targetAddress, length, FALSE, FALSE, NULL);
if (pmdl != NULL)
        MmProbeAndLockPages(pmdl, KernelMode, IoModifyAccess);
        /// error handling
    PVOID mappedPage = MmMapLockedPagesSpecifyCache(pmdl, KernelMode, MmNonCached, NULL, FALSE, NormalPagePriority);
    if (mappedPage != NULL)
        /// write to memory and clean up
        /// error handling

What this is doing is allocating an MDL with the address of cng!CfgAdtpFormatPropertyBlock with x bytes, the size of my hooking stub. I use MmProbeAndLockPages to page in and lock that section of memory and lastly, I use MmMapLockedPagesSpecifyCache to map the physical pages that are described by the MDL created with IoAllocateMdl. Basically what I am doing is remapping the virtual address of cng!CfgAdtpFormatPropertyBlock into a secondary range that points to the same set of physical pages with read/write permissions.

Now that the MDL is mapped, did I accomplish the goal I set for myself? Check it out:


Lastly, all that is left to do is develop the actual code to protect against this vulnerability after verifying we can indeed write to kernel memory. I will post the code and then do a simple walk through:


__detoured_function proc
        add rsp, 8			; adjust rsp to load the correct values
        mov rax, rsp			; restore the first overwritten instruction
        sub rsp, 8			; readjust for the ret
        mov qword ptr [rax + 8h], rbx	; restore the second overwritten instruction
        mov qword ptr [rax + 10h], rbp	; restore the third overwritten instruction
        mov qword ptr [rax + 18h], rsi	; restore the fourth overwritten instruction
        cmp dx, 2aabh
        jge status_unsuccessful

        xor rcx, rcx			; force it to fail
__detoured_function endp


I needed to begin restoring the instructions that I have overwritten for this hook to function correctly but I also need to take into account RSP being slightly modified due to the use of CALL. It is important that I know what I’m working with that way when I do hook it, I know what the registers are expecting. Take a look at the stack when that function is normally called:

1: kd> dps rsp l5
ffffd586`90a26978  fffff802`44291e39 cng!CfgAdtReportFunctionPropertyOperation+0x22d
ffffd586`90a26980  00000000`00000000
ffffd586`90a26988  00000000`00000001
ffffd586`90a26990  000000ed`00000000
ffffd586`90a26998  ffffd586`90a26a80

As long as I have cng!CfgAdtReportFunctionPropertyOperation+0x22d placed into RAX, I will be good. I adjust RSP by 8 on lines 4 through 6 to get exactly what RAX is expecting and then readjust it again to continue forward. The important parts are lines 10 and 11. The current state of the registers at this point are shown below:

1: kd> r
rax=0000000000000000 rbx=0000000000000000 rcx=ffffc206e9a85000
rdx=0000000000002aab rsi=ffffc206e9a85000 rdi=0000000000000001
rip=fffff8024429245c rsp=ffffd58690a26978 rbp=ffffd58690a26a80
 r8=ffffd58690a269c8  r9=ffffd58690a26ed0 r10=0000000000000004
r11=0000000000000000 r12=ffffc206e9a85000 r13=ffffd58690a26f38
r14=0000000000000003 r15=ffffd58690a26f28

RDX is holding the allocation size. If DX is greater than or equal to 0x2aab, I will jump to status_unsuccessful. The original cng!CfgAdtpFormatPropertyBlock function will check to ensure the source buffer, length and destination string parameters are not NULL and if it is, it will bail with either STATUS_INVALID_PARAMETER or STATUS_INVALID_RESOURCES. The easiest thing to do is just fail the first check rather than going the extra mile and failing with something like STATUS_INTEGER_OVERFLOW and then exiting. I do this by setting RCX to NULL, forcing the program to fail the check with STATUS_INVALID_PARAMETER and then exit.

After much troubleshooting, nuisances and overlooking tiny things in no particular order, everything finally worked perfectly!

Bing, bang, boom as the wifey likes to say. Vulnerability mitigated…at least until Patch Tuesday.



In today’s post, we went over the CNG vulnerability disclosed from Project Zero and looked into the mechanics of the PoC to be able to modify it for exploitation. We found a different way to trigger this vulnerability, only this time we were able to control the data that it crashes on. Eventually we were able to go from a PAGEFAULT_IN_NONPAGED_AREA crash to a STACK_BUFFER_OVERRUN crash, w00t! I then go over my thought process and steps on how we could tackle this problem to circumvent this vulnerability in case anyone was actively being targeted by X actor while having to wait for a patch that will come out nearly three weeks later. Although I failed at successful exploitation, we did succeed in protecting against a PoC that would be successful in exploitation.

I hope you enjoyed reading it as much as I did writing this! As I always say, have fun and keep kicking ass! If you are interested in the code, you can get to it here. It contains code for both PoC and Driver.

Leave a Reply

Your email address will not be published. Required fields are marked *