Focus on XWorm obfuscation 🪱

TL;DR XWorm#
The malware has many functionalities: Virtual environment detection, command execution, file download/upload, persistent after reboot, keylogging, etc… An interesting feature of the RAT is the plugin configuration; attackers can configure plugins that are downloaded and executed by the RAT during the infection. The plugins are mainly DLL in .NET, the last section explains how to extract XWorm plugins from network capture.
The next sections detail how the RAT managed its configuration deobfuscation, then how the Command and Control (C2) communication works.
Links to download the sample used for this analysis:
- MalwareBaazar: SHA256-6f2e22d541680c151da164b02f916a3d72da0517b2f052f7356d05e8b374690b
- Tria.ge sandbox
The RAT have been spotted at the end of November 2022 distributed in malspam campaigns, as reported in this tweet:
Most people don't tag their precise location in Tweets, so we're removing this ability to simplify your Tweeting experience. You'll still be able to tag your precise location in Tweets through our updated camera. It's helpful when sharing on-the-ground moments.
— Twitter Support (@TwitterSupport) June 18, 2019
One reason of the rise of XWorm observed in the wild could be the leak of the source code:
- XSS forum:
xss.]is/threads/75837/; - Shinyenigma Github repository: Shinyenigma/XWorm-RAT.
XWorm data obfuscation#
The RAT configuration is stored in a class of the namespace Stub, where variables value are encoded in base64, in this version of the sample namespaces, classes
and variables names are randomized to slow down the analysis, however, the structure of the code remains the same as clear sample!
This naming randomization is becoming more and more popular amoung threat actors that have XWorm in their arsenal.

XWorm first deobufscates its configuration, it initiates a CryptServiceProvider,
then the RAT compute the MD5 hash of a variable (named Mutex) located in its configuration
class. Finally the hash is manipulated (oddly 🙃) to create the decryption key used to decrypt
the rest of configuration (host, port, etc.).
XWorm uses RijndaelThe Advanced Encryption Standard (AES), also known by its original name Rijndael https://en.wikipedia.org/wiki/Advanced_Encryption_Standard encryption in ECB (Electronic CodeBook) mode, no IV (Initial Vector) is required here. To create the key, the malware creates the MD5 hash of a variable copies the 8 first bytes to store them at the beginning of a new allocated array (of 16 bytes; the decryption key length), then re-copies these same 8 bytes to the key array but not after the 8 first bytes but from the 7^th byte to the 15 byte. The last byte (16^th) is zero (due to the array initialization). The figure below schematizes how the key is built.
+----------+ Base64 decode +--------+
| Variable +------------------>| Array |
+----------+ +-----+--+
|
+-MD5
|
+-----v--------------------------+
|dfe3ee833129588467dcf63d547b5806|
+--------------------------------+
0 8 16 (bytes)
+ -----+------- +
| Copy
+---------------+
| |
| v
| + --------------+
v |dfe3ee8331295884|
+--------------- +
|dfe3ee8331295884|
+--------------+-+---------------+--+
| |X| |00|
+--------------+-+---------------+--+
0 7 8 15 16 (bytes)NB:
- the bytes 7 is overwritten
- the last byte of the key is fill with null byte
- the Rijndael key is 16 bytes long.
Code of the class in charge of the configuration ⤵️
1using System;
2using Microsoft.VisualBasic;
3
4// Token: 0x02000007 RID: 7
5public class 1155SI5ci4n22Hgtjad9sWuTzrR6TsPd3
6{
7
8 public static string 1mDweS2QdR()
9 {
10 return "1CWdwE8cqZ";
11 }
12
13 public static int EzSG3aY6W9()
14 {
15 return 99534703;
16 }
17
18 public static string Kxcu3hu0i2()
19 {
20 return "3SXF4Z2FNV";
21 }
22
23 public static int mOGPU4rRYz()
24 {
25 return 1112926;
26 }
27
28 public static string P9aS8dF2nvwBlMKsX6mGmz9cpNwVvrwuR = "VpmZ71vaW8IbCUUhXaZYmIZ+fg3gEPqzqfEpJeFIWv0=";
29
30 public static string O3fw8tjb6Ybd2xdW1giyBzQmbNOjR0msp = "a6SLXA5CKiTAVsnJwB7XJw==";
31
32 public static string hmapdKWqyxSWCy3WcMMHwiP4mJaWTptDB = "bvUFyfn2d6e7YEJt9/zw/A==";
33
34 public static string iOHL4ozmm3zDihnbDjArk0EELSZcjGaht = "suGMnRXFX/LRVfGLaUEsHA==";
35
36 public static int bq1mI0Edvncwx7ALBYKSeIcOOIBhaoFYg = 3;
37
38 public static string vJsr4si0NzLXgwfc4qHHmiEQitrHJl3Dk = "5D+67sZ9S91x7RZPyaBWwBnRhFtzU62dUOgoP7j7MdQ=";
39
40 public static string xDJM0Hxvl8n38SrNt95dqrkiScRfNClzh = "%AppData%";
41
42 public static string uVIC8wouZBTVmqNmkP5u6gKlFXjOtuSz8 = "FIubOp8mcEdS0i5b";
43
44 public static string w4IoWC1BczHdJNaHCSWko7r27h7MJfJJM = Interaction.Environ("temp") + "\\Log.tmp";
45}In this version of the RAT variables name are obfuscated, the two highlighted variables are the communication key (first variable from the top)
and the second one is what was named Mutex. The mutex is the variable holding the base string
used to build the decryption key for the configuration. The variable holding the key used for communication encryption/decryption need
to be first decrypted by the same method used for the rest of the configuration decryption before behing used for the communication.
Once decrypted, the configuration of XWorm is:
| Purpose | Value obfuscated | Cleartext |
|---|---|---|
| C2 Host | VpmZ71vaW8IbCUUhXaZYmIZ+fg3gEPqzqfEpJeFIWv0= | names-again.at.playit.]gg |
| C2 Port | a6SLXA5CKiTAVsnJwB7XJw== | 59741 |
| AES key for communication | bvUFyfn2d6e7YEJt9/zw/A== | <123456789> |
| Message split pattern | suGMnRXFX/LRVfGLaUEsHA== | <Xwormmm> |
| Installation name | 5D+67sZ9S91x7RZPyaBWwBnRhFtzU62dUOgoP7j7MdQ= | Microsoft OneDrive.exe |
Here is the cyberchef recipe.
Command and Control Communication#
XWorm communications are encrypted with AES algorithm in ECB mode. The RAT uses raw TCP connection on a port defined by the attacker. Exchanged messages are not fully encrypted, in fact the first bytes contains the size of the message followed by a null bytes (used as a split pattern) in cleartext, see the figure below where the size of the message is in black, the split pattern in red and the encrypted message in blue.

