virusLast July, a new banking malware called Kronos (the father of Zeus in the Greek mythology, probably a reference to this well-known banking malware), appeared on a Russian underground forum. Available for $7000, it comes with classic features for this kind of malware such as encrypted communications with its control server (C&C) as well as on-the-fly HTML and JavaScript injection in the web browser while visiting targeted financial websites. A French bank was noticed to be part of its configuration file last summer.

It is also advertised as coming with a rootkit and antivirus bypassing techniques which this blog post will detail. Banking features and configuration file decryption will be covered in an upcoming post.

Preliminary sandbox execution

In a Windows XP sandbox, an explorer.exe process catches the eye due to the following malicious behaviour:

  • A directory named as a GUID is created in a sub-directory of %APPDTA%, for example C:Documents and SettingsUserApplication DataMicrosoft{757931CE-B49C-4F86-8F0B-5654834F10};
  • A binary named 2bf42c45.exe as well as a file named 2bf42c45.cfg (malware configuration file) are created;
  • 2bf42c45 registry value pointing to this binary is created in the HKLM and HKCU Run keys; 
  • A 2bf42c45 registry value containing the GUID is created in the HKLM and HKCU SoftwareMicrosoftWindowsCurrentVersion key;
  • GMER flags it as hidden.

The image associated with this process is C:WINDOWSexplorer.exe. One could think of an classic injection in the legitimate explorer.exe process but the trick quickly emerges: it has a different PID and the image of the legitimate process is C:WINDOWSExplorer.EXE (notice the uppercase letters):

Deux processus Explorer

Two Explorer processes

This is therefore a case of process hollowing: the dropper turns a legitimate process into a malicious one by dynamically altering its memory.

Dropper initialization

The dropper packer heavily uses drawing functions from the GDI+ library such as Rectangle()CreateSolidBrush(), or SetTextColor(). But now that we know the dropper is using the process hollowing technique, it can be quickly unpacked despite the use of these functions by setting a breakpoint on the CreateProcess() function.

The unpacked malware begins by retrieving the content of the C:WINDOWSSystem32ntdll.dll file, then performs the following operations:

  • Checks for the presence of the MZ characters;
  • Retrieves the offset to the PE header at offset 0x3c;
  • Checks for the presence of the PE characters;
  • Retrieves the offset to the export table at offset 0x78.

The export table is a _IMAGE_EXPORT_DIRECTORY structure:

typedefstruct _IMAGE_EXPORT_DIRECTORY {
  DWORD Characteristics;       //offset 0x0
  DWORD TimeDateStamp;         //offset 0x4
  WORD  MajorVersion;          //offset 0x8
  WORD  MinorVersion;          //offset 0xa
  DWORD Name;                  //offset 0xc
  DWORD Base;                  //offset 0x10
  DWORD NumberOfFunctions;     //offset 0x14
  DWORD NumberOfNames;         //offset 0x18
  DWORD AddressOfFunctions;    //offset 0x1c
  DWORD AddressOfNames;        //offset 0x20
  DWORD AddressOfNameOrdinals; //offset 0x24
}

For each entry, the malware retrieves the function name address at offset 0x20, computes a hash of this name and compares it to a list of hashes hardcoded in the binary. The following functions are searched for:

D7T1H5F0F5A4C6S3 sprintf
H2G3F4F0F5A4D5E6 _vsnprintf
X1U5U8H8F5A4C8C5 _vsnwprintf
E3D7R6B3R4H5F3R7 RtlRandomEx
X8D3U3P7S6Q3S5R1 RtlInitAnsiString
X8D3T6Q6U3S3A6R1 RtlFreeAnsiString
R6G2D2R3A5E3C4U5 RtlFreeUnicodeString
H7Y6G2R3A5F4D3S8 RtlInitUnicodeString
P7Y3Q5P0Y8C2Y6F6 RtlCompareUnicodeString
R6Y7B3C6E7E6T7U7 RtlUnicodeStringToAnsiString
G2F3G6A6R3F1P6G2 RtlAnsiStringToUnicodeString
S3H8T8Y5F5B5B0X0 NtQueryKey
C8G2T3U3B1H3T5B5 NtCreateKey
C4R7A2P4X3B1H5A4 NtDeleteFile
R3Q7T7Q2R6S1Y3R5 NtSetValueKey
E3C3A2Y3C4U6S5F5 NtQueryObject
F3P7Y6P3U3E2U5F3 NtCreateSection
E5X0A4Q4F0Y0D6E2 NtGetContextThread
X2R0A4Q4F0Y0D6F3 NtSetContextThread
H1G7R4Y7D1E6R5F8 NtMapViewOfSection
G3C3R4H7R5T8E5R8 NtFreeVirtualMemory
F6H5P7T4F6D6Y6D4 NtQueryVirtualMemory
E3C7U2Y3C3R6R5D5 NtUnmapViewOfSection
F5E8X5G3Q6T7E6T3 NtProtectVirtualMemory
E1U3D5F7R2Y5S0H4 NtQueryInformationFile
H3Y5C8Y2D4U8Y4S3 NtAllocateVirtualMemory
U0U6H1T2F6S1P2Y5 NtQueryInformationThread
D5R3T8D5D3H0B4E2 NtQueryInformationProcess
D5B6G6R4A6H1P7A3 NtClose
F1Q3D0H4H3T6U1X5 LdrLoadDll
A4T6P1G7D6G0F3S5 LdrGetProcedureAddress
C7G5T6P7U5B1H0F5 NtWriteFile
X2C7E3U6F3A7Y1D5 NtOpenProcess
P4Y7T7R7R8X3E3A3 NtResumeThread
C5Y7R2R2H1R7A1B2 NtDeleteValueKey
S4A3E3S3S4T1T3D1 NtEnumerateValueKey
B4Y2H7F8A2T3G4H3 NtQueryDirectoryFile
B5D6X4H5G6S3R2B5 NtSetInformationFile
B6F6X4A8R5D3A7C6 NtQuerySystemInformation
C6P7E6P7A1R5Q4R7 NtDelayExecution
R8S7D7S8H6Y4T6B7 NtQueryValueKey
U0S3T3D3U5F5B4E8 NtOpenFile
F6C3U4P4X3B1H3T5 NtCreateFile
T2F2T3U2H5B1C1A7 RtlDosPathNameToNtPathName_U
T0E0H4U0X3A3D4D8 RtlCreateUserThread
C5R4X4H7R5T7A5R6 NtReadVirtualMemory
D3S0A7R4F6C8F2R5 NtWriteVirtualMemory
Y1C1B6A7H3C0E7E7 NtSuspendThread
H2E7A5B8Q6G3S7Y3 RtlRegisterWait
D3Q5F2F3R5Y5Y8S2 RtlDeregisterWaitEx
Y2C3G8R5R3A5F5B4 NtOpenEvent
F1D2B6A5T3X2C8R1 NtDebugActiveProcess
G5D3P2G0F6G2H8E6 _wcsicmp
Y6Q6P2G0E5E6G2H8 _wcsnicmp
Y7D3F3S7X2S4F2X3 NtDuplicateObject
X7D0E3R2R4Q0E4D3 NtTerminateThread

The malware then verifies it was launched with administrative privileges by calling the AllocateAndInitializeSid() function with the 0x20 (SECURITY_BUILTIN_DOMAIN_RID) and 0x220 (DOMAIN_ALIAS_RID_ADMINS) sub-authorities and the CheckTokenMembership() function; exactly as advised on MSDN:

Vérification des privilèges administrateur

Checking for administrative privileges

Then the malware tries to retrieve the serial number of the drive Windows has been installed on. The following steps are performed:

  • Decrypting the \.PhysicalDrive0 string;
  • Opening the hard drive in raw mode by calling CreateFile() on \.PhysicalDrive0;
  • Calling the DeviceIoControl() function with IOCTL 0x002d1400, that is IOCTL_STORAGE_QUERY_ PROPERTY, a null request type corresponding to a StorageDeviceProperty request and an output size set to 8.

The call returns the first 8 bytes of a STORAGE_DEVICE_DESCRIPTOR structure:

typedefstruct _STORAGE_DEVICE_DESCRIPTOR {
  DWORD            Version;                // offset 0x0
  DWORD            Size;                   // offset 0x4
  BYTE             DeviceType;             // offset 0x8
  BYTE             DeviceTypeModifier;     // offset 0x9
  BOOLEAN          RemovableMedia;         // offset 0xa
  BOOLEAN          CommandQueueing;        // offset 0xb
  DWORD            VendorIdOffset;         // offset 0xc
  DWORD            ProductIdOffset;        // offset 0x10
  DWORD            ProductRevisionOffset;  // offset 0x14
  DWORD            SerialNumberOffset;     // offset 0x18
  STORAGE_BUS_TYPE BusType;                // offset 0x1c
  DWORD            RawPropertiesLength;    // offset 0x1d
  BYTE             RawDeviceProperties[1]; // offset 0x21
} STORAGE_DEVICE_DESCRIPTOR, *PSTORAGE_DEVICE_DESCRIPTOR;

By requesting only 8 bytes, the malware is retrieving the structure size. Another call to the DeviceIoControl() function allows it to retrieve the whole structure:

Calling DeviceIoControl() twice

It then retrieves the value at offset 0x18 to get the serial number offset (number of bytes from the beginning of the structure). On our analysis system, we obtain the following value:

(gdb) hexdump 0x000df6d8 64
35 35 35 33 34 35 35 30 35 36 35 32 32 30 34 64  |555345505652204d|
32 30 32 30 32 30 32 30 32 30 32 30 32 30 32 30  |2020202020202020|
32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |2....... .......|

The malware has a dedicated function to convert these bytes into a printable string:

(gdb) hexdump $eax 64
53 55 50 45 52 56 4d 00 20 20 20 20 20 20 20 20  |SUPERVM.        |

Should an issue occur while getting this serial number, the malware uses the hardcoded « Kronos » string:

Récupération du numéro de série ou utilisation de la chaîne « Kronos »

Retrieving the serial number or using the « Kronos » string

Finally, it computes the MD5 hash of the string and obtains 2bf42c45a0ca7284efd6d325dd7a4061 in our system. This result is not unfamiliar to us as its first 8 characters we previously saw to be used to name the files and registry values mentioned in the first paragraph.

The malware also create a mutex named after this MD5 (Global2bf42c45a0ca7284efd6d325dd7a4061).

Process hollowing

Confirming our preliminary observation on process hollowing, the dropper then creates a new suspended process from C:WINDOWSexplorer.exe. Here are the first steps to perform live image memory alteration:

  • Calling GetModuleHandle(NULL) to get a handle to the dropper binary;
  • Analyzing this binary PE header to retrieve the value at offset 0x50, that is the image size;
  • Via the ReadProcessMemory() function, reading 4 bytes from the new process at address 0x7ffdd008.

What does this address correspond to? In the new process, the PEB (Process Environment Block) is mapped at address 0x7ffdd00. Indeed, the pslist Volatility command lists the following information for the new process:

$ ./vol.py -f kronos.dump pslist
Offset(V)  Name                    PID   PPID
0x80d68318 explorer.exe            480   1924

The volshell command can be used to read this process memory:

$ ./vol.py -f kronos.dump volshell –p 480
In [1]: dt('_EPROCESS', 0x80d68318)
[_EPROCESS _EPROCESS] @ 0x80D68318
[…]
0x1b0 :Peb                            2147340288

This shows that the PEB is mapped at address 2147340288, which  is 0x7ffdd000. Volshell can dump the content at address 0x7ffdd008:

In [1]: dt('_PEB', 0x7ffdd000)
0x0   : InheritedAddressSpace          0
0x1   : ReadImageFileExecOptions       0
0x2   : BeingDebugged                  0
0x3   : SpareBool                      0
0x4   : Mutant                         4294967295
0x8   : ImageBaseAddress               16777216

0x7ffdd008 is therefore the base address of the new process: 16777216, which is 0x1000000.

The malware execution carries on:

  • Retrieving the values at offsets 0x28 and 0x50 from the PE, respectively the entry point and image size, by reading the new process memory from the base address computed previously;
  • Calling the NtCreateSection() function to create two read/write/execute sections;
  • Using the NtMapViewOfSection() function to map them into the dropper address space;
  • Using the NtMapViewOfSection() to map the second section into the new process address space (this section is therefore shared between both processes).

In our case, the second section is mapped at address 0x90000 in the new process (3rd parameter of NtMapViewOfSection(), set by Windows after the call):

(gdb) x/x $esp+8     // before the call
0x105f990:   0x0105fccc
(gdb) x/x 0x0105fccc // after the call
0x105fccc:   0x00090000

The malware proceeds with its execution:

  • Reading the whole new process image into section 1;
  • Reading the whole dropper image into section 2;
  • In section 1, overwriting 7 bytes at the entry point by a PUSH/RET pointing to the address of the code injected into section 2 in the context of the new process.

Here is the new process entry point code before and after the overwrite:

(gdb) x/4i 0x0107e24e // before
0x107e24e: move edi,edi
0x107e250: push ebp
0x107e251: move ebp,esp
0x107e253: sub esp,0x44

(gdb) x/2i 0x0107e24e // after
0x107e24e: push   0x9f60b
0x107e253: ret
  • Using NtMapViewOfSection() to map section 1 at the new process base address (0x1000000), thus overwriting its entry point code.
  • Calling NtResumeThread() to run the new process.

By using this method, the malware manages to inject and execute code in a remote process without calling OpenProcess()WriteProcessMemory() or CreateRemoteThread().

Injecting into running processes

Like the dropper, the new process searches for ntdll.dll functions using their name hashes, checks for administrative rights and retrieves the hard drive serial number. It then calls the CoCreateGuid() and StringFromGUID2() functions to generate a GUID as a string:

(gdb) hexdump 0x000c4710 128
7b 00 37 00 35 00 37 00 39 00 33 00 31 00 43 00  |{.7.5.7.9.3.1.C.|
45 00 2d 00 42 00 34 00 39 00 43 00 2d 00 34 00  |E.-.B.4.9.C.-.4.|
46 00 38 00 36 00 2d 00 38 00 46 00 30 00 42 00  |F.8.6.-.8.F.0.B.|
2d 00 35 00 36 00 35 00 34 00 38 00 33 00 34 00  |-.5.6.5.4.8.3.4.|
35 00 34 00 46 00 31 00 30 00 7d 00 00 00 00 00  |5.4.F.1.0.}.....|

As mentioned in the first paragraph, this GUID is used to name the directory the malicious binary and configuration file will be copied into.

Afterwards, the malware calls CreateEvent() to create a Global2bf42c45R event (first 8 characters of the MD5, followed by R). It also calls the RegNotifyChangeKeyValue() so it can be notified should the HKCU Run key be tinkered with.

The malicious process uses the function name hashing technique mentioned above to search for functions in the wininet.dll library:

B7P8P7T4E3U2H5A5 InternetOpenA
H0S6D5Q7E8P3P6U5 InternetConnectA
E6F2A3S8Y4C7D5A5 InternetCrackUrlA
Y7D4D7E3T2T2A4U3 HttpOpenRequestA
Y4U1P2F2G7T2A4U3 HttpSendRequestA
C8C0U1A2G4G5Y2B5 HttpQueryInfoA
D6X2S6E3Q3C5B5X2 InternetReadFile
A7S3H3X3D5Y7T7F7 InternetCloseHandle

The malware then enumerates processes by calling CreateToolhelp32Snapshot() and injects code into each of them using a method similar to the one described for process hollowing:

  • Calling NtOpenProcess() to get a handle to the targeted process;
  • Creating a read/write/execute section via NtCreateSection();
  • Using NtMapViewOfSection() to map this section into the current process (malicious explorer.exe);
  • Copying malicious code to this section;
  • Using NtMapViewOfSection() to map this section in the targeted process;
  • Calling CreateRemoteThread() to launch a new thread in the targeted process with a start address pointing to the injected section.

This time, the process doesn’t try to be much stealthy and uses CreateRemoteThread(). It even leaves read/write/execute protections on the injected section, allowing the Volatility malfind plugin to detect the malicious code:

$ ./vol.py -f kronos.dump malfind -p 1396
Process: notepad.exe Pid: 1396 Address: 0x3a0000
Vad Tag: Vad  Protection: PAGE_EXECUTE_READWRITE
Flags: Protection: 6

e9 cf 27 00 00 00 00 00 3c 28 00 00 1c 12 00 00   ..'.....<(......
44 12 00 00 ac 18 00 00 b2 77 92 7c d6 12 91 7c   D........w.|...|
5f e4 91 7c fd dc 91 7c 82 d6 91 7c 5e df 91 7c   _..|...|...|^..|
dcdf 91 7c 76 d9 91 7c aa e1 91 7c bc e7 91 7c   ...|v..|...|...|

0x3a0000 e9cf270000       JMP 0x3a27d4
0x3a0005 0000             ADD [EAX], AL
0x3a0007 003c28           ADD [EAX+EBP], BH
0x3a000a 0000             ADD [EAX], AL

Knowing its base address of 0x3a000, the section can be quickly dumped with the vaddump plugin (malfind –D also works perfectly here):

$ ./vol.py -f kronos.dump vaddump -p 1396 –b 0x3a0000

Injected section

The section injected in the processes running at the infection time is a dozen of kilo-bytes in size and contains both data (full path to the binary, malicious keys and values, etc) and code. As shown above by malfind, execution begins by a JMP to the main code.

