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:
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:
Following that, the driver code creates device objects with
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
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
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
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
The code will start parsing the export table of the kernel image
Once the API addresses it needs are obtained, it starts parsing the entire section of the kernel image looking for the byte sequence
Once
For example, it aims to find the following code construction within
In the context of
Next, it enumerates all the section headers within
The section that it finds must also be
Once it locates precisely where
it starts matching the opcodes of this function to its own internal opcode mask:
The
If a byte in the mask is
By checking if
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
The driver opens the registry key
The decryptor function is called with some input values:
The initial content of the
For start, the decryption function can be replicated in a stand-alone tool, using in-line Assembler, by copy-pasting the disassembled code above:
The same function can also be implemented in C++ as:
Once implemented, the
The unencrypted data from the registry is known from the previous Duqu versions:
The dump above specifies the name of the DLL to inject (
The byte at the offset
For simplicity, these parameters will not be modified - they will be taken as they are, including the encryption key. That key (
Placing the parameters dump into a separate file and calling the replicated function
Now it's time to check how Duqu driver loads the specified DLL
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
Once compiled, the test DLL is encrypted by calling
With a virtual machine fired up, the compiled and encrypted DLL file
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
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
VoilĂ !
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:
\Device\{3093AAZ3-1092-2929-9391}
- Registry branch that will be used to store the driver information:
\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Services\mcd9x86
- 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:\Device\{BFF55DF2-6530-4935-8CF6-6D6F7DC0AA48}
\Device\{3093AAZ3-1092-2929-9391}
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
.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)
{
_asm
{
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]
l_loop:
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
l_exit:
}
}
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)
{
do
{
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)
{
HANDLE hFile;
HANDLE hMap;
LPBYTE lpbyBase;
DWORD dwSize;
if ((hFile = CreateFile(L"FILE_NAME_TO_DECRYPT",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL)) != INVALID_HANDLE_VALUE)
{
if (((dwSize = GetFileSize(hFile, NULL)) != INVALID_FILE_SIZE) &&
((hMap = CreateFileMapping(hFile,
NULL,
PAGE_READWRITE,
0,
0,
NULL)) != NULL))
{
if ((lpbyBase = (LPBYTE)MapViewOfFile(hMap,
FILE_MAP_ALL_ACCESS,
0,
0,
0)) != NULL)
{
Decrypt(dwSeed, lpbyBase, dwSize);
UnmapViewOfFile(lpbyBase);
}
CloseHandle(hMap);
}
CloseHandle(hFile);
}
}
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:[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\mcd9x86]
"FILTER"=hex:bb,89,0d,99,2c,35,5d,bb,21,86,5d,d3,36,ad,3a,7d,89,17,95,87,af,91,b5,39,\
ee,1d,5c,8d,0f,23,33,63,12,fb,bc,87,90,e7,1c,6b,c5,07,04,0b,c1,19,44,3d,\
ed,47,3b,01,21,2d,11,53,f8,c1,f6,35,ae,9f,71,e1,ca,99,b0,af,9b,87,3a,e3,\
08,83,79,e9,9b,9f,54,25,83,1f,07,9b,69,ed,41,6d,36,6b,ff,85,d5,71,82,71,\
6a,73,ba,dd,a9,45,4b,e1,29,5b,6d,2d,4d,43,f9
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:
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
wchar_t szProcessFileName[MAX_PATH];
GetModuleFileName(NULL,
szProcessFileName,
MAX_PATH);
MessageBoxW(NULL,
szProcessFileName,
L"Test DLL was loaded successfully!",
MB_OK);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
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Ă !
3 Comments:
Fine!
Looking for hashed names instead of names when importing functions from a PE header is an old shellcoders trick. I don't know how common it is for viruses, but it's extremely common when writing exploits.
Hi,
can you share your idb ?
Post a Comment
Subscribe to Post Comments [Atom]
<< Home