MiniRAT: A Go-based macOS RAT delivered via malicious npm package
MiniRAT is a Go-based macOS RAT dropped onto developer machines via a malicious npm package. It evades VMs, persists via a LaunchAgent disguised as an Apple component, and beacons over HTTPS using an AES-encrypted C2 config. Operators can run shell commands, exfiltrate files, and stage secondary payloads.
A newly analyzed Go-based macOS remote access trojan (RAT), internally named Minirat, has surfaced in the wild using anti-VM checks, LaunchAgent persistence, and AES-encrypted command and control (C2) configuration to maintain stealthy, long-term access on victim endpoints. According to SafeDep, the initial infection vector was a malicious npm package (velora-dex-sdk) that dropped the Go-based macOS RAT onto developer endpoints.
Triage and build information
The sample is compiled from Golang source code, as confirmed through string signatures and runtime artifacts present within the binary. This choice of language presents several challenges for static analysis:
-
Inflated binary footprint: Go binaries are statically linked by default, with the entire runtime and all imported packages embedded directly into the executable. This results in a significantly larger file size compared to binaries produced by C/C++ or other compiled languages.
-
Symbol obfuscation through bundling: Because Go statically compiles all third-party and standard library dependencies into a single artifact, function boundaries between malicious logic, legitimate libraries, and the Go runtime become obscured. Without symbol recovery, disassemblers present thousands of unnamed sub_XXXXXX routines, making it difficult to isolate attacker-authored code from benign runtime functions.
The binary appears to be ad hoc signed.

Go buildinfo (go version -m):
0a8ab3d16b12d3a453ee5a3208fe04744ad54514ef8ea27bb8fe32679efad270: go1.25.8
path alibaba.xyz/minirat
mod alibaba.xyz/minirat (devel)
build -buildmode=exe
build -compiler=gc
build -trimpath=true
build CGO_ENABLED=0
build GOARCH=arm64
build GOOS=darwin
build GOARM64=v8.0
build vcs=git
build vcs.revision=dfd224461edb06c556ee0d5677bd78ddda80b910
build vcs.time=2026-03-20T09:13:30Z
build vcs.modified=false
The source was committed on March 20, 2026, roughly two and a half weeks before the @velora-dex/sdk@9.4.1 npm compromise on April 7, 2026. This gap suggests premeditated tooling development rather than opportunistic post-compromise payload creation.
Symbol recovery with GoResolver
To overcome these reverse engineering hurdles, I leveraged GoResolver during the analysis. GoResolver parses Go-specific metadata structures retained in the binary to reconstruct original function names and map them to their corresponding virtual addresses.
Goresolver output:

