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:
- Setting the LPC_MSG.RandomPointer value to NULL,
- 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:
- {0, 1, … , 0x8000}
- {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 :-)
“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.”
> virtual 8086 environment memory.
http://indy-vx.narod.ru/Bin/v86.zip
@Yuhong Bao:
I bet it is more correct :-)
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.