Exploit-DB #41879
Description: Microsoft Windows Kernel - 'win32k.sys' Multiple 'NtGdiGetDIBitsInternal' System Call
Type: dos
Platform: windows
Date: 2017-04-13
Author: Google Security Research

Exploit:
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1078

We have discovered two bugs in the implementation of the win32k!NtGdiGetDIBitsInternal system call, which is a part of the graphic subsystem in all modern versions of Windows. The issues can potentially lead to kernel pool memory disclosure (bug #1) or denial of service (bug #1 and #2). Under certain circumstances, memory corruption could also be possible.

----------[ Double-fetch while handling the BITMAPINFOHEADER structure ]----------

At the beginning of the win32k!NtGdiGetDIBitsInternal system call handler, the code references the BITMAPINFOHEADER structure (and specifically its .biSize field) several times, in order to correctly calculate its size and capture it into kernel-mode memory. A pseudo-code representation of the relevant code is shown below, where "bmi" is a user-controlled address:

--- cut ---
  ProbeForRead(bmi, 4, 1);
  ProbeForWrite(bmi, bmi->biSize, 1); <------------ Fetch #1

  header_size = GreGetBitmapSize(bmi); <----------- Fetch #2
  captured_bmi = Alloc(header_size);

  ProbeForRead(bmi, header_size, 1);
  memcpy(captured_bmi, bmi, header_size); <-------- Fetch #3

  new_header_size = GreGetBitmapSize(bmi);
  if (header_size != new_header_size) {
    // Bail out.
  }

  // Process the data further.
--- cut ---

In the snippet above, we can see that the user-mode "bmi" buffer is accessed thrice: when accessing the biSize field, in the GreGetBitmapSize() call, and in the final memcpy() call. While this is clearly a multi-fetch condition, it is mostly harmless: since there is a ProbeForRead() call for "bmi", it must be a user-mode address, so bypassing the subsequent ProbeForWrite() call by setting bmi->biSize to 0 doesn't change much. Furthermore, since the two results of the GreGetBitmapSize() calls are eventually compared, introducing any inconsistencies in between them is instantly detected.

As far as we are concerned, the only invalid outcome of the behavior could be read access to out-of-bounds pool memory in the second GreGetBitmapSize() call. This is achieved in the following way:

1. Invoke NtGdiGetDIBitsInternal with a structure having the biSize field set to 12 (sizeof(BITMAPCOREHEADER)).
2. The first call to GreGetBitmapSize() now returns 12 or a similar small value.
3. This number of bytes is allocated for the header buffer.
4. (In a second thread) Change the value of the biSize field to 40 (sizeof(BITMAPINFOHEADER)) before the memcpy() call.
5. memcpy() copies the small structure (with incorrectly large biSize) into the pool allocation.
6. When called again, the GreGetBitmapSize() function assumes that the biSize field is set adequately to the size of the corresponding memory area (untrue), and attempts to access structure fields at offsets greater than 12.

The bug is easiest to reproduce with Special Pools enabled for win32k.sys, as the invalid memory read will then be reliably detected and will yield a system bugcheck. An excerpt from a kernel crash log triggered using the bug in question is shown below:

--- cut ---
  DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION (d6)
  N bytes of memory was allocated and more than N bytes are being referenced.
  This cannot be protected by try-except.
  When possible, the guilty driver's name (Unicode string) is printed on
  the bugcheck screen and saved in KiBugCheckDriver.
  Arguments:
  Arg1: fe3ff008, memory referenced
  Arg2: 00000000, value 0 = read operation, 1 = write operation
  Arg3: 943587f1, if non-zero, the address which referenced memory.
  Arg4: 00000000, (reserved)

  Debugging Details:
  ------------------

  [...]

  TRAP_FRAME:  92341b1c -- (.trap 0xffffffff92341b1c)
  ErrCode = 00000000
  eax=fe3fefe8 ebx=00000000 ecx=00000000 edx=00000028 esi=00000004 edi=01240000
  eip=943587f1 esp=92341b90 ebp=92341b98 iopl=0         nv up ei pl zr na pe nc
  cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010246
  win32k!GreGetBitmapSize+0x34:
  943587f1 8b7820          mov     edi,dword ptr [eax+20h] ds:0023:fe3ff008=????????
  Resetting default scope

  LAST_CONTROL_TRANSFER:  from 816f9dff to 816959d8

  STACK_TEXT:  
  9234166c 816f9dff [PII_REDACTED] 00000065 nt!RtlpBreakWithStatusInstruction
  923416bc 816fa8fd [PII_REDACTED] 00000002 nt!KiBugCheckDebugBreak+0x1c
  92341a80 816a899d 00000050 fe3ff[PII_REDACTED]0 nt!KeBugCheck2+0x68b
  92341b04 8165af98 00000000 fe3ff[PII_REDACTED]0 nt!MmAccessFault+0x104
  92341b04 943587f1 00000000 fe3ff[PII_REDACTED]0 nt!KiTrap0E+0xdc
  92341b98 9434383e fe3fefe8 00000000 067f9cd5 win32k!GreGetBitmapSize+0x34
  92341c08 81657db6 [PII_REDACTED] 00000000 win32k!NtGdiGetDIBitsInternal+0x17f
  92341c08 011d09e1 [PII_REDACTED] 00000000 nt!KiSystemServicePostCall
  [...]
--- cut ---

The out-of-bounds data read by GreGetBitmapSize() could then be extracted back to user-mode to some degree, which could help disclose sensitive data or defeat certain kernel security mitigations (such as kASLR).

Attached is a PoC program for Windows 7 32-bit (double_fetch_oob_read.cpp).

----------[ Unhandled out-of-bounds write to user-mode memory when requesting RLE-compressed bitmaps ]----------

The 5th parameter of the NtGdiGetDIBitsInternal syscall is a pointer to an output buffer where the bitmap data should be written to. The length of the buffer is specified in the 8th parameter, and can be optionally 0. The logic of sanitizing and locking the memory area is shown below ("Buffer" is the 5th argument and "Length" is the 8th).

--- cut ---
  if (Length != 0 || (Length = GreGetBitmapSize(bmi)) != 0) {
    ProbeForWrite(Buffer, Length, 4);
    MmSecureVirtualMemory(Buffer, Length, PAGE_READWRITE);
  }
--- cut ---

We can see that if the "Length" argument is non-zero, it is prioritized over the result of GreGetBitmapSize() in specifying how many bytes of the user-mode output buffer should be locke