Once the mutex and the communication key are identified you can use this script to deobfuscate the RAT communication.
46import sys
47import argparse
48import binascii
49import hashlib
50from base64 import b64decode
51from collections import deque
52from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
53from cryptography.hazmat.backends import default_backend
54
55backend = default_backend()
56
57
58def decrypt_XWorm_configuration(mutex: str, configuration: str) -> bytes:
59 """configuration are stored in base64, the key used for the
60 configuration decryption is the MD5 hash (re-arranged) of the
61 variable `Mutex`"""
62
63 md5 = hashlib.md5(mutex.encode())
64 md5 = md5.digest()
65
66 key = md5[:-1] + md5 + b"\x00"
67
68 decoded_configuration = b64decode(configuration)
69
70 decryptor = Cipher(algorithms.AES(key), modes.ECB(), backend).decryptor()
71
72 return decryptor.update(decoded_configuration)
73
74
75
76def decrypt_XWorm_communication(mutex: str, com_key: str, packet: str):
77 """Rijndael decryption of XWorm communcation"""
78
79 clear_com_key = decrypt_XWorm_configuration(mutex, com_key)
80 clear_com_key = clear_com_key[:-clear_com_key[-1]]
81
82 hpacket = binascii.unhexlify(packet)
83 hpacket = hpacket.split(b"\x00", maxsplit=1)
84 size, message = int(hpacket[0]), hpacket[1]
85
86 clear_com_key = hashlib.md5(clear_com_key).digest()
87
88 decryptor = Cipher(algorithms.AES(clear_com_key), modes.ECB(), backend).decryptor()
89 cleartext = decryptor.update(message)
90 cleartext = cleartext[:-cleartext[-1]]
91
92 if len(cleartext) < 1000 and cleartext:
93 print(size, cleartext)
94
95 if not abs(len(cleartext) - size) >= 16 and len(cleartext) > 1000:
96 print(f"Download plugin: {cleartext[:100]}")
97 with open(f"packet-{len(cleartext)}.bin", "wb") as f:
98 f.write(cleartext)
99
100 return False if abs(len(cleartext) - size) >= 16 else True
101
102
103def main(mutex: str, com: str):
104 """Main function to decrypt XWorm communication"""
105
106 queue = deque(map(lambda x: x.replace("\n", ""), sys.stdin.readlines()))
107
108 while queue:
109 try:
110 packet = queue.popleft()
111 if not decrypt_XWorm_communication(mutex, com, packet):
112 queue.insert(0, f"{packet}{queue.popleft()}")
113 except Exception as err:
114 if queue:
115 queue.insert(0, f"{packet}{queue.popleft()}")
116
117
118if __name__ == "__main__":
119 # mutex = "FIubOp8mcEdS0i5b"
120 # com = "bvUFyfn2d6e7YEJt9/zw/A=="
121
122 parser = argparse.ArgumentParser("Script to decrypt XWorm communication")
123 parser.add_argument("-m", "--mutex", help="Mutex holding the configuration decryption key")
124 parser.add_argument("-c", "--com", help="Communication key (encrypted version)")
125
126 args = parser.parse_args()
127
128 main(mutex=args.mutex, com=args.com)To dump the XWorm messages in cleartext from a PCAP, use the following command:
1tshark -r /tmp/dump.pcapng -Y "tcp.port == 3395" -e tcp.payload -Tjson |
2 jq -r ".[]._source.layers[][]" |
3 python3 decrypt_xworm_com.py --mutex="FIubOp8mcEdS0i5b" --com="bvUFyfn2d6e7YEJt9/zw/A=="This sample of XWorm communicates on port 3395.
b'INFO<Xwormmm>9623CE2085896DD4BD55<Xwormmm>Admin<Xwormmm> Windows 10 Pro 64bit<Xwormmm>XWorm V3.0<Xwormmm>23/02/2023<Xwormmm>False<Xwormmm>True<Xwormmm>False<Xwormmm>None'
b'PING!'
b'plugin<Xwormmm>179DCD0BAD17DB8E467A40D7B57437461CDC3263090966A687BDD40B279E4DF2<Xwormmm>0'
b'sendPlugin<Xwormmm>179DCD0BAD17DB8E467A40D7B57437461CDC3263090966A687BDD40B279E4DF2
...XWorm plugins#
As introduced, XWorm has the capability to download and execute plugins. This mechanism is simple, here is its workflow:
- The C2 send available plugins:
plugin<Xwormmm>Id of the plugin<Xwormmm> - The RAT ask the C2 to send the plugin:
sendPlugin<Xwormmm>Id of the plugin - The C2 send the plugin:
savePlugin<Xwormmm>Id of the plugin<Xwormmm>\x00\xc2\x07\x00<DLL .NET >
Referred to 2 where the last packet (in the figure) received by the infected host has a size of 420736 bytes,
in fact this is a plugin sent by the C2.
This sample of XWorm downloads a plugin name Recover.dll with the following SHA-256: 179dcd0bad17db8e467a40d7b57437461cdc3263090966a687bdd40b279e4df2,
the plugin attempts to loot passwords and other authentication items.
The script dumps messages whose size exceeds 1000 bytes into a file, which is useful to dump XWorm plugins downloaded from the C2.