> ## Content Index
> Fetch the complete content index at: https://f4n6.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# Salad on the Side: Unpacking LunarCrypt and the SalatStealer RAT
- URL: https://f4n6.co.uk/salad-on-the-side-unpacking-lunarcrypt-and-the-salatstealer-rat/
- Published: 2026-09-12T22:04:24.000Z
- Updated: 2026-09-13T15:31:02.000Z
- Author: Jeff Davies
- Tags: malware, malbox, Analysis, Reverse Engineering, AI Security

> *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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

1. Recover an embedded password by XORing its bytes with `0xf1`.
2. Derive a 32-byte key using PBKDF2-HMAC-SHA256, an embedded salt, and 100,000 iterations.
3. Decrypt the embedded payload blob with AES-GCM.
4. Decompress the result, write it to a temporary executable, and launch it.
5. 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:

```text
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:

```text
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.

```text
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](https://raw.githubusercontent.com/ncruces/go-sqlite3/v0.22.0/README.md?ref=f4n6.co.uk)

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:

1. Request Microsoft Defender exclusions for `Program Files`, `%AppData%`, and `%LocalAppData%` using `Add-MpPreference -ExclusionPath`.
2. Invoke `reagentc /disable`.
3. Download `7z.dll`, `7z.exe`, `MSTSCLib.dll`, `AxMSTSCLib.dll`, and `ffmpeg.exe` into `%TEMP%` from a raw GitLab repository path.
4. Set `EnableLUA` to `0`.

The configured download base was:

```text
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](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/reagentc-command-line-options?view=windows-11&ref=f4n6.co.uk), [UAC registry mapping](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/settings-and-configuration?ref=f4n6.co.uk), [UAC restart requirement](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/jj852217%28v%3Dws.11%29?ref=f4n6.co.uk)

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:

```text
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:

```text
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:

```text
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](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugging-a-user-mode-process-using-cdb?ref=f4n6.co.uk)

The search targeted the readable part of the DoH URL construction:

```text
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:

```text
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:

```text
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 function `main.decodeFromTonAddress`, and an embedded base64 sequence beginning `te6cckEBAQEAEgAA`.
- **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](https://docs.ton.org/foundations/serialization/boc?ref=f4n6.co.uk)

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](https://docs.ton.org/foundations/addresses/formats?ref=f4n6.co.uk)

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>.exe` beneath `%TEMP%`. That sequence was observed in this run; the filename pattern alone is weak evidence.
- **Recovered Go metadata:** the LunarCrypt build path and `stub_*.go` pattern belong to stage 1; the `salat` module 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`, an `EnableLUA` change, 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[.]cn` and `/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

```text
LunarCrypt wrapper — x64
84596497251e3847ce4389cb389a56fe87e08944474ffd4019ec315d306f449e

SalatStealer payload — UPX-packed x86
16afb5021e5a7eafb309798a4ec28ddec925f0577d2b3b5c7cf18db66167b704

SalatStealer payload — unpacked
d6a102b8ca947e5ca97a90843c295b714130156792ad56a7493d759719e7c641

```

### Import hashes

```text
Wrapper:           c2d457ad8ac36fc9f18d45bffcd450c2
Packed payload:    6ed4f5f04d62b18d96b26d6db7c18840
Unpacked payload:  1aae8bf580c846f39c71c05898e57e88

```

### Configured network locations, defanged

```text
# 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