Skip to content

CrystalX: unpacking a Go RAT through three encrypted layers

Kirk
11 min read
malwareratgolangcrystalxreverse-engineeringwebsocket
On this page

CrystalX is delivered as a small native Windows loader wrapped around a much larger Go payload. The outer file, NursultanCracked.exe, is only 3,068,210 bytes. Almost all of that is resource data.

That layout is the useful starting point. The .text section is about 15 KB. The .rsrc section is 3,031,040 bytes. Resource 970 contains an encrypted payload, and the stub turns it into a 6.9 MB Go RAT through three transforms: position-dependent XOR, ChaCha20, then raw DEFLATE.

The payload adds another layer of encryption for its runtime strings. Once decrypted, those strings expose the C2 path, builder token, build ID, persistence names, anti-analysis lists, browser and messaging targets, and a command set covering remote desktop, webcam control, file management, keylogging, clipboard access, process execution, and credential theft.

Kaspersky reported CrystalX (opens in new tab) as a March 2026 malware-as-a-service RAT, originally seen as WebCrystal RAT and later rebranded. Their public write-up describes a Go-based RAT with a builder, WebSocket C2, Telegram/YouTube marketing, and payload protection built around zlib compression plus ChaCha20 encryption. That lines up with the mechanics in this sample: the loader unwraps a Go payload with ChaCha20 and raw DEFLATE, then the payload speaks WebSocket to its configured C2.


Sample overview

FieldValue
SampleNursultanCracked.exe
SHA25634b84db8f10d34f711bb242b21bdf662ee489dcd0e9c23b9cc95240d324bb094
TypePE32+ x64 GUI, unsigned
Size3,068,210 bytes
CompilerMSVC
PE timestamp2024-11-19T07:18:34Z
C2wss://crystalxrat[.]net/api/ws

The loader: three transforms from resource 970

The loader is a compact unpacker. Entrypoint flows through 0x140001190 to the loader state machine at 0x1400026d0. It loads RCDATA resource 970 via FindResourceW(0, 970, RT_RCDATA), copies the buffer from offset +4 with rep movsd (0xb2188 DWORDs into a 0x2c8620-byte buffer), and runs three sequential transforms.

Pass one: position-dependent XOR

for each byte offset:
    data[offset] ^= ((offset & 0xff) % 0x5f) + 0x20

The mask depends on the low byte of the offset. 0x5f is 95, so the formula cycles through 95 unique XOR bytes starting at 0x20 (space).

Pass two: ChaCha20

Key:   d01526bdaad75c24f94b80a6fde12b958078fa82beb4741e1ccdd8eb15564470
Nonce: 598a7eeda372bc6d9992e03c
Initial counter: 0

The implementation at 0x14000328c-0x14000355b uses a ChaCha20 quarterround routine at 0x140004550 with rotation constants 16/12/8/7 and the standard expand 32-byte k constant. The key is embedded at 0x1400067c0, nonce at 0x140006690.

Pass three: raw DEFLATE

The ChaCha20 output feeds into a raw DEFLATE inflater at 0x140003a30 that handles both fixed and dynamic Huffman blocks. The inflated buffer starts with an MZ header.

Manual PE mapping

The loader does full PE loading from scratch. It copies headers and sections, applies relocations, resolves imports through an embedded import descriptor, sets section memory protections, runs TLS callbacks if present, then calls the payload entrypoint. The unpacked payload matches the mapped memory region from the behavioural memory dump at base 0x140000000, which ties the static unpacking path back to the runtime payload.

The Go payload

The decoded PE is 6,908,416 bytes. PE32+ x64 GUI. SHA256 a9340c46243f5d2b00e30ea649bd14fc146ebbb42e43dbe45f5ee0cc9fc9227a.

The Go runtime is present but the build info is stripped. go version -m returns unknown. GoReSym parsed the pclntab metadata as Go 1.20-era metadata, but it cannot recover the original package paths. The type names have been replaced with random 8-12 character strings.

The binary has roughly 3,000 obfuscated type names. Examples from the GoReSym output: EtCmeRH4, GKBqIpMv, YhleiAVa8oj, Sd2mofbx. The original package structure is gone. Recovering function semantics requires following xrefs from known library functions such as crypto/aes, crypto/rsa, and crypto/cipher.XORKeyStream into the obfuscated user code that calls them.

