CVE-2011-1282: User-Mode NULL Pointer Dereference & co.

After a short break, today I would like to present the details of another Windows CSRSS vulnerability, fixed during the recent Microsoft Patch Tuesday cycle (advisory MS11-056) – CVE-2011-1282, also called CSRSS Local EOP SrvSetConsoleLocalEUDC Vulnerability. Although not as spectacular as the previous one (see: CVE-2011-1281: A story of a Windows CSRSS Privilege Escalation vulnerability), I strongly consider the nature and reason of the flaw’s existence not less interesting than the lack of a basic sanity check in winsrv!SrvAllocConsole. Have fun!

Introduction

Before lurking into the strictly technical details related to the considered vulnerability, I would like to discuss some of its general charateristics. As stated in the original Microsoft Security Bulletins, the issue severity is Important in the context of Windows XP and 2003 (marked as the Elevation of Privileges class), and Low (Denial of Service conditions) on newer system platforms. This particular difference between the XP and Vista impacts is going to be addressed later in this post.

Furthermore, the product vendor explains the cause of the vulnerability existence in the following way:

What causes the vulnerability?
This is a memory corruption vulnerability that can allow code execution in the system context. A NULL pointer is passed without validation to a function in the CSRSS. This allows the CSRSS to write to a NULL page in its process space. On Windows XP systems, under certain conditions, this page can then be leveraged by an attacker to execute code with increased permissions.

The above is, however, not entirely true, as the NULL Pointer Dereference is not the reason of the bug, but rather a direct consequence of several other subtle assumptions, and one major implementational error present in the CSRSS code.

Last but not least, it is worth noting that the affected service itself – SrvSetConsoleLocalEUDC – is highly related to the EUDC (End-user-defined characters) Windows functionality. According to my research, the flaw can be only triggered on the CJK (Chinese-Japanese-Korean) language Windows editions, on their default configuration. Additionally, the issue does not pose a serious threat for an ordinary user, considering that even an ideal software platform does not guarantee successful exploitation (which remains a state-of-art, or hardly possible).

Having some background information about the severity, class and cause of the vulnerability, let’s face some details :-)

Inside SrvSetConsoleLocalEUDC

According to the Windows CSRSS API Table, the SrvSetConsoleLocalEUDC service has been present on all Windows editions, starting from Windows 2000, up to Windows 7. The service is handled by a corresponding routine within the Windows Subsystem process – winsrv!SrvSetConsoleLocalEUDC, where the discussed vulnerability actually resided. Interestingly, the functionality can be reached through an exported kernel32.dll function:

(...)
SetConsoleIcon                        7C87507F 743 
SetConsoleInputExeNameA               7C871DC8 744 
SetConsoleInputExeNameW               7C81B075 745 
SetConsoleKeyShortcuts                7C872D89 746 
SetConsoleLocalEUDC                   7C8756D9 747 <=====
SetConsoleMaximumWindowSize           7C88023B 748 
SetConsoleMenuClose                   7C872E50 749 
SetConsoleMode                        7C81AF10 750 
SetConsoleNlsMode                     7C8760F9 751
(...)

but its specification is nowhere to be found in the MSDN library. Given the circumstances, we will have to reverse engineer the routine definitions, together with the meaning of each single parameter – this will most likely come in handy during the development of a functional exploit. At this point, let’s take a closer look at the winsrv!SrvSetConsoleLocalEUDC routine assembly:

