A Go crypter, a RAT with SQLite in its WebAssembly belly, and an encrypted C2 configuration that eventually introduced itself through an error message. The salad came with several layers of dressing.
The archive password was infected. The executable inside was 5.27 MB of Go. Behind it sat another Go binary, this time a 32-bit RAT with browser and wallet theft routines, remote-control machinery, and some intriguing references to the TON blockchain.
The wrapper called itself LunarCrypt. The payload's module path was simply salat. Both had been stripped, but enough Go metadata survived to give us a guided tour.
The best discovery came from memory. After working through the wrapper and unpacking the payload, we let the implant decode its configuration inside the isolated lab. It then obligingly repeated its configured C2 hostname every time a connection attempt failed:
sa1atik[.]cn
Encryption had hidden the endpoint on disk. Error handling supplied the name tag.
The rig, and what the evidence means
The analysis ran through our malbox tooling. Dynamic execution took place in the isolated VMware guest for710-win10, configured with host-only networking and no route to the internet. The guest was reverted to a snapshot before and after each run. Static analysis, unpacking, Ghidra work, and inspection of Go runtime structures took place on the lab host.
Throughout this post, observed means recorded during the guest runs or recovered from process memory. Static means reconstructed from the binary, its embedded content, or decompiled code. Inferred means a plausible explanation that still needs a trace or a more complete reconstruction.
That distinction matters here: a function named GetAppBoundKey is a useful lead; demonstrating successful decryption of a particular browser's protected data is a separate result. Likewise, recovering a configured hostname does not establish that the server was live. No successful external C2 connection occurred in these runs.
Stage 0: the archive
The sample arrived in an AES-encrypted ZIP containing one executable. Its filename matched the stage-1 SHA-256 recorded in the case:
84596497251e3847ce4389cb389a56fe87e08944474ffd4019ec315d306f449e.exe
Format: PE32+ GUI executable, x86-64
Size: 5,268,480 bytes
Sections: 8
Imphash: c2d457ad8ac36fc9f18d45bffcd450c2
The password, infected, belongs to the sample-sharing convention. It tells us how this copy reached the lab, rather than how a victim would have received it.
The section layout gave us the first useful clue. Beside approximately 0.7 MB of .text and 0.86 MB of .rdata, the binary carried roughly 3.65 MB of .data, with a recorded entropy of 7.989 bits per byte. That large, high-entropy region was consistent with a packed or encrypted payload. Following the code established which.
Stage 1: LunarCrypt keeps a diary
The wrapper's Go build information survived:
path command-line-arguments
build -ldflags="-s -w -H=windowsgui"
build CGO_ENABLED=0
build GOARCH=amd64
build GOOS=windows
go1.22.0
So did this source path:
C:/Users/Administrator/Desktop/LunarCrypt/.go_cache/stub_1788423005216099200.go
LunarCrypt had removed its conventional symbols and debug information while leaving its name and a generated-stub path in the binary. The recovered Go function names were equally conversational:
main.getPassword main.decryptAESGCM
main.deriveKey main.decryptString
main.pbkdf2SHA256 main.decompress
main.executeFile main.melt
main.main
Static reconstruction: Ghidra reduced the main path to a short sequence:
- Recover an embedded password by XORing its bytes with
0xf1. - Derive a 32-byte key using PBKDF2-HMAC-SHA256, an embedded salt, and 100,000 iterations.
- Decrypt the embedded payload blob with AES-GCM.
- Decompress the result, write it to a temporary executable, and launch it.
- Invoke the cleanup path associated with
main.melt.
The binary also contains the temporary-file pattern payload_*.exe. Together with the reconstructed write-and-launch path, that gave us a concrete artifact to collect during detonation.
Observed: the wrapper, PID 6724, ran for approximately three seconds. It wrote:
C:\Users\REM\AppData\Local\Temp\payload_4002092102.exe
It launched that file as PID 6804 and exited with code 0. The case notes also record the wrapper's subsequent disappearance. The collector recovered the dropped executable before the snapshot was reverted:
File: payload_4002092102.exe
Format: PE32 GUI executable, Intel 80386
Packing: UPX
Size: 3,594,752 bytes
Sections: 3
SHA-256: 16afb5021e5a7eafb309798a4ec28ddec925f0577d2b3b5c7cf18db66167b704
The handoff changes architecture: an x64 Go wrapper launches an x86 Go payload. The different builds fit a modular wrapper-and-payload arrangement. They do not establish separate authors or a commercial crypter service.
Stage 2: UPX opens the salad bar
UPX unpacked the payload cleanly: 3.59 MB became approximately 12.57 MB, and another batch of Go metadata became readable.
path salat
mod salat (devel)
go1.24.0
The dependency list included github.com/andygrunwald/vdf, github.com/capnspacehook/taskmaster, github.com/gorilla/websocket, and github.com/tetratelabs/wazero: useful leads for Steam configuration parsing, Windows scheduled tasks, WebSocket transport, and WebAssembly execution respectively.
Source filenames included salat/main.go, salat/init.go, salat/funcs.go, salat/sets.go, salat/task.go, and salat/tsc.go, alongside an internal salat/screenshot package.
For this case, we use SalatStealer for the payload identified by that salat module. Its recovered code surface extends well beyond credential collection into remote-access functionality.
The menu: 301 recovered main-package symbols
This is a static capability map. Names, strings, and linked interfaces identify areas of interest; the isolated runs did not exercise every feature.
| Area | Evidence in the unpacked payload |
|---|---|
| Chromium data | getChromeLogins, getChromeCookies, getChromeAutofils, getChromeToken, GetChromiumMasterKeys; Login Data and Local State strings |
| App-Bound Encryption targeting | GetAppBoundKey, decryptAPPB, StartAPPB, and COM interface names including IElevator, IElevatorBrave, and IElevatorEdge |
| Firefox and Gecko data | getGeckoLogins, getGeckoCookies, key4.db, logins.json, and NSS-related cryptographic routines |
| Windows data protection | DPAPI, CryptUnprotectData, and DATA_BLOB references |
| Cryptocurrency wallets | Strings naming MetaMask, Exodus, Electrum, Phantom, Solflare, Keplr, Ronin, Rabby, Coinomi, Guarda, and numerous others |
| Messaging applications | Telegram-related tdata, found tg process, and found tg:// url strings; a separate getDiscord symbol |
| Steam | getSteams, parseVdf, decodeSteam, and Steam/users |
| Keylogging | startKeylogger, stopKeylogger, keyPressCallback, runKeylogger, virtual-key names, and idle-state helpers |
| Screen and webcam capture | salat/screenshot.CaptureRect, sendScreen, screenStream, getWebcams, getScreen, and mjpeg |
| Remote commands and files | executeCommand, shellCommand, downloadFile, unzip, zipFiles, newTask, and doTask |
| Privilege and token handling | Elevate, getSystemToken, impersonateSystem, enablePrivilege, and DuplicateUserTokenFromSessionID |
| Handle and process manipulation | openHndl, duplicateHandle, readFileFromHandle, suspendProcessThreads, unlockProcs, and Restart Manager structures |
| Scheduled-task support | taskmaster bindings and scheduler-related names including Actions, Triggers, and RunLevel |
| C2 and proxying | initConnection, wsSess, c2Server, changeEndpoint, getEp, periodicFlush, proxySocks, and p2pSocks |
| Cleanup and duplicate checks | selfDelete, Suicide, and checkDupe |
Two areas deserve particular care. The App-Bound Encryption names support an intended browser-key recovery path, but we did not demonstrate a successful bypass against a specified browser version. The handle and Restart Manager references suggest techniques for accessing files held by other processes; their exact sequence needs a call trace before we can describe how a lock is released or a database is read.
SQLite, served inside WebAssembly
The unusual dependency was wazero, a Go WebAssembly runtime. Beside it sat an embedded WASM module of approximately 1.3 MB, extracted at file offset 0x69c1a0 in the unpacked image.
Static extraction: its exports included sqlite3_step, sqlite3_column_text, and sqlite3_bind_*. The module was SQLite.
That arrangement has a legitimate precedent: earlier versions of ncruces/go-sqlite3 embedded a WASM build of SQLite and ran it through wazero, providing SQLite access without cgo. The combination in this sample is consistent with that design; the listed dependencies alone do not establish the exact driver or version. Versioned upstream documentation
For a stealer interested in browser databases such as Login Data, Cookies, and Web Data, a bundled SQLite engine is a practical ingredient. This design can supply it without a separate native sqlite3.dll.
The sample gives us evidence of packaging, rather than a measurement of detection resistance. Still, finding an entire database engine inside the RAT's WebAssembly belly was a pleasant diversion from the usual string soup.
The housekeeping script has opinions
Static decoding: an embedded base64 blob, approximately 5.4 KB in its encoded form, yielded a PowerShell script with four main jobs:
- Request Microsoft Defender exclusions for
Program Files,%AppData%, and%LocalAppData%usingAdd-MpPreference -ExclusionPath. - Invoke
reagentc /disable. - Download
7z.dll,7z.exe,MSTSCLib.dll,AxMSTSCLib.dll, andffmpeg.exeinto%TEMP%from a raw GitLab repository path. - Set
EnableLUAto0.
The configured download base was:
hxxps://gitlab[.]com/webrat1/importantfiles/-/raw/main/
The filenames suggest archive handling, RDP client components, and media processing. The exact relationship between those helpers and individual RAT features remains to be traced. GitLab supplies the delivery location in the embedded script; successful downloads were not demonstrated in the disconnected guest.
The recovery and UAC changes also need precise wording. reagentc /disable disables the active Windows Recovery Environment image associated with the running installation. Setting EnableLUA to 0 requests a system-wide UAC policy change, whose activation requires a restart. These commands express intent; the decoded script alone cannot establish that either change succeeded. REAgentC reference, UAC registry mapping, UAC restart requirement
Quite a lot of housekeeping for something that was never invited in.
The encrypted configuration: one layer down
The configured C2 was not recovered as a plaintext endpoint during the initial static inspection. The resolver strings were easier to find:
hxxps://cloudflare-dns[.]com/dns-query?name=
hxxps://dns[.]google/resolve?name=
hxxps://1[.]1[.]1[.]1/dns-query?name=
Runtime URL construction also included &type=A.
Static reconstruction: two 516-byte blobs at analysis VAs 0x00c966a0 and 0x00c968a4 began with the dword 0xa5a5a7a5. A routine at 0x008dfb70 was reconstructed as applying this transform to successive body dwords:
out[i] = bswap32(rol32(w[i], 4*i) ^ (4*i) ^ 0x9e3779b9)
The rotation is over 32 bits. The first dword separately gives a candidate output length:
0xa5a5a7a5 ^ 0xa5a5a5a5 = 0x200 = 512 bytes
Those addresses refer to this analysed image and should not be treated as portable signatures.
The recorded reimplementation did not produce a readable endpoint. Further decoding was a plausible explanation, with main.dec, main.getBestMethod, and CIPHER ERROR providing additional leads. It remained an incomplete config reconstruction: non-readable output alone cannot distinguish another encryption layer from a mistake in the reconstructed transform or its inputs.
At that point, we had a more direct route. The implant needed the answer too.
Letting the error message do the talking
Observed: we ran the packed x86 payload under the x86 CDB debugger inside the guest, allowed it approximately 60 seconds to initialise and attempt its network bootstrap, then interrupted execution and searched process memory.
This was user-mode process debugging. CDB supports launching or attaching to a process directly, which is the workflow used here. Microsoft's CDB documentation
The search targeted the readable part of the DoH URL construction:
0:000> s -a 0x01020000 L?0x7efd0000 "dns-query?name="
01c1e0bb dns-query?name=d
01cbc250 dns-query?name=g
...
Memory inspection exposed resolver URLs, apparent bootstrap probes involving dns[.]google, google[.]com, and 1[.]1[.]1[.]1, and repeated connection errors. The useful one read as follows, with the URL and hostname defanged for publication:
Get "hxxps://sa1atik[.]cn/sa1at/": dial tcp: lookup sa1atik[.]cn: no such host
There were dozens of copies. Every retry left another breadcrumb.
The recovered endpoint list associated with main.getEp contained four copies of sa1atik[.]cn:443, using the path /sa1at/. For this build, endpoint rotation led back to the same hostname.
The recovered configuration was therefore:
hxxps://sa1atik[.]cn/sa1at/
Port: 443
The binary includes a WebSocket client dependency, but the lab run did not complete a TLS connection or WebSocket handshake. What we recovered was the destination the implant tried to use.
It had spent considerable effort concealing that destination on disk, then printed it in a complaint about the lab's networking. We appreciate an actionable error message.
TON: an interesting lead, with some assembly required
The TON-related evidence consisted of several pieces:
- Static: references to
ton[.]access[.]orbs[.]network, the functionmain.decodeFromTonAddress, and an embedded base64 sequence beginningte6cckEBAQEAEgAA. - Observed in memory: lookup-failure text naming
ton[.]access[.]orbs[.]network. - Inferred: a TON-assisted configuration or endpoint-discovery path, potentially used as a fallback when the ordinary endpoint is unavailable.
The base64 prefix decodes to bytes beginning b5 ee 9c 72, the TON Bag of Cells serialization magic. A BoC can represent many kinds of cell data; identifying its container does not establish that it holds executable contract code or an operator-controlled record. TON serialization documentation
Address formats are another easy place to overread the evidence. A raw basechain address is 0: followed by a 256-bit account identifier represented as 64 hexadecimal characters. The user-friendly representation encodes a 36-byte structure into 48 base64 or base64url characters. A routine reportedly accepting 0: plus 64 base64url characters needs its own parser semantics checked before we call that a standard raw TON address. TON address formats
The draft analysis also identified an opaque string that decodes to 32 bytes. Length alone cannot identify it as an Ed25519 public key, a wallet identity, or a contract address.
The next useful result would connect a specific TON request and response to a decoded endpoint, then show that endpoint reaching the connection routine. Until then, a blockchain fallback is a working hypothesis. An operator updating a smart contract or TON DNS record to redirect the fleet remains unproven in this case.
There is enough here for another evening in the debugger. The blockchain can wait its turn.
Hunting the parts that leave footprints
The wrapper complicates static inspection, but the chain still supplies several useful points of correlation:
- Process and file activity: a short-lived x64 GUI executable writes and launches an x86
payload_<digits>.exebeneath%TEMP%. That sequence was observed in this run; the filename pattern alone is weak evidence. - Recovered Go metadata: the LunarCrypt build path and
stub_*.gopattern belong to stage 1; thesalatmodule and source paths belong to the unpacked payload. Match combinations with file context. - Script telemetry: the embedded script's combination of Defender exclusion requests,
reagentc /disable, anEnableLUAchange, and downloads from the specific raw GitLab path provides a behavioural hunting lead. Script-block and process telemetry could establish whether those operations actually ran. - Endpoint indicators: correlate
sa1atik[.]cnand/sa1at/with the responsible process. The URL path requires endpoint-level or decrypted HTTP visibility; ordinary encrypted network traffic does not expose it. - Memory artifacts: the configured hostname, repeated lookup failures, and resolver URL fragments were useful after runtime initialisation. Searching for strings assembled during failed connections can recover information absent from the initial on-disk view.
The public resolver and TON gateway names belong in contextual hunting, not standalone malicious-domain lists. The import hashes below are also supporting pivots: packers and shared toolchains can produce collisions across unrelated samples.
One extra string deserves an honourable mention: dQw4w9WgXcQ, recorded near the config parser. Even here, someone found room for a rickroll.
Indicators of compromise
These SHA-256 values identify the three executable artifacts recorded in the case. A separate archive hash was not supplied.
Executable SHA-256 hashes
LunarCrypt wrapper — x64
84596497251e3847ce4389cb389a56fe87e08944474ffd4019ec315d306f449e
SalatStealer payload — UPX-packed x86
16afb5021e5a7eafb309798a4ec28ddec925f0577d2b3b5c7cf18db66167b704
SalatStealer payload — unpacked
d6a102b8ca947e5ca97a90843c295b714130156792ad56a7493d759719e7c641
Import hashes
Wrapper: c2d457ad8ac36fc9f18d45bffcd450c2
Packed payload: 6ed4f5f04d62b18d96b26d6db7c18840
Unpacked payload: 1aae8bf580c846f39c71c05898e57e88
Configured network locations, defanged
# C2 recovered from process memory
sa1atik[.]cn
hxxps://sa1atik[.]cn/sa1at/
# Helper-download base in the decoded script
hxxps://gitlab[.]com/webrat1/importantfiles/-/raw/main/
# Requested helper filenames
7z.dll
7z.exe
MSTSCLib.dll
AxMSTSCLib.dll
ffmpeg.exe
# Legitimate public services referenced by the sample
cloudflare-dns[.]com/dns-query
dns[.]google/resolve
1[.]1[.]1[.]1/dns-query
ton[.]access[.]orbs[.]network
Hash of the day
LunarCrypt brought the encrypted wrapper. Salat brought browser-theft routines, remote-access machinery, and a database engine tucked into WebAssembly. The runtime supplied the final ingredient: a readable endpoint in an otherwise unhelpful connection failure.
The useful lesson is in that progression. The wrapper disclosed the launch path; the unpacked Go metadata mapped the code; the running process exposed the configuration it needed to use. Each layer gave us a different kind of evidence.
The archive said infected. The module said salat. Eventually, the error message told us where the salad wanted to go.
Analysis date: 12 September 2026. Case reference: malware:84596497251e. Executable artifacts remain in the lab; this post distributes hashes only. Network indicators are defanged throughout.
Working on a difficult security investigation or building similar tooling? I’d be interested to hear from you.
AI Platform Investigation hello@f4n6.co.uk