Dancing With Shellcodes: Analyzing Rhadamanthys Stealer

Threat Background

As for usage, in the dark web, the malware authors offer various deals for using the malware such as monthly or even lifetime payments.

Rhadamanthys

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).

  1. 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
  2. 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
  3. 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

The dropper

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).

Check ASLR

Unpacking mechanism: getting to the first shellcode

Blob

The first activity the dropper do is to create a new heap

Creating 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:

  1. sub_406A28 - this function is responsible for returning an address containing the data to be written.
  2. sub_408528 - a wrapper of memcpy

In the first iteration, the embedded blob will be written into the newly created heap

Decrypting the shellcode

Next, the same function will override the blob and will decrypt a shellcode.

Decrypting the 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.

Decrypting the shellcode

The entire chain can also be seen in the following pseudo-code of IDA pro:

Decrypting the shellcode

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.

Assign the shellcode address

Shellcode execution via Callback

The shellcode execution will go as the following:

  1. The function sub_405728 is responsible to invoke the API call ImmEnumInputContext
  2. sub_405728 receives as a parameter function named sub_407228 which is just a wrapper for another function that jumps to the shellcode address
  3. The final result is that ImmEnumInputContext will get the address of the shellcode in its second argument “lpfn” and will execute it.
ImmEnumInputcontext function in Microsoft documentation

The logic can be seen in the following pseudo-code

Shellcode execution

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.

Shellcode entry point

Investigating the first shellcode

  1. Dump the entire allocated buffer and run it in Blobrunner[5]
  2. Continue with the code dynamically (because why not?)

To investigate it statically, we obviously must dump the shellcode, to do it do the following:

  1. Right click on the address of the shellcode and click “Follow in Memory Map”
Going to the memory map

2. Then, in the memory map, right click on the shellcode address and then “Dump Memory to File”

Dumping the shellcode

Then, drag and drop the dumped file in IDA.

To summarize the steps until now see the following graph

Fixing the shellcode: Defining functions

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:

Defining functions

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

Defining functions

Now that we know the function boundaries, we can mark it all, and click “P”

Defining functions

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.

Defining functions

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

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.

Defining as code

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:

  1. Mark the data
  2. Right click
  3. Click on “Undefine”
Defining as code

Then, mark the 5 bytes and tell IDA to look at it as Code.

Defining as code
Defining as code

After doing it, we can see that the same data looks like the code from the debugger view

Defining as code

And as said, we can always turn it into a function of its own (because why not?)

Defining as function

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.

Defining as code and defining as function

After doing that, we now have 8 functions in the function name bar.

Function bar

Fixing the shellcode: Rebase the address

  1. Go to Edit
  2. Segments
  3. Rebase program
Rebase

4. Change the value to the value of the actual entry point of the shellcode in the debugger

5. Click OK

Rebase

And now we can see that the addresses statically and dynamically the same

Rebase

Finally, we can start and actually analyze the shellcode

Shellcode functionality

So let's “go with the flow” and understand this shellcode

  1. sub_450000 just jumps to sub_450028
  2. sub_450028 jump jumps to sub_45029E

sub_45029E is a larger function that contains multiple functions.

Shellcode functionality

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.

Get kernel32 address

sub_45036E
This function gets 3 arguments

  1. Kernel32 address
  2. Hashes
  3. 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.

Hashing function

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

Decrypting data

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).

shellcode functionality

sub_45003A
This function will happen several times and it is basically a memcpy that copies data from one variable to the other.

copy function

sub_45003A will get the decrypted content and our newly allocated memory as arguments and will copy the data to it.

copied data

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.

Jump to another shellcode

Summarize the first shellcode

Shellcode functionality

And from the following graph's point of view

Second shellcode decryption

The second shellcode aka Rhadamanthys loader

Note- In a similar way to the first shellcode, some fixes are needed.

Evasion Technique: Multiple Anti-Analysis

Some of the checks are checking for a virtual environment

Anti-analysis checks
Anti-analysis checks

Checks for specific users that could hint about a lab environment

Anti-analysis checks

Check for security-related DLLs

Anti-analysis checks

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

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:

  1. A pointer to the next SEH record
  2. 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.

Getting ZwQueryInformationProcess

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.

Iterating in KiUserExceptionDispatcher

In sub_5A5C the loader set the hook in the desired location of the call to ZwQueryInformationProcess

Patch KiUserExceptionDispatcher

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.

KiUserExceptionDispatcher after the patch

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.

_except_handler3

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

Manipulating the SEH

Evasion Technique: Avoiding error message

  1. SEM_NOOPENFILEERRORBOX - The system does not display the critical-error-handler message box. Instead, the system sends the error to the calling process.
  2. SEM_NOGPFAULTERRORBOX — The system does not display the Windows Error Reporting dialog.
  3. 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.

setErrorMode

Evasion Technique: Creating Mutex and impersonating a legitimate

Creating Mutex

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

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.

Check for hooks

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.

Check for hooks

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

Check for hooks

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.

Disable hooks

Except for ntdll.dll, the loader will check the following DLLs:

  1. User32.dll
  2. Advapi32.dll
  3. Ole32.dll
Check for hooks in other DLLs

The entire activity can be seen in the following graph (Was lazy so I just copy paste this from my previous blob)

Check for hooks logic

Config Decryption

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

Config decryption

sub_2911

This function is also part of the decryption algorithm

Config decryption

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).

Config decryption

In our case, we can see that the C2 will be http://185[.]209.160.99/blob/top.mp4

Network

  1. The default language using GetUserDefaultLangID
  2. 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.

Collect information about the machine
Set the User-Agent

To communicate, the loader dynamically resolves multiple functions such as socket, WSAIotcl, and CreateCompletionPort to use the IOCP socket model.

Network activity

The loader uses WSAIoctl to invoke a handler for LPFN_CONNECTEX to use the ConnectEx function.

Getting ConnectEx

Eventually, the loader communicates with the APIs WSARecv & WSASend.

Send & Recieve data

If we want to observe dynamically the data that is sent to the C2, do the following:

  1. Set a breakpoint at the address where WSASend is being executed.
  2. Follow in dump the address of the second parameter aka lpBuffers
  3. This buffer is a WSABUF structure, and its second parameter is a pointer to the actual buffer that is sent to the C2.
  4. To see it, just follow in dump
Observing data send to the C2
Observing data send to the C2

Loader’s goal

  1. The loader will download a DLL from the C2
  2. Write it to the disk with the name of nsis_uns[xxxxxx].dll
  3. Spawn Rundll32 to execute the DLL with the export function “PrintUIEntry” which is a name of a legitimate export function of the printui.dll.
Loader goal

NSIS Module: The Rhadamanthys stealer

  1. A loader (the Nsis module before unpacking)
  2. The actual stealer

NSIS Loader

Nsis module command

The interesting thing about the NSIS loader is that there are many loaders out there, but their detection rate is very low!

Nsis loader low detection rate

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.

Loader main goal

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

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

Keepass

Usage of SQLite

Sqlite

Target multiple browsers

  1. Coc CoC
  2. Pale Moon
  3. Sleipnir5
  4. Opera
  5. Chrome
  6. Twinkstar
  7. Firefox
  8. Edge
Browsers

Target OpenVPN

OpenVPN

Target steam accounts

Valve

Target FileZilla passwords

  1. recentservers.xml
  2. sitemanager.xml

These two files contain the passwords and other data of the FTP accounts.

FileZilla

Target CoreFTP

CoreFTP

Target Discord

Discord

Collecting Telegram data

Telegram

Collecting information from various email

  1. Foxmail
  2. Outlook
  3. The BAT
Emails

Extracting web credentials using Vaultcli functions

Vault activity

Target WinSCP

WinSCP

Target CryptoCurrency entities

  1. Dogecoin
  2. Litecoin
  3. Monero
  4. Qtum
  5. Armory
  6. Bytecoin
  7. Binance
  8. Electron
  9. Solar waller
  10. Zap
  11. WalletWasabi
  12. Zcash
  13. Ronin
  14. Avana
  15. OKX
Crypto
Querying registry keys for digital coming entities from Joe[

Resolving APIs dynamically

Dynamic resolving

Evasion technique: Modify and possibly manipulate AVAST modules

Check AVAST’s AMSI-related DLLs

More amsi-related functions and DLLs that are being targeted by the stealer are:

  1. avamsicli.dll
  2. amsi.dll
  3. AmsiScanString
  4. AmsiScanBuffer
  5. 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

References

[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

[8] https://elis531989.medium.com/the-chronicles-of-bumblebee-the-hook-the-bee-and-the-trickbot-connection-686379311056

[9] https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-170

[10] https://www.hexacorn.com/blog/2018/12/25/enter-sandbox-part-22-ctf-capturing-the-false-positive-artifacts/

[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/

--

--

Malware Researcher & Threat Hunter

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store