Dridex - Technical Malware Analysis
Table of Contents
- Overview
- Capabilities
- Static Code Analysis
- IOCs
- Conclusion
- References
Overview
After receiving feedback on my first malware analysis (Qbot), I realized I had missed many important details in my article. So, I decided to start fresh with a new sample — Dridex.
I chose Dridex because it is one of the most well-known and widespread banking Trojans, dating back to late 2014. Additionally, I find its use of the VEH (Vectored Exception Handling) technique particularly interesting.
Capabilities
Tactic | Technique |
---|---|
DEFENSE EVASION | Obfuscated Files or Information [T1027] |
DEFENSE EVASION | Indicator Removal [T1070] |
EXECUTION | Shared Modules [T1129] |
DISCOVERY | Process Discovery [T1057] |
DISCOVERY | System Information Discovery [T1082] |
DISCOVERY | Software Discovery [T1518] |
DISCOVERY | Query Registry [T1012] |
COMMAND AND CONTROL | Application Layer Protocol: Web Protocols [T1071.001] |
COMMAND AND CONTROL | Data Encoding [T1132] |
Static Code Analysis
Anti-Analysis: Dynamic API Resolving
API hashing has been a common technique in malware development for many years, so its presence in Dridex is not surprising. Dridex relies on three core functions for API resolution:
- mw_get_dllbase: Retrieves the base address of a specific DLL.
- mw_load_dll: Loads a DLL if it is not already available.
- mw_api_resolve: Resolves the address of a specific API function.
mw_get_dllbase
This function parses the InLoadOrderModuleList from the Process Environment Block (PEB) to enumerate all loaded modules, including system DLLs.
Then, it converts all DLL names to uppercase and hashes them using CRC32. The resulting hash is then XORed with the key 0x38ba5c7b and compared with dll_hash (arg1). If they match, the function returns Flink->DllBase.
mw_load_dll
If the current process has not loaded the required DLL, Dridex will call this function.
Basically, it first gathers system information using mw_get_system_info. This function retrieves details about the system Dridex is running on.
Dridex will determine whether to load the DLL from C:\Windows\System32 (for 64-bit DLLs) or C:\Windows\SysWOW64 (for 32-bit DLLs) based on whether it is running as a Wow64 process.
After determining the correct system folder path, it locates the appropriate DLL by comparing the XORed CRC32 hash, similar to mw_get_dllbase
Finally, Dridex uses LdrLoadDll
(ntdll.dll) to load the identified DLL.
mw_api_resolve
This function requires two arguments: the base address of the DLL and the API hash.
Dridex parses the export directory, hashes and XORs each API name, and compares it with the provided hash argument. If they match, it simply returns the API address.
Solving
I tried handling this API hashing using hashdb from OALabs in Binary Ninja. Unfortunately, the version released by Cindy Xiao on the Plugin Manager hadn’t been updated yet, so it lacked the XOR key feature.
Feeling a bit lazy to install it from Git, I decided to create a hashes enum myself instead.
This approach was inspired by a stream where Herrcore (from OALabs) collaborated with c3rb3r3u5d3d53c to tackle API hashing using her peexports
project.
Simply, I used peexports to extract all API names from each DLL in the system directory and save them to a JSON file. Then, I wrote a quick script to hash them and generate a header file, which I imported into Binary Ninja.
Anti-Analysis: Vectored Exception Handling
At the start of its routine, Dridex resolves RtlAddVectoredExceptionHandler
(from ntdll.dll) to register an exception handler.
This is the exception handler Dridex uses to invoke APIs differently than the call instruction.
We will focus only on the STATUS_BREAKPOINT exception code. By using pairs of int3 and retn instructions, Dridex can stealthily call APIs while also making the code more difficult to analyze.
Before calling an API, Dridex stores the API’s address in eax and pushes all parameters onto the stack. Then, execution reaches an int3, triggering a STATUS_BREAKPOINT exception.
At this point, the exception handler updates eip to point to the next instruction, which is retn. It then pushes the address of the instruction following retn, along with the API address, onto the stack.
Here is a demo of the stack.
The function then return to the API address. As shown in the graph above, esp
will point to the Address after retn
, forming a normal stack frame for a function call.
Gathering: System Information
As mentioned earlier, Dridex uses the mw_get_system_info
function to retrieve system information and store it in a structure.
Get the version information
Dridex resolves GetVersionExW to obtain version details, including the build number, minor version, and major version of the system.
Additionally, it calculates the version for later verification, as shown in the image below.
Get the architecture
As mentioned in mw_load_dll, Dridex calls IsWow64Process to check whether the current process is running as a Wow64 process. The result is then used to determine whether to load 64-bit or 32-bit system DLLs.
Check the current privilege
After determining the architecture, Dridex calls mw_check_Sid_administrators to check its privileges.
First, it uses GetTokenInformation with TokenGroups to retrieve information about the group security identifiers (SIDs) in the access token.
Then, it calls AllocateAndInitializeSid to create and initialize the Security Identifier S-1-5-32-544, which corresponds to the Administrators group in Windows.
Finally, it compares the Administrators SID with each group SID using EqualSid.
Get the Windows User Account Controls (UAC) level
Dridex stores the registry key SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System by XORing and CRC32 hashing each subkey. This key is then used to enumerate values from HKEY_LOCAL_MACHINE.
After that, it retrieves the values of EnableLUA, ConsentPromptBehaviorAdmin, and PromptOnSecureDesktop, determining a security level from 0 to 5 based on them.
Check elevated privileges
By querying TokenElevation with GetTokenInformation, Dridex checks whether the current process has elevated privileges.
Get the integrity level
Additionally, Dridex determines the integrity level of the current process.
First, it retrieves the mandatory integrity level by calling GetTokenInformation with TokenIntegrityLevel.
Then, Dridex determines the integrity level based on the result of GetSidSubAuthority.
Get the system information
Finally, it calls GetSystemInfo to retrieve system details, including the page size, number of processors, and more.
Final structure
Below is the final structure Dridex uses to store system information — some fields are still unknown to me.
struct system_info __packed
{
int32_t Version;
int32_t BuildNumber;
char MajorVersion;
char MinorVersion;
char unk4;
char Architecture;
int16_t unk7;
int16_t NumberOfProcessors;
int32_t PageSize;
int32_t MinimumApplicationAddress;
int32_t MaximumApplicationAddress;
int32_t AllocationGranularity;
int32_t SessionId;
int32_t UAC;
char if_AdminstratorsSid;
char if_ElevatedPrivileges;
char unk15;
char unk16;
int32_t IntegrityLevel;
};
String Encryption
Dridex encrypts strings using RC4 and stores them alongside their corresponding keys in data variables.
Sometimes, it decrypts strings using the RC4 function directly, but most of the time, it relies on two wrapper functions. The key length for decrypting strings stored in the binary is 0x28 bytes.
In this post, I won’t go into detail about handling this decryption. Instead, here’s a small project where I built a Binary Ninja snippet to automatically find and decrypt all strings — whether they are called directly or through a wrapper.
You can check out the project on Github. Below is a demo.
C&C Communication
To communicate with the C&C server, Dridex encrypts the payload using RC4 before sending it. Here’s what it collects to craft the payload:
First, it retrieves account information, including the computer name, user name, and installation date. Then, it hashes these values using MD5 in the following format: MD5(computername + username + \x00\x00\x00 + installdate + \x00\x00)
Dridex also retrieves an MD5 hash that includes the Volume Serial Number and Installation Date. This can be represented as: MD5(volume_serial_number + installdate + \x28\x00\x00\x00)
Next, a value derived from the system information structure is appended to the payload. However, some fields, such as unk4 and unk7, are still unknown to me.
A list from the registry key HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\ is appended to the final payload, with each entry separated by a semicolon.
Finally, the payload is encrypted using a key stored in the binary.
The encrypted payload, along with the CRC32 hash, is stored in the variable final, which is then sent to the C2 server.
The IP addresses and ports of the C2 server are stored in plain text in the .data section.
IOCs
Hash |
---|
F9495E968F9A1610C0CF9383053E5B5696ECC85CA3CA2A338C24C7204CC93881 |
IP address | Port |
---|---|
192.46.210.220 | 443 |
143.244.140.214 | 808 |
45.77.0.96 | 6891 |
185.56.219.47 | 8116 |
Conclusion
Dridex is a great case study for understanding how malware gathers system information.
Additionally, its use of the VEH (Vectored Exception Handling) technique is particularly interesting to me, as this is the first time I’ve seen it implemented in malware.