The September 2020 security update from Microsoft includes a fix for a kernel information leak vulnerability (CVE-2020-16854) that was reported by us in May 2020. This blog post discusses the vulnerability.


Overview

If the Intel Last Branch Record processor feature is available, an attacker running user mode code with low integrity may leak the address of the kernel and thus bypass kernel address space layout randomisation (KASLR). Such an information disclosure can be used to assist the exploitation of a kernel mode vulnerability, for example during a sandbox escape.


Branch Tracing

Intel CPU's provide several mechanisms for tracing the control flow transfer instructions executed by the processor. Two of these features are of interest, specifically single stepping on branching and last branch records. These features are exposed through the IA32_DEBUGCTL model specific register (MSR). Last branch records (LBR) are exposed through bit 0 of the IA32_DEBUGCTL MSR and when set will cause the processor to record recent branches, interrupts and exceptions taken by the processor. Single stepping on branching (known as BTF) is exposed through bit 1 of the IA32_DEBUGCTL MSR and when set will cause the processor to treat the Trap Flag (TF) of the EFLAGS register as single step on branch instructions rather than the default TF behaviour which is single step on all instructions. For more information, see the section 17.4.1 in the “Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3B: System Programming Guide, Part 2

Therefore by setting the LBR and BTF bits in the IA32_DEBUGCTL MSR in conjunction with the TF flag in the EFLAGS register, the processor will issue a debug exception for every branch, interrupt or exception taken and a record of the source and destination of the branch will be recorded in the LBR stack and available for inspection.


Branch Tracing on Windows

As it is not possible for user mode code to access MSR’s, the Windows kernel exposes the above branch tracing features to user mode through an undocumented mechanism whereby the LBR and BTF bits in the IA32_DEBUGCTL MSR are effectively proxied through the DR7 register. Specifically setting bit 8 in DR7 will cause the kernel to set the LBR bit in the IA32_DEBUGCTL MSR and setting bit 9 in DR7 will cause the kernel to set the BTF bit in the IA32_DEBUGCTL MSR. This allows user mode code to enable both single step on branching and last branch records. The kernel will make available the last branch source and destination pointers held in the LBR stack via the LastBranchFromRip and LastBranchToRip members of a threads CONTEXT structure.

To avoid leaking branch records containing kernel mode pointers back into user mode code, the kernel will sanitise the last branch pointers copied over to a CONTEXT structure. This occurs in nt!KeCopyLastBranchInformation:

void __cdecl KeCopyLastBranchInformation(__int64 p1, __int64 p2)

{

       // ...

 

       *(p1 + 1200) = *(p2 + 280); // LastBranchToRip

       *(p1 + 1208) = *(p2 + 280); // LastBranchFromRip

       *(p1 + 1216) = *(p2 + 296); // LastExceptionToRip

       *(p1 + 1224) = *(p2 + 296); // LastExceptionFromRip

       if ((unsigned char)(*(p2 + 368) & 0x1) != 0) {

              idx = 4;

              p_ptrval = p1 + 1200; // get the &ctx.LastBranchToRip

              while (1) {

                     // Is the pointer a kernel address?

                     if ((*p_ptrval & 0x7FFFFFFFFFFFFFFF) > 0x7FFFFFFEFFFF) {

                           *p_ptrval = 0; // NULL out the kernel address

                     }

                     if (idx == 1) {

                           return;

                     }

                     idx -= 1;// loop 4 times

                     (unsigned char*)p_ptrval += 8; // advance to next pointer...

              }

       }

 

       // ...

}


The Bug

While the above is effective to prevent kernel mode pointers leaking back into user mode code, the kernel has a second mechanism to provide last branch record information to user mode – via the exception parameters of an EXCEPTION_RECORD structure. The function nt!KiDebugTrapOrFault will issue the STATUS_SINGLE_STEP exception when the processor generates a debug exception. If the BTF bit and the LBR bit in DR7 have been set then KiDebugTrapOrFault will copy over the last branch records source and destination pointers over to the first two entries in the exception records ExceptionInformation array, as shown in the disassembly below.


These pointers are not sanitised in the same way that nt!KeCopyLastBranchInformation will sanitise the last branch pointers. The failure to sanitise these two values (R9 and R10 above) can therefore leak kernel mode pointers back to user mode; for example, when a user mode process performs a system call, the system call will return from kernel mode back to user mode via a sysret instruction. The user mode process will receive a single step exception after the sysret is executed and the exception record will expose the last branch source pointer being that of the location of the sysret from ntoskrnl.

Proof of Concept

A proof of concept (PoC) program to demonstrate the issue is available here. Below we can see how running the PoC leaks the address of the ntoskrnl image in kernel memory.


We can observe from the PoC that the last branch from pointer held in the CONTEXT structure has been nulled out to prevent the kernel pointer leaking, however the last branch from pointer held in the ExceptionRecord is available and leaks the kernel pointer.

Disassembling ntoskrnl.exe (Via Relyze Desktop of course!) shows the location we have leaked is that of the sysret instruction which was used by the kernel to return to user mode process after receiving a system call from that process.


Note #1: Some Intel processors will not provide either user or kernel pointers in the last branch records, as seen below. It is unclear if all CPU family's support the required last branch from/to MSR's, looking at volume 3 of the Intel software dev manual, section 35 omits these MSR for some family's, and some family's are not present in the tables.


Note #2: A hypervisor may choose to prevent the Intel performance monitoring features being made available in a guest system. If the LBR processor feature is not available to the guest OS then an attacker cannot leverage LBR to leak a kernel address. By default, Hyper-V will disable LBR in a guest. The Windows 10 Sandbox feature will allow LBR in its OS instance. A guide on how Hyper-V handles Intel performance monitoring is available here. It is unknown how other hypervisors handle LBR.

The Patch

By diffing the ntoskrnl from the September 2020 patch we can quickly examine the fix applied to nt!KiDebugTrapOrFault which resolves the issue described in this blog post.


The LastBranchFromRip and LastBranchToRip are compared against nt!MmUserProbeAddress (Which has a value of 0x7FFFFFFF0000 on x64) and if the last branch pointers are above this value they are changed to be the MmUserProbeAddress value instead, preventing a kernel address leaking back to user mode.