Rhadamanthys V0.6.0 : Automating Config Decryption

Jonathan Mccay
Walmart Global Tech Blog
7 min readAug 16, 2024

Rhadamanthys is a multi-function stealer written in C++ that was first made available in late 2022. Since Rhadamanthys debut, it has remained
an attractive and accessible option for cyber criminals¹. As with most products, they require capable support that can provide a consistent release schedule, enabling the malware to keep up with the constant changes and updates to the desktop environment.

Rhadamanthys includes some unique functionality in the binary. One of the more interesting components, from a reversing standpoint, is the configuration file. Once we are able to pinpoint the configuration file, we can then automate the extraction of indicators to create protections or analyze for any potential intelligence insights.

Encrypted Config

The encrypted config is stored in the .rdata section starting at offset 0xE1. We can access the encrypted config from the top layer of the binary, but the information needed to decrypt it will be a few layers down. To make the process of reverse engineering their product difficult, the developer continues to implement their own parsing routines, modified versions of known encryption algorithms, and custom encryption algorithms spread throughout multiple layers of code.

Encrypted Config: 256 bytes

Layer 1:

This stage is responsible for parsing sections to gather encrypted data, and decrypting the next layer using a xor routine.

  • Parsing Algorithm

To decrypt code needed for later stages, we will need to pull unparsed data from the .rdata & .text sections. Both of these sections will use the same function to parse the data, but each section will have its own values used to determine the amount of bytes to skip.

Parsing Algorithm : .text

The unparsed data will always exist at the same offsets in each binary. They will also use the same values per binary section needed to parse the data. The result of this algorithm will produce the amount of bytes to skip in the unparsed data. After the “div” instruction, the modulus of that operation will be the number of bytes to skip.

Python: Parsing Algorithm
Values sent to “assemble_encrypted” by binary section

After both sections of parsed data are returned they will concatenate and be sent to the next xor routine to decrypt.

  • Xor Two Key
Xor: Two Key

This function is responsible for decrypting the next relevant section of code. Two key values are supplied, but only one of them is used to xor the encrypted data. The second value “key_xor” is used to xor against the current key to create the “second_key”. The key value will swap every 16 bytes between the values “key” and “second_key”. This function is essentially creating a 32 byte xor key and decrypting data. In the conversion to python I’m doing the key calculation up front to create a 32 byte key instead of swapping every 16 bytes.

key_xor: 0x78787878787878787878787878787878

key: 0xf6358ddf69c577d9dce6bb77fa4fa798

second key: 0x8e4df5a711bd0fa1a49ec30f8237dfe0

final key: 0xf6358ddf69c577d9dce6bb77fa4fa7988e4df5a711bd0fa1a49ec30f8237dfe0

Xor routine used to decrypt second layer

After the parsed data is processed by “xor_two_key”, the data will be sent back through the function again. 56 bytes of the output from the first submission will be skipped, and will be sent again to the xor function using the same decryption keys.

Layer 2:

The decrypted output from the second round of “xor_two_key” will contain the compressed stealer module. Starting at offset 0x1281 will be data compressed using LZSS.

Stealer

After completing LZSS decompression, the main module of Rhadamanthys is available. This is where the routines needed to decrypt the config will be found. We can now go back to the .rdata section at offset 0xE1 and grab the encrypted config.

  • Custom Base64 Letter Set

Using the same string decryption² routine witnessed in earlier versions, a string containing a custom letter set will be decrypted. This custom letter set will be used to decode the base64 portion of the config.

Decrypting String: Custom Base64 Letter Set

Standard Base64:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Rhadamanthys Base64:

ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|

  • ChaCha20 with custom block counter³

After completing the base64 decoding, the next stage will be ChaCha20, but instead of starting at 0 ,the block counter will start at 128. The key and nonce needed to decrypt this stage were the same across all samples.

32 byte key:

52abdf06b6b13ac0da2d22dc6cd2be6c201769e012b5e6ec0eab4c14734aed51

12 byte nonce:

5f14d79cfcfc439ec3406bba

To test this in python and to generate the chacha20 blocks, I modified “pure-chacha20 0.1.0”⁴ to allow a custom block counter to be passed. After ChaCha20 is decrypted the data will be sent to the final decryption routine.

  • Final Xor
Final Xor Routine

This is the final xor routine before the config is decrypted. The last 4 bytes of the decrypted ChaCha20 output will be the starting xor key. The encrypted data, (everything but the last 4 bytes) will be accessed in 4 byte values. Each DWORD will be split into individual bytes. Bytes will be pulled from opposite ends of the key and encrypted data to xor individually. After the xor is complete, the four bytes of encrypted data will become the new key. Repeat until config is decrypted.

Python Config Decryption

All these layers and routines to get to the algorithm and keys needed to decrypt the config, but we don’t need most of them accomplish that. Knowing the key and nonce are the exact same across all samples outside of the “Final Xor” routine, we can shortcut the config decryption.

  • Algorithm: Config Decryption

Base64 — Custom Letter Set

ChaCha20 — Custom Block Counter

Final Xor

ChaCha20 will always generate the same blocks if the keys and nonce never change. This allows us to generate three blocks starting at block counter 128. The three blocks can be used as a contiguous xor key instead of using a modified block counter in ChaCha20. The code below will return the decrypted config from unpacked Rhadamanthys V0.6.0 binaries.

from pefile import PE
from binascii import a2b_base64, unhexlify
from sys import argv

def cha_xor(custom_b64_decoded):
# Nonce : 5f 14 d7 9c fc fc 43 9e c3 40 6b ba
# Key : 52 ab df 06 b6 b1 3a c0 da 2d 22 dc 6c d2 be 6c 20 17 69 e0 12 b5 e6 ec 0e ab 4c 14 73 4a ed 51
ChaCha20_Blocks = [
b'8cd4ef1c0ae344ff041eeacd9ec39186629f5bdee758d25dc7fb1bc5e1ee0a615a21a9703a97731507b08bfb99b36475866686a566f29fe7f7db7b29725b4bd6', #128
b'de566eb9d2fbb416a1f71781639d3ffca7dc712589d212553c828ad335fa5fdeceda6d75ef2699cd20c847ac2c3ecf47cb2b437f17c045a9177e43f17ba617a1', #129
b'75eac0ca5112e9f34d2fa040399dc4b52625c4df403dc0447f17672e51ca4e9961300989fcb8a5eaa25144b2d3d4df40b7e738330b05b73b478f236c141883bb' #130
]
cha_xor_key = b''.join([unhexlify(i) for i in ChaCha20_Blocks])
xor_out = b''

for j in range(192):
xor_out += bytes([custom_b64_decoded[j] ^ cha_xor_key[j]])

return xor_out

def final_xor(chaxor):
spl4 = [chaxor[i:i+4] for i in range(0, len(chaxor), 4)]
key = spl4[-1]
config = b''
for v in range(int(len(spl4)-1)):
for b in range(len(key)):
config += bytes([key[b] ^ spl4[v][b-4]])
key = spl4[v]

return config

pefile = PE(argv[1])
rdata = pefile.sections[2].get_data()
# encrypted rhadamanthys config
encrypted_config = rdata[225:481]

#base64 : custom letter set
sb64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
cb64 = b'ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|'
custom_b64_decoded = a2b_base64(encrypted_config.translate(bytes.maketrans(cb64, sb64)))

# xor using chacha20 blocks : counter = 128
chaxor = cha_xor(custom_b64_decoded)
# final xor routine before config
decrypted_config = final_xor(chaxor)
print(decrypted_config)
Decrypted Config

Sample Reversed:

edc64bf47d725c04bdba5fc77771a8c1f7459e6d66190504ec590d0fbf109078

Unpacked Filename:
TCPZ.exe

Yara:

rule rhadamanthys_v0.6.0_unpacked_x86
{
strings:
$parse1 = { 83 E1 03 8B C3 D3 E8 46 B9 03 00 00 00 81 C3 DF EA 0D 00 F7 F1 03 FA 3B FD 72 D2 }
$parse2 = { 83 E1 03 43 D3 E8 46 B9 05 00 00 00 81 C7 DF EA 0D 00 F7 F1 }
$xor_two_key = { 0F 11 4C 24 24 8A 44 0C 24 41 32 45 00 83 E1 0F 88 06 45 46 85 D2 75 DB 8B 44 24 10 }
condition:
uint16(0) == 0x5A4D and all of them
}

Rhadamanthys C2 — (decrypted from unpacked binaries):
api[.]pdfiso[.]com
api[.]dyk3j10rcxd1av9[.]xyz
api[.]qxugb3qpfpafmlto[.]xyz
api[.]xt6drjp542fz6j7xt[.]xyz
api[.]uaabcvsolwgl[.]xyz
api[.]hankirit[.]asia
www[.]carssell[.]online
one[.]renzoprotocols[.]co
api[.]kelimzorro[.]xyz
wanderpics[.]net
82.115.223[.]93
94.156.8[.]76
147.45.44[.]27
147.45.44[.]25
94.156.8[.]83
185.125.50[.]70
185.74.255[.]29
94.156.10[.]37
147.78.103[.]93
38.180.80[.]23
94.156.8[.]211
159.69.186[.]28
147.45.68[.]131
45.61.137[.]165
87.120.84[.]232
5.255.117[.]197
147.124.221[.]241
147.45.68[.]112
185.125.50[.]38
94.232.249[.]139
94.156.8[.]61
188.119.112[.]100
168.119.96[.]63
193.233.132[.]109
94.156.8[.]129
94.156.8[.]225
49.13.61[.]146
147.45.79[.]165
147.45.44[.]13
185.216.70[.]103
185.234.216[.]132
94.156.8[.]232
147.78.103[.]158
147.124.220[.]235
45.77.90[.]90
147.78.103[.]128
141.105.68[.]140
147.78.103[.]199
93.123.39[.]67
91.92.247[.]20
80.66.79[.]88
94.156.67[.]91
94.232.249[.]135
107.189.3[.]166
5.42.65[.]27
95.164.85[.]120

References:

  1. https://www.proofpoint.com/us/blog/threat-insight/security-brief-ta547-targets-german-organizations-rhadamanthys-stealer
  2. https://research.checkpoint.com/2023/rhadamanthys-v0-5-0-a-deep-dive-into-the-stealers-components/
  3. https://www.ciphertechsolutions.com/acce-release-notes-v2-5-20240313/
  4. https://pypi.org/project/pure-chacha20/

--

--