Thursday, 22 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:

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:

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:

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:

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:

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.

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:

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:

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:

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: