- Malware Analysis
- Threat Intelligence
- Reverse Engineering
- Incident Response
Evolution RAT: Reverse Engineering an Undocumented HVNC Session-Hijacking Platform
Static reconstruction of a five-stage VBScript chain uncovered Evolution RAT, an undocumented x86-64 HVNC platform that clones live browser profiles, synthesizes Chrome-compatible device fingerprints, and proxies traffic through the victim. We document its 59-command cleartext C2 protocol and build-invariant file and network signatures.
Giovanni Braccini
Researcher at Beelzebub Labs
TL;DR
- A urlscan.io hunt for directly served
.vbsfiles returned three results worldwide. Only one remained online long enough to recover: a 3.3 MB script embedding the complete chain to an undocumented x86-64 payload - The five-stage chain moves from obfuscated VBScript through generated PowerShell, AES-256-ECB, a custom Base85 alphabet, a reflectively loaded .NET injector, and process hollowing into
AppLaunch.exe. For persistence, the dropper hides its own reversed source inside a fake Windows Security XHTML document - A PDB path inside a nested PE, independently confirmed by an
Overlord HVNC DLL Loadedbanner, names the family Evolution RAT. Its builder-patched.cfgmarkers provide a host-independent file fingerprint; its fixed cleartext hello frame provides a build-independent network signature - The payload exposes 59 commands for interactive access, including HVNC, remote input, file and process control, keylogging, injection, connection termination, and a reverse proxy. Distribution and C2 shared the same Colombian IP at collection time
- The defining capability is session identity theft. Evolution RAT clones a live browser profile, removes its singleton locks, launches the clone in a hidden desktop, and formats 35 host attributes exactly as Chrome would expose them to a fingerprinting script. The reverse proxy adds the victim’s network origin to the stolen cookie and device identity
- Caronte mapped the final payload, extracted the nested PDB metadata, and recovered the
.cfgC2. The script chain itself was unrolled manually; the closing section separates those contributions explicitly
Key Findings at a Glance
| Category | Details |
|---|---|
| Research contribution | First public technical documentation of Evolution RAT, named from a PDB path inside a nested PE and independently confirmed by the Overlord HVNC DLL Loaded banner |
| Delivery chain | 3.3 MB VBS → generated PowerShell → AES-256-ECB loader → custom Base85 → reflective .NET injector → x86-64 payload hollowed into AppLaunch.exe |
| Persistence | Reversed VBScript embedded in a fake XHTML Windows Security document, restored and executed by an XOR-obfuscated Startup loader; a second path installs WindowsDefender.exe in Startup |
| Session hijacking | Live browser-profile cloning, lock-file removal, MinHook path redirection, Chrome App-Bound Encryption handling, and 35 browser-compatible fingerprint fields |
| Command and control | 59 commands over an unencrypted framed TCP protocol; serversniperxx.duckdns.org:4577 shared its IP with the distribution host at collection time |
| Durable detection | Builder markers ##CFG_START## / ##CFG_END#### in the .cfg section and fixed client hello AD DE 02 00 10 02 00 00 |
| Public detection | At collection: 0/4 public-source hits for the .NET injector and 1/4 for the x86-64 payload |
Attack Chain Overview

