# Anatomy of a “Simple” NT Kernel Exploit

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](https://web.archive.org/web/20201101133930/https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/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](https://cdn.hashnode.com/res/hashnode/image/upload/v1666063686744/0211Ry7Fd.png align="left")

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.

```c
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1666063849535/oyw-dHC4x.webp align="left")
![content_1665150170904.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1666063953901/Q7WUNEb1J.png align="left")
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.

![content_1665149455861.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1666064001999/G3riXcaYE.png align="left")

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.

```c
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.