Binary ninja snippet to rename function names:
# Binary Ninja Snippet: Rename functions from a JSON file
# Supports JSON structures like:
# { "Symbols": { "0x100001000": {"Name": "foo", ...}, ... } }
# { "Symbols": { "0x401000": "foo" } }
# { "0x401000": "foo" }
# [ {"address": "0x401000", "name": "foo"}, ... ]
import json
from binaryninja.interaction import get_open_filename_input
filename = get_open_filename_input("Select JSON file with renames", "JSON Files (*.json)")
if not filename:
print("[-] No file selected.")
else:
if isinstance(filename, bytes):
filename = filename.decode()
with open(filename, "r") as f:
data = json.load(f)
# Unwrap "Symbols" if present
if isinstance(data, dict) and "Symbols" in data:
data = data["Symbols"]
# Normalize into list of (addr:int, name:str) tuples
renames = []
if isinstance(data, dict):
for k, v in data.items():
addr = int(k, 0) if isinstance(k, str) else int(k)
if isinstance(v, dict):
name = v.get("Name") or v.get("name")
else:
name = v
if name:
renames.append((addr, name))
elif isinstance(data, list):
for item in data:
addr = item.get("address") or item.get("addr")
name = item.get("name") or item.get("Name")
if isinstance(addr, str):
addr = int(addr, 0)
if addr is not None and name:
renames.append((addr, name))
else:
raise ValueError("Unsupported JSON structure")
print(f"[*] Loaded {len(renames)} rename entries")
success = 0
for addr, new_name in renames:
func = bv.get_function_at(addr)
if func is None:
funcs = bv.get_functions_containing(addr)
func = funcs[0] if funcs else None
if func is None:
print(f"[-] No function found at {hex(addr)}")
continue
old_name = func.name
func.name = new_name
print(f"[+] Renamed {hex(addr)}: {old_name} -> {new_name}")
success += 1
bv.update_analysis()
print(f"Done. Renamed {success}/{len(renames)} functions.")
Anti-VM and sandbox evasion checks
The first action performed is a call to IsVirtualMachine, an anti-analysis check designed to detect whether the sample is executing inside a virtualized or sandboxed environment. If the routine returns a truthy value ((x0.d & 1) != 0), execution is terminated immediately via an early return.
.png)
To detect a VM the malware executes the following in the terminal:
-
scutil --get ComputerName: Checks output for word "Virtual Machine".
-
sysctl -n machdep.cpu.brand_string: Checks output for the word "Virtual".
-
ioreg -l | grep -e Manufacturer -e 'Vendor Name': Checks hardware vendors' information for:
-
Virtual Box
-
Oracle
-
VMware
-
Parallels
-
The output returned by scutil is passed into internal/stringslite.Index. The malware searches for the literal string "Virtual Machine" (length 0xf = 15 bytes) within the computer name.
alibaba.xyz/minirat/internal/utility.DuplicateInstanceRunning
.png)
Following the VM check, the malware verifies whether another instance of itself is already running on the host using a file-based locking mechanism:
-
Resolves temp directory: Calls os.Getenv("TMPDIR"); falls back to /tmp if unset.
-
Builds lock file path: $TMPDIR/updater.lock (e.g., /var/folders/<XX>/<random>/T/updater.lock).
-
Opens the lock file: Uses os.OpenFile with flags O_RDWR | O_CREAT (0x202) (creates the updater.lock file) and mode 0644 (0x1a4).
-
Attempts exclusive lock: Calls syscall.Flock with LOCK_EX | LOCK_NB (6), a non-blocking exclusive lock.
LaunchAgent persistence and execution
The Persist function within the actions package is invoked to establish persistence on the victim host by first appending .zshrc within the user home directory and writing launchctl submit -l zsh.profiler -- "<binary_path>".
The malware further creates a launch agent plist which launches the command in .zshrc when run with zsh -i: ~/Library/LaunchAgents/com.apple.Terminal.profiler.plist. The plist has RunAtLoad=true but no KeepAlive, meaning the LaunchAgent only fires at user login; if the RAT process dies, it won't respawn until the next login.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.apple.Terminal.profiler</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-i</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
AES-encrypted C2 configuration
The LoadServers routine loads the malware's C2 server list from the encrypted configuration file at ~/Library/Application Support/com.apple.Terminal/.cache. The resulting server context is carried forward into the main beacon loop.
On first execution, .cache does not yet exist on the victim host. The malware bootstraps it from three AES-encrypted JSON blobs embedded directly in the binary, which are decrypted on the first run and written to .cache for subsequent loads. This design also allows the operator to rotate C2s at runtime by replacing .cache out-of-band.
Decryption uses AES-CBC with PKCS7 padding. Per SafeDep's analysis, the 32-byte key v59l2uwlow9s1ebuscgfg9k9r4voxkbs is located at offset 0x264111 within the binary.
alibaba.xyz/minirat/internal/netcomm.CheckAgentExists
After loading the C2 server list, the malware checks whether it has already registered with the server as an active agent by looking for a marker file:
-
Initializes data client: Calls prepareDataClient to set up the authenticated communication context with the C2 server.
-
Validates session: If the preparation fails (x5 == 0 indicates a missing session token/error), the function returns 0 (agent not > registered).
-
Checks for marker file: On success, it verifies the presence of info.json on the remote C2 endpoint.
-
Returns result: The existence status of info.json determines whether the agent is already known to the C2 infrastructure.
Command-and-control communication
The alibaba.xyz/minirat/internal/actions.RegisterAgent function is invoked after the malware confirms that no prior registration exists on the C2 server (via CheckAgentExists). Its purpose is to profile the victim host, enrich the profile with external geolocation data, and upload the resulting dossier to the C2 infrastructure as a JSON file named info.json. This represents the malware's initial victim enrollment. Through static analysis, we collected the following information in the info.json.
| JSON field | Source | Description |
|---|---|---|
| username | ExecuteCommand("whoami") | Current username |
| hostname | ExecuteCommand("hostname") | Machine hostname |
| os | ExecuteCommand("sw_vers -productVersion") | macOS version |
| cpu | ExecuteCommand("sysctl -n machdep.cpu.brand_string") | CPU model |
| arch | ExecuteCommand("uname -m") | CPU architecture |
| public_ip | GET https://api.ipify.org | Public IP address (trimmed) |
| country | GET https://ipinfo.io/json → country | Country code |
| region | GET https://ipinfo.io/json → region | Region/state |
| created_at | time.Now() (Unix epoch ms) | Enrollment timestamp |
| last_access | time.Now() (Unix epoch ms) | Last-seen timestamp (refreshed each beacon) |
C2 command execution and polling
The malware communicates with its C2 server over HTTP(S) using two JSON-based channels, both keyed by the per-agent <agent_key> parameter. Action.json serves as the tasking channel, while info.json serves as the enrollment record and recurring heartbeat. On every beacon cycle, the implant performs a read-modify-write on both files: it reads the remote file via netcomm.ReadJsonFile, updates one or more fields locally, and writes the result back via netcomm.WriteJsonFile. The GET side of this exchange uses /file/download?file_path=<name>&key=<agent_key>; the write side calls a separate upload endpoint which could not be captured directly as the C2 infrastructure was unreachable during analysis.
| Channel | Purpose |
|---|---|
| action.json | Tasking channel. Read from the C2 via netcomm.ReadJsonFile each polling cycle. Contains action and params fields plus an executed flag. After a task is dispatched, the implant sets executed=true and pushes the updated document back to the C2 via netcomm.WriteJsonFile to prevent re-execution. |
| info.json | Enrollment record and recurring heartbeat. Initially created by RegisterAgent with host profile data. On every polling cycle, the implant reads info.json from the C2, stamps the current time into the last_access field via time.Now(), and writes it back via netcomm.WriteJsonFile. Also used by CheckAgentExists at startup to determine whether the agent is already known to the C2 infrastructure. |
Tasking Channel
Each polling cycle begins with a read of action.json from the C2. The document contains an action field (command type), a params field (command arguments), and an executed flag used for idempotency. If executed is false, the implant dispatches the task, sets executed=true, and writes the document back to the C2 so the same task is not re-run on the next cycle.
The malware supports three operator commands:
-
upload: Exfiltrates a file or entire directory from the victim to the attacker's server. The operator specifies a local path and a destination path. Directories are handled recursively (likely archived into a ZIP before upload).
-
command: Executes an arbitrary shell command on the victim machine via the internal helper alibaba.xyz/minirat/internal/utility.ExecuteCommand, which invokes `/bin/sh -c <command>`. The command's output (stdout/stderr) is captured and returned to the C2 in the result field of the response, giving the operator interactive shell-like capability.
-
download: Retrieves a file from the attacker's server and writes it to a specified location on the victim, enabling delivery of secondary payloads, tools, or updated implant versions.
Concurrency differs per command. Upload and download are dispatched asynchronously as goroutines (runtime.newproc); command runs synchronously in the beacon thread, meaning a long-running shell command will stall the next polling cycle until it completes.
Upload and download behaviors are inferred from recovered function symbols (UploadFileSessionStart/Append/Finish, download handler) and string references. Live execution of these actions was not observed, as the C2 infrastructure was unreachable at analysis time.
Polling request — GET /file/download?file_path=<channel>.json&key=<agent_key>, response read via io.ReadAll.
Logging and operational artifacts
The malware contains logging functionality that appears in updater.log via the alibaba.xyz/minirat/internal/logger.WriteLog function which will update any actions the malware takes. The file path will be $TMPDIR/updater.log.

Logging is written under $TMPDIR (resolves to /var/folders/<XX>/<random>/T/ on macOS).
Conclusion
MiniRAT demonstrates how modern adversaries are increasingly leveraging compromised npm packages as an initial access vector for malware delivery. Static indicators alone are insufficient; effective detection requires not only runtime visibility into process behavior, LaunchAgent abuse, and anomalous C2 beaconing, but also proactive monitoring for compromised packages in the software supply chain.
IOCs:
-
0b028b781950641818800fee2b4bf68e4ef2bcee53fe71a21755275ba108783d
-
0a8ab3d16b12d3a453ee5a3208fe04744ad54514ef8ea27bb8fe32679efad270
Network:
-
hxxps://datahub[.]ink
-
hxxps://cloud-sync[.]online
-
hxxps://byte-io[.]us
-
hxxps://api[.]ipify[.]org
-
hxxps://ipinfo[.]io/json
HTTP URIs:
-
/file/download?file_path=action.json&key=<agent_key>
-
/file/download?file_path=info.json&key=<agent_key>
Files:
-
~/.zshrc (modified — contains "launchctl submit -l > zsh.profiler")
-
~/Library/LaunchAgents/com.apple.Terminal.profiler.plist > (LaunchAgent persistence)
-
~/Library/Application Support/com.apple.Terminal/.cache > (AES-encrypted C2 config)
-
~/Library/Application Support/com.apple.Terminal/profiler (Persistance)
-
$TMPDIR/updater.lock (single-instance mutex)
-
$TMPDIR/updater.log (operational log via WriteLog)
Behavioral exec events:
-
scutil --get ComputerName
-
sysctl -n machdep.cpu.brand_string
-
ioreg -l | grep -e Manufacturer -e 'Vendor Name'
-
whoami
-
hostname
-
sw_vers -productVersion
-
uname -m
MITRE ATT&CK Mapping
| Tactic | Technique | ID | Observed Behavior |
|---|---|---|---|
| Initial Access | Supply Chain Compromise: Compromise Software Dependencies and Development Tools | T1195.002 | Malicious @velora-dex/sdk@9.4.1 npm package |
| Execution | Command and Scripting Interpreter: Unix Shell | T1059.004 | ExecuteCommand invokes /bin/sh -c |
| Persistence | Create or Modify System Process: Launch Agent | T1543.001 | ~/Library/LaunchAgents/com.apple.Terminal.profiler.plist |
| Persistence | Event Triggered Execution: Unix Shell Configuration Modification | T1546.004 | ~/.zshrc modified with launchctl submit |
| Defense Evasion | Virtualization/Sandbox Evasion: System Checks | T1497.001 | scutil, sysctl, ioreg string matching |
| Defense Evasion | Subvert Trust Controls: Code Signing | T1553.002 | Ad-hoc code signature |
| Defense Evasion | Masquerading: Match Legitimate Name or Location | T1036.005 | com.apple.Terminal.profiler label and Application Support/com.apple.Terminal/ path |
| Discovery | System Information Discovery | T1082 | whoami, hostname, sw_vers, uname -m |
| Discovery | System Owner/User Discovery | T1033 | whoami |
| Collection | Archive Collected Data | T1560 | Directories archived prior to exfiltration |
| Command and Control | Application Layer Protocol: Web Protocols | T1071.001 | HTTPS beaconing to C2 |
| Command and Control | Encrypted Channel: Symmetric Cryptography | T1573.001 | AES-CBC encrypted config |
| Command and Control | Ingress Tool Transfer | T1105 | download action |
| Exfiltration | Exfiltration Over C2 Channel | T1041 | upload action |