Anatomy of a “Simple” NT Kernel Exploit

Anatomy of a “Simple” NT Kernel Exploit

Some words of discouragement if you're looking to research NT Kernel vulnerabilities and think it's going to be easy

·

4 min read

If you’ve been reading closely to my recent blogposts, alot of them are about the exploitation of Windows machines. While this gives the perception that hacking Windows machines are easy, the analysis of the following exploit will prove that its anything but.

In CVE-2020-1034, an elevation of privilege vulnerability exists in ntoskrnl.exe the way that the Windows Kernel handles objects in memory. An attacker who successfully exploited the vulnerability could execute code with elevated permissions. One can download a patched and unpatched versions of this module. The analysis of this exploit was carried out on VmWare Fusion 12 running Windows 10 1903 x64.

A cursory glance of bindiff of ntoskrnl.exe version 18362.1049 and 18362.1082 reveals the following :

content_1665149428748.png

It was clear that EtwpNotifyGuid was changed, but what part was changed and why?

The Vulnerability

First, we need to examine NtTraceControl **which is the gateway to the EtwpNotifyGuid.

This function is the central control point for Event Tracing For Windows (ETW). It supports many user-mode API functions for managing tracing sessions. Even the private tracing sessions that are implemented mostly in user mode need some support from the kernel and get it from this function.

The FunctionCode that leads to the call of EtwpNotifyGuid is 0x11, and a few checks need to be satisfied before the actual call.

case 0x11:
    if (v16 < 0x48 || v17 != 72 || *((_DROWRD *)v9 + 1) != v16)
        goto LABEL_117
    if ( *(_DWORD *)v9 == 3 )
    {
        if (v16 < 0x78)
            goto LABEL_117;
        LOBYTE(v24) = 1;
        v21 = EtwpEnableGuid(v35, v9, v24);
        LODWORD(v34) = 72;
    }
    else
    {
        LOBYTE(v24) = 1;
        v21 = EtwpNotifyGuid(v35, v9, v24);
        LODWORD(v34) = 72;
    }
    goto LABEL_33;

Then, I analyzed the function in question. If we take a careful look at EtwpNotifyGuid, we will discover some interesting items at the spot where the patch was made.

content_1665138186509.webp content_1665150170904.png The rdi register contains the address of the input buffer. The diagram says the byte at address rdi+0Ch decides whether to create a UmReplyObject. The initial value of r12b was 4 but was reset to 1. So when the value of byte ptr [rdi+0Ch] equals to 1, the qword at rdi+18h **is set to the address of newly created UmReplyObjectbecause otherwise the qword will remain intact. This is dangerous because the input can never be trusted.

The input buffer is passed to EtwpSendDataBlock **and EtwpQueueNotification. Looking into EtwpQueueNotification, we can see where the UmReplyObject was referenced.

content_1665149455861.png

The value of bl is zero. If the byte at rbp+0Ch is not 0, then the qword at rbp+18h is read as a pointer to an object. And the reference of the object is increased.

LONG_PTR __stdcall ObfReferenceObject(PVOID Object)
{
    volatile signed __int65 *v1; // rsi
    signed __int64 BugCheckParameter4; // rbx

    v1 = (volatile signed __int64 *)Object;
    if (ObpTraceFlags)
        ObpPushStackInfo((char *)Object + 0xFFFFFFD0);
    BugCheckParameter4 = _InterlockedIncrement64(v1 - 6);
    if (BugCheckParameter4 <= 1)
        KeBugCheckEx(0x18u, 0i64, (ULONG_PTR)v1, 0x10ui64, BugCheckParameter4);
    return BugCheckParameter4;
}

The culprit for the exploit was the inconsistent comparison of the byte at rbp+0C. In the bindiff done in EtwpNotifyGuid, the value was compared to 1 to decide whether to create a new UmReplyObject. But in the last comparison, the value was compared to zero.

While the unpatched version looks like if (*(bool*)(rdi+0x0C) == true), the patched code might look like if (*(bool*)(rbp+0x0C)).

If the value is other than 1 or 0, an arbitrary value will be taken as an object address. Then, ObfReferenceObject is called for it which means the operation qword ptr [[InputBuffer + 0x18] - 0x30] ++ is carried out and an arbitrary address increment was achieved.

The function doesn’t account for all possible values of a specific input parameter (ReplyRequested) and for values other than 0 and 1 will treat an address inside the input buffer as an object pointer and try to reference it, which will result in an increment at ObjectAddress - offsetof(OBJECT_HEADER, Body). The root cause is essentially a check that applies the BOOLEANlogic of “!= FALSE”in one case, while then using “== TRUE” in another. A value such as 2 incorrectly fails the second check, but still hits the first.

One of the things I intended to demonstrate in this blog post is that "simple" exploits are becoming increasingly rare. A vulnerability like CVE-2020-1034, which is simple to comprehend and exploit, requires a lot of work and knowledge of the internals of the Windows kernel to turn into an exploit that doesn't immediately crash the machine. It also requires even more work to make the exploit into anything usable.