Research by: hasherezade
Rhadamanthys is an information stealer with a diverse set of modules and an interesting multilayered design.
In our last article on Rhadamanthys [1], we focused on the custom executable formats used by this malware and their similarity to a different family, Hidden Bee, which is most likely its predecessor.
In this article we do a deep dive into the functionality and cooperation between the modules. The first part of the article describes the loading chain that is used to retrieve the package with the stealer components. In the second part, we take a closer look at those components, their structure, abilities, and implementation.
Since we published our previous reports [1][2], Rhadamanthys keeps evolving. At the beginning of October, the author announced version 0.5.0 which comes with many changes, and interesting features.
The full list of changes, as described by the author:
V0.5.0 Change List 01. Added observer mode 02. Diversify the construction of stubs and provide x86 x32 native Exe Shellcode Dotnet4 Dotnet2 to better adapt to various usage scenarios and crypt service needs. 03. The client execution process is completely rewritten, and the BUG in the syscall unhook code that caused the crash in the old version is fixed. The execution success rate is very high, and the runtime status is better. 04. Fixed the wallet upgrade support for several wallets where the cracking algorithm fails. Currently supported (UniSat Wallet Tronlink Trust Terra Station TokenPocket Phantom Metamask KardiaChain Exodus Desktop Exodus Web3 Binance ) Online real-time brute force cracking 05. Fixed Discord token acquisition, the correct encrypted token can now be decoded. 06. Break through the browser data acquisition when the browser is protected by third-party programs, and add the login data decryption algorithm of 360 Secure Browser 07. The panel search condition settings have been upgraded. You can now select conditions in batches and select categories with one click. 08. Add a quick setting search filter menu to directly menu the search conditions you need to check frequently. 09. Modify some changes required by users in the Telegram notification module and add new templates for use 10. When building a page, the traffic source tag can directly set the previously used tag, and the URL address will be updated simultaneously. 11. If permissions permit, data collection under other user accounts used on the same machine is supported. 12. The file collection module adds browser extension collection settings. For the Chrome kernel browser, you only need to provide the extension directory name and whether to collect Local Storage data at the same time. Firefox kernel browser can provide extension ID 13. Fix the issue of using the browser to use the online password library after logging in to a Google account in Chrome, and obtaining the login password. 14. The task module has been greatly upgraded, and a new plug-in module has been introduced to support users in secondary development of their own plug-ins. Supports multiple task execution modes: Normal execution In Memory LoadPE Execution Powershell Execution DotNet Reflection Execution DotNet Extension Execution DotNet Extension with Zip Execution VbScript Execution JScript Execution X86 shellcode execution X64 shellcode execution Native Plugin Loader 15. Keylogger: supports recording all keyboard input, process details, file name, window title, supports setting process filtering, sending time, buffer size 16. Data spy plug-in: currently supports correct login access and IP username and password for remote RDP access. The correct certificate file and password imported by the user. 17. Plug-ins and loader modules support secondary development and provide SDK support.
In this blog, we present a walkthrough a sample that belongs to this release.
The initial loader is a 32-bit Windows executable (PE). It was in large part rewritten, but still contains artifacts that make it similar to the previous edition (0.4.9).
The author added a check of the executable’s name. When the samples are uploaded to sandboxes for automated analysis, they are often renamed as hashes. Therefore, if the name consists of characters from a hexadeciamal charset [0-9, a-f, A-F], and its length is 16, 32, 40, or 64, the malware assumes that it is being analyzed, and exits immediately.
Similarly as we saw in the earlier releases, the initial executable contains the configuration (embedded by the builder) as well as the package with additional modules, that constitutes the next stage. As the execution progresses, these later components are unpacked, and the configuration is passed to them.
The first change that we can observe during the initial triage is a new section: .textbss
. This section is empty in the raw binary (raw size = 0). However, will be filled at runtime with an unpacked content: a shellcode similar to the one used by the previous versions. The difference is that in the past versions the analogous code was loaded into a newly allocated memory region. Regardless of location, its role didn’t change: it is meant for unpacking and loading the first module from the package.
Below is a fragment of the tracelog (created with TinyTracer) that illustrates the loading of the shellcode, and the calls executed from it (v.0.5.0):
1e91;kernel32.WaitForSingleObject 1e99;kernel32.CloseHandle 1eb7;[.text] -> [.textbss] ; <- redirection to the code that unpacks the XS module 27000;section: [.textbss]** 27311;kernel32.VirtualAlloc 270c1;kernel32.VirtualAlloc 2726d;kernel32.VirtualFree 27363;kernel32.VirtualAlloc 273bd;kernel32.VirtualProtect 273f0;kernel32.VirtualFree 27052;called: ?? [12081000+8a] ; <- redirection to the new module (XS)
For comparison, this is the corresponding tracelog fragment from version 0.4.9:
18b3a;kernel32.HeapFree 18b43;kernel32.HeapDestroy 184dc;ntdll.ZwProtectVirtualMemory 184e6;called: ?? [a7a0000+0] <- redirection to the shellcode that unpacks the XS module > a7a0000+2fe;kernel32.LocalAlloc > a7a0000+ba;kernel32.LocalAlloc > a7a0000+260;kernel32.LocalFree > a7a0000+34c;kernel32.VirtualAlloc > a7a0000+3a4;kernel32.VirtualProtect > a7a0000+3bb;kernel32.LocalFree > a7a0000+52;called: ?? [10641000+88] <- redirection to the new module (XS)
The next stage, as before, is in a custom executable format. The format itself hasn’t changed since the release of 0.4.9. Once more we are dealing with XS1 (as described in our article [1]) which can be converted into a PE with the help of our tools.
The component revealed (in the XS1 format) is part of the second stage of the loading process. Looking at this module we can see many similarities to the one described in [1]. Yet, clearly some parts are enhanced and improved.
One of the changes shows up in the initial triage, at the attempt to dump strings from the binary. In the past, we could obtain a lot of hints about the module functionality by dumping the strings i.e. with the help of the Flare FLOSS tool. This is no longer possible as the author decided to obfuscate them (more details in “String deobfuscation and the use of TLS”).
After converting the module to PE, and opening it in IDA, we can follow up with a more detailed assessment. Looking at the outline of the start function, we see a slightly different, refined design.
As before, the module uses the configuration passed from the previous layer. The decoded buffer starts with the !RHY
marker, and contains, among others, the URL of the C2 to be contacted. This is the reconstructed structure:
struct rhy_config { DWORD magic; BYTE flags1; DWORD flags2; _BYTE iv[16]; _BYTE key[32]; char C2_URL[128]; };
Just like in the previous version, the connection with the C2 is established by the netclient
module which is loaded from package #1 (see Figure 3).
In addition, the current component unpacks and uses multiple modules that are shipped in package #1. We already saw some of them in the previously described versions. The full list is below.
Name | Format | Description | New in 0.5.0? |
---|---|---|---|
stage.x86 | shellcode (32-bit) | Shellcode used in the injection of the main module of the Stage 2 into another process; responsible for accessing the named mapping, copying its content, and reloading the main module in the context of the new process | ✓ |
early.x86 | XS | Process injector (using raw syscalls) | ✓ |
early.x64 | XS | Process injector (using raw syscalls) | ✓ |
phexec.bin | XS | Injects from a 32-bit process into a 64-bit | – |
prepare.bin | shellcode (64-bit) | – | |
unhook.bin | XS | Checks DLLs against hooks, and does unhooking if needed | – |
strategy.x86 | XS | Checks if any of the forbidden processes is running (evasion) | ✓ |
process.x | TXT | List of forbidden processes (evasion) | ✓ |
ua.txt | TXT | A list of user-agents (a random user-agent from the list will be selected and used for the internet connection) | – |
dt.x86 | XS | Evasion checks based on Al-Khaser | – |
proto.x86 | shellcode | Encrypts and decrypts netclient module with the help of RC4 and a random key | – |
netclient.x86 | XS | Responsible for the connection with the C2 and downloading of further modules | – |
A simplified flow:
More details about the execution flow, and possible diversions from the presented path, are explained later in this report.
One of the new features in this release is the introduction of TLS (Thread Local Storage) for temporary buffers. They are used, among others, for decoding obfuscated strings.
TLS is first allocated in the initialization function (denoted as init_xs_module
in Figure 4). The value received from TlsAlloc
is stored in a global variable. The malware then allocates a custom structure with a buffer and attaches it to the TLS.
Later on, that saved buffer is retrieved and used multiple times, as a workspace for deobfuscating data, such as strings.
The string decryption function is passed as a callback to the function retrieving a buffer attached to the TLS storage.
After the string is used, the buffer is cleared (see Figure 6).
The use of TLS in the implementation of this functionality is quite atypical, and it isn’t clear what was the reason behind this design.
The algorithms used for the string deobfuscation differ at different stages of the malware. In the case of the current module (XS1, Stage 2) the following algorithm is deployed:
#!/usr/bin/env python3 def mix_key_round(ctx, size, key3, key2, key_size): if not size: return for i in range(size): pos = key_size % size key_size += 87 val = ctx[pos] result = (key2 + ((val >> 5) & 0xFF)) + ctx[(val % size)] + (i * (((val + key3) >> 3) & 0xFF)) + 1 ctx[i] = (ctx[i] + result) & 0xFF return ctx def decrypt_data(in_buf, in_size, key_buf, key_size, key2, key3): out_buf = [0] * in_size ctx = [key_buf[(i % key_size)] for i in range(in_size)] for _ in range(4): ctx = mix_key_round(ctx, in_size, key3, key2, key_size) for i in range(in_size): out_buf[i] = (ctx[i] ^ in_buf[i]) & 0xFF return out_buf def decrypt_string(in_buf, key_buf): return decrypt_data(in_buf, len(in_buf), key_buf, 16, 0x3779E9B, 0)
The decryption key is stored at the beginning of the block and is always 16 bytes long. The data is terminated by 0.
The IDA script for strings deobfuscation (assuming that the decrypting functions are named, appropriately: dec_cstring
and dec_wstring
):
https://gist.github.com/hasherezade/c7701821784c436d40d1442c1a623614
The list of the deobfuscated strings for the Stage 2 loader:
https://gist.github.com/hasherezade/fb91598f6de62bdecf06edf9606a54fb
One of the techniques that we can observe across different Rhadamanthys modules is the use of raw syscalls for calling native API. This is a known way to evade function hooking and monitoring, and also helpful in obfuscating the names of the APIs used. This technique has a long history and occurs in multiple different variants (described, i.e. here: [3]).
The method relies on the fact that each native system function (implemented kernel-mode) is represented by a syscall ID. These IDs may differ depending on the Windows version. For the programmer’s convenience, Windows allows access to them via API exported by system DLLs such as NTDLL and WIN32U. NTDLL is often hooked by antimalware products, in order to watch the called APIs and detect suspicious activity.
Malware tries to bypass the installed hooks by copying the numbers of syscalls directly to its own stubs, so that it won’t have to use the NTDLL. However, doing so generates another event that some monitoring tools may find suspicious, as it’s not usual to execute a syscall from a non-system module.
Looking at the implementation, we can see that the author was well aware of this problem and used a variant of the technique called indirect syscalls. The stub within the malware only prepares the syscall and its actual execution is done by returning to the NTDLL at the end of a function. In this way, the initial part of the function that contains the hook is bypassed, but the syscall is called from the NTDLL.
The raw syscalls are used by Rhadamanthys in both 32 and 64-bit modules. As we know, a syscall can be performed only from a module that has the same bitness as the operating system. Doing a syscall from the WoW64 process (32-bit process on 64-bit Windows) requires a different approach, and using wrappers that may be different for different versions of Windows [4]. Underneath, each of those wrappers temporarily switch the process from 32-bit to 64-bit mode. The author of Rhadamanthys decided not to use existing wrappers, but instead implement his own, using the Heaven’s Gate [6] technique.
The role of the modules involved in the Stage 2 hasn’t changed since the last version. They are meant to prepare and obfuscate the downloading of the actual stealers that are shipped in package #2 supplied by the C2. As we know, the download operation is performed by the netclient
module. The author used several other modules to scrupulously check the environment and make it harder for an analyst or various monitoring tools to track when the download occurs.
Depending on the settings, it is possible to load next modules into the current process, or to add an extra round by injecting the current module into a new process, and deleting the original file.
In the case of the analyzed sample, the restart flag was selected. This caused the main loader module to run twice, with two different paths of execution.
On the first run, the malware injects itself into a newly created process. It is implemented in the following steps:
early.x86
and early.x64
. They may be different depending on the build, as since 0.5.0 distributors can customize what injection method will be used.stage.x86
with the help of functions exported from the injector component.main_alt
in the XS1 structure [1]), to deploy the second execution path.The target for the injection is selected from a hardcoded list, and can be one of the following:
L"%Systemroot%\\system32\\dialer.exe" L"%Systemroot%\\system32\\openwith.exe" L"%Systemroot%\\system32\\dllhost.exe" L"%Systemroot%\\system32\\rundll32.exe"
The execution second path is deployed when the module runs inside the new process. It involves deleting the original malware file (if the relevant flag was selected in the configuration) and loading the other modules from package #1, including netclient
.
After performing the evasion checks, and making sure that the process is not monitored (with the help of additional modules), the malware connects to the C2 and downloads the next package (package #2 – Figure 5) with the stealer modules. Just like in the previous version, the package is shipped in a steganographic way, appended to the JPG or WAV file (for more details, refer to [1]).
Two modules are involved in downloading the payload: netclient
(in XS1 format) and proto
(shellcode). They are both loaded into the same memory region, and proto
is copied after the netclient
. First netclient
is called from the main module, with the appropriate parameters: the address of the C2 copied from the configuration, and the User Agent, selected from the ua.txt
list (if the list fails to load, a hardcoded User Agent is used as a backup).
The interesting element of the flow is that at the beginning of the netclient
execution, the main XS1 component gets encrypted. It is decrypted later, just before the execution returns to it. The encryption and decryption are done with the help of the proto
module, which is called twice by netclient
. This is yet another obfuscation step to minimize the memory artifacts.
The netclient
is responsible for connecting to the C2 and downloading the payload, then decrypting it. As before ([1]) the correctness of the resulting buffer is verified by comparing the hash calculated from the content with the expected hash that is stored in the header of the data block. In the currently analyzed case, the payload was shipped appended to a file in the WAV format.
After the payload is downloaded, and passes the verification step, netclient
module calls proto
for the second time to decrypt the previously encrypted main module (XS1), using the RC4 algorithm and the key that was generated and saved at the first run.
The execution goes back to the (now decrypted) main module, which first destroys the memory region where netclient
and proto
were loaded. It then proceeds to load the next stage from the downloaded package #2.
Depending on whether the system is 32 or 64-bit, further execution may proceed in the current process, or in a newly created, 64-bit process, where the required elements are injected.
Most of the time we encounter a 64-bit version of Windows, which means there are several extra steps before the content from the downloaded package can be fetched and executed.
The malware creates a new, 64-bit process, selecting one of the paths from the hardcoded list:
L"%Systemroot%\\system32\\credwiz.exe" L"%Systemroot%\\system32\\OOBE-Maintenance.exe" L"%Systemroot%\\system32\\openwith.exe" L"%Systemroot%\\system32\\dllhost.exe" L"%Systemroot%\\system32\\rundll32.exe"
As writing into a 64-bit process from a 32-bit one is not officially supported by the Windows API, it uses Heaven’s Gate technique [6] to do so. This time the injection functions are encapsulated in an additional module from package #1: phexec.bin
.
The malware now uses a 64-bit module prepare.bin
, which is to be injected and used to retrieve the downloaded package #2 from inside the new process. The module prepare.bin
is a shellcode containing an empty data section to be filled. The unfilled version of the section starts with the marker YHR!
which is replaced with 0xDEADBEEF
after it is filled.
Data transmission using BaseNamedObject and the use of Exception Handlers
The loader (32-bit executable) creates a named object that is used to share the information between the current process and the infected one (64-bit). The content downloaded from the C2 is passed to this object and then received in the other process, via prepare.bin
.
During the injection, the prepare.bin
is implanted into a fresh 64-bit process. The execution is redirected by patching the Entry Point of the main executable with a jump that leads to the shellcode.
The shellcode (prepare.bin
) contains a loop waiting for the BaseNamedObject to be filled. It sets a Vectored Exception Handler that is triggered after the data transfer from the other process is finished. The use of an exception handler is intended to be an additional obfuscation of the execution flow.
During its execution, the shellcode installs some more patches in the original executable of the process, and returns back to the patched code after the exception handler was registered. The overwritten code changes the memory protection and waits for the first malware process to send data over the BaseNamedObject.
When the wait is over, it triggers an exception executing the int3
instruction. As a result the execution lands in the Vectored Exception Handler, that is installed by the shellcode.
The exception handler is responsible for unpacking the retrieved package, fetching from it the next shellcode, and redirecting the execution. The further flow, starting from the retrieved shellcode from package #2, is analogous to the execution on a 32-bit system described below.
The first element fetched from the new package is a shellcode.
The shellcode decrypts and decompresses the rest of the downloaded package. It then fetches the main stealer module, which is in the XS2 format [1].
The unpacked module is loaded into a newly allocated memory region. However, the redirection contains some additional steps. Once again, the author took extra care to minimize the memory artifacts and made sure that the loading shellcode is released after use. A a small chunk of the shellcode is copied to the new memory region, just after the loaded XS2 module. After that block, other necessary data is copied, including the pointer to the package, and the Entry Point of the next module. After finishing, the loading function jumps to such a prepared stub. As the stub is in the new memory region, it can free the region containing the unpacking shellcode before the redirection to the next stage.
After passing the full loading chain, the execution finally reaches the main stealer module: coredll.bin
(in XS2 format). The module coordinates all the work, collects the results, and reports them back to the C2. It is also responsible for retrieving other modules from package #2, and using them to perform various tasks.
In the current release, the following modules are available in the package:
Module path | Type | Role | Is new in 0.5.0? |
---|---|---|---|
/bin/i386/coredll.bin /bin/amd64/coredll.bin | XS | Main stealer module (analogous to the one described in this chapter) | – |
/bin/i386/stubmod.bin /bin/amd64/stubmod.bin | XS | Prepares a .NET environment inside the process, to load other .NET modules | – |
/bin/i386/taskcore.bin /bin/amd64/taskcore.bin | XS | Manages additional modules for the tasks supplied by the C2 | ✓ |
/bin/i386/stubexec.bin /bin/amd64/stubexec.bin | XS | Injects into regsvr32.exe, and remaps the module into a new process | – |
/bin/KeePassHax.dll | PE (.NET) | Steals KeePass credentials | – |
/bin/runtime.dll | PE (.NET) | Runs PowerShell scripts and plugins in the form of .NET assemblies | – |
/bin/loader.dll | PE (.NET) | General purpose .NET assemblies runner | ✓ |
As seen earlier, the malware supports up to 100 LUA extensions. They are loaded from the package, retrieved by paths in the format: /extension/%08x.xs
.
The coredll.bin
not only coordinates the work of other modules, but also has a lot of vital functionality hardcoded. It comes with a collection of built-in stealers that can be enabled or disabled depending on the configuration.
In case of this module not all strings are obfuscated, so we can obtain some of them with the help of common applications such as FLOSS . Those are mainly strings from the statically linked libraries. The most important strings, relevant to the malware functionality, are encrypted and revealed just before use. Once again, TLS storage is used for the temporary buffer. This time the algorithm used for the decryption is RC4. The first 16 bytes of the input buffer is the key, followed by the encrypted data.
IDA script for deobfuscation:
https://gist.github.com/hasherezade/51cb827b101cd31ef3061543d001b190
Deobfuscated strings from the sample:
https://gist.github.com/hasherezade/ac63c0cbe7855276780126be006f7304
As there are multiple modules involved in the execution of various tasks, and some of them are injected into remote processes, they must communicate with the central module (coredll.bin
), to coordinate the work and pass the collected results. The communication is implemented with the help of a pipe.
The pipe is created by the main module, using the name Core
and the current PID. This information, concatenated together, is hashed (SHA1), and the hash is used to generate the unique pipe name (following the pattern \\.\pipe\{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}
).
The PID is further passed to the submodules so they can regenerate the name, following the same algorithm, and then establish the connection.
After the module finishes initialization, which is typical for the XS2 format [1], execution is redirected to the main function of the module. The function takes 3 arguments: the pointer to the current module, a command to be executed, and a structure, including all the data passed from the previous layers. Depending on the command argument, the module can follow one of the 4 different execution paths. In each case, the most important functionality is to load additional components, establish the connection with the C2, and deploy the built-in stealers.
In some cases, the function responsible for running all the modules can be executed at the exit of the application where it is injected. It is implemented by hooking the ExitProcess
function.
At the beginning of the main function (Figure 23), we can see a function patching NTDLL. Its role is to disable Event Tracering (ETW), which is a built-in Windows feature allowing to collect information about the application behavior. The bypass is implemented similarly to that described in the article [5].
In the case of the WoW64 process, both the 32-bit and the 64-bit version of the NTDLL are patched. By scanning the process with PE-sieve, we can quickly dump the modified versions of NTDLLs and receive the .tag
file informing about the hooks. The found patches are listed below.
In NTDLL 64-bit:
The installed patch is simple, it forces the EtwEventWrite
function to always return 0.
The patches (NTDLL 32-bit):
Here we can see two functions patched: EtwEventWrite
and NtTraceEvent
– they both are forced to return immediately at the start.
Additionally, we can see a hook inside the exception dispatcher, whose role was described in detail in our previous article on Rhadamanthys [1] (”Handling exceptions from a custom module”). In contrast to other hooks that are security bypasses, this hook is part of the XS format loading and allows to catch the exceptions thrown by the custom module.
The main component of Stage 3 comes with a rich set of built-in stealers. They are stored as two distinct global lists. The first group involves stealers that rely on searching and parsing files of particular targets (i.e., profiles, local databases), which we refer to as “passive stealers”. The second set which we refer to as “active stealers” consists of more invasive components: they can interfere with running processes, inject into them, etc.
Each stealer has an initialization function and a run function. They also use a top-level structure from where they fetch the data and where they report back the results. By sharing this structure, they can pass data from one to another, working as elements of the pipeline. The overview of the function responsible for running the stealers:
First, the malware enumerates all the processes and stores their names, along with their PIDs, in a structure that will be used later. Then, all the built-in active stealers are initialized in a loop. At this point, they may reference the list of the processes to check if their targets are on the list. Some of them may also read additional information from the process, which will be passed further into the pipeline. Next, the passive stealers are initialized. The User Profile Path is retrieved and walked recursively to check if it contains some targeted directories from which the malware can steal.
After the initialization of both types of stealers is finished and information about the targets is collected, the functions performing the actual stealing activities run. First, all the initialized profile parsers are executed in a loop to collect the information from the files. Finally, the active stealers are executed.
After all the hardcoded stealers were run, the malware initializes a built-in LUA interpreter and uses it to execute the additional set of stealers, implemented as LUA scripts (described in more detail in “The LUA scripts“).
The results of all the executed stealers are collected in a global structure. They are posted into the queue and later uploaded to the C2.
As mentioned earlier, passive stealers rely on reading and parsing files belonging to particular applications to retrieve saved credentials.
The passive stealers are initialized by parsing recursively all the subdirectories in %USERPROFILE%
and checking the presence of targeted paths. The inner function enumerates all the directories in the %USERPROFILE%
, and on each new directory found, it executes a passed callback.
The callback then runs the full list of initialization functions (is_target
) of the stealers. If the path is identified as a target, the relevant passive stealer is further initialized.
The paths used for the target identifications (passive stealers):
Target | Paths/extensions | Is Optional |
---|---|---|
Chrome | “Web Data”, “Web Data Ts4” | no |
Firefox | “cookies.sqlite” | no |
FileZilla | “recentservers.xml” | yes |
OpenVPN | “OpenVPN Connect\profiles”, “.ovpn” | yes |
Telegram | “configs”, “\D877F783D5D3EF8C\configs” | yes |
Discord | “DiscordStorage”, “discordptbStorage”, “discordcanaryStorage” | yes |
StickyNotes | “Microsoft.MicrosoftStickyNotes”, “plum.sqlite” | yes |
The malware comes with a statically linked SQLite library which allows it to load and query local SQLite databases, and fetch saved data such as cookies.
The set of active stealers is more interesting and diverse. Some of these stealers open running processes and inject additional components. One of such components is KeePassHax.dll
, injected into the KeePass process, to steal credentials from this open-source password manager (more details in: “Stealing from KeePass”). The malware may also inject its main component (core.bin
) into other applications.
The set of the stealers is too big to describe each of them in detail. In summary, the following applications are targeted:
Chrome | Vault | WinSCP | StickyNotes | KeePass |
Mozilla products (Thunderbird, Firefox) and some of the Firefox-based browsers, such as K-Meleon.exe | FoxMail | Telegram | Stream | TeamViewer |
Internet Explorer | OpenVPN | Discord | CoreFTP | 360Browser |
In some cases, the active stealer’s initialization function retrieves from the running process information about the current profile and configuration files. It is done for the following targets:
The approach to finding the loaded profile is a bit different in each case.
In the case of K-Meleon.exe it identifies the target files by paths retrieved from associated handles. First, the malware retrieves all object handles that are currently opened in the system, using NtQuerySystemInformation
(with the argument SystemHandleInformation
). It walks through obtained handles and selects the ones owned by the target process. Then, it iterates over those handles to check which names they are associated with (using NtQueryObject
), and compares each name with the hardcoded one (\places.sqlite
) . If the check passed, the full path is retrieved and stored for further use.
In the case of Chrome, the current profile is provided in the command line used to run the application. The command line of a process is stored in a dedicated field of the PEB (PEB
→ ProcessParameters
→ CommandLine
), which is where the malware retrieves it from. The path to the profile is given after the —user-data-dir=
command line argument, so this hardcoded string is used for the search.
As mentioned earlier, the main stealing actions are executed in the run
function of each stealer. The stealers targeting Mozilla products may deploy additional processes and inject there the main module of the malware: coredll.bin
. Note that although it is the same module as the one currently described, its execution path is not identical. Looking at the XS2 header [1], we can see that this format lets you provide alternative entry points of the application (denoted as entry_point
– primary, and entry_point_alt
– secondary). The execution of the implanted coredll.bin
starts in entry_point_alt
. Looking at the injection routine, we can see the corresponding address being added to the APC queue of the target’s main thread.
The alternative entry point leads to the execution of a minimalist main, which contain only Mozilla-specific stealers.
Depending on the given settings, the stealing functions can be executed immediately, or when the application exits. The execution at exit may be necessary in case the current process blocks some files from which the module wants to read.
There are three stealers implemented, and selected depending on the given configuration. Each of them sends the stolen content over the pipe back to the main element.
One of the modules shipped in the package is a .NET DLL named KeePassHax.dll
. As the name suggests, this module targets the KeePass password manager (which is a .NET application). The DLL is injected into the target along with the assisting native module, called stubmod.bin
. An interesting feature of the implementation is that it initializes the complete .NET environment of an appropriate version within the attacked KeePass process, just to deploy the stealing DLL.
The function leading to the deployment of those components belongs to the active stealers list. As explained earlier, every stealer has two callback functions: init
and run
. In this case, the first function is empty, as there is nothing to initialize. The activity starts in the run
function, given below. First, it searches for the keypass.exe
on the previously prepared list of running processes. If the name is found, it tries to inject the malicious modules into the corresponding process.
Two modules are fetched from the package: stubmod.bin
(in a 32 or 64-bit version, matching the process) and KeePassHax.dll
. The stubmod.bin
is an executable in the custom XS format [1], containing native code. This module is executed first, and prepares the stage for loading the DLL.
The start function of the stubmod.bin
initializes the whole .NET environment inside the process where it is injected, similar to what is described in the following writeup [6].
It then runs the KeePassHax.dll
. As an argument, it passes a callback function which is responsible for sending the collected data to the main malware process, over the previously created pipe (more details in “Communication between the modules”). The way of passing the callback is a bit unusual, yet simple and effective: the pointer is first printed as a string and then given as an argument to the DllMain
function of the KeePassHax.dll
.
Looking at the DllMain
function, we can see the argument being parsed and used as two combined pointers. One of the pointers, stored in the variable NativePtr
, leads to the function that sends data over the pipe (that is a part of stubmod.bin
). The other one stored in NativeData
contains a PID of the process running the core module, which is needed to regenerate the pipe name.
The main actions of dumping the credentials are done by the function KcpDump
, which retrieves values of relevant fields from the main KeePass form.
The collected data is serialized and passed to the previously retrieved native function. The first supplied argument is a NativeData
containing the PID passed from the caller.
The function Program.FnSendData
is implemented in the native module, which was responsible for executing the KeePassHax.dll
, and has the following prototype:
int __cdecl to_read_write_to_pipe(int seed, DWORD numberOfBytesToWrite, _BYTE *data, int data_size)
The supplied seed argument is used in the generation of the pipe name, where the data is written to.
This is how various components, native (in a custom format) as well as .NET modules, are blended together as one injected payload.
Although the list of stealers available by default is already very rich, the author decided to provide even more options by incorporating a LUA script runner.
The main module can load up to 100 LUA scripts. In the analyzed cases, we found approximately 60 of them are used. They implement a variety of stealers that target cryptocurrency wallets, FTP applications, e-mail agents, SSH, and more.
First, the malware loads all the available scripts from the package in a loop. They are stored in the internal structure, that is later passed to the interpreter.
Scripts work as plugins to the built-in framework.
Stealers are grouped by one-character identifiers:
ID | Type |
---|---|
W | wallets |
E | e-mails |
F | FTP |
N | note-keeping apps |
M | messengers |
V | VPN |
2 | authentication related, password managers, etc. |
The stealer starts with a check if the group it belongs to is enabled in the framework.
if not framework.flag_exist("W") then return
Some examples are given below.
Stealer for Psi:
if not framework.flag_exist("M") then return end local filename = framework.parse_path([[%AppData%\Psi+\profiles\default\accounts.xml]]) if filename ~= nil and framework.file_exist(filename) then framework.add_file("accounts.xml", filename) framework.set_commit("$[M]Psi+") end
Stealer for a DashCore wallet:
local file_count = 0 if not framework.flag_exist("W") then return end local filenames = { framework.parse_path([[%AppData%\DashCore\wallets\wallet.dat]]), framework.parse_path([[%LOCALAppData%\DashCore\wallets\wallet.dat]]) } for _, filename in ipairs(filenames) do if filename ~= nil and framework.file_exist(filename) then if file_count > 0 then break end framework.add_file("DashCore/wallet.dat", filename) file_count = file_count + 1 end end if file_count > 0 then framework.set_commit("!CP:DashCore") end
Stealer for Notezilla:
if not framework.flag_exist("N") then return end local filename = framework.parse_path([[%AppData%\Conceptworld\Notezilla\Notes9.db]]) if filename ~= nil and framework.file_exist(filename) then framework.add_file("Notes9.db", filename) framework.set_commit("$[N]Notezilla") end
List of all the targeted applications:
Armory | AtomicDEX | AtomicWallet | Authy Desktop | AzireVPN | BinanceWallet |
BinanceWallet | BitcoinCore | CheckMail | Clawsmail | Clawsmail | CuteFTP |
Cyberduck | DashCore | Defichain-Electrum | Dogecoin | Electron-Cash | Electrum-SV |
Electrum | EMClient | Exodus | Frame | FtpNavigator | FlashFXP |
FTPRush | GmailNotifierPro | Guarda | Jaxx | Litecoin-Qt | Litecoin-Qt |
LitecoinCore | Monero | MyCrypto | MyMonero | NordVPN | Notefly |
Notezilla | SSH | Outlook | Pidgin | PrivateVPN | ProtonVPN |
Psi+ | PuTTY | Qtum-Electrum | Qtum | RoboForm | Safepay |
SmartFTP | Solar Wallet | The Bat | TokenPocket | Total Commander | Tox |
TrulyMail | WinAuth | WalletWasabi | WindscribeVPN | Zap |
In addition to running the modules that were embedded in the package and sending back their results, the malware establishes a connection with the C2 to listen for additional instructions. On demand, it can drop and run additional executable files, or execute scripts via other modules.
There are 11 different supported commands that are identified by numbers.
The additional modules that can be run on demand, are taskcore.bin
, runtime.dll
, and loader.dll
.
The content received from the C2 is passed in the variable denoted above as buf
. Depending on the specifics of the particular command, it can be saved into a file, or into a named mapping, whose handle is passed to the other malicious module.
For example, the module received from the C2 is passed to be executed via taskcore.bin
:
In addition to the ability to run LUA scripts, the malware allows the execution of PowerShell scripts, and since the version 0.5.0, also .NET assemblies. Unlike LUA, the scripts/assemblies to be executed are not shipped in the package but dynamically received from the C2.
This part of the functionality is managed by additional modules from package #2: runtme.dll
(runs PowerShell scripts, and plugins) and loader.dll
(a general-purpose loader of .NET assemblies).
The modules are run within a new process under the guise of AppLaunch.exe
or dllhost.exe
.
Evolution of PowerShell runner (runtime.exe/dll)
In the previous versions, the PowerShell runner was implemented as runtime.exe
, which is a very small executable written in .NET. The runner consisted of the Main
function presented below. It takes two string arguments, which are in fact pointers in hexadecimal form. The pointers lead to the functions in the native module that loaded the current one and are used to pass the input and output. The scripts are received by calling the function sysNativeWrapper.GetScript()
, then results are passed back by sysNativeWrapper.SendDumpData(data)
.
The new version of the runner is a DLL, not an EXE (runtime.dll
). It is fully rewritten, and much more complex. The runner exposes a new interface, to support the API of the newly introduced plugin system.
The plugins are in the form of .NET assemblies.
Simple PowerShell scripts, in the form of a string, are supported as well.
Loader of .NET assemblies (loader.dll)
The current version has yet another module, loader.dll
, that is also responsible for running .NET assemblies, but in a much simpler way, and outside of the plugin system. It expects two arguments – a pointer to a byte array representing the assembly, and the array size. Then, the passed assembly is simply invoked.
In the published changelog, the author advertises multiple changes in the task-running module:
14. The task module has been greatly upgraded, and a new plug-in module has been introduced to support users in secondary development of their own plug-ins. Supports multiple task execution modes: Normal execution In Memory LoadPE Execution Powershell Execution DotNet Reflection Execution DotNet Extension Execution DotNet Extension with Zip Execution VbScript Execution JScript Execution X86 shellcode execution X64 shellcode execution Native Plugin Loader
Indeed, we can see the new module taskcore.bin
which was added in this release. Depending on the specific command passed from the C2, it may be run under the guise of multiple applications, such as: AppLaunch.exe
, dllhost.exe
, rundll32.exe
, OpenWith.exe
, notepad.exe
, rkeywiz.exe
, wmpntwrk.exe
, wmpconfig.exe
.
Similar to coredll.bin
, it comes with RC4-encrypted strings. The list of decrypted strings (format: RVA, string) is given below:
As the module is in the XS2 format. The start function first finishes the initialization, then deploys the core functionality of the modules, i.e. command parsing. Each command is responsible for executing a module of a particular type.
The passed structure contains parameters filled in by the coredll.bin
, and the mapping contains the buffer with a module to be executed that was received from the C2. While some of the other modules interact only with the coredll.bin
, this one is also capable of establishing its own connection with the C2 and reports there directly.
As the author mentioned, the module is able to run a variety of content formats.
The simplest of them is running shellcodes.
Scripts, such as JScripts and VBScripts, are both executed by the same function:
Underneath, it uses a COM interface and parses the given script with the help of a function ParseScriptText
.
Similar to the previously mentioned stubmod.bin
(”Stealing from KeePass”), the .NET environment is manually initialized. It is then it is used to run PowerShell scripts as well as plugins in the custom format (that are delivered in the form of .NET assemblies).
Before running the PowerShell scripts, the malware zeroes out the Environment Variable _PSLockdownPolicy
. This modification disables the PowerShell lockdown policy, which is a security feature restricting the execution of certain actions within the PowerShell environment. As a result, this allows more freedom in execution of scripts.
The module also disables other security-related features: for example, it patches out AMSI checks and event tracing in NTDLL.
The functionality of the modules is vast, but because we dumped and deobfuscated all the strings, we can get a good overview of different aspects of the modules by reviewing them. Selected artifacts described below.
There are multiple strings indicating that the module comes with a statically linked SQLite library. The presence of SQLite modules in malware strongly suggests that it steals cookies, as cookies are kept in local SQLite databases. The Rhadamanthys core comes with the version string, which leads to the particular commit of the SQLite: https://www3.sqlite.org/src/info/fe7d3b75fe1bde41 (updated since the previous version of the malware, which used an earlier commit: https://www2.sqlite.org/src/info/8180e320ee4090e4)
SQLite format 3 2016-04-08 15:09:49 fe7d3b75fe1bde41511b323925af8ae1b910bc4d
In addition to the artifacts pointing to the library itself, we can see some queries among the decrypted strings, such as the following:
SELECT title, url FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id) SELECT url FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id
Those queries are used for retrieving Mozilla browsing history and will be applied on the found database. They are part of the stealer described in “Stealing from Chrome and Mozilla products”.
We can find multiple strings suggesting that the malware makes use of a statically linked library with Elliptic curves. Some of the strings can help uniquely identify the library and its version.
8335DC163BB124B65129C96FDE933D8D723A70AADC873D6D54A7BB0D 14DEF9DEA2F79CD65812631A5CF5D3ED
It turns out to be Mbed Crypto (source), an old version of the library now renamed Mbed TLS.
The source file referencing the found strings can be seen on the project’s Github: [reference 1] [reference 2]. Note that the mentioned strings are not present in the new version of the library (under the new name Mbed-TLS) which suggests that the malware author used the old copy.
After analyzing the application, we know that this library is used to establish the TLS connection with the C2.
Some unencrypted strings suggest the use of JSON, implemented by the statically linked library cJSON:
cjson BUG: Unable to fetch CJSON configuration JSON parser does not support UTF-16 or UTF-32 '[' or '{' expected unexpected token '}' expected ':' expected string or '}' expected ']' expected
Looking further at the context in which those strings are used, we can see that the JSON format was applied to create reports about the stolen content, that was retrieved by the LUA scripts. We can also find relevant artifacts among the decrypted strings:
e7bcc,'{"root":' e7bb0,'root' e7d58,'profile' e3814,'username' e37f8,'password' e7d40,'openvpn'
Among unencrypted strings, we see the following strings that can be found in the open-source 360 Browser Password decoder.
cf66fb58f5ca3485 (4B01F200ED01)
The first of the strings is an AES key that is used for decrypting the password. The second is the prefix of the encrypted password that needs to be dropped.
One of the strings is quite unusual, and looks like along key or an obfuscated content:
0a{1b2c3d$4e5f6g7h_8@i9jAkBl`CmDnEo[FpGqHrI~s-JtKuLvMw%NxOyPz(Q9R8S7T6)U5V4]W3X2}Y1Z0
However, Googling for it gives few results only, leading to a Chinese website with a simple code snippet. The snippet illustrates how to generate a random file name, and the aforementioned string is just used as a charset. The implementation used in Rhadamanthys is not identical, but has analogous functionality:
The string is referenced in the fragment of code responsible for generating names for the additional executables downloaded from the C2 that are dropped and executed.
This piece of code is not particularly interesting, but the choice is unusual and may give a subtle hint that the author is a Chinese speaker. Hovewer, it is not enough to draw any conclusions.
During the writing of this article, the author already released version 0.5.1. The newest version contains additions such as a Clipper plugin, which watches a clipboard and replaces wallet addresses with attackers’ addresses. This version enables even more customization – distributors can order private stubs, prepared especially for them.
List of changes, as presented by the author:
1.Added Clippers plug-in 2.Telegram notification, you can now choose whether to send wallet crack and seed records in the log ZIP 3.Google Account Cookie Recovery 4.Default build stub cleaning for Windows Defender, including cloud protection
We can already see that the Clipper plugin supports a rich set of targets.
Rhadamanthys is a well-designed, modular stealer. In this article, we presented some details of its implementation, showing the incorporated techniques and execution flow. Although the core component comes with a lot of interesting built-in features, the power of this malware lies in its extensibility.
The currently analyzed version 0.5.0 supports multiple scripting languages, from LUA (whose interpreter is built-in to the main module) to PowerShell and other scripting languages, that are supported via an additional module.
As we can see, the author keeps enriching the set of available features, trying to make it not only a stealer but a multipurpose bot, by enabling it to load multiple extensions created by a distributor. The added features, such as a keylogger, and collecting information about the system, are also a step towards making it a general-purpose spyware.
It is evident that with the fast pace and ongoing development, Rhadamanthys is trying hard to become a big player on the malware market and is most likely here to stay.
Check Point customers remain protected from the threats described in this research.
Check Point’s Threat Emulation provides comprehensive coverage of attack tactics, file types, and operating systems and has developed and deployed a signature to detect and protect customers against threats described in this research.
Check Point’s Harmony Endpoint provides comprehensive endpoint protection at the highest security level, crucial to avoid security breaches and data compromise. Behavioral Guard protections were developed and deployed to protect customers against threats described in this research.
BG:
BS:
TE/Harmony Endpoint protections:
ID | Hash | Module type | Format |
---|---|---|---|
#1.1 | bb8bbcc948e8dca2e5a0270c41c062a29994a2d9b51e820ed74d9b6e2a01ddcf | Stage 1 (version 0.5.0) | PE |
#1.2.1 | 22a67f510dfb7ca822b5720b89cd81abfa5e63fefa1cdc7e266fbcbb0698db33 | Stage 2: main module | XS1 |
#1.2.2 | 6ed3ac428961b350d4c8094a10d7685578ce02c6cd41cc7f98d8eeb361f0ee38 | dt.x86.bin | XS1 |
#1.2.3 | 4fd469d08c051d6997f0471d91ccf96c173d27c8cff5bd70c3f2c5008faa786f | early.x86.bin | XS1 |
#1.2.4 | 633b0fe4f3d2bfb18d4ad648ff223fe6763397daa033e9c5d79f2cae89a6c3b2 | early.x64.bin | XS1 |
#1.2.5 | 50b1f29ccdf727805a793a9dac61371981334c4a99f8fae85613b3ee57b186d2 | phexec.bin | XS1 |
#1.2.6 | 01609701a3ea751dc2323bec8018e11742714dc1b1c2dcb39282f3c4a4537c7d | netclient.x86.bin | XS1 |
#1.2.7 | a905226a2486ccc158d44cf4c1728e103472825fb189e05c17d998b9f5534d63 | proto_x86.bin | shellcode |
#1.2.8 | ed713454c20844522304c49cfe25fe1490418c300e5ab0c9fca431ede1e91d7b | strategy.x86.bin | XS1 |
#1.2.9 | f82ec2246dde81ca9edb69fb9c7ce3f7101f5ffcdc3bdb86fea2a5373fb026fb | unhook.bin | XS1 |
#1.3.1 | ee4a487e78f23f5dffc35e73aeb9602514ebd885eb97460dd26635f67847bd16 | Stage 3: main stealer component: “coredll.bin” (32-bit) | XS2 |
#1.3.2 | fcb00beaa88f7827999856ba12302086cadbc1252261d64379172f2927a6760e | Stage 3 submodule: “KeePassHax.dll” | PE |
#1.3.3 | a87032195e38892b351641e08c81b92a1ea888c3c74a0c7464160e86613c4476 | Stage 3 submodule: “runtime.dll” | PE |
#1.3.4 | 3d010e3fce1b2c9ab5b8cc125be812e63b661ddcbde40509a49118c2330ef9d0 | Stage 3 submodule: “loader.dll” | PE |
#1.3.5 | ecab35dfa6b03fed96bb69ffcecd11a29113278f53c6a84adced1167b66abe62 | Stage 3 submodule: “stubmod.bin” (32-bit) | XS2 |
#1.3.6 | 5890b47df83b992e2bd8617d0ae4d492663ca870ed63ce47bb82f00fa3b82cf9 | Stage 3 submodule: “taskcore.bin” (32-bit) | XS2 |
#1.3.7 | 2b6faa98a7617db2bd9e70c0ce050588c8b856484d97d46b50ed3bb94bdd62f7 | Stage 3 submodule: “stubexec.bin” (32-bit) | XS2 |
#1.3.8 | f1f33618bbb8551b183304ddb18e0a8b8200642ec52d5b72d3c75a00cdb99fd4 | Stage 3: main stealer component: “coredll.bin” (64-bit) | XS2 |
Our other writeups on Rhadamanthys:
[1] “From Hidden Bee to Rhadamanthys – The Evolution of Custom Executable Formats”.
[2] “Rhadamanthys: The”Everything Bagel” Infostealer”
More about used techniques:
[3] https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls
[5] https://whiteknightlabs.com/2021/12/11/bypassing-etw-for-fun-and-profit/
[6] https://www.malwarebytes.com/blog/news/2018/01/a-coin-miner-with-a-heavens-gate
[7] https://www.codeproject.com/Articles/11003/The-coding-gentleman-s-guide-to-detecting-the-NET
[8] https://codingvision.net/calling-a-c-method-from-c-c-native-process