|
Technical Analysis of MS06-001
Abstract
Microsoft Windows is vulnerable to remote code execution in GDI32.dll (Graphical Device Interface). This vulnerability was assigned Microsoft security bulletin MS06-001 Vulnerability in Graphics Rendering Engine Could Allow Remote Code Execution (912919). An exploit containing this vulnerability was found in the wild by Websense Security Labs on 12/27/2005.
This vulnerability was exploited in the wild as early as 12/15/2005 to install various malicious programs. In order to successfully exploit this vulnerability, an attacker is only required to lure the victim to an infected website. The number of websites currently hosting malicious code has steadily increased since the exploit was made public. In this article, Stephan Chenette walks through the disassembly of GDI32.dll, providing a detailed analysis of the code flow leading to the vulnerability. Readers are expected to be familiar with x86 assembly instructions to follow this document. Summary
Windows Meta Files (WMF) files contain GDI commands/functions in the form of meta records. The malicious code is executed by using a special meta record called a "SetAbortProc" escape metafile record. This record instructs GDI32.dll to execute arbitrary user-supplied code. This vulnerability is unique in that it is not a standard security vulnerability, such as a buffer or integer overflow. This vulnerability is triggered because the WMF file can contain error handling code directly located in a special escape meta record that GDI32.dll will execute. Exploit code was publicly published for this vulnerability on 12/28/2005. A third-party patch was provided 12/31/2005 and Microsoft addressed this vulnerability with the release of MS06-001 on 01/05/2006. A number of public exploit generators have been released that allow malicious WMF files to be created with little programming knowledge. An explanation of the disassembled code is found below. Technical Details
GDI32.dll (5.1.2600.2770) was disassembled on Windows XP SP2 using Interactive Disassembler (IDA) v4.9. The execution path begins with a call to gdi32_PlayMetaFile, which will then call CommonEnumMetaFile to enumerate all meta records in the WMF file. CommonEnumMetaFile then calls gdi32_PlayMetaFileRecord on each meta record to retrieve the meta record information. Each record contains a record size, a function code, and a variable number of function parameters. gdi32_PlayMetaFileRecord sets the higher 8 bits of every function code to zero for each meta record (i.e. 0x626 becomes 0x26). If the new function code is 0x26, PlayIntoAMetafile will be called. gdi32_PlayMetaFileRecord then checks the escape code, the first function parameter. gdi32_Escape will be called if the escape code is anything other than 0x0F (for example: 0x09). If the escape code is 0x09, gdi32_SetAbortProc will be called to retrieve the GDI entry from the GDISharedHandleTable and set pUserInfo->0x14 for that GDI entry to the address of the abort function, which is also supplied as a parameter. Upon enumeration of the next record, CommonEnumMetaFile will check to see if pUserInfo->0x14 was set from the analysis of the last meta record. If an address was set at this offset, that address will be called and executed. This address points to arbitrary user-supplied code within the WMF file. gdi32_PlayMetaFile: 77F2595E ; BOOL __stdcall gdi32_PlayMetaFile(HDC,HMETAFILE) 77F2595E public gdi32_PlayMetaFile 77F2595E gdi32_PlayMetaFile proc near ; CODE XREF: sub_4ECB5604+B7 77F2595E ; sub_4ECB5604+CE ... 77F2595E 77F2595E arg_0 = dword ptr 8 77F2595E arg_4 = dword ptr 0Ch 77F2595E 77F2595E mov edi, edi 77F25960 push ebp 77F25961 mov ebp, esp 77F25963 push 0 77F25965 push 0 77F25967 push [ebp+arg_4] 77F2596A push [ebp+arg_0] 77F2596D call _CommonEnumMetaFile@16 ; CommonEnumMetaFile(x,x,x,x) 77F25972 pop ebp 77F25973 retn 8 77F25973 gdi32_PlayMetaFile endp 77F25973 _pldcGet returns the pUserInfo field for a particular GDI HDC. The address of pUserInfo is then stored in var_44. CommonEnumMetaFile: 77F23FDE call _pldcGet@4 ; pldcGet(x) 77F23FE3 mov [ebp+var_44], eax Jumps to loop that enumerates each meta record CommonEnumMetaFile: 77F240DF jz loc_77F25912 This is the meta record loop. This code enumerates each meta record and then plays the meta record. If the previous meta record sets pUserInfo->0x14 to a value other than zero, CommonEnumMetaFile will execute the code at the set address. CommonEnumMetaFile: 77F25912 ; START OF FUNCTION CHUNK FOR _CommonEnumMetaFile@16 77F25912 77F25912 loc_77F25912: ; CODE XREF: CommonEnumMetaFile(x,x,x,x)+198 77F25912 ; CommonEnumMetaFile(x,x,x,x)+1A10j 77F25912 cmp esi, ebx ; loop for each meta record (loop start) 77F25914 jz loc_77F24134 77F2591A cmp esi, 0FFFFFFFFh 77F2591D jz short loc_77F25988 77F2591F cmp [ebp+var_20], ebx 77F25922 jnz short loc_77F2592D 77F25924 mov eax, [ebp+var_24] 77F25927 test byte ptr [eax+5], 8 77F2592B jz short loc_77F25988 77F2592D ; 77F2592D ; [ebp+var_44] contains the address of pUserInfo for the GDI Entry 77F2592D ; 77F2592D ; The entry was set at 77F23FE3 77F2592D 77F2592D loc_77F2592D: ; CODE XREF: CommonEnumMetaFile(x,x,x,x)+19DB 77F2592D mov eax, [ebp+var_44] 77F25930 cmp eax, ebx ; ebx = 0x0 77F25932 jz short loc_77F2593F 77F25934 ; [eax+14h] (pUserInfo+0x14) contains the address of escape shellcode 77F25934 ; This was set in SetAbortProc. 77F25934 mov eax, [eax+14h] 77F25937 ; If ebx is 0x0. If eax is not 0x0 it will contain the address 77F25937 ; of the escape function where the shellcode is located 77F25937 cmp eax, ebx 77F25939 ; If pUserInfo+0x14 is not zero then code at this address will be executed 77F25939 ; Else the next meta record will be parsed 77F25939 jnz loc_77F333FE 77F2593F 77F2593F loc_77F2593F: ; CODE XREF: CommonEnumMetaFile(x,x,x,x)+19EB 77F2593F ; CommonEnumMetaFile(x,x,x,x)+F4BDj 77F2593F push [ebp+var_30] ; UINT 77F25942 push esi ; LPMETARECORD 77F25943 push [ebp+hMem] ; LPHANDLETABLE 77F25946 push edi ; HDC 77F25947 call gdi32_PlayMetaFileRecord 77F2594C push esi 77F2594D push [ebp+var_2C] 77F25950 call _GetEvent@8 ; GetEvent(x,x) 77F25955 mov esi, eax 77F25957 ; Loop to next meta file record 77F25957 jmp short loc_77F25912 ; jmp back to start of loop 77F25957 ; END OF FUNCTION CHUNK FOR _CommonEnumMetaFile@16 gdi32_PlayMetaFileRecord: 77F11788 ; --------------------------------------------------------------------------- 77F11788 ; START OF FUNCTION CHUNK FOR gdi32_PlayMetaFileRecord 77F11788 77F11788 loc_77F11788: ; CODE XREF: gdi32_PlayMetaFileRecord+Fj 77F11788 mov eax, ___security_cookie 77F1178D mov [ebp+var_1C], eax 77F11790 mov eax, [ebp+arg_0] 77F11793 mov [ebp+var_7C], eax 77F11796 mov eax, [ebp+arg_4] 77F11799 77F11799 loc_77F11799: ; CODE XREF: _GUID_D3DCallbacks2j 77F11799 mov [ebp+var_88], eax 77F1179F mov ebx, [ebp+arg_8] 77F117A2 xor esi, esi 77F117A4 mov [ebp+var_80], esi 77F117A7 mov [ebp+hMem], esi 77F117AD mov edi, [ebx] 77F117AF mov [ebp+var_E4], edi 77F117B5 xor ecx, ecx 77F117B7 mov cx, [ebx+4] 77F117BB mov [ebp+var_8C], ecx 77F117C1 movzx ecx, cx 77F117C4 ; var_B4 holds the full function code 77F117C4 mov [ebp+var_B4], ecx 77F117CA ; The higher 8 bits of the function code are set to zero 77F117CA ; (i.e. 0x626 is now 0x26) 77F117CA ; The value is stored in ecx 77F117CA and ecx, 0FFh 77F117D0 mov edx, 0F0h ; '=' 77F117D5 cmp ecx, edx 77F117D7 jg loc_77F23EB7 77F117DD jmp loc_77F23E84 77F117DD ; END OF FUNCTION CHUNK FOR gdi32_PlayMetaFileRecord 77F117DD ; --------------------------------------------------------------------------- After the higher 8 bits of the function code are set to zero, there is a switch statement on the new function code to determine the code path. gdi32_PlayMetaFileRecord: 77F23E84 ; START OF FUNCTION CHUNK FOR gdi32_PlayMetaFileRecord 77F23E84 77F23E84 loc_77F23E84: ; CODE XREF: gdi32_PlayMetaFileRecord-1266F 77F23E84 jz loc_77F25043 77F23E8A cmp ecx, 49h ; 'I' ; switch 74 cases 77F23E8D ja loc_77F25067 ; default 77F23E93 jump table for truncated function code (i.e. 0x26) 77F23E93 jmp ds:off_77F254E0[ecx*4] ; switch jump This is the jump table for the switch statement of the new function code. gdi32_PlayMetaFileRecord: 77F254E0 off_77F254E0 77F254E0 dd offset loc_77F25067 ; DATA XREF: gdi32_PlayMetaFileRecord+47 77F254E0 ; jump table for switch statement 77F254E4 dd offset loc_77F25780 ; case 0x1 77F254E8 dd offset loc_77F2578D ; case 0x2 77F254EC dd offset loc_77F24E7F ; case 0x3 77F254F0 dd offset loc_77F25BAB ; case 0x4 77F254F4 dd offset loc_77F25067 ; default 77F254F8 dd offset loc_77F24EA3 ; case 0x6 77F254FC dd offset loc_77F24EB5 ; case 0x7 77F25500 dd offset loc_77F24E1A ; case 0x8 77F25504 dd offset loc_77F257B6 ; case 0x9 77F25508 dd offset loc_77F24EDE ; case 0xA 77F2550C dd offset loc_77F25683 ; case 0xB 77F25510 dd offset loc_77F2569C ; case 0xC 77F25514 dd offset loc_77F24DB6 ; case 0xD 77F25518 dd offset loc_77F24DCF ; case 0xE 77F2551C dd offset loc_77F24DE8 ; case 0xF 77F25520 dd offset loc_77F24D70 ; case 0x10 77F25524 dd offset loc_77F24E01 ; case 0x11 77F25528 dd offset loc_77F24D93 ; case 0x12 77F2552C dd offset loc_77F2579F ; case 0x13 77F25530 dd offset loc_77F25767 ; case 0x14 77F25534 dd offset loc_77F24F37 ; case 0x15 77F25538 dd offset loc_77F24F58 ; case 0x16 77F2553C dd offset loc_77F24FA4 ; case 0x17 77F25540 dd offset loc_77F24EF5 ; case 0x18 77F25544 dd offset loc_77F24D37 ; case 0x19 77F25548 dd offset loc_77F2500E ; case 0x1A 77F2554C dd offset loc_77F24F16 ; case 0x1B 77F25550 dd offset loc_77F24F79 ; case 0x1C 77F25554 dd offset loc_77F25738 ; case 0x1D 77F25558 dd offset loc_77F24E45 ; case 0x1E 77F2555C dd offset loc_77F24D0D ; case 0x1F 77F25560 dd offset loc_77F24EC7 ; case 0x20 77F25564 dd offset loc_77F248F2 ; case 0x21 77F25568 dd offset loc_77F2422B ; case 0x22 77F2556C dd offset loc_77F2422B ; case 0x22 77F25570 dd offset loc_77F25B78 ; case 0x24 77F25574 dd offset loc_77F25B78 ; case 0x24 77F25578 dd offset loc_77F256F2 ; case 0x26 77F2557C dd offset loc_77F24CFB ; case 0x27 77F25580 dd offset loc_77F249D5 ; case 0x28 case 0x26 (escape function code): PlayIntoAMetafile is called and the escape code is checked (i.e. 0x09=SetAbortProc) gdi32_PlayMetaFileRecord: 77F256F2 ; --------------------------------------------------------------------------- 77F256F2 ; START OF FUNCTION CHUNK FOR gdi32_PlayMetaFileRecord 77F256F2 77F256F2 loc_77F256F2: ; CODE XREF: gdi32_PlayMetaFileRecord+47 77F256F2 ; DATA XREF: 77F25578 77F256F2 push [ebp+var_7C] ; case 0x26 77F256F5 push ebx 77F256F6 call _PlayIntoAMetafile@8 ; PlayIntoAMetafile(x,x) 77F256FB mov [ebp+var_80], eax 77F256FE test eax, eax 77F25700 jz loc_77F24914 ; leads to SetAbortProc 77F25706 jmp loc_77F258FD 77F2570B ; --------------------------------------------------------------------------- word ptr [ebx+6] and eax hold the value of the escape code (i.e. 0x09). If the escape code is anything other than 0x0F, gdi32_Escape will be called. gdi32_PlayMetaFileRecord: 77F24914 ; --------------------------------------------------------------------------- 77F24914 get escape number 77F24914 77F24914 loc_77F24914: ; CODE XREF: gdi32_PlayMetaFileRecord+18B4j 77F24914 movzx eax, word ptr [ebx+6] 77F24918 cmp eax, 0Fh 77F2491B jz loc_77F25067 ; default 77F24921 ; We will get here once eax does not equal 0x0F (i.e. when eax equals 0x09) 77F24921 push 0 ; LPVOID 77F24923 lea ecx, [ebx+0Ah] 77F24926 push ecx ; LPCSTR 77F24927 movzx ecx, word ptr [ebx+8] 77F2492B push ecx ; int 77F2492C push eax ; int 77F2492D push [ebp+var_7C] ; HDC 77F24930 call gdi32_Escape 77F24935 jmp loc_77F23F23 77F2493A ; --------------------------------------------------------------------------- A small switch statement determines the code path for the escape code. The execution jumps to 0x77F33EEF if the escape code is 0x09. gdi32_Escape: 77F2695C jz loc_77F346AB 77F26962 cmp eax, 8 77F26965 jnz loc_77F33E5B 77F33EC4 ; If the escape code is equal to 0x09, jump to 0x77F33EEF. 77F33EC4 sub eax, 6 77F33EC7 jz short loc_77F33EEF ; leads to call of SetAportProc gdi32_SetAbortProc is called at 0x77F33EF3. gdi32_Escape: 77F33EEF loc_77F33EEF: ; CODE XREF: gdi32_Escape+D5A6 77F33EEF push esi ; ABORTPROC 77F33EF0 push [ebp+var_8] ; HDC 77F33EF3 call gdi32_SetAbortProc 77F33EF8 jmp loc_77F34525 gdi32_SetAbortProc calls _pldcGet to get the pUserInfo address of the HDC. It then sets pUserInfo->0x14 to the address of the abort procedure, which is the address of the shellcode. gdi32_SetAbortProc: 77F43942 ; int __stdcall gdi32_SetAbortProc(HDC,ABORTPROC) 77F43942 public gdi32_SetAbortProc 77F43942 gdi32_SetAbortProc proc near ; CODE XREF: gdi32_Escape+D5D2 77F43942 77F43942 arg_0_HDC = dword ptr 8 77F43942 arg_4_ABORTPROC = dword ptr 0Ch 77F43942 77F43942 mov edi, edi 77F43944 push ebp 77F43945 mov ebp, esp 77F43947 mov ecx, [ebp+arg_0_HDC] 77F4394A and ecx, 7F0000h 77F43950 or eax, 0FFFFFFFFh 77F43953 cmp ecx, 10000h 77F43959 push esi 77F4395A jz short loc_77F439A9 77F4395C cmp ecx, 660000h 77F43962 jz short loc_77F439A9 77F43964 push [ebp+arg_0_HDC] 77F43967 ; _pldcGet@4 returns the address to pUserinfo that will be used 77F43967 ; to store the escape function address of the shellcode 77F43967 call _pldcGet@4 ; pldcGet(x) For references purposes, the code of _pldcGet is displayed below. The purpose of this function is to return the address of pUserInfo for a given HDC.
_pldcGet:
77F159B4 ; __stdcall pldcGet(x)
77F159B4 _pldcGet@4 proc near ; CODE XREF: gdi32_GdiReleaseDC+13p
77F159B4 ; InternalDeleteDC(x,x)+22p ...
77F159B4
77F159B4 arg_0_HDC = dword ptr 8
77F159B4
77F159B4 mov edi, edi
77F159B6 push ebp
77F159B7 mov ebp, esp
77F159B9 mov edx, [ebp+arg_0_HDC]
77F159BC mov ecx, edx
77F159BE ; Put the lower 16 bits of HANDLE into ecx so we can
77F159BE ; use it to get the index in the table.
77F159BE and ecx, 0FFFFh
77F159C4 xor eax, eax
77F159C6 ; Test if the handle index is below the first 10,000h entries.
77F159C6 ; If it is not, go to the end of the function.
77F159C6 cmp ecx, 10000h
77F159CC jnb short loc_77F159FD
77F159CE ; Shift right the lower 16 bits indicating the index
77F159CE ; into the table to the higher 16 bits in ecx.
77F159CE ;
77F159CE ; We do this because each entry is 16 bytes (2^4).
77F159CE ; By shifting we are accounting for the entry size
77F159CE shl ecx, 4
77F159D1 ; GdiSharedHandleTable (PEB contains ptr to this table)
77F159D1 ;
77F159D1 ; This is a table where GDI stores its handles,
77F159D1 ; even those created by other processes.
77F159D1 ;
77F159D1 ; This is the structure of a GDI entry on Windows 2000/XP:
77F159D1 ;
77F159D1 ; typedef struct
77F159D1 ; {
77F159D1 ; DWORD pKernelInfo; (offset=00h)
77F159D1 ; WORD ProcessID; (offset=04h)
77F159D1 ; WORD _nCount; (offset=06h)
77F159D1 ; WORD nUpper; (offset=08h)
77F159D1 ; WORD nType; (offset=0Ah)
77F159D1 ; DWORD pUserInfo; (offset=0Ch)
77F159D1 ; } GDITableEntry;
77F159D1 ;
77F159D1 ; Each entry stores the details of a GDI handle.
77F159D1 ; Its lower 16 bits are the index in the table,
77F159D1 ; its upper 16 bits are saved in the nUpper field,
77F159D1 ; the ProcessID field contains the ID of the process that created the object.
77F159D1 ;
77F159D1 ; ecx holds the offset*gdi_entry_size into the table.
77F159D1 ; The operation below adds the address of
77F159D1 ; pGdiHandleTable and the offset held in ecx
77F159D1 add ecx, _pGdiSharedHandleTable
77F159D7 ; Check to see if the nType (offset 0Ah) is equal to 1
77F159D7 cmp byte ptr [ecx+0Ah], 1
77F159DB jnz short loc_77F159FD
77F159DD ; Shift right 16 bits, now edx contains the upper 16bits (nUpper)
77F159DD shr edx, 10h
77F159E0 ; Verify that nUpper in [ecx+08h] matches nUpper in dx
77F159E0 cmp [ecx+8], dx
77F159E4 jnz short loc_77F159FD
77F159E6 ; Move the ProcessID into edx
77F159E6 mov edx, [ecx+4]
77F159E9 and edx, 0FFFFFFFEh
77F159EC ; Compare ProcessID (should be equal)
77F159EC ; (Note: _gW32PID comes from TEB-->clientID&FFFFFFFCh)
77F159EC cmp edx, _gW32PID
77F159F2 jnz short loc_77F159FD
77F159F4 ; [ecx+0ch] = pUserInfo
77F159F4 mov ecx, [ecx+0Ch]
77F159F7 ; Test if ecx is zero
77F159F7 test ecx, ecx
77F159F9 jz short loc_77F159FD
77F159FB ; Move the 4 byte value ecx contains into eax.
77F159FB ; The 4 byte value is the address of pUserInfo
77F159FB mov eax, [ecx]
77F159FD
77F159FD loc_77F159FD: ; CODE XREF: pldcGet(x)+18
77F159FD ; pldcGet(x)+27 ...
77F159FD pop ebp
77F159FE retn 4
77F159FE _pldcGet@4 endp
After the return of _pldcGet, gdi32_SetAbortProc sets the abort procedure (shellcode) and returns. gdi32_SetAbortProc: 77F4396C ; eax holds the address of pUserInfo from the GDI entry from the GDISharedHandleTable. 77F4396C 77F4396C ; esi also now contains the address of pUserInfo. 77F4396C mov esi, eax 77F4396E ; Test to make sure esi is not zero. 77F4396E test esi, esi 77F43970 jz short loc_77F4399F 77F43972 push edi 77F43973 ; SetAbortProc sets edi to abort function address. 77F43973 mov edi, [ebp+arg_4_ABORTPROC] 77F43976 ; Verify that the address is not NULL. 77F43976 test edi, edi 77F43978 jz short loc_77F43992 77F4397A ; eax will now hold the 4 byte value at [esi+4] 77F4397A ; (Note: esi contains the address of pUserInfo) 77F4397A mov eax, [esi+4] 77F4397D test al, 40h 77F4397F jz short loc_77F43996 gdi32_SetAbortProc: 77F43996 loc_77F43996: ; CODE XREF: gdi32_SetAbortProc+3D 77F43996 ; gdi32_SetAbortProc+4E 77F43996 xor eax, eax 77F43998 ; edi holds the address of the escape shellcode. 77F43998 ; edi is copied into the memory at [esi+14h]. 77F43998 ; 77F43998 ; esi holds an address pUserInfo. 77F43998 ; [esi+14h] will contain the address of the abort procedure. 77F43998 mov [esi+14h], edi 77F4399B ; eax was zero and now will become one. 77F4399B inc eax 77F4399C pop edi 77F4399D jmp short loc_77F439A9 77F439A9 loc_77F439A9: ; CODE XREF: gdi32_SetAbortProc+18 77F439A9 ; gdi32_SetAbortProc+20 ... 77F439A9 pop esi 77F439AA pop ebp 77F439AB retn 8 77F439AB gdi32_SetAbortProc endp var_44 holds the address of pUserInfo which was set at the beginning of the function. Offset 0x14 of this variable is compared against ebx, which is always zero. If the address at pUserInfo->0x14 is not zero, then the supplied address at this offset is executed. CommonEnumMetaFile: 77F25912 cmp esi, ebx 77F25914 jz loc_77F24134 77F2591A cmp esi, 0FFFFFFFFh 77F2591D jz short loc_77F25988 77F2591F cmp [ebp+var_20], ebx 77F25922 jnz short loc_77F2592D 77F25924 mov eax, [ebp+var_24] 77F25927 test byte ptr [eax+5], 8 77F2592B jz short loc_77F25988 77F2592D ; [ebp+var_44] contains the address of pUserInfo for the GDI Entry. 77F2592D ; 77F2592D ; The entry was set at 0x77F23FE3. ebx is always 0h. 77F2592D 77F2592D loc_77F2592D: ; CODE XREF: CommonEnumMetaFile(x,x,x,x)+19DB 77F2592D mov eax, [ebp+var_44] 77F25930 cmp eax, ebx 77F25932 jz short loc_77F2593F 77F25934 ; [eax+14h] contains the address of escape shellcode. 77F25934 ; This was set in SetAbortProc. 77F25934 mov eax, [eax+14h] 77F25937 ; ebx is 0x0. If eax is not 0x0, it will contain the address of the 77F25937 ; escape function where the shellcode is located. 77F25937 cmp eax, ebx 77F25939 ; This makes us jump to our escape shellcode. 77F25939 jnz loc_77F333FE 0x77F33400 is the address where the user-supplied shellcode will be called. CommonEnumMetaFile: 77F333FE loc_77F333FE: ; CODE XREF: CommonEnumMetaFile(x,x,x,x)+19F2 77F333FE push ebx 77F333FF push edi 77F33400 ; Arbitrary shellcode payload is called and executed here. 77F33400 call eax References
Websense Security Labs: MS06-001 Analysis Websense Security Labs Blog |