Dridex Loader Analysis

Dridex Loader Analysis

Introduction

Dridex is an old banking Trojan that appeared in 2014 and is still very active today. This is mainly due to its evolution and its complex design/architecture based on proxy layers to hide the main command and control (C&C). This article is a detailed analysis of the Dridex loader found in the wild earlier this year (2021).

The first part is about anti-debug bypass and string/API recovery and the second part is more focused on the loader functionality.

Anti-debug - RtlAddVectoredExceptionHandler

At the beginning of the Dridex loader code, a function is registered using the native API RtlAddVectoredExceptionHandler to handle all the exceptions raised by the "int 3" instructions placed everywhere in the loader:

int_3

This instruction is always followed by the "ret" instruction, preceded by push instructions and a function that takes two DWORDs. This function is actually a custom "GetProcAddress" API and int 3 is a trampoline to execute the previously resolved API via the registered exception handler.

The handler checks if the Exception Code is EXCEPTION_BREAKPOINT and modifies the ESP register in the PCONTEXT structure accordingly for the next ret instruction to execute the real API:

PCONTEXT_struct

To get a better control flow graph and to avoid having your debugger break for each API, a small IDA script can be made to find and patch at runtime all the int 3; ret instructions by call eax:

from idaapi import get_segm_by_name
from idc import patch_byte, add_bpt, set_bpt_cond, BPT_EXEC, load_and_run_plugin
import ida_search

load_and_run_plugin("idapython", 3)


def find_all_occurences(start, end, bin_str, flags):
    occurences = list()
    ea = start
    while ea <= end:
        occurence = ida_search.find_binary(ea, end, bin_str, 0, flags)
        ea = occurence + 1
        occurences.append(occurence)
    return occurences[0:-1]


def patch_binary():
    segment = get_segm_by_name('.text')
    occurences = find_all_occurences(segment.start_ea, segment.end_ea, "CC C3", ida_search.SEARCH_DOWN)

    datas = [0xFF, 0xD0]
    for occurence in occurences:
        for i, byte in enumerate(datas):
            patch_byte(occurence + i, byte)
    return True

APIs

As usual, all the API names are obfuscated and as mentioned earlier, addresses are resolved without using the classical GetProcAddress. Instead, loaded libraries are parsed and functions names are enumerated from the PE export directory header until the CRC32 of the name XORed with a hard-coded key match:

Enumerates_exports_functions

Before resolving the API, the loader checks if the module is loaded using the PEB and PEB_LDR_DATA structures:

list_of_loaded_modules

Once again, it uses a combination of CRC32 and XOR with the same hard-coded key to check the module name. If the module is not loaded, it enumerates DLLs in the Windows directory using the GetSystemWow64DirectoryW and FindFirstFileExW/FindNextFileW APIs and loads it using LdrLoadDLL:

find_DLLs

The following Python script can be used to find which DLL and API are resolved:

import json
import zlib
import sys

#  python3 resolve_api_hash.py 0x588AB3EA 0x649746EC
#  ntDLL.DLL -> NtProtectVirtualMemory

lib_hash = sys.argv[1]
func_hash = sys.argv[2]

with open('exports.json', 'r') as f:  # {"shell32.DLL": ["AppCompat_RunDLLW", "AssocCreateForClasses", ....}
    apis = json.loads(f.read())

xor_key = 0x38BA5C7B  # To change
xor_func_hash = xor_key ^ int(func_hash, 16)
xor_lib_hash = xor_key ^ int(lib_hash, 16)

for lib, funcs in apis.items():
    crc = zlib.crc32(lib.upper().encode('utf-8'))
    if crc == xor_lib_hash:
        for func in funcs:
            crc = zlib.crc32(func.encode('utf-8'))
            if crc == xor_func_hash:
                print("%s -> %s" % (lib, func))

Strings

Strings are decrypted using a function that takes 3 parameters (char *output, char *enc_strings, int string_index):

decrypt_strings

This function decrypts the enc_strings buffer using the RC4 algorithm with a key located in the first 0x28 bytes (in reverse order). Then the index selects the strings to return in the output: SOFTWARE/TrendMicro/Vizor\x00\\VizorUniclientLibrary.DLL\x00ProductPath\x00\x00

The algorithm can be summed up to the following Python script:

import sys
from Crypto.Cipher import ARC4

filepath = sys.argv[1]

with open(filepath, 'rb') as f:
    datas = f.read()

rc4_key = datas[0:0x28]
rc4_key = rc4_key[::-1]

arc4 = ARC4.new(rc4_key)
data = arc4.decrypt(datas[0x28:])

print(data)

System information

Very early in the code, a function is in charge of gathering information about the infected system. This information is stored in a global structure and used when needed for other operations. Below are more details on the gathered information:

Operating version

The build number, the Windows version and the product type are collected through the GetVersionExW function:

GetVersionExW

Process architecture

The current process architecture is obtained using IsWow64Process:

IsWow64Process

Process privilege

The Dridex loader checks his privilege level by comparing the current process token group SID to the local administrator SID (S-1-5-32-544). It uses the GetTokenInformation API with TokenGroups as Token Information:

gettokeninformation

Finally, Dridex allocates the local administrator SID using AllocateAndInitializeSid and compares it using EqualSid:

AllocateAndInitializeSid

UAC level

Dridex checks in the registry SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System the values EnableLUA, ConsentPromptBehaviorAdmin, PromptOnSecureDesktop and attributes a level from 0 to 5 based on the results:

Check_UAC

TokenElevation

Using OpenProcessToken and GetTokenInformation with the parameter TokenElevation, Dridex checks if the current process has elevated privileges:

TokenElevation

SessionId

Dridex also gets the Terminal Services session ID associated with the current process:

SessionId

Process Integrity Level

Similarly, Dridex get the process integrity level using GetTokenInformation with the parameter TokenIntegrityLevel, then attributes a level from 1 to 7 based on the results:

Process Integrity Level

System Info

Finally, Dridex calls GetSystemInfo to get information on:

  • Number of processors;
  • Page size;
  • Minimum application address;
  • Maximum application address;
  • Allocation granularity.
    GetSystemInfo

Final information structure

At the end of the function, we get the following structure:

00000000   info_Struct  struc   ;    (sizeof=0x30,   mappedto_36)
00000000   major_plus_minor              dd   ?
00000004   buildnumber                   dd   ?
00000008   dwMajorVersion                db   ?
00000009   dwMinorVersion                db   ?
0000000A   ServicePackMajor              db   ?
0000000B   x64_x32_bit                   db   ?
0000000C   productType                   dw   ?
0000000E   dwNumberOfProcessors          dw   ?
00000010   dwPageSize                    dd   ?
00000014   lpMinimumApplicationAddress   dd   ?
00000018   lpMaximumApplicationAddress   dd   ?
0000001C   dwAllocationGranularity       dd   ?
00000020   SessionId                     dd   ?
00000024   UAC_level                     dd   ?
00000028   SID_local_administrator       db   ?
00000029   TokenElevation                db   ?
0000002C   RID_level                     dd   ?
00000030   info_Struct                   ends

C&C Requests

The Dridex loader talks to its C&Cs to download the core module and the node list. The communication is encrypted using RC4 and the protocol used is HTTPS. Below is a more detailed explanation of how the function does this job. First, it takes a hash in its parameters that will later identify the request type:

Make_CnC_Requests

By parsing the .data section, it builds a structure with the bot ID and a list of hard-coded IPs:

00000000:  ff  ff  ff  ff  01  00  00  00  00  00  00  00  00  00  00  00    ................
00000010:  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00    ................
00000020:  f3  73  10  57 [7f  27  00  00] 00  7a  01 [03][51  a9  e0  de]   .s.W.'...z..Q...
00000030: [3d  0d][3e  4b  a8  6a][2e  0f][52  a5  98  7f][3d  0d] d6  ec    =.>K.j..R...=...

Bot_Id: [7f 27 00 00] -> 10111

IPs (0x03):

  • [51 a9 e0 de][3d 0d] -> 81.169.224.222:3389
  • [3e 4b a8 6a][2e 0f] -> 62.75.168.106:3886
  • [52 a5 98 7f][3d 0d] -> 82.165.152.127:3389

Generate_IPs

Then, it starts building the requests to be sent to the C&Cs in binary format using previously gathered information. The request looks like this:

00000000: [2b][58  58  58  58  58  58  58  58  58  58  5f  63  35  39  31    +XXXXXXXXXX_c591
00000010:  39  35  34  37  30  31  39  31  64  64  66  34  63  30  66  39    95470191ddf4c0f9
00000020:  65  35  34  65  33  33  30  34  36  33  38  36][32  63  33  38    e54e330463862c38
00000030:  61  39  66  30  30  38  64  61  63  39  61  36  63  64  37  39    a9f008dac9a6cd79
00000040:  38  36  66  62  39  66  65  64  66  62  62  32][00  6f][1d  b0    86fb9fedfbb2.o..
00000050:  f0  11][01  1f  04  11][40] 00  00 [03  3f][49  6e  74  65  6c    ......@...?Intel
00000060:  ...skip...
00000390:  69  6e  67  20  70  61  74  68  3a  20] 00  00 [07  4e][41  4c    ing  path:  ...NAL
000003a0:  ....skip..
00000ae0:  72  3d  43  3a  5c  57  69  6e  64  6f  77  73]                   r=C:\Windows

From left to right, the fields are the following:

  • len(unique_account);
  • unique_account;
  • unique_system_hash;
  • bot_id;
  • sys_info;
  • command;
  • process_arch;
  • len(process_installed);
  • process_installed;
  • len(envs);
  • envs.

The unique_account field is the concatenation of the Computer Name and the MD5 hash of the following expression: md5(computer_name + user_name + \x00 + \x00\x00 + installdate + \x00\x00).

The unique_system_hash is also an MD5 hash: md5(serial volume + install date + arch + \x00\x00).

The sys_info field is built using the following code:

sys_info_field

In this example, [1d b0] (7600) is the Windows build number, [f0] is a bit field that depends on the current product types, process architecture, UAC flag and Administrator rights. [11] indicates the Windows version ((majorversion << 4 - 0x50) + minorversion).

The command field is the command name CRC32 code (e.g. "bot" == 0x11041f01) and it is given as a parameter. The following commands were found in the loader:

  • 0x11041f01 -> ("bot");
  • 0x18F8C844 -> ("list");
  • 0x745A17F5 -> ("mod9" -> TrendMicro Exclusion vulnerability);
  • 0xD3EF7577 -> ("dmod5" -> DllLoaded);
  • 0x69BE7CEE -> ("dmod6" -> DllStarted);
  • 0x32DC1DF8 -> ("dmod11" -> StartedInHi).

The process_installed field is extracted from the following registry: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall

The envs field is generated using the GetEnvironmentStringsW API.

Before sending the request, the payload is encrypted using RC4 (the key comes from the recovered strings) and prepended by its CRC32 code:

RC4_key

The request is sent in a POST message using the WinINet library (InternetOpenA, InternetConnectW, HttpOpenRequestW, HttpSendRequestW):

POST_methods

The answer is read by calling the InternetReadFile function and if the response code is 200 or 404, the content is decrypted using RC4 with the same RC4 key as for encrypting the payload:

rc4_decrypt_rep_content

The response always starts with a CRC32 code of the content, followed by the content itself, which is RC4-encrypted with the same key as for sending the command.

Bot command

Once decrypted, the "bot" command response reveals an RSA signature (0x80 bytes long) and the Dridex "core" DLL at offset 0x80:

00000000: 921c 0824 eef2 954a a522 5014 0384 e394  ...$...J."P.....
00000010: b053 b2ce a5fd aeef 6796 bd1c 5edd 764d  .S......g...^.vM
00000020: 2c28 ea58 7e40 2132 8389 5259 333b 9d80  ,(.X~@!2..RY3;..
00000030: bcfa 5af5 9eeb 0ac0 22c8 e079 1510 b48e  ..Z....."..y....
00000040: d53c e43f b9d7 19ea 23a9 8e2e 4f9f 0397  .<.?....#...O...
00000050: c3a5 d586 f1b0 864b 5b2e 03e7 3750 b371  .......K[...7P.q
00000060: 3e42 f62b f1da f555 954e 4bee fae7 823c  >B.+...U.NK....<
00000070: 2a7a 812c ba90 cfba bf0a 8965 2a5c 122d  *z.,.......e*\.-
00000080: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............
00000090: b800 0000 0000 0000 4000 0000 0000 0000  ........@.......
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 8401 0000  ................
000000c0: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468  ........!..L.!Th
000000d0: 6973 2070 726f 6772 616d 2063 616e 6e6f  is program canno
000000e0: 7420 6265 2072 756e 2069 6e20 444f 5320  t be run in DOS
000000f0: 6d6f 6465 2e0d 0d0a 2400 0000 0000 0000  mode....$.......
00000100: d006 fb75 9467 9526 9467 9526 9467 9526  ...u.g.&.g.&.g.&
00000110: 9935 4826 2567 9526 21f9 4b26 da66 9526  .5H&%g.&!.K&.f.&
00000120: f289 5926 e667 9526 0f8c 5b26 6a66 9526  ..Y&.g.&..[&jf.&
00000130: ...skip...

This signature is in the PKCS#1 v1.5 SHA1withRSA format. It can be used to verify the payload content using an RSA1024 public key found in the decrypted strings from the core DLL:

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA9pRumL/WGRvdjoENFuUFZi/f
OB4AaC5yUmFnTYON2qothUQiLagPsXnVNPC/lF30qb/DJbdkWk4i4nbm715TE1np
cmC9Fm4Dh9IPFpaFAuI73R6ywzxsSodkfHqDlS8N0Nf69sOX58bSf96IPKSGY2FV
ra3DZaYLeH6S7EwinQIDAQAB
openssl dgst -sha1 -verify public.pem -signature signature_core 200_rep_content_bot_decoded_mz.bin
Verified OK

Node list command

The decrypted "list" command response is not as easy as the "bot" command to understand:

00000000: 4ea7 8684 7e01 2b49 f3df 2efa e02d 9621  N...~.+I.....-.!
00000010: 05e0 6318 e3f7 298a 1d67 e4fa 1349 f7c9  ..c...)..g...I..
00000020: 60b4 06b2 c41c 91a6 4cad 9427 d32e 3775  `.......L..'..7u
00000030: 0f58 bed2 5b92 383a 3b49 8892 0d42 e85b  .X..[.8:;I...B.[
00000040: b335 6132 5223 2d3c 4e0e 3b65 0596 f4a6  .5a2R#-<N.;e....
00000050: 4b97 5c79 f4ef 964f 27a9 7654 b67b 65af  K.\y...O'.vT.{e.
00000060: 8f5e 0b02 a61e 521b 1a49 eb19 8af2 c08e  .^....R..I......
00000070: 8c37 6e51 cf3c ba62 f249 17ea a816 8c8e  .7nQ.<.b.I......
00000080: bd10 3a0d ac9c 7b44 4281 5bda 48e9 05c1  ..:...{DB.[.H...
00000090: b5e4 faeb 8ca2 7554 c375 7803 4b86 c3d5  ......uT.ux.K...
000000a0: 3233 3100 7321 c702 213e 953b 6577 011b  231.s!..!>.;ew..
000000b0: 091d 5a91 4b33 6f3e eff2 5ffa 7e38 e389  ..Z.K3o>.._.~8..
000000c0: 4c94 7d02 5077 4143 1c96 e768 9e7e b097  L.}.PwAC...h.~..
000000d0: 2438 1feb 7a46 a694 28f2 cfbb 9a7c f72c  $8..zF..(....|.,
000000e0: cfdf b42c b69c 9b4e 97bd 2291 1f1a ec79  ...,...N.."....y
000000f0: abfe 25fe c13c fefd 131b 0146 40cf 4244  ..%..<.....F@.BD
00000100: d628 00bc f85d f599 5cf2 e27f 58bb c753  .(...]..\...X..S
00000110: 3be3 2da8 02d1 4ef9                      ;.-...N.

The format is as follows:

  • CRC32 code of the content (4 Bytes);
  • SHA1withRSA1024 signature (128 Bytes);
  • First RC4 key (16 Bytes);
  • Content length, RC4-encrypted (4 Bytes);
  • Second RC4 key (16 Bytes);
  • List content, RC4-encrypted (length Bytes).

The structure looks like this:

00000000: [4e  a7  86  84][7e  01  2b  49  f3  df  2e  fa  e0  2d  96  21    N...~.+I.....-.!
00000010:  05  e0  63  18  e3  f7  29  8a  1d  67  e4  fa  13  49  f7  c9    ..c...)..g...I..
00000020:  60  b4  06  b2  c4  1c  91  a6  4c  ad  94  27  d3  2e  37  75    `.......L..'..7u
00000030:  0f  58  be  d2  5b  92  38  3a  3b  49  88  92  0d  42  e8  5b    .X..[.8:;I...B.[
00000040:  b3  35  61  32  52  23  2d  3c  4e  0e  3b  65  05  96  f4  a6    .5a2R#-<N.;e....
00000050:  4b  97  5c  79  f4  ef  96  4f  27  a9  76  54  b6  7b  65  af    K.\y...O'.vT.{e.
00000060:  8f  5e  0b  02  a6  1e  52  1b  1a  49  eb  19  8a  f2  c0  8e    .^....R..I......
00000070:  8c  37  6e  51  cf  3c  ba  62  f2  49  17  ea  a8  16  8c  8e    .7nQ.<.b.I......
00000080:  bd  10  3a  0d][ac  9c  7b  44  42  81  5b  da  48  e9  05  c1    ..:...{DB.[.H...
00000090:  b5  e4  fa  eb][8c  a2  75  54][c3  75  78  03  4b  86  c3  d5    ......uT.ux.K...
000000a0:  32  33  31  00  73  21  c7  02][21  3e  95  3b  65  77  01  1b    231.s!..!>.;ew..
000000b0:  09  1d  5a  91  4b  33  6f  3e  ef  f2  5f  fa  7e  38  e3  89    ..Z.K3o>.._.~8..
000000c0:  4c  94  7d  02  50  77  41  43  1c  96  e7  68  9e  7e  b0  97    L.}.PwAC...h.~..
000000d0:  24  38  1f  eb  7a  46  a6  94  28  f2  cf  bb  9a  7c  f7  2c    $8..zF..(....|.,
000000e0:  cf  df  b4  2c  b6  9c  9b  4e  97  bd  22  91  1f  1a  ec  79    ...,...N.."....y
000000f0:  ab  fe  25  fe  c1  3c  fe  fd  13  1b  01  46  40  cf  42  44    ..%..<.....F@.BD
00000100:  d6  28  00  bc  f8  5d  f5  99  5c  f2  e2  7f  58  bb  c7  53    .(...]..\...X..S
00000110:  3b  e3  2d  a8  02  d1  4e  f9]                                   ;.-...N.

After checking that the first 4 bytes are the CRC32 code, it extracts the first RC4 key in 0x84:0x94 just after the RSA signature (128 bytes). With this key, it can decrypt the next four bytes [8c a2 75 54] to 00 00 00 70. Then, the second RC4 key can be extracted (0x98:0xa8) to decrypt the remaining data:

cat 200_rep_content_list_decoded.bin | snip 0xa8: | rc4 h:C37578034B86C3D5323331007321C702 | xxd
00000000: 1f8b 0800 0000 0000 0203 0159 00a6 ff10  ...........Y....
00000010: 0000 0054 6bb5 bb4c 01c5 2d91 37aa 01bb  ...Tk..L..-.7...
00000020: add4 ced3 01bb 175f 842c 0709 c003 8842  ......._.,.....B
00000030: 01c5 4e52 b1d6 0d73 2d0d c772 01c5 adb7  ..NR...s-..r....
00000040: dad1 01bb ce77 5a31 0d3a bdb3 7eb1 01bb  .....wZ1.:..~...
00000050: 5921 a46c 20fb 68a8 d570 0410 83c4 fd94  Y!.l .h..p......
00000060: 01bb c0fa c665 01bb 006b c3f4 5900 0000  .....e...k..Y...

Indeed, the content is gzip-encoded and the first 4 decrypted bytes (00 00 00 70) are the content size:

cat 200_rep_content_list_decoded.bin | snip 0xa8: | rc4 h:C37578034B86C3D5323331007321C702 | gzip -d | xxd -g 1
00000000: [10][00  00  00  54][6b  b5  bb  4c][01  c5][2d  91  37  aa][01    ....Tk..L..-.7..
00000010:  bb][ad  d4  ce  d3][01  bb][17  5f  84  2c][07  09][c0  03  88    ........_.,.....
00000020:  42][01  c5][4e  52  b1  d6][0d  73][2d  0d  c7  72][01  c5][ad    B..NR...s-..r...
00000030:  b7  da  d1][01  bb][ce  77  5a  31][0d  3a][bd  b3  7e  b1][01    ......wZ1.:..~..
00000040:  bb][59  21  a4  6c][20  fb][68  a8  d5  70][04  10][83  c4  fd    .Y!.l  .h..p.....
00000050:  94][01  bb][c0  fa  c6  65][01  bb]                               ......e..

The first field is a marker (0x10), the second is the node list size and the rest is the list of nodes.

Sharing the node list with the core DLL

Because the decryption is not done in the loader but in the core DLL, it needs a way to share the answer content of the list command. This is why the loader uses the Windows registry Software\Microsoft\Windows\CurrentVersion\Explorer\CLSID\%s\shellfolder where %s is a CLISD built from the unique_account hashes with the hard-coded number 0x1c:

reg_nodes_list_path

The saved data contains the bot ID (7f 27], the node list size, the node list and the scheduled tasks URI (details in the next parts):

00000000: [00  00  00  00][7f  27][18  01][4e  a7  86  84  7e  01  2b  49    .....'..N...~.+I
00000010:  f3  df  2e  fa  e0  2d  96  21  05  e0  63  18  e3  f7  29  8a    .....-.!..c...).
00000020:  1d  67  e4  fa  13  49  f7  c9  60  b4  06  b2  c4  1c  91  a6    .g...I..`.......
00000030:  4c  ad  94  27  d3  2e  37  75  0f  58  be  d2  5b  92  38  3a    L..'..7u.X..[.8:
00000040:  3b  49  88  92  0d  42  e8  5b  b3  35  61  32  52  23  2d  3c    ;I...B.[.5a2R#-<
00000050:  4e  0e  3b  65  05  96  f4  a6  4b  97  5c  79  f4  ef  96  4f    N.;e....K.\y...O
00000060:  27  a9  76  54  b6  7b  65  af  8f  5e  0b  02  a6  1e  52  1b    '.vT.{e..^....R.
00000070:  1a  49  eb  19  8a  f2  c0  8e  8c  37  6e  51  cf  3c  ba  62    .I.......7nQ.<.b
00000080:  f2  49  17  ea  a8  16  8c  8e  bd  10  3a  0d  ac  9c  7b  44    .I........:...{D
00000090:  42  81  5b  da  48  e9  05  c1  b5  e4  fa  eb  8c  a2  75  54    B.[.H.........uT
000000a0:  c3  75  78  03  4b  86  c3  d5  32  33  31  00  73  21  c7  02    .ux.K...231.s!..
000000b0:  21  3e  95  3b  65  77  01  1b  09  1d  5a  91  4b  33  6f  3e    !>.;ew....Z.K3o>
000000c0:  ef  f2  5f  fa  7e  38  e3  89  4c  94  7d  02  50  77  41  43    .._.~8..L.}.PwAC
000000d0:  1c  96  e7  68  9e  7e  b0  97  24  38  1f  eb  7a  46  a6  94    ...h.~..$8..zF..
000000e0:  28  f2  cf  bb  9a  7c  f7  2c  cf  df  b4  2c  b6  9c  9b  4e    (....|.,...,...N
000000f0:  97  bd  22  91  1f  1a  ec  79  ab  fe  25  fe  c1  3c  fe  fd    .."....y..%..<..
00000100:  13  1b  01  46  40  cf  42  44  d6  28  00  bc  f8  5d  f5  99    ...F@.BD.(...]..
00000110:  5c  f2  e2  7f  58  bb  c7  53  3b  e3  2d  a8  02  d1  4e  f9]   \...X..S;.-...N.
00000120: [4d  69  63  72  6f  73  6f  66  74  5c  57  69  6e  64  6f  77    Microsoft\Window
00000130:  73  5c  49  6e  73  74  61  6c  6c  53  65  72  76  69  63  65    s\InstallService
00000140:  5c  53  6d  61  72  74  52  65  74  72  79  2d  53  2d  31  2d    \SmartRetry-S-1-
00000150:  35  2d  32  31  2d  34  30  37  32  35  37  39  31  36  2d  31    5-21-407257916-1
00000160:  38  33  31  36  35  34  35  30  37  2d  32  36  34  33  30  33    831654507-264303
00000170:  36  33  36  34  2d  31  30  30  31  7c  4d  69  63  72  6f  73    6364-1001|Micros
00000180:  6f  66  74  5c  57  69  6e  64  6f  77  73  5c  57  44  49  5c    oft\Windows\WDI\
00000190:  4c  67  77  7a  6f  71  6a]                                       Lgwzoqj

uncrypted_nodes_list_struct

Before writing the registry value, the original response content of the "list" command and its length are encrypted using RC4 with two random keys (0x10-byte-long) and the CRC32 code of the content is prepended:

00000000: [ba  3d  cc  e0][f0  e9  b3  53  b0  46  e0  d2  f2  8e  b0  7b    .=.....S.F.....{
00000010:  fb  c2  c8  8d][b2  4b  c6][93  30  d0  52  2d  92  66  de  da    .....K..0.R-.f..
00000020:  db  fd  d0  db  18  cc  7b  b2][93  ed  d6  3b  98  e6  ec  8f    ......{....;....
00000030:  84  4d  74  66  48  ab  72  7a  f8  08  f2  08  a7  4e  53  3e    .MtfH.rz.....NS>
00000040:  ce  09  f4  f9  26  93  f2  33  3b  76  db  23  df  91  90  b9    ....&..3;v.#....
00000050:  86  96  f6  3f  5b  c1  97  b8  41  32  39  ac  7e  00  94  c0    ...?[...A29.~...
00000060:  0c  35  b7  d7  96  fa  b7  57  71  07  63  09  b1  23  e4  72    .5.....Wq.c..#.r
00000070:  6c  5b  a3  72  ed  31  e8  f7  62  1e  d3  67  06  29  5c  aa    l[.r.1..b..g.)\.
00000080:  b4  dc  36  18  a8  e4  1f  b4  3a  e4  5a  a0  0b  cc  aa  ba    ..6.....:.Z.....
00000090:  b3  cc  2c  25  eb  cf  e5  b4  21  a6  e7  63  64  88  10  1a    ..,%....!..cd...
000000a0:  c7  44  03  9f  bd  87  9e  0c  98  1b  23  5a  bc  22  75  4d    .D........#Z."uM
000000b0:  84  8c  0b  d4  c1  8a  fb  98  ec  bd  60  66  7d  05  89  7f    ..........`f}...
000000c0:  bf  3b  8c  8f  55  88  5c  59  ed  dc  bd  53  ee  8f  52  b8    .;..U.\Y...S..R.
000000d0:  24  9a  34  70  62  87  0a  64  26  83  ff  78  79  2e  2e  25    $.4pb..d&..xy..%
000000e0:  e4  8f  3a  75  ad  93  e4  52  21  7f  ba  0c  b8  25  e7  a7    ..:u...R!....%..
000000f0:  f8  7f  31  6b  e6  95  72  c5  77  e2  c5  0e  6a  19  98  2a    ..1k..r.w...j..*
00000100:  59  01  40  aa  59  d0  cd  a3  64  eb  d5  00  7d  5e  93  3b    Y.@.Y...d...}^.;
00000110:  af  e1  ad  0c  f2  01  b9  c3  dd  45  b9  15  51  f0  33  81    .........E..Q.3.
00000120:  8e  3f  54  41  c5  41  22  96  bf  88  ce  80  e4  ba  97  32    .?TA.A"........2
00000130:  6d  72  98  7f  24  42  3e  b8  63  12  9f  4c  2c  b4  73  f8    mr..$B>.c..L,.s.
00000140:  86  58  12  d6  95  d6  59  41  b5  92  c9  0d  23  62  1f  7d    .X....YA....#b.}
00000150:  65  c1  a1  b8  1d  a8  d5  ad  46  ba  9c  70  5c  9e  40  4d    e.......F..p\.@M
00000160:  21  ab  5b  a5  6e  7f  2c  d4  5e  8b  38  ef  b7  50  6b  be    !.[.n.,.^.8..Pk.
00000170:  55  4d  e0  04  22  6e  15  d2  99  d7  9f  fe  0c  f5  78  72    UM.."n........xr
00000180:  e3  d5  cf  0b  b9  08  bf  10  a7  64  38  d9  e1  0e  9b  57    .........d8....W
00000190:  9b  24  a5  00  c2  ca  e3  12  94  35  3c  74  00  49  eb  93    .$.......5<t.I..
000001a0:  41  bf  28  45  1a  bf  5d  d9  50  87  25  82  d7  1e  17  f6    A.(E..].P.%.....
000001b0:  b8  b3  4a  0b  6e  03  e6  76  2e  02  96  12  da  a9  70]       ..J.n..v......p

From left to right, the fields are the following:

  • original content CRC32 code;
  • first random RC4 key;
  • length of the original content, RC4-encrypted (with the first RC4 key);
  • second random RC4 key;
  • original content, RC4-encrypted (with the second RC4 key).

And finally, it is RC4-encrypted again using the previous generated unique_account hashes with the hard-coded number 0x1c and set using RegSetValueExA:

RegSetValueExA

The Dridex core DLL can now access the content.

Response parsing implementation

Below are functions that can be used to parse, validate and decrypt the "list" and "bot" command output:

from Crypto.Cipher import ARC4
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA
import gzip
import binascii
import struct


def validate_decrypt_bot(botcont, rc4k, public_key):
    # Check CRC32
    crc = binascii.crc32(botcont[4:])
    chk = struct.unpack(">I", botcont[:4])[0]
    if crc != chk:
        print("Incorrect CRC32, wrong file ?")
        return None

    # Decrypt data
    arc4 = ARC4.new(rc4k)
    data = arc4.decrypt(botcont[4:])

    # Check RSA signature
    key = RSA.importKey(public_key)
    h = SHA.new(data[0x80:])
    verifier = PKCS1_v1_5.new(key)
    if not verifier.verify(h, data[:0x80]):
        print("Incorrect signature, wrong public key?")
        return None

    return data[0x80:]


def validate_decrypt_list(content, rc4k, public_key):
    # Check CRC32
    crc = binascii.crc32(content[4:])
    chk = struct.unpack(">I", content[:4])[0]
    if crc != chk:
        print("Incorrect CRC32, wrong file?")
        return None

    # Decrypt data
    arc4 = ARC4.new(rc4k)
    data = arc4.decrypt(content[4:])

    # Check decrypted CRC32
    crc = binascii.crc32(data[4:])
    chk = struct.unpack(">I", data[:4])[0]
    if crc != chk:
        print("Incorrect CRC32, wrong key?")
        return None

    # Check RSA signature
    key = RSA.importKey(public_key)
    h = SHA.new(data[0x84:])
    verifier = PKCS1_v1_5.new(key)
    if not verifier.verify(h, data[0x04:0x84]):
        print("Incorrect signature, wrong public key?")
        return None

    # Decrypt node list length
    arc4 = ARC4.new(data[0x84:0x94])

    # Decrypt node list
    arc4 = ARC4.new(data[0x98:0xA8])
    third = arc4.decrypt(data[0xA8:])
    list_bot = gzip.decompress(third)

    # Parse node list
    ret = list()
    if list_bot[0] == 0x10:
        size = struct.unpack(">I", list_bot[1:5])[0]
        if size + 5 == len(list_bot):
            for l in range(5, len(list_bot), 6):
                j = struct.unpack(">BBBBH", list_bot[l:l+6])
                ret.append("https://%d.%d.%d.%d:%d/" % (int(j[0]), int(j[1]), int(j[2]), int(j[3]), int(j[4])))
        else:
            print("Length error")
            return None
    else:
        print("Magic error")
        return None

    return ret

Persistence and execution of the core DLL

Dridex copies a random legitimate program from C:\Windows\System32 to a new directory in %AppData% (randomly named) and the Dridex core DLL is copied to the same folder and renamed to one of the DLLs imported by the legitimate program. Later on, a scheduled task will run the legitimate binary and one of its DLLs will be hijacked by the Dridex core DLL. Below is a more detailed description of how this is done.

DLL hijack

First, Dridex scans *.exe files in C:\Windows\System32\ and selects one binary that does not have the property to AutoElevated. It also enumerates imported DLLs in the binary and checks if the name of one of them matches a CRC32 whitelist:

check_import_DLL

Below are the corresponding DLLs based on the CRC32 whitelist:

  • ACTIVEDS.DLL
  • APPWIZ.CPL
  • CREDUI.DLL
  • D3D10.DLL
  • D3D10_1.DLL
  • D3D9.DLL
  • DPX.DLL
  • DUI70.DLL
  • DUSER.DLL
  • DWMAPI.DLL
  • DXGI.DLL
  • DXVA2.DLL
  • FVEWIZ.DLL
  • HID.DLL
  • ISCSIDSC.DLL
  • ISCSIUM.DLL
  • MAGNIFICATION.DLL
  • MFC42U.DLL
  • MFPLAT.DLL
  • MMCBASE.DLL
  • MSCMS.DLL
  • MSSWCH.DLL
  • NDFAPI.DLL
  • NETPLWIZ.DLL
  • NEWDEV.DLL
  • OLEACC.DLL
  • P2P.DLL
  • P2PCOLLAB.DLL
  • QUARTZ.DLL
  • REAGENT.DLL
  • SECUR32.DLL
  • SLC.DLL
  • SPP.DLL
  • SQMAPI.DLL
  • SRCORE.DLL
  • SRVCLI.DLL
  • SYSDM.CPL
  • TAPI32.DLL
  • UXTHEME.DLL
  • VERSION.DLL
  • WER.DLL
  • WINBRAND.DLL
  • WINMM.DLL
  • WINSTA.DLL
  • WMSGAPI.DLL
  • WTSAPI32.DLL
  • XMLLITE.DLL

When a binary with a matching imported DLL is found, the legitimate DLL export directory content replaces the missing one in the core DLL:

original_core_DLL

new_core_DLL_data_dir

new_core_DLL_export_dir

The core DLL is copied to a new directory (randomly named) in AppData\Roaming and the DLL filename is borrowed from the legitimate one.

appdata_roaming_write

The selected legitimate binary is also copied in the same directory:

ls_roaming_dir

Everything is set up for the scheduled task.

Task scheduler

Depending on the process privilege, one or two scheduled tasks are registered. The function which registers the scheduled task uses a COM object and the task properties are set using the XML format. Important properties are set dynamically by the following tags:

  • <author> (specifies the author of the task);
  • <URI> (specifies where the registered task is placed in the task folder hierarchy);
  • <UserID> (specifies the user identifier required to run those tasks associated with the principal);
  • <exec><command> (specifies an action that executes a command line operation).

With administrator privileges, two scheduled tasks are set. In both cases, the task URI is located in a random dir in C:\Windows\System32\Tasks\Microsoft\Windows\:

get_task_folder

echo -en 'tasks' | crc32 | xor h:38BA5C7B | xxd
00000000: 68e2 39ec                                h.9.
echo -en 'microsoft' | crc32 | xor h:38BA5C7B | xxd
00000000: 0523 75f3                                .#u.
echo -en 'windows' | crc32 | xor h:38BA5C7B | xxd
00000000: db5d d9e0                                .]..

The difference starts with the URI filename. In the first task, the URI takes a legitimate task file and appends the user SID (e.g. <URI>\Microsoft\Windows\CloudExperienceHost\CreateObjectTask-S-1-5-21-407257916-1831654507-2643036364-1001</URI>):

task_uri_SID

The <author> element is hard-coded <Author>$(@%systemroot%\system32\wininet.DLL,-16000)</Author>.

The <exec><command> is the path to the binary with the hijacked DLL as seen previously in the AppData\Roaming dir:
<Exec><Command>C:\Users\YYYYYYYYYYYY\AppData\Roaming\Xsbzewcltzyxfl\rstrui.exe</Command></Exec>

On the second scheduled task, the <URI> starts with the randomly selected dir and the filename is built with a pseudo-random algorithm based on the previously seen unique_account hashes. Basically, it generates MD5 hashes and picks only ASCII letters to build a string until it is long enough:

generate_pseudo_random_strings

The <author> element is a field copy from the randomly selected task in the C:\Windows\System32\Tasks\Microsoft\Windows\. To get the value, it scans the XML task file until it finds the <author> tags using the traditional CRC32 code methods:

get_author_tag

echo -en 'author' | crc32 | xor h:38BA5C7B | xxd
00000000: 8515 84b3                                ....

In the second task, the <exec><command> is a random dir in C:\Windows\System32 that does not exist at that moment. It is still unclear what the purpose of this scheduled task is:

random_dir_in_sys32

To register the task, Dridex uses a COM Object by calling CoCreateInstance to create a TaskService instance:

ITaskService

The Connect, getfolder, NewTask and RegisterTaskDefinition methods are called to register the task. Both tasks are launched at the opening session and every 30 minutes:

First_task

secund_task

The full permission is granted to the task file:

Grant_task_URI_Full_permissions

The differences with normal privileges are the following:

  1. The URI path of the created task is in the task root folder;
  2. The name is generated from the pseudo-random function.

Mutex

Before and after the scheduled task is registered, the loader checks the presence of a mutex. If the mutex is found, it means that the core DLL has been successfully started by the scheduled task and the core DLL is already injected in the explorer.exe process. Otherwise, it tries to reschedule a task.

mutex_check

The mutex name is generated using the same technique as the unique_account with a hard-coded number (md5(computer_name + user_name + \x00 + \x02\x00 + installdate + \x00\x00)) and formatted as a CLSID.

The following script can be used to check if your computer is infected.

Function Test-IsMutexAvailable {
    <#
    from: https://www.powershellgallery.com/packages/PSBuildSecrets/1.0.31/Content/Private%5CTest-IsMutexAvailable.ps1
    .SYNOPSIS
        check if current thread is able to acquire an exclusive lock on a system mutex.
    .DESCRIPTION
        A mutex can be used to serialize applications and prevent multiple instances from being opened at the same time.
        Wait, up to a timeout (default is 1 millisecond), for the mutex to become available for an exclusive lock.
    .PARAMETER MutexName
        The name of the system mutex.
    .EXAMPLE
        Test-IsMutexAvailable -MutexName 'Global\B475815D-EA35-2753-859C-6D042FE3C161'
    .NOTES
        This is an internal script function and should typically not be called directly.
    #>
        [CmdletBinding()]
        Param (
            [Parameter(Mandatory=$true)]
            [ValidateLength(1,500)]
            [string]$MutexName
        )

    Try {
        Write-Host "[+] Check to see if mutex $MutexName is available."
        ## Using this variable allows capture of exceptions from .NET methods. Private scope only changes value for current function.
        $private:previousErrorActionPreference = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        ## Open the specified named mutex, if it already exists, without acquiring an exclusive lock on it. If the system mutex does not exist, this method throws an exception instead of creating the system object.
        [Threading.Mutex]$OpenExistingMutex = [Threading.Mutex]::OpenExisting($MutexName)
        $IsMutexExist = $true
        $OpenExistingMutex.Close()
    }
    Catch [Threading.WaitHandleCannotBeOpenedException] {
        Write-Host "The named mutex does not exist"
        $IsMutexFree = $true
        $IsMutexExist = $false
    }
    Catch [ObjectDisposedException] {
        Write-Host "Mutex was disposed between opening it and attempting to wait on it"
        $IsMutexFree = $true
        $IsMutexExist = $true
    }
    Catch [UnauthorizedAccessException] {
        Write-Host "The named mutex exists, but the user does not have the security access required to use it"
        $IsMutexFree = $false
        $IsMutexExist = $true
    }
    Catch [Threading.AbandonedMutexException] {
        Write-Host "The wait completed because a thread exited without releasing a mutex. This exception is thrown when one thread acquires a mutex object that another thread has abandoned by exiting without releasing it."
        $IsMutexFree = $true
        $IsMutexExist = $true
    }
    Catch {
        $IsUnhandledException = $true
        Write-Host "Return $true, to signify that mutex is available, because function was unable to successfully complete a check due to an unhandled exception. Default is to err on the side of the mutex being available on a hard failure."
        Write-Verbose "Unable to check if mutex [$MutexName] is available due to an unhandled exception. Will default to return value of [$true]. `n$(Resolve-Error)" -Severity 3
        $IsMutexFree = $true
        $IsMutexExist = $true
    }
    $HashObject = @{
        MutexName = $MutexName
        IsMutexExist = $IsMutexExist
    }
    $Result += New-Object PSObject -Property $HashObject
    return $Result
}

$enc = [system.Text.Encoding]::UTF8
$datas = $enc.GetBytes($env:ComputerName) + $enc.GetBytes($env:UserName) + [byte]0x00 + [byte]0x02 + [byte]0x00
$date = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\' | select -ExpandProperty InstallDate

$x = [BitConverter]::GetBytes($date)
$datas = $datas + $x + [byte]0x00 + [byte]0x00

$md5 = [System.Security.Cryptography.MD5]::Create("MD5")
$md5.TransformFinalBlock($datas, 0, $datas.length)
$hash_txt = ''
$md5.Hash | foreach {
    $hash_txt += '{0:X2}' -f $_
}
$hash_guid = [System.guid]::New($hash_txt)
$hash_guid = '{' + $hash_guid + '}'

Write-Host "[---------------- Mutex ----------------]"
$IsMutexExist = Test-IsMutexAvailable -MutexName $hash_guid
Write-Host $IsMutexExist

Output on an uncompromised system:

[---------------- Mutex ----------------]
[+] Check to see if mutex {879f371e-fa61-7ba4-a4ab-805bbe55a0c7} is available.
The named mutex does not exist
@{MutexName={879f371e-fa61-7ba4-a4ab-805bbe55a0c7}; IsMutexExist=False}

Output on a compromised system:

[---------------- Mutex ----------------]
[+] Check to see if mutex {879f371e-fa61-7ba4-a4ab-805bbe55a0c7} is available.
@{MutexName={879f371e-fa61-7ba4-a4ab-805bbe55a0c7}; IsMutexExist=True}

Conclusion

Dridex loader techniques are common and do not integrate any novel features. The string and API obfuscating mechanisms are very standard but the anti-debug technique using Vector Exception Handler can be very painful without any sort of bypass, because it is on every API call. The network communication with the C&C combines HTTPS with RC4. Moreover, the binary format makes it very hard to understand without any sort of reverse engineering. Finally, the persistence mechanism using the scheduled task is also common, but the use of DLL hijacking makes it very effective.

IOCs

Sample hash

  • SHA256: 7b38b9c14389d7c57591a3aa4ae8a8f847ff7314f40e9cd2987ee5d4d22e84e9
  • SHA1: a1a07f9d5801b73214ce5d3675faaeb1e4a70c02
  • MD5: 509000b87e20c31a8975a035ba8af42c

C&C Server

  • 81.169.224.222:3389
  • 62.75.168.106:3886
  • 82.165.152.127:3389