.text:75B51B3B ; int __stdcall SrvSetConsoleLocalEUDC(LPBYTE msg, LPBYTE reply)
.text:75B51B3B
_SrvSetConsoleLocalEUDC@8 proc near     ; DATA XREF: .text:75B38B10o
.text:75B51B3B
.text:75B51B3B
HandleDescriptor= byte ptr -0Ch
.text:75B51B3B
dst_buf         = dword ptr -8
.text:75B51B3B
src_buf         = dword ptr -4
.text:75B51B3B
msg             = dword ptr  8
.text:75B51B3B
reply           = dword ptr  0Ch
.text:75B51B3B
.text:75B51B3B                
mov     edi, edi
.text:75B51B3D                
push    ebp
.text:75B51B3E                
mov     ebp, esp
.text:75B51B40                
sub     esp, 0Ch
.text:75B51B43                
push    esi
.text:75B51B44                
mov     esi, [ebp+msg]
.text:75B51B47                
lea     eax, [ebp+msg]
.text:75B51B4A                
push    eax
.text:75B51B4B                
push    dword ptr [esi+LPC_MSG.hConsole]
.text:75B51B4E                
call    _ApiPreamble@8  ; ApiPreamble(x,x)
.text:75B51B53                
test    eax, eax
.text:75B51B55                
jl      error_return
.text:75B51B5B                
mov     eax, large fs:18h
.text:75B51B61                
mov     eax, [eax+TEB.CsrClientThread]
.text:75B51B64                
mov     eax, [eax+CSR_THREAD_INFO.Unknown]
.text:75B51B67                
lea     ecx, [ebp+HandleDescriptor]
.text:75B51B6A                
push    ecx
.text:75B51B6B                
push    40000000h
.text:75B51B70                
push    2
.text:75B51B72                
push    dword ptr [esi+LPC_MSG.hOutput]
.text:75B51B75                
push    dword ptr [eax+UNKNOWN.Process]
.text:75B51B78                 
call    _DereferenceIoHandle@20 ; DereferenceIoHandle(x,x,x,x,x)
.text:75B51B7D                
test    eax, eax
.text:75B51B7F                
jge     short output_valid
.text:75B51B81                
mov     esi, eax
.text:75B51B83                 
jmp     error_return

As shown above, the handler starts off with verifying the console handle – LPC_MSG.hConsole – and output handle – LPC_MSG.hOutput – specified by the client. If either of these checks fails, further service execution is aborted.

.text:75B51B88 output_valid:                           ; CODE XREF: SrvSetConsoleLocalEUDC(x,x)+44j
.text:75B51B88                
movsx   eax, word ptr [esi+LPC_MSG.Coords.Y]
.text:75B51B8C                
push    edi
.text:75B51B8D                
push    eax
.text:75B51B8E                
movsx   eax, word ptr [esi+LPC_MSG.Coords.X]
.text:75B51B92                
push    8
.text:75B51B94                
add     eax, 7
.text:75B51B97                
pop     ecx
.text:75B51B98                
cdq
.text:75B51B99                
idiv    ecx
.text:75B51B9B                
lea     edi, [esi+LPC_MSG.RandomString]
.text:75B51B9E                
push    eax
.text:75B51B9F                
push    edi
.text:75B51BA0                
push    esi
.text:75B51BA1                
call    ds:__imp__CsrValidateMessageBuffer@16 ; CsrValidateMessageBuffer(x,x,x,x)
.text:75B51BA7                
test    al, al
.text:75B51BA9                
jz      short status_invalid_parameter

Next than, the “RandomString” input buffer is validated, based on the COORD structure fields (both controlled by the client). The assembly snippet can be translated into the following C code:

if(CsrValidateMessageBuffer(msg,
                            &msg->LPC_MSG.RandomPointer,
                            ((msg->LPC_MSG.Coords.X+7)/8),
                            msg->LPC_MSG.Coords.Y) == FALSE)
{
  // bail out with STATUS_INVALID_PARAMETER
}

The sanity check is performed in the first place, because RandomPointer is expected to point at a buffer, localized inside the CSRSS shared section, that is used to exchange large amounts of data between the subsystem process and its clients (for a more detailed explanation of the CSRSS communication channels, see Windows CSRSS Write Up: Inter-process Communication (part 2/3)). By investigating the function’s parameters, we can easily conclude that the second parameter is a pointer to the address to validate, while the third and fourth ones indicate the amount of items contained in the buffer, and the size of a single item. Obviously, we want the validation to be successful; thus, we need to take a look at the specific csrsrv!CsrValidateMessageBuffer implementation:

.text:75B14421 ; int __stdcall CsrValidateMessageBuffer(LPBYTE msg, PVOID *Pointer, ULONG Size, ULONG Count)
.text:75B14421                
public _CsrValidateMessageBuffer@16
.text:75B14421
_CsrValidateMessageBuffer@16 proc near  ; CODE XREF: CsrSrvClientConnect(x,x)+3Dp
.text:75B14421
.text:75B14421
msg                = dword ptr  8
.text:75B14421
Pointer        = dword ptr  0Ch
.text:75B14421
Size            = dword ptr  10h
.text:75B14421
Count           = dword ptr  14h
.text:75B14421
.text:75B14421                
mov     edi, edi
.text:75B14423                
push    ebp
.text:75B14424                
mov     ebp, esp
.text:75B14426                
cmp     [ebp+Count], 0
.text:75B1442A                
push    esi
.text:75B1442B                
push    edi
.text:75B1442C                
mov     edi, [ebp+msg]
.text:75B1442F                
mov     esi, [edi+18h]
.text:75B14432                
jz      short error_return
.text:75B14434                
or      eax, 0FFFFFFFFh
.text:75B14437                
xor     edx, edx
.text:75B14439                
div     [ebp+Count]
.text:75B1443C                
mov     ecx, [ebp+Size]
.text:75B1443F                
cmp     ecx, eax
.text:75B14441                
ja      short error_return
.text:75B14443                
mov     eax, [ebp+Pointer]
.text:75B14446                 
imul    ecx, [ebp+Count]
.text:75B1444A                
mov     edx, [eax]
.text:75B1444C                
test    edx, edx
.text:75B1444E                
jnz     short further_checks
.text:75B14450                
test    ecx, ecx
.text:75B14452                 
jz      short success_return
(…)

In other (than assembly) words, the function first makes sure that the (Size * Count) multiplication doesn’t result in an integer overflow, and than checks the following condition:

if((*Pointer == NULL) && (Size * Count == 0))
  return TRUE;

It is possible to satisfy both of the above prerequisites, by:

  1. Setting the LPC_MSG.RandomPointer value to NULL,
  2. Setting the LPC_MSG.Coords.X value to one of {-14, -13, … , -1, 0} OR
    Setting the LPC_MSG.Coords.Y value to 0.

By following these steps, it is possible to pass the input buffer verification, and successfully execute the remainder of the winsrv!SrvSetConsoleLocalEUDC routine:

.text:75B51BAB                 mov     al, [esi+LPC_MSG.Byte2]
.text:75B51BAE                
mov     byte ptr [ebp+src_buf], al
(…)
.text:75B51BD5                
push    [ebp+dst_buf]
.text:75B51BD8                
push    [ebp+msg]
.text:75B51BDB                
call    _IsEudcRange@8; IsEudcRange(x,x)
.text:75B51BE0                
test    eax, eax
.text:75B51BE2                
jz      short status_invalid_parameter
.text:75B51BE4                
push    dword ptr [edi]
.text:75B51BE6                
push    dword ptr [esi+LPC_MSG.Coords]
.text:75B51BE9                 
push    [ebp+dst_buf]
.text:75B51BEC                
push    [ebp+msg]
.text:75B51BEF                
call    _RegisterLocalEUDC@16 ; RegisterLocalEUDC(x,x,x,x)
.text:75B51BF4                
test    eax, eax
.text:75B51BF6                
jl      short loc_75B51C07
.text:75B51BF8                
mov     eax, [ebp+msg]
.text:75B51BFB                
mov     eax, [eax+Console.EudcEnabled]
.text:75B51C01                
mov     dword ptr [eax], 1

Two bytes present in the input packet (LPC_MSG.Byte1 and LPC_MSG.Byte2) are converted to a single UNICODE character (stored in a local dst_buf variable). Afterwards – the converted value is checked against the currently registered EUDC character ranges – by default, only set on the CJK Windows localizations. If the unicode character lies within one of the EUDC ranges (i.e. winsrv!IsEudcRange returns TRUE), the routine proceeds straight into the following call:

RegisterLocalEUDC(msg, dst_buf, msg->LPC_MSG.Coords, msg->LPC_MSG.RandomPointer);

Please keep in mind, that in order to get to this point, the Coords and RandomPointer fields must meet the previously mentioned conditions (RandomPointer = NULL, …). After a short investigation of the internal winsrv!RegisterLocalEUDC symbol, the following part of the routine turns out to be crucial, in terms of potential security flaws:

.text:75B5F8CA loc_75B5F8CA:                           ; CODE XREF: RegisterLocalEUDC(x,x,x,x)+15j
.text:75B5F8CA                
push    1
.text:75B5F8CC                
push    dword ptr [ebp+Coords]
.text:75B5F8CF                 
call    _CalcBitmapBufferSize@8 ; CalcBitmapBufferSize(x,x)
.text:75B5F8D4                
test    eax, eax
.text:75B5F8D6                
mov     ecx, [ebp+RandomPointer]
.text:75B5F8D9                
jz      short nothing_to_do
.text:75B5F8DB
.text:75B5F8DB
loc_75B5F8DB:                           ; CODE XREF: RegisterLocalEUDC(x,x,x,x)+36j
.text:75B5F8DB                
not     byte ptr [ecx]
.text:75B5F8DD                
inc     ecx
.text:75B5F8DE                
dec     eax
.text:75B5F8DF                
jnz     short loc_75B5F8DB

Simply enough, the presented code boils down to calling winsrv!CalcBitmapBufferSize, in order to compute some kind of size (let’s call it BufferSize) based on the input COORD structure, and than perform a NOT operation on each of the first BitmapSize bytes, pointed to by RandomPointer. One, last look at the CalcBitmapBufferSize implementation is enough to spot the vulnerability, and consider possible exploitation scenarios:

.text:75B5DE1B ; __stdcall CalcBitmapBufferSize(x, x)
.text:75B5DE1B
_CalcBitmapBufferSize@8 proc near       ; CODE XREF: SetRAMFontCodePage(x)+99p
.text:75B5DE1B                                         
; SetRAMFontCodePage(x)+153p …
.text:75B5DE1B
.text:75B5DE1B
arg_0           = dword ptr  8
.text:75B5DE1B
arg_4           = dword ptr  0Ch
.text:75B5DE1B
.text:75B5DE1B                
mov     edi, edi
.text:75B5DE1D                
push    ebp
.text:75B5DE1E                
mov     ebp, esp
.text:75B5DE20                
movsx   eax, word ptr [ebp+arg_0]
.text:75B5DE24                
xor     ecx, ecx
.text:75B5DE26                
cmp     [ebp+arg_4], 1
.text:75B5DE2A                
setnz   cl
.text:75B5DE2D                
lea     ecx, ds:8[ecx*8]
.text:75B5DE34                
lea     eax, [ecx+eax-1]
.text:75B5DE38                
sar     eax, 3
.text:75B5DE3B                
dec     ecx
.text:75B5DE3C                
not     ecx
.text:75B5DE3E                
sar     ecx, 3
.text:75B5DE41                
and     eax, ecx
.text:75B5DE43                
movsx   ecx, word ptr [ebp+arg_0+2]
.text:75B5DE47                
imul    eax, ecx
.text:75B5DE4A                
pop     ebp
.text:75B5DE4B                
retn    8
.text:75B5DE4B
_CalcBitmapBufferSize@8 endp

Given the fact that the second parameter is a constant value equal one, we can simplify the above snippet into the following formula:

return (InputCoord.Y * ((InputCoord.X+7) >> 3)));

Due to the fact that both X and Y are signed numbers, and are fully controlled by the attacker (despite the constraints, forced by csrsrv!CsrValidateMessageBuffer), it is possible to make CalcBitmapBufferSize return the following numeric ranges:

  1. {0, 1, … , 0x8000}
  2. {0xffff8000, 0xffff8001, … , 0xffffffff}

Neither of these ranges should normally be returned as a valid buffer size, because the RandomPointer address was NOT tested against such lengths (it was not tested, at all). After receiving a size belonging to one of the above sets, the following (previously mentioned) loop executes:

while(BufferSize--)
{
  *RandomPointer = ~(*RandomPointer);
  RandomPointer++;
}

