RE

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:

The RAT have been spotted at the end of November 2022 distributed in malspam campaigns, as reported in this tweet:

One reason of the rise of XWorm observed in the wild could be the leak of the source code:

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 configuration decryption using AES (ECB No padding)
Figure 1: XWorm configuration decryption using AES (ECB No padding)

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.

output 23 lines
+----------+  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 ⤵️

csharp 45 lines
 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}
Code Snippet 1: Configuration class of XWorm (source: DnSpy)

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:

PurposeValue obfuscatedCleartext
C2 HostVpmZ71vaW8IbCUUhXaZYmIZ+fg3gEPqzqfEpJeFIWv0=names-again.at.playit.]gg
C2 Porta6SLXA5CKiTAVsnJwB7XJw==59741
AES key for communicationbvUFyfn2d6e7YEJt9/zw/A==<123456789>
Message split patternsuGMnRXFX/LRVfGLaUEsHA==<Xwormmm>
Installation name5D+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.

XWorm raw TCP message in hexdump format
Figure 2: XWorm raw TCP message in hexdump format

Once the mutex and the communication key are identified you can use this script to deobfuscate the RAT communication.

python 83 lines
 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)
Code Snippet 2: Python script to decrypt XWorm communication

To dump the XWorm messages in cleartext from a PCAP, use the following command:

bash 3 lines
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.

output 5 lines
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
	...
Code Snippet 3: Output of XWorm PCAP decryption by the Python script

XWorm plugins#

As introduced, XWorm has the capability to download and execute plugins. This mechanism is simple, here is its workflow:

  1. The C2 send available plugins: plugin<Xwormmm>Id of the plugin<Xwormmm>
  2. The RAT ask the C2 to send the plugin: sendPlugin<Xwormmm>Id of the plugin
  3. 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.