Advanced analysis of the 2010-11-24 local Windows kernel exploit

24/7, 365, party is always on my mind and doing security research too! Ah, my life is just beautiful. Getting up at 11:30 to go to sleep again =) Reading news, emails, doing some security research work, and still not giving a about the federal court investigating against me since more than a year without any result. This time I am writing about the new local kernel exploit which was just published 2 days ago, and attacks XP, Vista and 7, 32-bit as 64-bit. Originally it was published on codeproject and hours later the article was taken down.

Peter Kleissner, Software Development Guru

Copy of the original article
Elevation of privileges under Windows Vista/7 (UAC Bypass) 0day ( entry)

The proof of concept driver

The local Windows kernel exploits a vulnerability in NtGdiEnableEudc(), which can be exploited even from non-elevated rights (as non-administrator). In the "poc.cpp" (from the package) there is an embedded driver which is the "payload":

BYTE DrvBuf[] = {
      0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
      0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

This can be easily extracted from the compiled executable. This poc-driver does nothing more than elevating the rights of a driver of a running cmd.exe process to those of services.exe. In fact the driver code is exactly based on my proof of concept "command line privilege escalation driver" from Black Hat 2009. In the chinese community that code is quite popular, see here and here (look for my last name to reveal who wrote the source).

The code works by getting the current process through IoGetCurrentProces() and going through the list until services.exe is found, and copying the security token there and overwriting the one of cmd.exe (which in fact elevates it). For the different OS versions there will be the correct offset selected for the fields in the _EPROCESS structure (this is IDA pro):

It is very characteristic for my code that I use PsGetVersion first, because RtlGetVersion is only available with XP+. For this exploit this does not make much sense here, because the exploit poc is for Vista/7 only. Also you see the DbgPrint in case the OS version is not recognized. The only real change to my code is that it does KfRaiseIrql and KfLowerIrql around the code that copies the security token.

Interestingly, like with TDL, Sinowal, ZeuS, Stuxnet before, the driver contains debug information which reveals the project (development) path:


It does not reveal a lot in this case but still can be considered as sensitive information.

The exploit

The problem is that the Windows function EnableEUDC() (and NtGdiEnableEudc) thinks a registry key has the type REG_SZ, it does not verify it (that is the whole problem). Subsequently in the kernel there will be a UNICODE_STRING structure allocated and the address of it passed to RtlQueryRegistryValues() which should fill the value.

typedef struct _UNICODE_STRING {
  USHORT Length;	
  USHORT MaximumLength;
  PWSTR  Buffer;

Now what happens is that RtlQueryRegistryValues fills the input parameter DestinationString with binary data, rather than initializing the Unicode string and interpreting the structure members. That means if we have for example the binardy data 11 11 22 22 33 33 33 33 44 44 44 44 it would be filled as follows:

typedef struct _UNICODE_STRING {
  USHORT Length = 1111h;
  USHORT MaximumLength = 2222h;
  PWSTR  Buffer = 333333h;
44444444h <------ buffer overflow!

Which means we can write outside the buffer, and given the structure is allocated on the stack, we can manipulate the stack. The next important thing is the stack trace and the stack frame:

GDI32.EnableEUDC ->
NtGdiEnableEudc ->
GreEnableEUDC ->	
sub_BF81B3B4 ->
sub_BF81BA0B ->

.text:BF81BA0B sub_BF81BA0B    proc near               ; CODE XREF:
sub_BF81B3B4+B2 p
.text:BF81BA0B DestinationString= LSA_UNICODE_STRING ptr -20h
.text:BF81BA0B var_18          = dword ptr -18h
.text:BF81BA0B var_14          = dword ptr -14h
.text:BF81BA0B KeyHandle       = dword ptr -10h
.text:BF81BA0B var_C           = dword ptr -0Ch
.text:BF81BA0B var_8           = dword ptr -8
.text:BF81BA0B Path            = dword ptr -4
.text:BF81BA0B arg_0           = dword ptr  8
.text:BF81BA0B arg_4           = word ptr  0Ch
.text:BF81BA0B                 mov     edi, edi
.text:BF81BA0D                 push    ebp
.text:BF81BA0E                 mov     ebp, esp
.text:BF81BA10                 sub     esp, 20h

The important thing is the variable DestinationString, which will be overwritten with binary data (and has the type UNICODE_STRING). This is the code of win32k.sys (Windows 7, 32 bit), which slightly differs with other Windows versions. For that reason the exploit code has to check for the version and use the right offsets (for the variables) in the exploit. The DestinationString variable is -20h on the stack (at the bottom), so the frame would look like:

DWORD Argument 2
DWORD Argument 1
++ (higher address)
DWORD Return Address
DWORD Original EBP
DWORD Variable 0
DWORD Variable 1
-- (lower address)

This stack information is extremely important, because we need to overwrite the return address to jump from the kernel to our own code (and thus exploiting the kernel). There is now one important thing, the binary data doesn't go immediately to &DestinationString, but to +8 of that address. The RtlQueryRegistryValues documentation says,

Nonstring data with size, in bytes, > sizeof(ULONG)

 	The buffer pointed to by EntryContext must begin with a signed LONG value. The magnitude of the value must specify the size, in bytes, of the buffer. If the sign of the value is negative, RtlQueryRegistryValues will only store the data of the key value. Otherwise, it will use the first ULONG in the buffer to record the value length, in bytes, the second ULONG to record the value type, and the rest of the buffer to store the value data.

The stack looks like: 20h stack variables, + original ebp, + return eip. The binary data comes to +8 at the bottom of the stack variables, so effectively we have to patch the dword at +18h (skipping stack variables) + 4h (skipping ebp), which is the final value 1Ch. If we check now the code of this public open source exploit (RegBuf is the binary registry data that is going to be stored in the registry, pMem the address of the shellcode):

			*(DWORD*)(RegBuf + 0x1C) = (DWORD)pMem;

Now there is one last thing. We overwrite the stack variables, which is not really nice. After calling RtlQueryRegistryValues(), there are still operations done, the most important one is this, right before returning from the function:

.text:BF81BB9B                 movzx   eax, [ebp+DestinationString.Length]
.text:BF81BB9F                 push    eax
.text:BF81BBA0                 push    [ebp+DestinationString.Buffer]
.text:BF81BBA3                 movzx   eax, [ebp+arg_4]
.text:BF81BBA7                 push    eax
.text:BF81BBA8                 push    [ebp+arg_0]
.text:BF81BBAB                 call    _wcsncpy_s

The function wants to copy the string. Now, there will be unexpected values in Length and Buffer, so this would cause undefined behaviour. We cannot control those 2 variables (they are set by RtlQueryRegistryValues(), but we can change arg_0 and arg_4, the two function parameters. They are located on the stack after the return eip. If we overwrite them with zeros, thanks to safe string functions, wcsncpy_s will verify them (and recognizes them as illegal) and returns. All we have to do is increasing the binary data size from 20h to 28h, which is also done in the code (ExpSize is the size of the binary registry data):

			ExpSize = 0x28;

The entire exploit is really just setting up the "fake" registry key (containing binary data) and firing up EnableEUDC. According to Prevx, this security flaw (not checking the type of this certain registry key) is available with all Windows operating systems, making it definitely to the bug of the year. One last thing, the original poc fails to initialize the registry buffer properly (should be filled with zeros), which could fail the exploit (depending on what was before in the memory, if wcsncpy accepts it or not). In fact it was crashing my Vista with BAD_POOL_CALLER, and worked fine with 7 in my testings.