Threat Background
Rhadamanthys is a newly emerged Information-Stealer that is written in C++. according to multiple reports[1] the malware has been active since late 2022.
In addition, the malware appears to masquerade itself as legitimate software such as AnyDesk installers[2], and Google Ads[3][13] to get the initial foothold.
As for usage, in the dark web, the malware authors offer various deals for using the malware such as monthly or even lifetime payments.
Also, the authors emphasize the malware's capabilities ranging from stealing digital coins, and system information collection, to execution of other processes such as Powershell.
In this article, I will investigate the Rhadamanthys stealer and reverse engineer the entire chain, from the first dropper to the malware itself.
As always, I will do it in a hybrid step-by-step tutorial and an actual presentation and will focus on the parts that I personally find more interesting(the ways the malware tries to evade detection).
- PART 1: The Dropper
- Unpacking mechanism: getting to the first shellcode
- Shellcode execution via Callback
- Investigating the first shellcode
- Fixing functions statically: Defining functions
- Fixing functions statically: Defining code
- Fixing the shellcode: Rebase the address
- Shellcode functionality
- Summarize the first shellcode - PART 2: The second shellcode aka Rhadamanthys loader
- Evasion technique: Multiple Anti-Analysis
- Evasion technique: Manipulate exception handling
- Evasion technique: Avoiding error messages
- Evasion technique: Creating Mutex and impersonating a legitimate
- Evasion technique: Unhooking API calls
- Config Decryption
- Network
- Loader’s goal - PART 3: The Nsis module- The Rhadamanthys stealer
- Nsis loader
- Rhadamanthys stealer capabilities
- Resolving APIs dynamically
- Evasion technique: Check and possibly manipulate AVAST’s AMSI-related modules
The Dropper
File hash: 89ec4405e9b2cab987f2e4f7e4b1666e
The Rhadamanthys’s dropper is a 32-bit file and similar to many droppers, it has relatively large entropy which indicates potentially packed content inside of it.
One of the relatively new features of PEstudio is the ability to check if the ASLR[4] feature is enabled. In my analysis, I always prefer to disable the ASLR so the addresses in IDA and in XDBG will be the same for tracking purposes.
In PEstudio, go to “optional-header” and then to the ASLR bar, then you can see under the “detail” column if it is false (disabled) or true (enabled).
Unpacking mechanism: getting to the first shellcode
As we observe the dropper in IDA, we see a large embedded “blob” in the .rdata section. Usually, these kinds of blobs can potentially contain data that will be decrypted during runtime.
The first activity the dropper do is to create a new heap
Then, the function sub_408028 will be the core function that will deal with encrypting the blob. Inside sub_408028, there are two interesting functions:
- sub_406A28 - this function is responsible for returning an address containing the data to be written.
- sub_408528 - a wrapper of memcpy
In the first iteration, the embedded blob will be written into the newly created heap
Next, the same function will override the blob and will decrypt a shellcode.
Then, a call to VirtualAlloc will happen to create a newly allocated memory followed by memcpy to copy the shellcode from the heap to the new memory. Lastly, a VirtualProtect API call will be used to change the permission of the memory segment to RWX.
The entire chain can also be seen in the following pseudo-code of IDA pro:
The next thing we’ll do is go to the address 004065A1 in the WinMain function (remember, ASLR is disabled so we can navigate easily in IDA and the debugger).
We could see that the value of the shellcode (that is dynamically located in the EAX register) is being transferred to another offset variable 42F6F0.
Shellcode execution via Callback
After having a shellcode with EXECUTE permission, we need a way to execute it, in this case, the authors choose a cool trick in form of a Callback function.
The shellcode execution will go as the following:
- The function sub_405728 is responsible to invoke the API call ImmEnumInputContext
- sub_405728 receives as a parameter function named sub_407228 which is just a wrapper for another function that jumps to the shellcode address
- The final result is that ImmEnumInputContext will get the address of the shellcode in its second argument “lpfn” and will execute it.
The logic can be seen in the following pseudo-code
The reason for choosing this way is most likely to evade anti-virus products that rely on CreateThread \ CreateRemoteThread as a trigger point to scan addresses that may contain malicious content.
Investigating the first shellcode
To investigate the shellcode we can choose one of the two:
- Dump the entire allocated buffer and run it in Blobrunner[5]
- Continue with the code dynamically (because why not?)
To investigate it statically, we obviously must dump the shellcode, to do it do the following:
- Right click on the address of the shellcode and click “Follow in Memory Map”
2. Then, in the memory map, right click on the shellcode address and then “Dump Memory to File”
Then, drag and drop the dumped file in IDA.
To summarize the steps until now see the following graph
Fixing the shellcode: Defining functions
After the shellcode was loaded, we can see 5 functions that appear in the Function name bar. In addition, in the navigation bar, we can see the colors blue and brown.
According to the IDA website[6] blue means “Regular functions, i.e. functions not recognized by FLIRT or Lumina.”
And brown means “Instructions(code) not belonging to any functions. These could appear when IDA did not detect or misdetected function boundaries, or hint at code obfuscation being employed which could prevent proper function creation. It could also be data incorrectly being treated as code.”
And when we look at an area in the IDA view that contains both we see the following:
We can obviously see that the brown color is a legit code, however, IDA doesn't consider it as a code and therefore does not show it as a function.
To fix this, we can just scroll and observe statically from where this function starts and when it ends.
In our case, it starts at the address 000029E, we also see the prologue:
push ebp
mov ebp, esp
And ends at the address 000036B with the epilogue:
leave
retrn
Now that we know the function boundaries, we can mark it all, and click “P”
Then, we can see that the brown code is now considered a function, and a new function sub_29E was added to the function name bar.
NOTE: When fixing functions do not assume that the first “retrn” is the end of a function, pay attention to the jumps that might bypass this return and might indicate a longer function.
Fixing the shellcode: Defining code
In addition to the convenient scenario of a code that looks like code and just doesn't interpret as a function, we have a more tricky scenario when we need to change the data itself.
At the beginning of the shellcode, we can see dynamically the assembly code “call 450028” that suppose to take us to the address in 450028 which starts with “pop eax” and eventually calls to the function in the address 45029E which in our case called sub_29E.
However, as we can see, statically we just see jibberish and it does not look like the dynamic view.
To fix it, we need to tell IDA that some specific addresses are actual code.
For example: in the dynamic view, we can see that the first 5 bytes are:
Call 450028
Therefore, we should tell IDA that the first 5 bytes are code, then, we can tell IDA to look at it as a function.
To do it, do the following:
- Mark the data
- Right click
- Click on “Undefine”
Then, mark the 5 bytes and tell IDA to look at it as Code.
After doing it, we can see that the same data looks like the code from the debugger view
And as said, we can always turn it into a function of its own (because why not?)
As we see, the function jumps to the address at “loc_28” (IDA) or “450028” (debugger), however in IDA this content also needs to be fixed. Combining the two approaches of defining as code and defining as function can fix will do the trick.
After doing that, we now have 8 functions in the function name bar.
Fixing the shellcode: Rebase the address
The last thing we need to do if we want to properly analyze the shellcode alongside the debugger is to match the addresses. To do it do the following:
- Go to Edit
- Segments
- Rebase program
4. Change the value to the value of the actual entry point of the shellcode in the debugger
5. Click OK
And now we can see that the addresses statically and dynamically the same
Finally, we can start and actually analyze the shellcode
Shellcode functionality
The first thing we can see is that the actual code in shellcode is very small, there are 8\9 functions, and the rest is a big chunk of data. From this, we can assume that the shellcode will potentially use that data.
So let's “go with the flow” and understand this shellcode
- sub_450000 just jumps to sub_450028
- sub_450028 jump jumps to sub_45029E
sub_45029E is a larger function that contains multiple functions.
sub_450249
This function access the Process Environment Block to get the address of Kernel32.dll. This behavior is traditional and happens in many shellcodes.
sub_45036E
This function gets 3 arguments
- Kernel32 address
- Hashes
- An array that holds 4 functions
It then iterates through the kernel32 export functions and sends the names of the functions to another function named sub_45040C. The only job of sub_45040C is to hash the function name it receives and return the hash.
Then, sub_45036E checks if the hashed function name matches the hash it got as an argument, if yes, it puts it in the array and sends it back to sub_45029E.
Overall the functions will be “VirtualAlloc, LocalFree, LocalAlloc, VirtualFree”
sub_450077
This function will decrypt the large data that is stored in our shellcode, and write it to the LocalAlloc we saw. This beginning of the decrypted data will look like this
Next, in the address 00450314, we can see the call for VirtualAlloc, don't forget to observe the allocated memory using follow in dump of the EAX register (in my case it's 00470000).
sub_45003A
This function will happen several times and it is basically a memcpy that copies data from one variable to the other.
sub_45003A will get the decrypted content and our newly allocated memory as arguments and will copy the data to it.
And finally, in the address 00450365, we have a “call ebx” that will take us into this our allocated memory in the offset 5BAB, and as we can see, it's also another shellcode.
Summarize the first shellcode
To summarize the entire shellcode activity, we can look at it from a code point of view
And from the following graph's point of view
The second shellcode aka Rhadamanthys loader
The main objective of this shellcode is to be the actual loader of the Rhadamanthys stealer. This shellcode has multiple evasion capabilities and we will observe some of them.
Note- In a similar way to the first shellcode, some fixes are needed.
Evasion Technique: Multiple Anti-Analysis
The Rhadamanthys loader contains large anti-analysis checks stolen from the al-khaser project[7]. This project was also used in the Bumblebee malware.
Some of the checks are checking for a virtual environment
Checks for specific users that could hint about a lab environment
Check for security-related DLLs
At this point, it will be useless to continue writing the anti-analysis capabilities, so for those who want to see all, please visit the al-khaser project GitHub page.
Evasion Technique: Manipulate Exception Handling
One of the most interesting capabilities of the Rhadamanthys loader is exception-handling manipulation.
What is Exception handling?
According to Microsoft’s documentation[9]: “Structured exception handling (SEH) is a Microsoft extension to C and C++ to handle certain exceptional code situations, such as hardware faults, gracefully.”
The SEH is basically a linked list that has two pointers:
- A pointer to the next SEH record
- A pointer to the function that contains the code to deal with the error
Examples of errors are division by 0, and excessive string length.
Microsoft allows programmers to create their own exception handlers in order to manage errors by themselves.
How the loader uses it?
First, the loader gets the address of ZwQueryInformationProcess, then it saves it on another variable. Eventually, we enter the function named sub_5978.
In sub_5978, the loader gets the address of KiUserExceptionDispatcher and starts to iterate on it to search for a specific location where ZwQueryInformationProcess is called.
In sub_5A5C the loader set the hook in the desired location of the call to ZwQueryInformationProcess
So how the change looks like?
In the following image, we can see the call to ZwQueryInformationProcess that happens inside KiUserExceptionDispatcher from Ntdll as part of KiUserExceptionDispatcher's legitimate behavior.
After the change, we can see that the call was replaced to jump to a function in the loader that will perform the ZwQueryInformationProcess and will modify the ProcessInformation flag to be 6D or MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE.
Why does this flag matters?
This flag determines whether to allow execution outside the memory space of the loaded module. In other words, it enables exception handling to be performed on shellcode.
So how the exception handling will be managed?
Without being noticed, the initial dropper has registered an SEH record in the process memory with the name _except_handler3. Therefore, every exception that will be triggered by the shellcode will go there and will be managed by whatever logic the author decided.
This activity is most likely done to avoid raising suspicions if errors or exceptions anomalies will trigger.
The entire activity can be seen in the following graph
Evasion Technique: Avoiding error message
After controlling the exceptions, the loader will use the API call SetErrorMode with 0x8003 as an argument, this argument consists of the following three:
- SEM_NOOPENFILEERRORBOX - The system does not display the critical-error-handler message box. Instead, the system sends the error to the calling process.
- SEM_NOGPFAULTERRORBOX — The system does not display the Windows Error Reporting dialog.
- SEM_FAILCRITICALERRORS — The OpenFile function does not display a message box when it fails to find a file. Instead, the error is returned to the caller.
In other words, the loader doesn't want the system to display any error on the screen, and wants to handle them by himself.
Similar to controlling the exception handling, this is another maneuver of the loader to not raise any suspicions.
Evasion Technique: Creating Mutex and impersonating a legitimate
The loader continues with creating a Mutex with the name that starts with “Global\MSCTF.Asm.{digits}”.
Note that mutexes with this name are already found in the OS and are created by MSCTF.dll, and more info can be found in this[10] article.
After creating the Mutex, we moved to a function named sub_2B92 which holds the core activity and the main purpose of the loader.
Evasion Technique: Disabling hooks
In the function named sub_8060, we see one of the cool tricks of malware to protect themselves against user mode hooking.
It first gets a handle to ntdll.dll and loads it to virtual memory, then, the loader gets the handle of the real ntdll.dll that is already loaded.
It will then copy the bytes of the SYSCALL of ZwProtectVirtualMemory into another virtual memory in order to use it without explicitly using the ZwProtectVirtualMemory in ntdll address space.
Then, it will get the export table of both real and fake modules and will iterate on them. They will be compared using memcmp, and if they will found different, the loader will change the protection of the real function of ntdll and will use memcpy to copy the data from the fake to the real one. In this way, the malware verifies that no hooks are set.
If we inspect it dynamically, this is a normal state when two functions are compared. We can see that the virtual address is different but the bytes are the same
For learning purposes, I changed the first byte of the real function to start with E9. Then, the loader took us to the memcpy function that copied the data from the fake to the real to correct the change I made.
Except for ntdll.dll, the loader will check the following DLLs:
- User32.dll
- Advapi32.dll
- Ole32.dll
The entire activity can be seen in the following graph (Was lazy so I just copy paste this from my previous blob)
Config Decryption
The config decryption occurs in a function named sub_3DD4, which is a function that will do various activities that the main loader activity requires.
In sub_3DD4 we have two functions that will deal with the config decryption: sub_28AA and sub_2911.
sub_28AA
This function is basically just an RC4 algorithm
sub_2911
This function is also part of the decryption algorithm
When we step over sub_2911 dynamically, we can see the data that hold the encrypted config at the third argument (address 42F6F8 in my case).
In our case, we can see that the C2 will be http://185[.]209.160.99/blob/top.mp4
Network
To start the network activity, the loader first collects two key pieces of information from the machine:
- The default language using GetUserDefaultLangID
- The Locale using GetLocaleInfoW
Then, the same function will start to set the user-agent to send the data to the C2 which is the decrypted config we saw.
To communicate, the loader dynamically resolves multiple functions such as socket, WSAIotcl, and CreateCompletionPort to use the IOCP socket model.
The loader uses WSAIoctl to invoke a handler for LPFN_CONNECTEX to use the ConnectEx function.
Eventually, the loader communicates with the APIs WSARecv & WSASend.
If we want to observe dynamically the data that is sent to the C2, do the following:
- Set a breakpoint at the address where WSASend is being executed.
- Follow in dump the address of the second parameter aka lpBuffers
- This buffer is a WSABUF structure, and its second parameter is a pointer to the actual buffer that is sent to the C2.
- To see it, just follow in dump
Loader’s goal
After performing its various capabilities and tricks, the loader will execute its main goal.
- The loader will download a DLL from the C2
- Write it to the disk with the name of nsis_uns[xxxxxx].dll
- Spawn Rundll32 to execute the DLL with the export function “PrintUIEntry” which is a name of a legitimate export function of the printui.dll.
NSIS Module: The Rhadamanthys stealer
The Nsis module consists of two parts:
- A loader (the Nsis module before unpacking)
- The actual stealer
NSIS Loader
The loader is executed via a very long command that changed in every iteration
The interesting thing about the NSIS loader is that there are many loaders out there, but their detection rate is very low!
For the loader behavior, the NSIS loader just allocates data using LocalAlloc and copies it to mapped memory using MapViewOfFile and memmove. Eventually, it will jump to the shellcode address.
Due to time constraints, I will not display this shellcode, however, it is just a small shellcode that unpacks and inject into the memory the Rhadamanthys stealer itself.
Rhadamanthys stealer capabilities
Finally, we arrived at the stealer himself!!!
Disclaimer: because of not abling to dynamically analyze the sample when the C2 was on, I only got the stealer from the following tria.ge sandbox link[11].
Also, for this part, I will only focus on the stealing capabilities and its targets.
Stealing KeePass passwords
The malware appears to be able to use the DLL KeePassHax[12], an open-source tool used to decrypt the password database.
Usage of SQLite
The malware can collect and extract data using SQLite
Target multiple browsers
The malware target the following browsers in their info-stealing activity:
- Coc CoC
- Pale Moon
- Sleipnir5
- Opera
- Chrome
- Twinkstar
- Firefox
- Edge
Target OpenVPN
The malware appears to get the profile, username, and password of OpenVPN.
Target steam accounts
The malware appears to aim at Steam’s config\loginusers.vdf which contains information about Steam’s users.
Target FileZilla passwords
The malware search for FileZila’s specific files:
- recentservers.xml
- sitemanager.xml
These two files contain the passwords and other data of the FTP accounts.
Target CoreFTP
Target Discord
The malware collects information from the discord directories, possibly to extract further data.
Collecting Telegram data
The malware targets Telegram desktop data which is located in encrypted files (such as D877F783D5D3EF8) in the “tdata” directory.
Collecting information from various email
The malware target the following email clients:
- Foxmail
- Outlook
- The BAT
Extracting web credentials using Vaultcli functions
Target WinSCP
The malware target sensitive registry keys of the WinSCP in order to collect information.
Target CryptoCurrency entities
The malware target the following cryptocurrencies entities and wallets:
- Dogecoin
- Litecoin
- Monero
- Qtum
- Armory
- Bytecoin
- Binance
- Electron
- Solar waller
- Zap
- WalletWasabi
- Zcash
- Ronin
- Avana
- OKX
Resolving APIs dynamically
The stealer is resolving dynamically his APIs using the GetModuleHandle and GetProcAddress API calls.
Evasion technique: Modify and possibly manipulate AVAST modules
The stealer uses the same code that was used in the loader to verify and unhook functions and the same function appears to aim for the AVAST-related modules aswhook.dll & aswAMSI.dll.
More amsi-related functions and DLLs that are being targeted by the stealer are:
- avamsicli.dll
- amsi.dll
- AmsiScanString
- AmsiScanBuffer
- EtwEventWrite
At this stage, I decided to stop my analysis
For everyone's convenience, I also uploaded all the files from my analysis including the shellcodes to VirusTotal.
Rhadamanthys files
https://www.virustotal.com/gui/file/8384322d609d7f26c6dc243422ecec3d40b30f29421210e7fba448e375a134f6
References
[1] https://threatmon.io/rhadamanthys-stealer-analysis-threatmon/
[2] https://mobile.twitter.com/JAMESWT_MHT/status/1610620178441568261
[3] https://mobile.twitter.com/1ZRR4H/status/1610590795278712832
[4] https://en.wikipedia.org/wiki/Address_space_layout_randomization
[5] https://github.com/OALabs/BlobRunner
[6] https://hex-rays.com/blog/igors-tip-of-the-week-49-navigation-band/
[7] https://github.com/LordNoteworthy/al-khaser
[9] https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-170
[11] https://tria.ge/221227-vprhbsae8t/behavioral2#report
[12] https://github.com/HoLLy-HaCKeR/KeePassHax
[13] https://twitter.com/1ZRR4H/status/1614728368334716932
[14] https://www.joesandbox.com/analysis/783578/0/html#
[15] https://blog.cyble.com/2023/01/12/rhadamanthys-new-stealer-spreading-through-google-ads/