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
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 :
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.
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 UmReplyObject
because 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.
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 BOOLEAN
logic 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.