Notably, RandomPointer is still pointing at a zero-address – otherwise, it would be impossible for the code execution to reach this point. Under normal circumstances, trying to access a NULL address would result in immediate application crash. It turns out, however, that the CSRSS process has the NULL-page (and several successive pages, as well) correctly mapped into physical memory, on pre-Vista Windows versions. As a result, it becomes possible to either modify the bytes placed within the first eight virtual memory pages (via the NOT operation), or terminate the CSRSS process (e.g. by trying to access -1 bytes, thus generating an unhandled Access Violation exception).

An crash log generated upon triggering the discussed issue is presented below:

 *** An Access Violation occurred in C:\WINDOWS\system32\csrss.exe Obje(...)

The instruction at 764EF8DB tried to write to an invalid address, 00101000

 *** enter .exr 0070FBB8 for the exception record
 ***  enter .cxr 0070FBD4 for the context
 *** then kb to get the faulting stack

Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
001b:7c94120e cc              int     3

kd> .exr 0070FBB8 
ExceptionAddress: 764ef8db (winsrv!RegisterLocalEUDC+0x00000032)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 00101000
Attempt to write to address 00101000

kd> .cxr 0070FBD4
eax=ffefefff ebx=00166158 ecx=00101000 edx=0070f8f0 esi=037f0710 edi=0070ff24
eip=764ef8db esp=0070fea0 ebp=0070fea4 iopl=3         nv up ei ng nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00013296
winsrv!RegisterLocalEUDC+0x32:
001b:764ef8db f611            not     byte ptr [ecx]        ds:0023:00101000=??

As stated before, a functional exploit can be developed either by using the undocumented kernel32!SetConsoleLocalEUDC routine, of the following defitinion:

BOOL WINAPI SetConsoleLocalEUDC(HANDLE hOutput,
                                WCHAR Character,
                                COORD Coords,
                                LPVOID RandomPointer);

or by implementing the entire CSRSS communication by ourselves, and using the following input structure for the flawed service (reverse-engineer, based on the purpose and offsets of the structure fields):

struct LPC_MSG
{
  HANDLE hConsole;      // An internal console handle, allocated by AllocConsole

  HANDLE hOutput;       // GetStdHandle(STD_OUTPUT_HANDLE)

  BYTE Byte1;           // (Byte2 * 0x100) | (Byte1) must be found within one
  BYTE Byte2;           // of the system EUDC ranges

  COORD Coords;         // X, Y must meet the CsrValidateMessageBuffer constraints

  LPVOID RandomPointer; // Must be NULL
}

What really happened?

The actual root cause of the vulnerability is the fact that the input COORD structure fields are interpreted in two different ways, here:

if(CsrValidateMessageBuffer(msg,
                            &msg->LPC_MSG.RandomPointer,
                            ((msg->LPC_MSG.Coords.X+7)/8),
                            msg->LPC_MSG.Coords.Y) == FALSE)

and here:

return (InputCoord.Y * ((InputCoord.X+7) >> 3)));

In both cases, the developer’s intention was to calculate the following value:

The effect was succesfully achieved during the input pointer validation, but not during the buffer size calculation. Why?

The answer is – because of the Coord.X integer signedness, and the specific way in which right shifts are implemented by common C compilers. In general, there are two types of bit shifts: logical shifts, and arithmetic shifts. A major difference between these two binary operations, is whether or not the integer signedness is respected while performing a right-shift (there is virtually no difference between logical and arithmetic left-shifts). In case of a logical right-shift, the left-most bits are always complemented with zeros (rouding towards 0x00000000), while performing an arithmetic right-shift results in copying the sign-bit to the new, left-most bits of the new number (rouding towards 0xffffffff, or -1).

To my best knowledge, none of the C language documentations specify the result of performing a bitwise right-shift, when the source operand is a signed, negative number. Instead, the decision of how to handle such condition is left to the compiler developers, e.g. according to ISO/IEC 9899:1999:

The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of . If E1 has a signed type and a negative value, the resulting value is implementation-defined.

As experiments show, both the Microsoft C/C++ Optimizing Compiler (cl.exe) and GCC implement right-shifts on signed source, as a typical arithmetical shift. More specifically, a dedicated SAR instruction is used on the Intel x86 platform, as could be observed in the winsrv!CalcBitmapBufferSize implementation, and also indicated by Wikipedia:

For example, in the x86 instruction set, the SAR instruction (arithmetic right shift) divides a signed number by a power of two, rounding towards negative infinity.[1] However, the IDIV instruction (signed divide) divides a signed number, rounding towards zero. So a SAR instruction cannot be substituted for an IDIV by power of two instruction nor vice versa.

All in all, using a bitwise shift on a signed and potentially negative value, instead of a typical division operator, was the root cause of the vulnerability. If the buffer length was correctly (or at least: in the same way) calculated during both validation and operating on input data, the flaw would no longer exist. Therefore, the conclusion is to never confuse the division and right-shifting operators in the C programming language, if the operation is performed on a signed-type operand.

NULL Page in CSRSS

One interesting detail, which I came across while investigating the vulnerability, was the fact that every CSRSS process running on the system had the NULL-address occupied by a 1 MB memory area. It later turned out (thanks to Alex Ionescu), that this low-address memory mapping was used in the very same way, as in the NTVDM (NT Virtual DOS Machine) environment. More specifically, a simplified version of the VDM environment is set up in the context of the Windows Subsystem, in order to implement the kernel-mode INT10 Video Port API functionality (see Int10CallBios). Whenever required, the Windows kernel attaches to the CSRSS address space, performs some basic initialization, and switches to the virtual 8086 mode, in order to execute one, single instruction:

int 10h

which in turns calls the Video BIOS, and changes the current graphical properties. Therefore, modifying the zero-address memory contents (i.e. NOT-ing some of its bytes) may influence the Interrupt Vector Table (most importantly, IVT[10h]), thus leading to a CSRSS crash, or potentially allowing an attacker to execute arbitrary code in the context of the subsystem process (and within the virtual 8086 mode).

What should be noted here, is that a user is able to trigger the INT10 execution, by performing several types of actions, easiest of which are:

  • Switch a console window to full-screen mode,
  • Change the current screen resolution (does not always work).

Also, an attacker is able to negate any byte of his choice without affecting the values of other, surrounding bytes within the 0x8000-long memory area. That is because of the fact that the negation operation is reversible, and the NOT-ing loop can be triggered several times, with different Size values.

Conclusion

Generally speaking, the only reason of the security flaw’s existence was a wrong implementation of the signed integer division. As opposed to what the official advisory might suggest, the actual error was present in the buffer length calculation, and not in the input pointer’s validation code. What is even more, the conditions which must have been met for the csrsrv!CsrValidateMessageBuffer routine to avoid further address validation are even stricter, than the function’s kernel-mode equivalents – nt!ProbeForRead or nt!ProbeForWrite:

VOID ProbeForWrite(
    __inout PVOID Address,
    __in SIZE_T Length,
    __in ULONG Alignment
);

The latter functions perform any kind of validation, only if the Length parameter is non-zero. If the CSR validation routine didn’t force the (RandomPointer == NULL) condition, this vulnerability would have been far more serious, as it would make it possible to alter (still, in a limited way) bytes at virtually any location within the subsystem’s local address space, and not only the zero-page mapping.

On the other hand, the fact that a NULL-page mapping was available on some (even old) system platforms turned out to be fortunate, as it leveraged the potential vulnerability impact to an Elevation of Privileges. All in all, the threat was only theoretical – it is hardly possible to escalate the user’s privileges, or do any other kind of harm (despite an OS reboot), by only reverting several bytes in the virtual 8086 environment memory. Still, I believe that the flaw makes a good example of how Integer Signedness errors are not only the ones caused by invalid comparisons or negative memcpy parameters :-)

Constructive comments are welcome, as usual! Also, stay tuned for more CSRSS-related materials :-)

5 thoughts on “CVE-2011-1282: User-Mode NULL Pointer Dereference & co.”

  1. “According to my research, the flaw can be only triggered on the CJK (Chinese-Japanese-Korean) language Windows editions, on their default configuration. ”
    More correct:
    “According to my research, the flaw can only be triggered on the default configuration of CJK (Chinese-Japanese-Korean) language Windows editions.”

  2. Nice post man ;)
    Wonder what happens to it on Windows 8.
    AFAIK ntvdm.exe maps ROM to its own process, but it’s a different case here.

Comments are closed.