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!


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)
_SrvSetConsoleLocalEUDC@8 proc near     ; DATA XREF: .text:75B38B10o
HandleDescriptor= byte ptr -0Ch
dst_buf         = dword ptr -8
src_buf         = dword ptr -4
msg             = dword ptr  8
reply           = dword ptr  0Ch
mov     edi, edi
push    ebp
mov     ebp, esp
sub     esp, 0Ch
push    esi
mov     esi, [ebp+msg]
lea     eax, [ebp+msg]
push    eax
push    dword ptr [esi+LPC_MSG.hConsole]
call    _ApiPreamble@8  ; ApiPreamble(x,x)
test    eax, eax
jl      error_return
mov     eax, large fs:18h
mov     eax, [eax+TEB.CsrClientThread]
mov     eax, [eax+CSR_THREAD_INFO.Unknown]
lea     ecx, [ebp+HandleDescriptor]
push    ecx
push    40000000h
push    2
push    dword ptr [esi+LPC_MSG.hOutput]
push    dword ptr [eax+UNKNOWN.Process]
call    _DereferenceIoHandle@20 ; DereferenceIoHandle(x,x,x,x,x)
test    eax, eax
jge     short output_valid
mov     esi, eax
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
movsx   eax, word ptr [esi+LPC_MSG.Coords.Y]
push    edi
push    eax
movsx   eax, word ptr [esi+LPC_MSG.Coords.X]
push    8
add     eax, 7
pop     ecx
idiv    ecx
lea     edi, [esi+LPC_MSG.RandomString]
push    eax
push    edi
push    esi
call    ds:__imp__CsrValidateMessageBuffer@16 ; CsrValidateMessageBuffer(x,x,x,x)
test    al, al
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:

                            msg->LPC_MSG.Coords.Y) == FALSE)

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)
public _CsrValidateMessageBuffer@16
_CsrValidateMessageBuffer@16 proc near  ; CODE XREF: CsrSrvClientConnect(x,x)+3Dp
msg                = dword ptr  8
Pointer        = dword ptr  0Ch
Size            = dword ptr  10h
Count           = dword ptr  14h
mov     edi, edi
push    ebp
mov     ebp, esp
cmp     [ebp+Count], 0
push    esi
push    edi
mov     edi, [ebp+msg]
mov     esi, [edi+18h]
jz      short error_return
or      eax, 0FFFFFFFFh
xor     edx, edx
div     [ebp+Count]
mov     ecx, [ebp+Size]
cmp     ecx, eax
ja      short error_return
mov     eax, [ebp+Pointer]
imul    ecx, [ebp+Count]
mov     edx, [eax]
test    edx, edx
jnz     short further_checks
test    ecx, ecx
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]
mov     byte ptr [ebp+src_buf], al
push    [ebp+dst_buf]
push    [ebp+msg]
call    _IsEudcRange@8; IsEudcRange(x,x)
test    eax, eax
jz      short status_invalid_parameter
push    dword ptr [edi]
push    dword ptr [esi+LPC_MSG.Coords]
push    [ebp+dst_buf]
push    [ebp+msg]
call    _RegisterLocalEUDC@16 ; RegisterLocalEUDC(x,x,x,x)
test    eax, eax
jl      short loc_75B51C07
mov     eax, [ebp+msg]
mov     eax, [eax+Console.EudcEnabled]
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
push    1
push    dword ptr [ebp+Coords]
call    _CalcBitmapBufferSize@8 ; CalcBitmapBufferSize(x,x)
test    eax, eax
mov     ecx, [ebp+RandomPointer]
jz      short nothing_to_do
loc_75B5F8DB:                           ; CODE XREF: RegisterLocalEUDC(x,x,x,x)+36j
not     byte ptr [ecx]
inc     ecx
dec     eax
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)
_CalcBitmapBufferSize@8 proc near       ; CODE XREF: SetRAMFontCodePage(x)+99p
; SetRAMFontCodePage(x)+153p …
arg_0           = dword ptr  8
arg_4           = dword ptr  0Ch
mov     edi, edi
push    ebp
mov     ebp, esp
movsx   eax, word ptr [ebp+arg_0]
xor     ecx, ecx
cmp     [ebp+arg_4], 1
setnz   cl
lea     ecx, ds:8[ecx*8]
lea     eax, [ecx+eax-1]
sar     eax, 3
dec     ecx
not     ecx
sar     ecx, 3
and     eax, ecx
movsx   ecx, word ptr [ebp+arg_0+2]
imul    eax, ecx
pop     ebp
retn    8
_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:

  *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)
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
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:

                                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:

                            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.


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.


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.

Leave a Comment