The chain is elaborate, but its purpose becomes visible only at the end. Each stage removes a different detection surface; once the payload is running, every major capability contributes one part of a reusable browser session: authentication state, a launchable profile, a matching device fingerprint, and the victim’s own network origin.
Discovery: Three Results Worldwide
One query returned three URLs. By the next morning, two no longer answered. The third opened into five execution stages, a hidden desktop, 59 commands, and a malware family whose internal names had never appeared in public reporting.
The sample did not arrive from a submission or a report. It came from a hunt built on a single observation: URLs that serve a script payload directly, with no landing page and no redirect chain, are rare enough that they can be enumerated globally.
The urlscan.io query is trivial:
task.url:".vbs" AND date:>now-24h
For that window it returned three results across the entire platform:
http://asegurar2026.duckdns.org/sostener2.vbs
http://172.245.214.91/fridaexploit1.vbs
http://64.89.160.17/System1.vbs
At that volume the usual triage problem inverts: there is no signal-to-noise ratio to manage, because there is almost no noise. Each result can be retrieved and examined individually in the time normally spent writing a filter.
Two of the three were gone within a day. 172.245.214.91 and 64.89.160.17 had both stopped answering by the following morning, before either could be characterised, and nothing in this report describes them. That is the honest shape of this kind of hunting: the query is cheap and the window is narrow, and most of what it surfaces expires before it can be examined. The one that was still serving was asegurar2026.duckdns.org, and it is the only one this report is about.
The host serves the same payload under at least two names, sostener2.vbs and envifa.vbs, and the two files are byte-identical (SHA256 c8f229c7843f5fec053c04fb78b2ed045c029671412d74a07ce40608eeef1797). The bare IP answers as well: http://181.237.42.61/envifa.vbs returns 200 OK with the same hash, meaning the DuckDNS name is a convenience rather than a requirement, and takedown of the dynamic DNS entry alone would not remove the payload.
The server banner is the detail worth pausing on:
Server: Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.0.30
Apache on Windows, with PHP 8.0, serving a .vbs from the document root of a Colombian address. The combination is atypical of commercial hosting and consistent with a WAMP-style machine exposed to the internet and repurposed as a distribution point, although the server’s ownership could not be established from the available evidence.
The naming is Spanish throughout: asegurar (to secure, to insure), sostener (to hold, to sustain). Together with the Colombian address, that suggests a LATAM operational context, but it does not establish the victims’ geography or the operator’s intended targets.
The Dropper: One Base64 Stream, Split 343 Ways
sostener2.vbs is 3,346,929 bytes, and it is worth being precise about where those bytes go, because the intuitive answer is wrong.
The file is not mostly padding. It is mostly payload. A single base64 stream of 3,308,332 characters, 98.9% of the file, is the AES-encrypted next stage carried inline. Everything else, the logic, the persistence routine, the decoy functions, fits in the remaining 38 KB. There is no dropper that downloads anything: the entire chain up to the x86-64 binary ships in this one text file.
Three obfuscation techniques sit on top of that stream:
- XOR 0x99 on strings. Every meaningful literal is stored as a pipe-delimited array of decimal byte values, XORed with a single byte and reassembled at runtime by one helper function. Static string extraction against the file yields nothing usable: no API name, no path, no URL is present in readable form.
- 343 literals, reassembled out of order. The base64 stream is not one literal. It is split across 343
Dimassignments and concatenated intov0x86fadby 343 separate statements, in an order that matches the declaration order at zero of 343 positions. Reading the file top to bottom and joining the literals as they appear produces a string that decodes to nothing. Recovering the payload requires resolving the concatenation sequence, not just grepping for base64. - Decoy code. 34 function and sub definitions, most of which compute a value and discard it, interleaved with 170 lines of the form
If 0 Then Randomize Timer. The condition is constant-false and the statement never executes.
The decoy code is often described as volumetric padding, and here it is not: those 170 lines total roughly 4 KB, or 0.12% of the file. They do not exhaust a scanner’s byte budget. What they do is defeat the eye and the naive parser. A reader skimming for control flow has to discard nine tenths of the visible statements, and a tool that indexes function definitions gets 34 names that lead nowhere.
Whatever cost this file imposes on a scanner comes from the payload, not from the padding. Removing every decoy line would leave a 3.34 MB script. The size is the malware it carries.
Persistence: A Fake XHTML Page That Contains the Malware Backwards
The persistence mechanism is the most distinctive part of the dropper, and it is worth describing precisely because it defeats a category of detection rather than a specific product.
The script first checks whether it is already running from the Startup folder, and installs persistence only if it is not. If it is not, it reads its own source with WScript.ScriptFullName, reverses it character by character with StrReverse, and embeds the reversed text inside a document that presents as XHTML:
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Windows Security</title></head>
<body>
<div id="ms-2EDe7CMv96" data-key="8MxUjqEtBpLuwtsg">
<!--2EDe7CMv96-->
[reversed VBScript source]
<!--/2EDe7CMv96-->
</div></body></html>
That file is written to two locations:
%APPDATA%\Microsoft\manifest_ckCGj9.xhtml
C:\Users\Public\manifest_ckCGj9.xhtml
A separate loader is dropped into the Startup folder as CfgShieldckCGj9.vbs, headed with three comment lines designed for a human reading the file:
' Windows SmartScreen Filter Config
' Version 4.8.494 - Microsoft Corporation
' (c) 2026 Microsoft. All rights reserved.
The loader uses XOR 0xCD for its own strings, a different key from the dropper’s 0x99, so a decoder written for one does not read the other.
Once decoded, the routine expands %APPDATA% and looks for \Microsoft\manifest_ckCGj9.xhtml, falling back to C:\Users\Public\manifest_ckCGj9.xhtml. It validates the file through the literal data-key="8MxUjqEtBpLuwtsg", extracts the text between the two 2EDe7CMv96 comment markers, reverses it to restore the original source, strips a leading Option Explicit, and executes it with ExecuteGlobal.
Three further decoded strings, Microsoft.Update.Session, Win32_OperatingSystem and root\SecurityCenter2, are declared in the loader and never used. They are the same class of decoy as the dropper’s dead functions, placed where a hurried analyst would read them as environment checks.
The consequence for detection is direct. The artifact that survives reboot is not recognisable as a script: it has an .xhtml extension, a <title> claiming to be a Windows security component, and a body containing VBScript written backwards.
No signature for the original dropper matches that body because every string, API name, and keyword is reversed. The only persistent file with recognisable script syntax is the small Startup loader, whose own strings are XOR-encoded.
The same six-character token, ckCGj9, appears in both the XHTML filename and the loader filename, tying the two artifacts together on a host during triage.
Stages Two Through Five: AES, a Custom Base85, and Hollowing
The dropper does not decrypt anything itself. It generates a second script and hands the work to PowerShell.
Concretely, the VBScript builds a short PowerShell program in memory, writes it through ADODB.Stream as UTF-8 without a byte order mark to %TEMP%\<random hex>.ps1, and runs it:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<%TEMP%\...ps1>"
The generated second stage is small. Its only job is to base64-decode the 343-literal blob, decrypt it with AES-256 in ECB mode with PKCS7 padding, and write the plaintext to disk.
Every type and method name is assembled from string fragments ('System'+'.Con'+'vert', ('From'+'Base64S'+'trin'+'g'), ('Create'+'Decryp'+'tor')) and reached by reflection, leaving no complete .NET identifier for a scanner to match. The key is the one literal it does not hide:
Be1OluQXuNlV3c/vviqtY1xEW70mAk13+uxHyLqeTZ8=
which is 32 bytes, 05ed4e96e417b8d955ddcfefbe2aad635c445bbd26024d77faec47c8ba9e4d9f.
The 2,481,247-byte plaintext is itself a PowerShell script. It is written to %TEMP%\<random hex><random hex>\ClJdZcnG.ps1 and invoked with the call operator rather than through a second powershell.exe launch. The third stage therefore runs inside the process that decrypted it and never appears as a distinct command line.
After three seconds, the generated script deletes that file and its directory; the VBScript deletes the second stage behind it. The chain removes its intermediate artifacts, leaving the XHTML persistence and whatever the final payload writes.
The reconstruction is exact. Resolving the 343 concatenations, base64-decoding the result, and decrypting with that key yields 2,481,247 bytes matching SHA256 a49c01a5cc1c06be5f2e6cbd0f63554c7755e45f3931c08541da643014c8c9e5 byte for byte.
The third stage carries the binaries as a Base85-encoded blob, but not standard Base85. The alphabet is custom:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~
Digits first, then upper case, then lower case, then a punctuation tail that excludes both quote characters and the backslash, which is what lets the blob sit inside a PowerShell here-string without escaping. The group size, the radix and the short-group padding are conventional; only the character-to-value mapping is not. It matches neither Ascii85 nor RFC 1924’s Z85 set, which is enough: standard decoders produce garbage, and unpackers that try known encodings in sequence find no match. The cost to the operator is roughly forty lines of decode logic carried in the script itself; the cost to an analyst is that the blob does not respond to any off-the-shelf tool.
Decoding yields the x86-64 payload. A second, smaller PE travels in the same script as a plain base64 string:
| File | Size | Role |
|---|---|---|
| .NET injector | 9,728 B | base64 in the script, loaded in-memory via Reflection.Assembly::Load; type yMOxml.nRCN06Y, method C3FrcKQp |
| x86-64 payload | 1,972,736 B | the custom Base85 blob; Evolution RAT itself |
The injector’s entry method takes exactly two arguments: the payload bytes and the target process path. There is no configuration, no C2, no options. It performs one operation, process hollowing, into AppLaunch.exe, picking the path by pointer width:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\AppLaunch.exe (64-bit host)
C:\Windows\Microsoft.NET\Framework\v4.0.30319\AppLaunch.exe (32-bit host)
AppLaunch.exe is a signed Microsoft binary shipped with the .NET Framework. It is a legitimate host process that exists on essentially every Windows install, and a running instance of it is not anomalous on its face.
The injector is worth one more paragraph, because its import surface is deliberately uninformative. Its only declared P/Invokes are GetModuleHandleA, GetProcAddress, VirtualAlloc, CreateProcessA and CloseHandle, all from kernel32. Nothing in the metadata names an injection primitive. The real API set is stored as strings XORed with 0x2F and resolved at runtime out of ntdll.dll, whose name is encoded the same way:
a[zABN_yFJX`I|JL[F@A -> NtUnmapViewOfSection
a[nCC@LN[JyF][ZNCbJB@]V -> NtAllocateVirtualMemory
a[x]F[JyF][ZNCbJB@]V -> NtWriteVirtualMemory
a[hJ[l@A[JW[{G]JNK -> NtGetContextThread
a[|J[l@A[JW[{G]JNK -> NtSetContextThread
a[}J\ZBJ{G]JNK -> NtResumeThread
41 5B 4B 43 43 01 4B 43 43 -> ntdll.dll
Each is turned into a callable through Marshal.GetDelegateForFunctionPointer. This is native-API hollowing rather than the Win32 sequence: no VirtualAllocEx, no WriteProcessMemory, no SetThreadContext. Detection logic that keys on the Win32 names, whether in the import table or in a hook set placed at that layer, sees this assembly create a process and nothing else.
Naming the Family: A PDB Path Inside a Nested PE
The x86-64 payload has no PDB path of its own. Its debug directory contains a single POGO record, the profile-guided optimization section layout Microsoft’s linker emits, and nothing else. Read the outer binary’s debug information and the family is anonymous.
The name is one level deeper. At offset 1810200 inside the payload’s resource section sits another complete PE file, and that one was not built with its debug path stripped:
C:\Users\USER\Desktop\Evolution RAT\Overlord-Server\dist-clients\HVNCInjection.pdb
GUID 59965EFB-F602-42DC-94D0-FCF15841EFC1 age=1
Three things are legible in that path. The project is called Evolution RAT. Its server-side component is called Overlord-Server, and the client binaries are produced into a dist-clients directory, the layout of a builder that compiles per-operator clients from a common server project. And the nested module is HVNCInjection, which names the payload’s principal capability before a single function is decompiled.
The path also begins with C:\Users\USER\Desktop, so the project was built from a desktop directory on an account named USER. The debug path survived in the nested module and not in the outer binary, which is why the family name is invisible to any tool that reads only the top-level PE.
The nested module is a 64-bit DLL whose section table ends at 156,160 bytes from that offset, and its RSDS debug record sits at file offset 1944736 of the outer payload. Its own strings confirm the name a second time and independently of the PDB: the module writes the banner Overlord HVNC DLL Loaded into %TEMP%\crashlogovd.log. Two artifacts, produced by different parts of the build, agree on Overlord.
Searches for Evolution RAT, Overlord-Server, SniperLogs and HVNCInjection return no public documentation: no vendor report, no sandbox writeup, no malware repository entry, no forum listing. Queries surface only unrelated families that happen to implement HVNC. The technique set is well established. This particular implementation of it is not described anywhere in the public record.
The C2 Is in Cleartext, and That Is a Design Decision
The payload stores its configuration in a dedicated PE section named .cfg, virtual size 163 bytes at RVA 0x1A0000, file offset 0x193600, unencrypted, with a fixed layout. The whole section, byte for byte:
offset len contents
0 13 "##CFG_START##"
13 3 zero padding, aligning the next field to 16
16 128 host, null-padded: "serversniperxx.duckdns.org"
144 2 port, uint16 LE: E1 11 = 4577 (0x11E1)
146 1 zero padding
147 13 "##CFG_END####"
160 3 zero padding
The cleartext storage is not an oversight. The markers explain it: ##CFG_START## and ##CFG_END#### are what the builder searches for in the compiled binary in order to patch in the operator’s host and port. The section must be plaintext and fixed-width because a build tool with no compiler access is writing into it after the fact. The 128-byte null-padded host field is the same constraint, an in-place overwrite that cannot change the file’s size or layout, and the alignment padding around the fields is what lets a patcher seek to a constant offset instead of parsing.
This makes the markers a family-level fingerprint. They are invariant across every build the toolkit produces, regardless of which C2 an operator patches in, which is why the YARA rule below anchors on the markers and not on serversniperxx.duckdns.org. The host and port rotate. The section structure is the product.
serversniperxx.duckdns.org resolves to 181.237.42.61, the same address that serves sostener2.vbs and envifa.vbs. Distribution runs on port 80 through the Windows Apache instance; command and control runs on 4577 on the same machine. Port 4577 accepts TCP connections and does not answer HTTP, consistent with the framed binary protocol the client implements, reconstructed later in this report.
The internal naming is consistent across the whole toolkit: the C2 hostname is serversniperxx, the per-victim identifier format is SniperX_%d, and the keylog directory is C:\ProgramData\SniperLogs. The same root runs from the operator’s infrastructure down to a directory name on the victim’s disk.
Capabilities: HVNC, and a Hidden Desktop That Screenshots Cannot See
Everything in this section was established statically, from the payload binary.
The core module is HVNC. modules\hvnc_injection.dll is loaded reflectively through a ReflectiveLoader export, never touching disk as a loadable image. HVNC, hidden virtual network computing, creates a second Windows desktop that the victim cannot see and the attacker drives interactively. Applications launched on that desktop run in the victim’s own session, with the victim’s own tokens and network position, while the visible desktop shows nothing.
Around that core:
GhostStandby. A transparent window created withSetWindowDisplayAffinity(0x11), the flag combination that excludes a window from screen capture. Anything the operator places behind it is absent from screenshots, screen recordings, and remote-support sessions. This is the same API legitimate DRM and password managers use to keep content out of captures, applied to keep operator activity out of them.winlogon.exeimpersonation.SeDebugPrivilegeis acquired, thenDuplicateTokenExandImpersonateLoggedOnUseragainstwinlogon.exe, yielding a SYSTEM-level security context.- Anti-analysis.
IsDebuggerPresent,SetUnhandledExceptionFilter, and theNtSuspendProcess/NtResumeProcesspair. - Browser thread suspension. Browser threads are suspended before the SQLite credential databases are read, and resumed afterwards, so the databases are read in a consistent state and without contending for the browser’s own file handles.
- Defender exclusions. The command prefixes
powershell -WindowStyle Hidden -Command "Add-MpPreference -ExclusionPath 'and the matching-ExclusionProcessvariant are both present as string constants, adding its own paths and process names to Defender’s exclusion list rather than attempting to disable the service. - Second persistence path. The payload copies itself into
Start Menu\Programs\StartupasWindowsDefender.exe, independent of the VBScript persistence established three stages earlier. It also copies itself assvchost.exe. - Capture. Screen capture through
BitBltand GDI+ encoded to JPEG; Media Foundation (MF.dll,MFPlat.DLL,MFReadWrite.dll) for webcam and microphone. - Keylogging.
WH_KEYBOARD_LLandWH_MOUSE_LLhooks writing toC:\ProgramData\SniperLogs\keylog_%04d%02d%02d_%02d%02d%02d.txt, with a small log-management command set over the C2 channel:CONTENT|,DELETE,DELETED,EMPTY. - Synchronization primitives. Mutexes
hvnc:ready,hvnc:launch_ok,hvnc:launch_fail,hvnc_watchdog:stuck,Local\hvnc_rdi_,SingletonSocket. The presence of a watchdog with astuckstate indicates the HVNC session is expected to be long-lived and monitored, not fired once.
Network Control
Three of the payload’s network capabilities go beyond what a stealer needs:
- Arbitrary TCP connection termination via
SetTcpEntry, exposed at command0xE3. The command payload is 12 bytes: local IP, local port, remote IP, remote port. The operator can kill any single connection on the host. - Connection enumeration, command
0xE0, reported as JSON, one object per connection:{"pid":%u,"process":"%s","local_ip":"%s","local_port":%u,"remote_ip":"%s","remote_port":%u,"state":"%s"}. - System proxy hijack, writing
127.0.0.1:1intoSoftware\Microsoft\Windows\CurrentVersion\Internet Settings. Port 1 on loopback is not a proxy; nothing listens there. Setting it removes the victim’s internet access entirely for every application that honors the system proxy.
Read together, these three form a containment toolkit. Connection enumeration identifies which processes are talking to what. SetTcpEntry terminates a specific session. The dead-proxy setting cuts the host off wholesale. A stealer that has already exfiltrated has no use for any of it. An operator working a live interactive session, who needs a specific connection dropped while keeping their own channel open, does.
A fourth network capability, a reverse proxy at command 0xB0, is covered with the C2 command set below.
Credential Access: Twenty Browsers, and Chrome’s Newest Protection
The payload targets roughly twenty browsers, covering effectively the entire Chromium ecosystem plus Firefox: Chrome, Edge, Brave, Opera, OperaGX, Vivaldi, Chromium, Yandex, CentBrowser, Iridium, CocCoc, Torch, Comodo Dragon, Epic Privacy, Slimjet, URBrowser, 7Star, Amigo, Sputnik and Firefox.
The queries are the standard ones:
SELECT origin_url, username_value, password_value FROM logins;
SELECT host_key, name, encrypted_value, path, is_secure, is_httponly, expires_utc FROM cookies;
with history read from moz_places on Gecko profiles and urls on Chromium profiles.
Two details distinguish this from a commodity implementation.
First, the payload handles app_bound_encrypted_key. Chrome 127 introduced App-Bound Encryption, which binds the cookie and password encryption key to the specific application, so that copying the Local State file and calling CryptUnprotectData from another process no longer yields the key. This payload handles the app-bound key path explicitly, meaning it works against current Chrome rather than against the pre-127 scheme.
Second, it calls NCryptDecrypt with the key name "Google Chromekey1", the CNG key identifier in Chrome’s app-bound key flow. This is not a generic DPAPI wrapper; it is code written against the current protection scheme rather than against the one that existed before it.
The Pivot: Cloning the Session, Not Reading the Password
Up to this point, Evolution RAT can still be read as an unusually capable credential stealer. One function changes that interpretation: CloneProfile. Its purpose is not to extract another secret but to make the victim’s authenticated browser usable somewhere else.
The sequence:
- Locate the browser’s user data directory, then copy the profile to
hvnc_%08Xunder an attacker-controlled path. The copy runs in two passes, critical files first and the remainder afterwards, with a 60-second timeout and a progress report over the C2 channel. Alitemode skips the bulk directories that carry no session state:cache,code cache,service worker,blob_storage, the extension stores and the telemetry databases. - Delete the lock files from the copy:
SingletonLock,SingletonCookie,parent.lock,.parentlock. - Launch the browser against the cloned profile, inside the hidden HVNC desktop.
Step 1 has a complication the implementation takes seriously. A running browser holds its most interesting files open, and a plain copy fails on them. The code handles that case explicitly: it enables SeDebugPrivilege, enumerates system handles with NtQuerySystemInformation to find which processes hold the locked path, and duplicates the handle out of the locking process to read the file anyway. The status strings are unambiguous about the intent (Hijack: %d locking PIDs for %s, Hijack: %llu handles, searching..., Hijack OK: %s (%lld bytes)). The victim does not need to close the browser for the profile to be taken.
Step 2 is the mechanism that makes the copy usable. Those four files are how Chromium and Gecko enforce a single browser instance per profile directory; with them present, a second launch against the same profile refuses to start or hands off to the running instance. Removing them from the copy makes the clone independently launchable while the victim’s own browser is still running on the original.
What launches is not a browser asking for credentials. It is a browser that is already logged in, to every site whose session cookie was in that profile. Session cookies are, by construction, proof that authentication already happened, including any second factor. There is no password prompt to fill, no OTP to intercept, no push notification for the victim to approve, because from the application’s perspective no new authentication is occurring. The second factor is not defeated. It was satisfied by the victim, and the cookie is the receipt.
To keep the browser on the clone, the payload installs MinHook hooks over the file API surface:
NtCreateFile NtQueryAttributesFile
NtOpenFile NtQueryFullAttributesFile
NtDeleteFile NtQueryDirectoryFile
NtSetInformationFile NtQueryDirectoryFileEx
CreateProcessW
Every path by which a browser process can open, inspect, enumerate or delete a file is intercepted, along with CreateProcessW so that child processes the browser spawns inherit the redirection. The browser believes it is using its normal profile. It is using the copy.
The Fingerprint Set: Thirty-Five Fields, and What They Are For
The client does not send these fields on connect. They are collected on demand: command 0x35 triggers the full collection pass, browser stealer included, and the result comes back as a JSON document with opcode 0x64. Thirty-five fields:
os_name os_version os_platform machine_name
user_name domain processor_count system_dir
screen_width screen_height screen_depth timezone
timezone_offset timezone_display language language_display
languages cpu_name cpu_id cpu_cores
cpu_threads gpu_name gpu_driver gpu_ram
ram_gb device_memory mac_address disk_serial
fonts fonts_count browsers chrome_version
user_agent webgl_vendor webgl_renderer
A host profile is standard in this class of malware: enough fields to sort victims by country, privilege level, and whether the machine is worth returning to. The client already sends one at connect time, a 528-byte fixed-field structure covered in the protocol section below. This is a second and much larger collection, and the excess is concentrated in a specific subset:
webgl_vendor · webgl_renderer · fonts · fonts_count · device_memory · languages · user_agent
That is not a system inventory. It is, field for field, the input set of browser fingerprinting, the technique anti-fraud platforms use to decide whether a session that presents valid credentials is actually coming from the device it claims to. WebGL vendor and renderer strings identify the GPU and driver as the browser reports them. The installed font list and its cardinality are among the highest-entropy signals available to a fingerprinting script. device_memory and languages correspond to navigator properties readable from JavaScript, not to anything an operating system exposes under those names.
The Values Are Formatted, Not Just Collected
The decisive detail is not which fields are collected. It is what the code does to them on the way out.
None of these values can be read from a browser by a native process, so the payload builds them from system sources: WMI, the registry, GDI. It then formats each one into the exact string a browser would have produced. The format strings are in the binary, adjacent to the field names they serve:
user_agent Mozilla/5.0 (Windows NT %lu.%lu; %s) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/%s Safari/537.36
webgl_vendor "Google Inc. (" + <adapter vendor> + ")"
webgl_renderer "ANGLE (" + <adapter name> + ", Direct3D11 vs_5_0 ps_5_0, D3D11)"
device_memory %.0f (integer gigabytes, as navigator.deviceMemory reports)
ram_gb %.1f (the same quantity, one decimal, for the operator)
languages <UI language> + ",en-US"
The GPU strings come from Win32_VideoController and are wrapped in Chrome’s ANGLE presentation. The Chrome version is read from Software\Google\Chrome\BLBeacon and spliced into the user agent template, with 131.0.6778.86 hardcoded as the fallback when the registry value is absent. Win64; x64 is substituted for the platform token. Physical memory is retrieved once and emitted twice, at %.1f precision under ram_gb and rounded to an integer under device_memory, because those are two different consumers: a human triaging victims, and a fingerprint comparison that expects the value navigator.deviceMemory would have returned.
That last pair settles the question of intent. Collecting RAM twice, in two precisions, under two names, is not something a system inventory does. It is something you do when one copy is for you and the other has to match what a website saw.
Read plainly: this is not a machine inventory that happens to overlap with fingerprinting. It is a fingerprint forgery kit, assembling from the host’s real hardware the values that host’s browser would have reported, in the syntax the reporting side uses, ready to be replayed from somewhere else.
Combine the three capabilities and the design intent resolves:
CloneProfilegives the operator the victim’s authenticated sessions.- The cookie theft gives the operator the tokens those sessions rest on.
- The 35-field fingerprint set gives the operator the device identity the fraud engine expects to see attached to those sessions.
The result is a replayed session that matches on the axes a risk engine evaluates from the client: correct cookie, correct user agent, correct language and timezone set, correct declared memory, correct GPU vendor and renderer, correct font inventory. The signals that normally flag account takeover, an unrecognized device, a locale that does not match the account’s history, a new browser fingerprint on a known profile, are the exact signals this collection is assembled to satisfy.
This is the analytical distinction that matters for defenders: the objective is not the credential. It is the session identity. Password rotation after this infection changes nothing about a session already cloned, and MFA re-enrollment matters more than a password reset does.
What Is Actually New Here
Session theft is not a new finding, and this report does not claim it as one. Cookie stealing has been commodity for years, adversary-in-the-middle phishing kits have been relaying live sessions past MFA at scale for longer than that, and profile cloning appears in more than one commodity stealer. Chrome’s App-Bound Encryption, shipped in Chrome 127 precisely to break the copy-the-Local-State approach, exists because that pressure was already well understood.
Three things in this sample sit past that line.
First, the collection is format-aware. Plenty of stealers grab a hardware inventory. This one knows what shape the receiving end expects and produces it, which only makes sense if the values are destined for a comparison against something a browser reported.
Second, the three capabilities are co-located and composed. Cookies, a launchable profile clone, a device fingerprint in browser syntax and a reverse proxy for network origin are usually split across a stealer, a loader, a proxyware family and whatever the buyer bolts on. Here they are one binary, driven by one operator, over one channel.
Third, the clone is taken without stopping the victim’s browser, through handle duplication out of the locking process. A copy that requires the browser closed is an opportunistic capability. A copy that works against a live browser is an operational one.
None of this requires novel technique. What it requires is having decided that the product is the session rather than the password, and then building every component to that specification.
The C2 Protocol: Eight Bytes of Header, and Nothing Encrypted
The capability set explains what the operator can reproduce. The protocol explains how every part of that operation is controlled, and exposes two durable detection opportunities in the process.
The protocol below was reconstructed in Ghidra from the payload binary. No traffic was exchanged with the C2, no sample was executed, and no capture was involved. Everything stated here comes from the code that builds the frames and the code that validates them.
Framing
Every message is a fixed 8-byte header followed by a raw payload:
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 2 | magic | 0xDEAD, little-endian, on the wire AD DE |
| 2 | 2 | opcode | little-endian |
| 4 | 4 | length | little-endian, capped at 0x1000000 (16 MiB) |
Exactly length bytes of payload follow. There is no closing marker, no checksum and no encryption layer. The transmit side is FUN_140039620; the receive side validates the same structure in FUN_140017050.
The magic is legible in the instruction stream. At 0x140039634 the bytes are B8 AD DE 00 00, which disassembles to mov eax, 0xDEAD: the constant loaded into the header before a frame goes out.
The Traffic Is Not Encrypted, and ChaCha20 Never Touches the Socket
Both directions were traced end to end. Outbound, FUN_140039620 builds the header and hands the buffer to FUN_140039380, a send-all loop. Inbound, FUN_140017050 validates the header and reads the body through FUN_140038590, a recv-all loop. No cryptographic primitive appears anywhere on either path, and the buffers reach and leave the socket intact.
That is worth stating precisely, because the binary does contain modern cryptography. FUN_140010710 is ChaCha20 as specified in RFC 8439. Its only caller is FUN_140019040, a ChaCha20-Poly1305 AEAD wrapper, whose only caller in turn is FUN_1400197a0, which decrypts Chrome’s App-Bound Encryption keys. The three keys hardcoded in .data:
B31C6E241AC846728DA9C1FAC4936651
E98F37D7F4E1FA433D19304DC2258042
CCF8A1CEC56605B8517552BA1A2D061C
belong to that path. They are credential-theft material, not transport keys.
One further artifact initially pointed in the wrong direction. The ChaCha20 sigma constant expand 32-byte k sits in .rdata at 0x14014E248, exactly where a constant scanner expects it and where its presence suggests a stream cipher wrapping the protocol.
It has zero cross-references. No RIP-relative displacement in .text resolves to that address, and no absolute pointer to it exists in the file. It is dead data left by the ChaCha implementation; the live cipher initialises its state from a separate copy on the Chrome key path. A crypto constant without a reference graph would therefore produce the wrong transport assessment.
This is the general failure mode: an assessment built from crypto constants or imports alone reads the binary backwards and concludes the channel is protected. Only the reference graph settles it. Command and control runs in cleartext, and a sensor positioned to see the flow can parse every frame in it.
The Handshake
getaddrinfo, thensocket, thenconnect.- Socket options are set before anything is sent:
TCP_NODELAY,SO_SNDBUF0x80000,SO_RCVBUF0x40000, a 30000 ms timeout, andSIO_KEEPALIVE_VALSwith a 400000 ms idle and a 5000 ms probe interval. - The client sends its first frame,
AD DE 02 00 10 02 00 00: magic, opcode0x02, length0x210. The 528 bytes that follow are a fixed-field structure built byFUN_14000e760:
| Offset | Size | Contents |
|---|---|---|
0x000 | 0x40 | HWID: ComputerName plus volume serial |
0x040 | 0x40 | ComputerName |
0x080 | 0x40 | UserName |
0x0C0 | 0x40 | OS version from RtlGetVersion |
0x100 | 0x10 | Architecture |
0x110 | 0x40 | unidentified |
0x150 | 0x80 | Foreground window title |
0x1D0 | 0x40 | reserved |
- The client then waits for a frame with opcode
0x03before it proceeds. - It starts a beacon carrying the active window title with opcode
0x90, once immediately and then on every change, at a 250 ms cadence. - It enters the main loop: receive header, validate, receive body, dispatch.
The 35-field JSON described in the previous section is not part of this sequence. The connect-time profile is the 528-byte fixed-field structure; the fingerprint document is produced only when the operator asks for it with command 0x35.
Fifty-Nine Commands
The dispatcher is FUN_140019c20. It rejects any value where opcode > 0xF9 and indexes a jump table at 0x14001B694. 59 opcodes have a handler. The dominant convention is that the response opcode is the command opcode plus one: 0x00 is answered with 0x01, 0x20 starts the screen stream and the frames come back as 0x21.
The most significant of them:
| Op | Capability |
|---|---|
0x00 | Ping, answered with 0x01 |
0x20 | Start screen stream, frames sent with opcode 0x21 |
0x23 | Remote mouse: action, x, y |
0x24 | Remote keyboard: down/up, virtual key |
0x33 | File staging and self-update |
0x35 | Full collection: browser stealer, 35-field JSON returned with opcode 0x64 |
0x38 | Process listing |
0x3A | Kill process by PID |
0x42 | Write to the remote shell’s stdin |
0x44 | File manager and drive listing |
0x4A | LockWorkStation, screen resolution change, ExitProcess |
0x4C | Delete a file or a directory recursively |
0x4D | Execution via ShellExecute |
0x80 | Microphone capture, 44.1 kHz, four buffers |
0xA8 | Stream quality and frame rate, clamped to 10..100 |
0xA9 | Write to the clipboard |
0xB0 | Reverse proxy: connection to an arbitrary host |
0xC7 / 0xC8 | BlockInput on and off |
0xCB | Registry persistence under HKCU Run, set or delete |
0xD0 | PE injection and hollowing |
0xE0 | TCP connection table |
0xE2 | Kill process by name |
0xE3 | Terminate a TCP connection via SetTcpEntry, 12-byte payload |
0xF7 | Retrieve keylog files |
The set is a complete interactive remote access surface, not a stealer’s command list. Screen, mouse, keyboard and input blocking cover the session; the file manager, process control, ShellExecute and PE injection cover the host; registry persistence and self-update cover the foothold.
0xB0: The Victim as an Exit Node
One opcode deserves separate attention. 0xB0 opens a connection to an operator-specified host and relays through it. That is a reverse proxy, and it is a different class of capability from everything else in the table: it does not read data off the host and it does not control the host. It makes the host a network egress point for the operator.
It also completes the pattern this report has been describing. A cloned profile supplies the authenticated session. The 35-field collection supplies the device identity that session is expected to present. 0xB0 supplies the third axis a risk engine can evaluate from the client side, the network origin, because traffic sent through it leaves from the victim’s own address rather than the operator’s. Cookie, device, network: three capabilities in one binary, each covering a different check.
What the Protocol Does Not Tell Us
Three gaps are worth stating rather than papering over:
- The payload of the
0x03acknowledgement is read from the socket, but no consumer of that buffer was identified. What the server sends back at that point is not established. - Nine opcodes have an identified handler and an unresolved capability:
0x30,0x31,0x48,0x49,0xA0,0xA1,0xC0,0xC4,0xF5. - The
0x40-byte field at offset0x110of the hello structure is not identified.
Network Signature
The first eight bytes a client sends are fixed: magic, opcode 0x02, length 0x210. Neither the host nor the port that the builder patches into the .cfg section changes them, so the hello is a build-invariant network artifact in the same way the ##CFG_START## markers are a build-invariant file artifact. Two rules follow directly:
alert tcp $HOME_NET any -> $EXTERNAL_NET any (
msg:"Evolution RAT C2 hello (magic 0xDEAD, opcode 0x02, 528B sysinfo)";
flow:established,to_server;
content:"|AD DE 02 00 10 02 00 00|"; depth:8; offset:0;
threshold:type limit,track by_src,count 1,seconds 300;
classtype:trojan-activity; sid:1000001; rev:1;)
alert tcp $HOME_NET any -> $EXTERNAL_NET any (
msg:"Evolution RAT active-window beacon (opcode 0x90)";
flow:established,to_server;
content:"|AD DE 90 00|"; depth:4; offset:0;
classtype:trojan-activity; sid:1000002; rev:1;)
The first fires once per source per five minutes on the initial check-in. The second catches an infection already established, since the window-title beacon runs for the life of the session and its four leading bytes are equally fixed.
As a secondary signal, the keepalive configuration is distinctive: a 400 second idle with a 5 second probe interval produces a long-lived TCP session that stays silent for minutes and then emits tight probe bursts. On a sustained connection to a non-standard port, that timing pattern is worth surfacing on its own, independent of content.
Defensive Priorities
Evolution RAT exposes durable signals at four different layers. Defenders do not need the campaign’s current DuckDNS names to detect the toolkit or contain its objective.
- File and persistence detection. Hunt for XHTML files containing large reversed text regions alongside a small Startup VBS loader. The paired
ckCGj9filenames, the2EDe7CMv96comment markers, anddata-key="8MxUjqEtBpLuwtsg"are campaign-specific;##CFG_START##and##CFG_END####identify the payload builder across configurations. - Execution telemetry. Correlate
wscript.exelaunching PowerShell with-ExecutionPolicy Bypass, transient.ps1files under randomly named%TEMP%directories, and an unexpectedAppLaunch.exechild or hollowed image. A laterWindowsDefender.exeunder the user’s Startup folder is a second high-confidence signal. - Network inspection. The cleartext protocol makes content detection practical. The fixed client hello
AD DE 02 00 10 02 00 00, theAD DE 90 00active-window beacon, and the 400-second/5-second keepalive pattern remain valid when the C2 hostname and port change. - Browser and identity response. Profile cloning means remediation must extend beyond malware removal and password rotation. Terminate active application sessions server-side, review account activity from the affected device and network, re-enrol MFA where appropriate, and assume browser cookies and device-fingerprint data were exposed.
- Containment sequencing. The operator can enumerate and terminate TCP connections, alter the system proxy, block input, and retain access through HVNC. Isolate the endpoint at the network control plane rather than relying on actions performed only inside the compromised desktop.
Public Detection at Collection
The two PE stages were checked against OTX, MalwareBazaar, ThreatFox and URLhaus:
| Sample | Public sources with a hit |
|---|---|
| .NET injector (9,728 B) | 0 of 4 |
| x86-64 payload (1,972,736 B) | 1 of 4 (OTX only) |
Caronte classified the payload as infostealer with hVNC with a high verdict from behavior alone, with no reliance on reputation data.
At the time of collection, 181.237.42.61 was answering on port 80 with the droppers and accepting connections on 4577. Both observations are dated 2026-08-13 and nothing here should be read as a statement about the host’s current state; the two sibling URLs from the same urlscan window were offline within a day, and this one is on dynamic DNS.
Indicators of Compromise
Stage hashes (SHA256):
Stage 1 VBScript dropper 3,346,929 B
c8f229c7843f5fec053c04fb78b2ed045c029671412d74a07ce40608eeef1797
served as sostener2.vbs and envifa.vbs, byte-identical
Stage 3 PowerShell loader 2,481,247 B
a49c01a5cc1c06be5f2e6cbd0f63554c7755e45f3931c08541da643014c8c9e5
= AES-256-ECB(base64_decode(343-literal blob)), PKCS7 stripped
Stage 4 .NET injector 9,728 B
5b5f3f49769493f6ed3de978d41bc4bb8dd286467becb4d25036f037462cb013
Stage 5 x86-64 payload (Evolution RAT) 1,972,736 B
7a3c619827557de9a3687daa137f772f40e1bba1a4ca32bac1b06557f42ce522
Nested HVNCInjection module carved from stage 5 offset 1810200 to EOF
6f6b10e5cce18005231da32a4344e764d7c1e4f22bcc1d78f264561966a1b46d
(162,536 B as carved; the module's section table ends at 156,160 B,
so this hash depends on the carve boundary, not on the module alone)
Stage 2 is generated at runtime by stage 1 and has no fixed hash.
Dropper:
Names: sostener2.vbs, envifa.vbs (byte-identical)
SHA256: c8f229c7843f5fec053c04fb78b2ed045c029671412d74a07ce40608eeef1797
Size: 3,346,929 bytes (98.9% of it is the base64 payload literal)
Type: Obfuscated VBScript. XOR 0x99 strings; payload split across 343
string literals concatenated in non-declaration order; 34 decoy
functions; 170 constant-false padding lines (~4 KB total)
Dropped / staged files:
%TEMP%\<hex>.ps1 stage 2, generated AES decryptor
(UTF-8 no BOM, deleted by stage 1)
%TEMP%\<hex><hex>\ClJdZcnG.ps1 stage 3, self-deleted after 3 s
(fixed basename "ClJdZcnG")
Launch: powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<stage 2>"
stage 3 is then invoked in-process with the call operator, so it
never appears as a separate command line
Distribution:
Host: asegurar2026.duckdns.org
IP: 181.237.42.61 (Colombia)
URLs: http://asegurar2026.duckdns.org/sostener2.vbs
http://asegurar2026.duckdns.org/envifa.vbs
http://181.237.42.61/envifa.vbs
Server: Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.0.30
(Apache on Windows; consistent with a compromised host)
C2:
Host: serversniperxx.duckdns.org
Port: 4577 (0x11E1)
IP: 181.237.42.61 <-- same machine as distribution
Protocol: framed binary over raw TCP, cleartext (does not speak HTTP)
Stored in PE section .cfg, RVA 0x1A0000, file offset 0x193600,
virtual size 163 bytes, cleartext:
+0 13 "##CFG_START##"
+13 3 zero padding
+16 128 host, null-padded
+144 2 port, uint16 LE (E1 11 = 4577)
+146 1 zero padding
+147 13 "##CFG_END####"
+160 3 zero padding
C2 protocol:
Frame: [magic uint16 LE 0xDEAD][opcode uint16 LE][length uint32 LE][payload]
no closing marker, no checksum, no encryption; length cap 0x1000000
Functions: FUN_140039620 (TX framing) -> FUN_140039380 (send_all)
FUN_140017050 (RX validation) -> FUN_140038590 (recv_all)
magic at 0x140039634: B8 AD DE 00 00 (mov eax, 0xDEAD)
Hello: AD DE 02 00 10 02 00 00 + 528-byte fixed-field struct (FUN_14000e760)
0x000 0x40 HWID (ComputerName + volume serial)
0x040 0x40 ComputerName
0x080 0x40 UserName
0x0C0 0x40 OS version (RtlGetVersion)
0x100 0x10 architecture
0x110 0x40 unidentified
0x150 0x80 foreground window title
0x1D0 0x40 reserved
Server ACK: opcode 0x03 (client waits for it before proceeding)
Beacon: opcode 0x90, active window title, immediately then on change (250 ms)
Socket: TCP_NODELAY, SO_SNDBUF 0x80000, SO_RCVBUF 0x40000, timeout 30000 ms
SIO_KEEPALIVE_VALS idle 400000 ms, interval 5000 ms
Dispatcher: FUN_140019c20, guard opcode > 0xF9, jump table 0x14001B694, 59 opcodes
response opcode = command opcode + 1 (dominant convention)
Notable opcodes:
0x00 ping (reply 0x01) 0x20 screen stream (frames 0x21)
0x23 remote mouse 0x24 remote keyboard
0x33 file staging/self-update 0x35 full collection (JSON via 0x64)
0x38 process list 0x3A kill process by PID
0x42 shell stdin write 0x44 file manager / drive listing
0x4A LockWorkStation, resolution change, ExitProcess
0x4C delete file/dir 0x4D ShellExecute
0x80 microphone (44.1 kHz) 0xA8 stream quality/FPS (clamp 10..100)
0xA9 clipboard write 0xB0 reverse proxy to arbitrary host
0xC7 / 0xC8 BlockInput on/off 0xCB HKCU Run persistence (set/delete)
0xD0 PE injection/hollowing 0xE0 TCP connection table
0xE2 kill process by name 0xE3 SetTcpEntry kill (12-byte payload)
0xF7 keylog file retrieval
Handler identified, capability unresolved:
0x30 0x31 0x48 0x49 0xA0 0xA1 0xC0 0xC4 0xF5
Malware identity:
Family: Evolution RAT
PDB: C:\Users\USER\Desktop\Evolution RAT\Overlord-Server\dist-clients\HVNCInjection.pdb
PDB GUID: 59965EFB-F602-42DC-94D0-FCF15841EFC1 age=1
Note: PDB is in a PE nested at offset 1810200 of the payload (inside the
.rsrc range, raw 0x193800..0x1E0200). RSDS record at offset 1944736.
The outer payload's debug directory has a single POGO record.
Second confirmation: "Overlord HVNC DLL Loaded" written to
%TEMP%\crashlogovd.log by the nested module
Client ID format: SniperX_%d
Crypto:
Stage 2: AES-256-ECB / PKCS7 (decrypts stage 3)
Key: Be1OluQXuNlV3c/vviqtY1xEW70mAk13+uxHyLqeTZ8= (base64)
= 05ed4e96e417b8d955ddcfefbe2aad635c445bbd26024d77faec47c8ba9e4d9f
Stage 3: Base85 with non-standard alphabet (carries stage 5);
stage 4 travels in the same script as plain base64
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~
Stage 4: API name strings XOR 0x2F, resolved from ntdll.dll at runtime
Payload: ChaCha20-Poly1305 (RFC 8439) at FUN_140010710 / FUN_140019040,
reached only from FUN_1400197a0 (Chrome App-Bound Encryption key
decryption). Not used for C2 traffic.
Decoy: "expand 32-byte k" at 0x14014E248 in .rdata has ZERO xrefs.
Dead data. Do not infer an encrypted C2 channel from it.
Hardcoded keys in .data:
B31C6E241AC846728DA9C1FAC4936651
E98F37D7F4E1FA433D19304DC2258042
CCF8A1CEC56605B8517552BA1A2D061C
Persistence:
%APPDATA%\Microsoft\manifest_ckCGj9.xhtml
C:\Users\Public\manifest_ckCGj9.xhtml
(fake XHTML: <title>Windows Security</title>,
container <div id="ms-2EDe7CMv96" data-key="8MxUjqEtBpLuwtsg">,
markers <!--2EDe7CMv96--> ... <!--/2EDe7CMv96-->,
body = StrReverse of the original VBScript)
Startup\CfgShieldckCGj9.vbs
(header: "Windows SmartScreen Filter Config" /
"Version 4.8.494 - Microsoft Corporation" /
"(c) 2026 Microsoft. All rights reserved."
XOR 0xCD strings, ExecuteGlobal;
unused decoy strings: Microsoft.Update.Session,
Win32_OperatingSystem, root\SecurityCenter2)
Installed only when the running script is not already in Startup
Start Menu\Programs\Startup\WindowsDefender.exe (payload self-copy)
Payload also copies itself as svchost.exe
Injection:
.NET injector type/method: yMOxml.nRCN06Y :: C3FrcKQp
Loaded via: Reflection.Assembly::Load
Entry args: (payload bytes, target process path)
Hollowing target: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\AppLaunch.exe
(Framework\ instead of Framework64\ on 32-bit)
Declared P/Invokes (kernel32, all the metadata reveals):
GetModuleHandleA, GetProcAddress, VirtualAlloc,
CreateProcessA, CloseHandle
Real API set, strings XOR 0x2F, resolved from ntdll.dll at runtime:
NtUnmapViewOfSection, NtAllocateVirtualMemory, NtWriteVirtualMemory,
NtGetContextThread, NtSetContextThread, NtResumeThread
(bound via Marshal.GetDelegateForFunctionPointer)
NOTE: the Win32 names VirtualAllocEx / WriteProcessMemory /
SetThreadContext / ResumeThread do NOT appear in this injector.
Filesystem (payload):
C:\ProgramData\SniperLogs\
C:\ProgramData\SniperLogs\keylog_%04d%02d%02d_%02d%02d%02d.txt
modules\hvnc_injection.dll (reflectively loaded, ReflectiveLoader export)
Mutexes / synchronization:
hvnc:ready hvnc:launch_ok hvnc:launch_fail
hvnc_watchdog:stuck Local\hvnc_rdi_ SingletonSocket
Registry:
Software\Microsoft\Windows\CurrentVersion\Internet Settings
ProxyServer set to 127.0.0.1:1 (dead proxy; severs the host's internet access)
Command execution:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "<%TEMP%\<hex>.ps1>"
powershell -WindowStyle Hidden -Command "Add-MpPreference -ExclusionPath '
powershell -WindowStyle Hidden -Command "Add-MpPreference -ExclusionProcess '
cmd.exe /Q (remote shell host)
taskkill /IM <browser> /F (before profile clone)
Hooked APIs (MinHook, browser profile redirection):
NtCreateFile NtOpenFile NtDeleteFile NtSetInformationFile
NtQueryAttributesFile NtQueryFullAttributesFile
NtQueryDirectoryFile NtQueryDirectoryFileEx
CreateProcessW
Browser targets (~20):
Chrome, Edge, Brave, Opera, OperaGX, Vivaldi, Chromium, Yandex,
CentBrowser, Iridium, CocCoc, Torch, Comodo Dragon, Epic Privacy,
Slimjet, URBrowser, 7Star, Amigo, Sputnik, Firefox
Profile-clone artifacts:
Clone directory name: hvnc_%08X
Deleted from the copy: SingletonLock SingletonCookie
parent.lock .parentlock
Locked files are read by duplicating handles out of the locking process
(SeDebugPrivilege + NtQuerySystemInformation handle enumeration).
Log strings: "Hijack: %d locking PIDs for %s",
"Hijack: %llu handles, searching...",
"Hijack OK: %s (%lld bytes)",
"CloneProfile: TIMEOUT after 60s"
lite mode skips: cache, code cache, service worker, blob_storage,
jumplisterrors, extensions, extension state/rules/scripts,
local/sync extension settings, managed extension storage,
segmentation_platform, commerce_local_db,
optimization_guide_prediction_model_downloads
Chrome App-Bound Encryption:
app_bound_encrypted_key handled
NCryptDecrypt with key name "Google Chromekey1"
Fingerprint fields (35), returned with opcode 0x64 on command 0x35:
os_name os_version os_platform machine_name user_name domain
processor_count system_dir screen_width screen_height screen_depth
timezone timezone_offset timezone_display language language_display
languages cpu_name cpu_id cpu_cores cpu_threads gpu_name gpu_driver
gpu_ram ram_gb device_memory mac_address disk_serial fonts fonts_count
browsers chrome_version user_agent webgl_vendor webgl_renderer
Browser-facing values are synthesized into Chrome's own formats:
user_agent Mozilla/5.0 (Windows NT %lu.%lu; %s) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/%s Safari/537.36
platform token "Win64; x64"
chrome_version from HKCU Software\Google\Chrome\BLBeacon "version",
fallback literal 131.0.6778.86
webgl_vendor "Google Inc. (" + <Win32_VideoController vendor> + ")"
webgl_renderer "ANGLE (" + <adapter> + ", Direct3D11 vs_5_0 ps_5_0, D3D11)"
device_memory %.0f from Win32_OperatingSystem TotalVisibleMemorySize
ram_gb %.1f same source, operator-facing precision
languages <UI language> + ",en-US"
rule Evolution_RAT_VBS_Dropper_XHTML_Persistence {
meta:
description = "VBScript dropper for Evolution RAT: stores its own reversed source inside a fake XHTML file and reloads it from a Startup loader"
author = "Beelzebub Research"
date = "2026-08-13"
reference = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
strings:
$marker = "2EDe7CMv96" ascii
$manifest = "manifest_ckCGj9" ascii
$loader = "CfgShieldckCGj9" ascii
$datakey = "8MxUjqEtBpLuwtsg" ascii
$title = "<title>Windows Security</title>" ascii
$hdr = "Windows SmartScreen Filter Config" ascii
$f1 = "StrReverse" ascii nocase
$f2 = "ExecuteGlobal" ascii nocase
$pad = "If 0 Then Randomize Timer" ascii nocase
condition:
(any of ($marker, $manifest, $loader, $datakey))
and 2 of ($title, $hdr, $f1, $f2, $pad)
}
rule Evolution_RAT_PowerShell_Stage {
meta:
description = "Evolution RAT stage 3/4: AES-256-ECB decrypted PowerShell loader carrying a Base85 blob, reflective .NET injector load and AppLaunch.exe hollowing"
author = "Beelzebub Research"
date = "2026-08-13"
reference = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
strings:
$key = "Be1OluQXuNlV3c/vviqtY1xEW70mAk13+uxHyLqeTZ8=" ascii wide
$b85 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~" ascii wide
$dec = "Decode-Base85" ascii wide
$tgt = "AppLaunch.exe" ascii wide nocase
$refl = "Reflection.Assembly" ascii wide
$type = "yMOxml.nRCN06Y" ascii wide
$meth = "C3FrcKQp" ascii wide
$mode = "ECB" ascii wide
$pad = "PKCS7" ascii wide
$ps = "-ExecutionPolicy Bypass" ascii wide nocase
condition:
// $b85 is the strongest anchor: the alphabet is a toolkit constant and
// survives rebuilds, while $key and the type/method names may not.
$b85
or $key
or (3 of ($dec, $tgt, $refl, $type, $meth, $mode, $pad, $ps)
and any of ($type, $meth))
}
rule Evolution_RAT_DotNet_Injector_XOR_NT_API {
meta:
description = "Evolution RAT stage 4: 9,728-byte .NET injector that resolves native NT hollowing APIs from XOR 0x2F encoded strings, so the Win32 names never appear"
author = "Beelzebub Research"
date = "2026-08-13"
reference = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
strings:
// API names as stored: plaintext XOR 0x2F
$x_unmap = "a[zABN_yFJX`I|JL[F@A" ascii // NtUnmapViewOfSection
$x_alloc = "a[nCC@LN[JyF][ZNCbJB@]V" ascii // NtAllocateVirtualMemory
$x_write = "a[x]F[JyF][ZNCbJB@]V" ascii // NtWriteVirtualMemory
$x_getctx = "a[hJ[l@A[JW[{G]JNK" ascii // NtGetContextThread
$x_setctx = "a[|J[l@A[JW[{G]JNK" ascii // NtSetContextThread
$x_resume = "a[}J\\ZBJ{G]JNK" ascii // NtResumeThread
$x_ntdll = { 41 5B 4B 43 43 01 4B 43 43 } // ntdll.dll
$type = "yMOxml" ascii
$meth = "C3FrcKQp" ascii
condition:
uint16(0) == 0x5A4D
and (
3 of ($x_*)
or ($x_ntdll and any of ($type, $meth))
)
}
rule Evolution_RAT_Payload_Config_Section {
meta:
description = "Evolution RAT x86-64 payload: builder config markers in the .cfg PE section plus HVNC, GhostStandby and SniperLogs artifacts. The ##CFG_START## markers are build-invariant and match any operator's host/port."
author = "Beelzebub Research"
date = "2026-08-13"
reference = "https://beelzebub.ai/blog/evolution-rat-hvnc-browser-session-hijacking"
strings:
$cfg_start = "##CFG_START##" ascii
$cfg_end = "##CFG_END####" ascii
$h1 = "hvnc:ready" ascii
$h2 = "hvnc:launch_ok" ascii
$h3 = "hvnc:launch_fail" ascii
$h4 = "hvnc_watchdog:stuck" ascii
$h5 = "hvnc_injection.dll" ascii
$g1 = "GhostStandby" wide
$s1 = "SniperLogs" ascii
$s2 = "SniperX_" ascii
$s3 = "CloneProfile" ascii
$s4 = "Google Chromekey1" wide
$ov = "Overlord HVNC DLL Loaded" ascii
$wgl = "ANGLE (" ascii
$ua = "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" ascii
condition:
uint16(0) == 0x5A4D
and (
($cfg_start and $cfg_end)
or $ov
or 3 of ($h1, $h2, $h3, $h4, $h5, $g1, $s1, $s2, $s3, $s4, $wgl, $ua)
)
}
Conclusion
The five-stage chain is camouflage, not the story’s destination. Reversed-source persistence, XOR strings, AES, a custom Base85 alphabet, reflective loading, and process hollowing each remove a different detection surface until the payload can reach the browser. What happens there is what separates Evolution RAT from the commodity stealer population.
The operation is built from four pieces. Cookies provide authenticated state. CloneProfile makes that state launchable without stopping the victim’s browser. The 35-field collection reproduces the device identity an anti-fraud system expects, down to Chrome’s ANGLE (...) renderer syntax. Opcode 0xB0 lets traffic leave through the victim’s network. Together, they make a stolen session resemble a returning user on the same device and connection. Nobody formats a GPU name for Chrome merely to inventory a host.
The strongest detections also live above one campaign’s infrastructure. serversniperxx.duckdns.org and port 4577 identify one build; the builder’s ##CFG_START## / ##CFG_END#### markers identify the toolkit. The fixed AD DE 02 00 10 02 00 00 hello does the same on the network. Hosts rotate. Product constraints persist.
Where Caronte Helped, and Where It Did Not
Reports like this one tend to describe tooling as though it did the whole job. This one did not work that way, and the split is worth stating precisely, because the part that failed is more instructive than the part that worked.
What Caronte did not do: it did not unroll the chain. The VBScript was submitted to Caronte first. That analysis returned zero sub-analyses. It did not resolve the 343-literal concatenation, did not recover the AES key from the generated PowerShell, did not decrypt stage 3, did not decode the custom Base85, and therefore never reached the two PE files at the end. Every step from the dropper to the x86-64 binary in this report was done by hand, with the scripts described in the earlier sections. The payload that Caronte analysed was submitted manually, as a separate sample, after that work was finished.
That is a real gap and it has a clear shape. The chain is five stages of format transformations, each one a different encoding with a decoder embedded in the stage above it. Unrolling it requires executing the transformation the sample describes, not reasoning about what the sample does. Those are different operations, and an engine built for the second does not get the first for free.
What Caronte did do: it mapped the payload. Given the x86-64 binary, it identified GhostStandby and the SetWindowDisplayAffinity(0x11) screen-capture exclusion, the winlogon.exe token impersonation path, the NCryptDecrypt call with the Google Chromekey1 key name, the Defender exclusion commands, the browser thread suspension preceding the SQLite reads, the full hvnc:* mutex set, SniperLogs, and the process hollowing sequence with the specific function addresses that implement it. That last part is worth naming plainly: a manual pass produces a list of behaviors, and Caronte produced a list of behaviors bound to addresses, which is a different artifact and a more useful one. The classification, infostealer with hVNC at a high verdict, came from that behavioral evidence rather than from reputation lookups.

It also produced the two identifiers that name this family, both from deterministic extraction rather than from reasoning about code.
The C2 address is not referenced by any instruction that decompilation would traverse. It sits in a non-standard PE section named .cfg, written there by a builder after compilation. Enumerating every section and dumping the ones with anomalous size finds it in one step: the summary above names serversniperxx.duckdns.org and reads the trailing 0x11e1 as port 4577, straight out of the config blob between the ##CFG_START## and ##CFG_END#### markers.
The PDB path is not in the payload’s debug directory either, which holds one POGO record and nothing else. It is inside a separate PE embedded at offset 1810200, inside the resource section’s raw range. Reasoning about decompiled code does not reach it; walking the file, carving every MZ header that has a valid PE signature behind it, and parsing each one’s debug directory does. That carving produced pdb_path and the 59965EFB-F602-42DC-94D0-FCF15841EFC1 build GUID as structured indicators.
Given the carved module as structural context, offset, size, per-section entropy and a three-DLL import list, the run then extracted that module and queued it as an analysis of its own:

The module scores 0 on its own and carries no network or persistence logic: it is purely a loader and a set of file-API hooks, with the capabilities living in the second stage it injects. Its own summary names the Overlord HVNC DLL Loaded banner it writes to %TEMP%\crashlogovd.log, which is the second independent confirmation of the family name after the PDB path.
So the honest summary is a split one. On a binary, the engine carried the analysis and produced more structure than a manual pass would have. On a multi-stage script chain, it stopped at the first encoding and a human did the rest. Nested-PE carving and anomalous-section enumeration now ship as extraction passes, which closes the smaller of the two gaps. Recursive unpacking of script chains is the larger one, and it is still open.