The string obfuscation layer

The Go binary encrypts every operational string at rest. The string decryptor at 0x1402da180 is called at runtime whenever the RAT needs a string. The flow:

  1. main.(*dSRZL8OUOz).Execute calls the decryptor.
  2. RMdVNutIO.(*Uxm_0m).DecodeString at 0x140127b20 Base64-decodes the blob.
  3. DZs79I.IS5J3lcHT at 0x140123680 validates the AES key length (16, 24, or 32 bytes) and constructs an AES block state.
  4. F1zR1avBxpyH.JVIiHaAiMgQ at 0x140121cc0 sets up GCM mode: nonce size 12 bytes, tag size 16 bytes.

The static key lives in a Go string tuple at 0x140656b30:

Hk4fOCLbqKFbbAxwyAcFKUKXK4iqVaMD

Encrypted strings are stored as Base64. Decoded, the first 12 bytes are the AES-GCM nonce. The remainder is the ciphertext plus 16-byte authentication tag.

Decrypting the string table recovered 265 plaintexts.

What the strings contained

  • C2 endpoints: crystalxrat[.]net, /api/ws, wss
  • Authentication: X-Builder-Token, zenc0rn
  • Build tracking: YBFZUW1U32T
  • Persistence names: SecurityHealthSystray.exe, Windows Security Health Service, NvContainerTask_YBFZUW1U32, Global\WinSecMutex_YBFZUW1U32
  • Anti-analysis: VM MAC OUIs, sandbox hostnames and usernames, analysis tool process names, proxy/MITM tool names
  • Browser paths: Chrome, Edge, Opera, Yandex, Login Data, Cookies, Web Data, Local Storage, leveldb
  • Messaging paths: Discord (discord, discordcanary, discordptb), Telegram Desktop, AyuGram, 64Gram, Kotatogram
  • Gaming paths: Steam (loginusers.vdf, ssfn), Roblox (ROBLOSECURITY)
  • System API names: CreateProcessW, VirtualAllocEx, WriteProcessMemory, NtTraceEvent, AmsiScanBuffer, EtwEventWrite

The build ID YBFZUW1U32T seeds all persistence artifact names. The mutex is Global\WinSecMutex_YBFZUW1U32. The scheduled task is NvContainerTask_YBFZUW1U32. The lock file lands as %ProgramData%\GoogleUpdate\YBFZUW1U32T.lock.

That AES-GCM path is separate from the loader's ChaCha20 pass. GoReSym also surfaced AES-CTR, RC4, and ChaCha20 XORKeyStream wrappers inside the payload. The confirmed string decryptor for this sample is AES-GCM.

C2 protocol

The C2 orchestrator at 0x14031b940 decrypts the five required endpoint components from the string table at runtime, then calls Qeomb2.NGG9XOj.Dial and .Upgrade for WebSocket handshake over TLS.

Endpoint: wss://crystalxrat[.]net/api/ws
Header:  X-Builder-Token: zenc0rn
Build:   YBFZUW1U32T

The WebSocket frame writer at 0x1402cf860 uses standard framing: FIN/opcode byte, 7/16/64-bit payload length encoding, mask bit with 4-byte mask key. No additional post-TLS crypto. After WebSocket decode, the payload body is JSON.

Inbound message dispatcher

The inbound dispatcher at 0x14031d120 reads incoming WebSocket frames, handles control opcodes (close, ping, pong), parses the JSON body, and routes on a type field.

typeHandler
commandRoutes to direct command dispatcher
rd_start / rd_stopRemote desktop stream control
rd_inputRemote desktop mouse/keyboard input events
rd_list_monitorsEnumerate connected monitors
rd_block_mouseBlock mouse input
rd_block_keyboardBlock keyboard input
rd_block_displayBlank or lock the display
webcam_listEnumerate webcam devices
webcam_start / webcam_stopWebcam streaming control
chat_to_victimDisplay a message box to the victim

Message parameters include monitor_index, quality, device_index, message, and block.

Direct command dispatcher

The direct command dispatcher at 0x1402feaa0 compares inbound command strings against a table of 40+ known commands and dispatches to handler closures:

CategoryCommands
Shell executioncmd:, bg:, start, .exe, .bat, .cmd
Systemreboot, bsod, voltage_drop, lock_desktop, close_all_windows
Displayshake_on/off, block_input_on/off, rotate_* (0/90/180/270), taskbar_hide/show, hide_desktop_icons/show_desktop_icons, hide_cursor/show_cursor, invert_colors_on/off, monitor_on/off, visible:
Admin togglesdisable_taskmgr/enable_taskmgr, disable_cmd/enable_cmd
File managerfm:drives, fm:ls:, fm:del:, fm:rename:
Data theftclipboard:get, clipboard:set:, clipboard:start, clipboard:stop, keylogger:start, keylogger:stop, software:list, software:uninstall:, steal:manual
UI tricks`msgbox
Network downloadsbg: downloads a background image to bg_image.jpg and applies it

The cmd: prefix runs commands through CreateProcessW. The bg: prefix downloads and applies a remote image as wallpaper. The /remote/ path fragment is used by URL-related helpers for downloading and executing files from remote URLs.

Persistence

The sample copies itself to %LOCALAPPDATA%\Microsoft\DeviceMetadataStore\SecurityHealthSystray.exe. The installed copy is byte-for-byte identical to the original and has the same SHA256. The build ID appears in every persistence name.

MechanismDetail
Scheduled taskNvContainerTask_YBFZUW1U32 runs SecurityHealthSystray.exe worker on logon, highest privileges
Startup folderWindows Security Health Service.lnk
WMI filterWindows Security Health Service_Filter monitors __InstanceModificationEvent on Win32_PerfFormattedData_PerfOS_System every 300 seconds
WMI consumerWindows Security Health Service_Consumer (CommandLineEventConsumer)
MutexGlobal\WinSecMutex_YBFZUW1U32

Defender exclusions are added for C:\Windows\System32\svchost.exe and the entire System32 directory. The TaskScheduler Operational and Services Diagnostic event logs are disabled via wevtutil. Inbound firewall notification is turned off. Inbound and outbound firewall rules named System Network Service are created.

Anti-analysis

The payload runs a multi-category environment check before any C2 activity. The decrypted string set contains the full detection lists.

CategoryChecks
MAC prefixes00:05:69, 00:0c:29, 00:50:56, 08:00:27, 52:54:00 (VMware, VirtualBox, QEMU)
Hostname blacklistsandbox, malware, virus, analysis, cuckoo, vmware, vbox, qemu
Username blacklistsandbox, virus, malware, maltest, test, john, user, currentuser
Registry VM artifactsVMware Tools, VirtualBox Guest Additions, VBoxGuest/Mouse/Service/SF/Video, vmci, vmhgfs, VMTools, VMMEMCTL, Xen
VM process detectionVBoxService, VBoxTray, vmtoolsd, VMwareTray, VMwareUser, prl_tools, qemu-ga, xenservice, vboxservice
Analysis toolswireshark, procmon, processhacker, x64dbg, ollydbg, ida, windbg, dnSpy, fiddler, burp, Sysmon
Proxy/MITM toolscharles, burp, mitmproxy, portswigger, zap proxy, Proxifier, HTTPDebugger

If detection triggers, the sample runs PowerShell to disable Defender (Stop-Service WinDefend, disable Defender scheduled tasks, set registry tamper flags), disable Sysmon, purge all event logs with wevtutil el | ForEach-Object { wevtutil cl $_ }, and apply restrictive ACLs on its installation directory.

Network behavior

Two behavioural runs connected to crystalxrat[.]net:443 over TLS. The resolved addresses sit behind Cloudflare AS13335, making them CDN edge observations.

The first PCAP shows crystalxrat[.]net in the TLS SNI with 56,862 bytes transmitted and 8,701 bytes received. The second PCAP shows similar volumes: 56,640 bytes transmitted and 8,639 bytes received. The CrystalX TLS session client random (5a3a660ff209398a8a81bd8dc6ca8b9880c0456879456a4f930531eba0557a9d) is absent from the keylog, so the captured WebSocket payload bodies remain opaque. The keylog contains 12 CLIENT_RANDOM entries, all from a different session prefix.

The domain is young. WHOIS shows crystalxrat[.]net registered through Tucows on 2026-04-14 at 10:07:57Z, updated at 10:11:21Z, and pointed at braden.ns.cloudflare.com and teresa.ns.cloudflare.com. SecurityTrails only lists one subdomain, www.

Certificate history is also narrow. CertSpotter shows two public certificates for crystalxrat[.]net / *.crystalxrat[.]net on 2026-04-14: one Let's Encrypt E8 certificate and one Sectigo DV E36 certificate. The live TLS certificate is the Let's Encrypt leaf, valid from 2026-04-14 to 2026-07-13, with SHA1 fingerprint 087d077990ecbc7ac2b09e261df80ffc49475a61. SSLBL has no entry for that certificate.

ThreatFox lists crystalxrat[.]net as a botnet_cc domain tagged CrystalX and RAT, reported by abuse.ch. OTX has public botnet C2 pulses for the domain and URL observations for hxxp://crystalxrat[.]net, hxxps://crystalxrat[.]net, and hxxps://crystalxrat[.]net/login/, all returning HTTP 403. URLhaus has no host entry.

Public Triage results show a small visible CrystalX cluster: two May 11 submissions of this NursultanCracked.exe hash, two May 1 samples using crystalxrat[.]net, and one May 1 sample using crystalxrat[.]top. The .top domain was registered through Spaceship on 2026-03-12, used the same Cloudflare nameservers, and had apex/wildcard certificates issued the same day; RDAP currently shows serverHold. Public URL submissions on Apr 8 also touched webcrystal[.]lol and webcrystal[.]sbs, echoing the WebCrystal branding Kaspersky described.

Detection

YARA rule: github.com/kirkderp/yara/tree/main/crystalx_go_rat (opens in new tab).

IOC summary

Hashes

ArtifactSHA256
NursultanCracked.exe (loader)34b84db8f10d34f711bb242b21bdf662ee489dcd0e9c23b9cc95240d324bb094
Unpacked Go payloada9340c46243f5d2b00e30ea649bd14fc146ebbb42e43dbe45f5ee0cc9fc9227a
RCDATA 970 (encrypted)8a6f8ef99384152df63a39b6ba9f08f0a1e9cc33b14319e8a1b184beb4a06cf7
Memory dump (mapped)2497e0aa88af681872194966bfc2bd67013ea75c96f4b5717abe4a4f43e69394

Network

IndicatorValue
C2crystalxrat[.]net:443
Related C2crystalxrat[.]top
WebSocket pathwss://crystalxrat[.]net/api/ws
Nameserversbraden.ns.cloudflare.com, teresa.ns.cloudflare.com
RegistrarTucows Domains Inc.
Created2026-04-14T10:07:57Z
CT historyLet's Encrypt E8 and Sectigo DV E36 certs for apex/wildcard
ThreatFoxbotnet_cc, tags: CrystalX, RAT
Builder tokenX-Builder-Token: zenc0rn
Build IDYBFZUW1U32T

Host

IndicatorValue
Persistence path%LOCALAPPDATA%\Microsoft\DeviceMetadataStore\SecurityHealthSystray.exe
Scheduled taskNvContainerTask_YBFZUW1U32
Startup shortcutWindows Security Health Service.lnk
WMI filterWindows Security Health Service_Filter
WMI consumerWindows Security Health Service_Consumer
Firewall rulesSystem Network Service, System Network Service Out
MutexGlobal\WinSecMutex_YBFZUW1U32
Lock file%ProgramData%\GoogleUpdate\YBFZUW1U32T.lock

Static keys

KeyUse
Hk4fOCLbqKFbbAxwyAcFKUKXK4iqVaMDAES-GCM string decrypt
d01526bdaad75c24f94b80a6fde12b958078fa82beb4741e1ccdd8eb15564470ChaCha20 loader key
598a7eeda372bc6d9992e03cChaCha20 loader nonce

CrystalX makes you earn the view. The loader burns three transforms to hide a Go payload, the payload hides its strings behind AES-GCM, and the useful shape only appears after both layers are peeled back: a builder-tagged WebSocket RAT with durable host artifacts, a loud command surface, and C2 infrastructure that already shows a small public trail from WebCrystal-era domains into the current CrystalX naming.

Share this article