The injected code is the same in every process but is not necessarily located at the same base address; the malware therefore often uses a function to get the current value of the EIP register via a CALL/POP primitive (EIP cannot be used as the source operand for a MOV in x86).

This code aims at setting 10 hooks in the current process via the following steps:

  • Calling SYSENTER with EAX = 0x89, that is NtProtectVirtualMemory(), in order to set 8 targeted bytes in read/write/execute protection;
  • Atomic write of these bytes with the CMPXCHG8B instruction;
  • Calling NtProtectVirtualMemory() to set the read/execute protection back.

Each hook redirects the execution flow to a code portion located in the injected section, for example at offset 0x18ac:

(gdb) x/4i 0x7c91e45f //prior
0x7c91e45f: mov    eax,0xce
0x7c91e464: mov    edx,0x7ffe0300
0x7c91e469: call   DWORD PTR [edx]
0x7c91e46b: ret    0x8

(gdb) x/i 0x7c91e45f //after
0x7c91e45f: jmp    0x3a18ac

These hooks are correctly detected by the apihooks Volatility plugin:

$ ./vol.py -f kronos.dump apihooks –p 1396
Hook mode: Usermode
Hook type: Inline/Trampoline
Process: 1396 (notepad.exe)
Victim module: ntdll.dll (0x7c910000 - 0x7c9c7000)
Function: ntdll.dll!NtResumeThread at 0x7c91e45f
Hook address: 0x3a18ac
Hooking module: <unknown>
 
Disassembly(0):
0x7c91e45f e94834a883       JMP 0x3a18ac

The following functions are hooked in every process except System and smss.exe (between brackets the offset to the corresponding code portion in the injected section):

  • NtResumeThread (0x18ac): used to inject into new processes. The malware uses NtCreateSection() followed by calls to NtMapViewOfSection() once again, but this time the execution flow is redirected with NtSetThreadContext();
  • NtQuerySystemInformation (0x1c63): searches for the PID of the malicious explorer.exe process and modifies the NextEntryOffset field of the SYSTEM_PROCESS_INFORMATION describing the previous process to jump to the next process, thus hiding this process;
  • NtCreateFile (0x19a3): doesn’t perform the call if the path matches the one used to store the binary and the configuration file;
  • NtOpenFile (0x1a01): same verification as NtCreateFile;
  • NtQueryDirectoryFile (0x1a50): hides the directory used by the malware;
  • NtEnumerateValueKey (0x1bb4): hides the registry values used by the malware;
  • NtSetValueKey (0x1cdb): doesn’t perform the call if related to one of the malicious values in one of the keys used by the malware;
  • NtDeleteValueKey (0x1d3a): same verification as NtSetValueKey;
  • NtQueryValueKey (0x1d8d): same verification as NtSetValueKey;
  • NtOpenProcess (0x1dec): doesn’t perform the call if the targeted PID, retrieved by dereferencing the 4th parameter ClientId, matches the one of the malicious explorer.exe process.

Rootkit bypass

The way Kronos hooks these functions is not optimal. As an example, it is possible to call the MoveFileEx() function on the malicious binary with the MOVEFILE_DELAY_UNTIL_REBOOT option, so it’s moved to another directory  the next time the system boots. This operation indeed only modifies the HKLMSYSTEMCurrentControlSetControlSession Manager PendingFileRenameOperations key which is not monitored by Kronos, and will be performed when the rootkit is not yet active.

Besides, calls to the NtDeleteKey and NtRenameKey functions are not verified; it is therefore possible to rename or delete both Run keys (HKLM and HKCU) with regedit. Kronos developer didn’t forget this contingency. As mentioned earlier, there is a routine related to RegNotifyChangeKeyValue() which will replace both Run keys when the HKCU Run key is altered. As Windows only notifies the process of such a modification once, the developer also thought of resetting the corresponding event and calling the RegNotifyChangeKeyValue() again. However, when deleting or renaming these keys, it can be observed that the second call always fails with ERROR_KEY_DELETED, stating the operation is being performed on a key marked for deletion, whereupon the corresponding thread is terminated. This means that the first modification will be fixed by the malware, but not the following ones!

In both cases, the malware will not be active anymore on reboot without it being necessary to boot on an alternative system, for example to rename the malicious binary.

Conclusion

Kronos’ rootkit is common and uses well-known userland hooking techniques. It nevertheless uses some tricks, such as manipulating sections to inject code into a remote process, which may not be detected by all antivirus or HIPS correctly.

Note: Thanks to our friends at S21sec for sharing the malware MD5 (f085395253a40ce8ca077228c2322010).