← 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 FCTJQlq

As before, this class does not appear to rely on other classes, so it is a good candidate for analysis. It contains several encoded strings, which can be renamed as before.

Obfuscated NameRenamed asContent
ds9rgn5MhzCuV7xsVI8tGooglePasteUrl%PasteUrl%
c54QTcBcmf6rGgyNXjL6GooglewindowsUpdateURLhttps://windowsupdate.microsoft.com
yKv0YAEQkb99PUiIYS7DGooglewindowsDefenderURLhttps://winatp-gw-cus.microsoft.com
ls0B3Lof8k5bwCv19DrTGooglemicrosoftWatsonURLhttps://watson.microsoft.com
7ksKGxclxqpEMHkGktxNGooglemsEdgeURLhttps://msedge.api.cdp.microsoft.com
LZhAGxcMZQ9U1O5AAPv5GooglewindowsFwLinkURLhttps://go.microsoft.com/fwlink/
9Dhiv0ZpRphfFeHIDrEIGooglemicrosoftActivationURLhttps://activation.sls.microsoft.com
1yGgUQDMNWiX2IFEFpmLGoogleXSXSXSXSXSXSX
N3fGJvjHrDlZqOhGoH9JGoogleViolet<Violet>
ofuoTiOcboEgTpGyTz7ZGoogleUSBexeUSB.exe
7vUJbT6N0zPxQdW88rm0GooglerandomStringQakjxWa1r8Oh6UTB
gH6CzFFBIjnoaKcsUlMzGooglemutexVarNone
MAMc9HZf28Z1NGiZZSK5GoogleboolVarNone
N4FgM0WGB4NnemOgvt3rGooglecurrentProcessNameThe output of Process.GetCurrentProcess().MainModule.FileName

The other strings in the module are base64 encoded, but they are also passed as an argument to the method fZ8aUVb31M2Cahmhu5ZI:

Core.FCTJQlq
public static string 8owwMuJjqLlo9pPUhCrsGoogle = Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String("WkVkT01XWklXbkpSU0ZKdlpFaDRkMkZSUFQwPQ=="))));
// Token: 0x0400001A RID: 26
public static string DQSEYDpOuwltw1eNOARKGoogle = FCTJQlq.fZ8aUVb31M2Cahmhu5ZI(FCTJQlq.8owwMuJjqLlo9pPUhCrsGoogle);

Looking at that method, the code is:

Core.FCTJQlq
public static string fZ8aUVb31M2Cahmhu5ZI(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());
}
}

It starts by creating a variable called string, which is the double decoded value of UlZCRlVrZFpiZz09. It then decodes the argument it received, and for each of the bytes in the decoded string it converts the byte to a number and XORs it with the integer value of the character at the index of the current iteration. For example, if the content of string is abcdef and the content of encryptedText is moise, then on the first iteration, where num is 0, it takes a, converts it to an integer (97) and XORs it with the integer of m (109). The result is added to list and num gets incremented by 1.

The instruction:

Core.FCTJQlq
num = (num + 1) % @string.Length;

This increments num while also keeping it inside the bounds of string. If the input string is 30 characters long while string is 10 characters long, at the 11th iteration there would be no characters left to extract from string, resulting in an index error. Using the modulo operator (%), num always grows up to the length of string and then restarts from 0.

To get the strings without executing the code, we can run them through the following script:

Core.FCTJQlq
import base64
import sys
def decrypt(encoded_constant: str) -> str:
key = base64.b64decode(base64.b64decode("UlZCRlVrZFpiZz09").decode("utf-8")).decode(
"utf-8"
)
encrypted_text = base64.b64decode(
base64.b64decode(encoded_constant).decode("utf-8")
).decode("utf-8")
encrypted_bytes = base64.b64decode(encrypted_text)
result = []
key_index = 0
for b in encrypted_bytes:
result.append(chr(b ^ ord(key[key_index])))
key_index = (key_index + 1) % len(key)
return "".join(result)
def main() -> None:
encoded_constant = sys.argv[1]
result = decrypt(encoded_constant)
print(result)
if __name__ == "__main__":
main()

The script is a direct translation of fZ8aUVb31M2Cahmhu5ZI, with the two decoding steps of the field initializer folded into it so that a base64 literal can be copied straight out of the decompiled source and passed on the command line.

decrypt starts by rebuilding the key: UlZCRlVrZFpiZz09 decoded twice from base64 gives EPERGYn, the seven character key used for every constant in the class. The argument then goes through the same two decodes, which is what the field initializer does before calling the method, followed by a third one, which is the Convert.FromBase64String(encryptedText) inside the method itself. Three decodes in total, and only then is there anything to XOR.

The loop is the foreach from the C#: every byte is XORed with the key character at key_index, and key_index = (key_index + 1) % len(key) is the same wrap-around described above, so a seven byte key covers an input of any length. Running it against the first constant of the table returns the IP address:

$ python3 string_decryptor.py WkVkT01XWklXbkpSU0ZKdlpFaDRkMkZSUFQwPQ==
130.12.181.70

This allows us to understand the content of some variables:

Obfuscated NameRenamed asContent
8owwMuJjqLlo9pPUhCrsGoogleEncryptedIpAddressEncrypted value of 130.12.181.70
DQSEYDpOuwltw1eNOARKGoogleIpAddress130.12.181.70
ztkMDEbqIOb4pkOR5r8JGoogleEncryptedPortEncrypted value of 7000
Fuou0srIKBFgkhtK6GATGooglePort7000

The assumption that 7000 is a port number is based on the previous variable being an IP address; if it turns out not to be used as a port number, it can be renamed.

So for now the function has been renamed to stringDecryptor and the class to EncodedConstants.

The longer strings do not appear to be passed to the stringDecryptor function. Decoding them with Base64 twice returns another very long string that does not look like Base64 either. For now, they can be skipped while their usage is investigated.