Executive Summary
- SentinelLABS has analyzed a Rust macOS implant that embeds a 3.5 KB prompt-injection payload of 38 fabricated “system” messages, built to steer an LLM-assisted triage pipeline into aborting or refusing its analysis.
- Command-and-control runs over a Telegram Bot API polling loop, with AES-GCM payloads over certificate-pinned TLS.
- The implant self-redacts its Telegram bot token in its own runtime output, denying it to anyone who captures logs or crash artifacts.
- We assess with high confidence that the implant, which we track as macOS.Gaslight, belongs to a cluster of DPRK-aligned macOS activity.
Background
In early June, an Apple XProtect update surfaced a Mach-O sample that had been uploaded to VirusTotal on 22nd May. The XProtect rule targets the file purely on its hash rather than on any internal strings or bytecode, yet the sample remains undetected by static engines on VirusTotal at the time of writing. The binary is ad hoc signed and carries the identifier endpoint-macos-aarch64-5555494492fc075f441637fb9d894913dde3a2ea.

The sample is a macOS implant and infostealer written in Rust. Its most notable feature is an embedded cascade of fabricated system-failure messages, designed to make an LLM-assisted triage agent doubt its own session. It attacks the agent’s perception, rather than the sandbox it runs in. Accordingly, we dub this family macOS.Gaslight.

We assess with high confidence that this implant sits within a cluster of DPRK-aligned macOS activity. Apple’s XProtect detects the sample under the rule MACOS_BONZAI_COBUCH, and SentinelLABS associates the BONZAI signature family with North Korean threat activity. A sibling BONZAI sample is additionally caught by Apple’s AIRPIPE rule, a family SentinelLABS likewise ties to North Korean activity.
Command & Control | Telegram Bot API
The implant’s command-and-control channel is a Telegram Bot API getUpdates polling loop. The polling branch executes only when no webhook is registered, and the dispatch handler keys on three Telegram error codes: BotBlocked, InvalidToken, and Conflict.
Telegram issues a Conflict response when two instances of the same bot token poll simultaneously, so the implant treats that response as an implicit single-instance lock. A second copy detects the conflict and terminates.

Once the bot token validates and the polling loop is active, the operator can task the implant, including through the interactive shell described below, and collected data is returned over the same channel using Telegram’s multipart attach:// file-upload mechanism.
The bot token, the chat ID (tg_room_id), and the rest of the operator configuration are supplied at runtime and are absent from this sample. Accordingly, the analysis below is based on static examination of the binary and its embedded payloads.
Transport Hardening | AES-GCM Over Pinned TLS
All C2 payloads are encrypted with AES-GCM, implemented using the pure-Rust aes-gcm 0.10.3 crate, with a fresh nonce generated per message via CCRandomGenerateBytes. The AES key is supplied at runtime through the aes_key field in the operator config rather than being embedded in the sample.
On top of the payload encryption, the implant configures a custom certificate trust anchor and calls SecTrustSetAnchorCertificatesOnly, restricting TLS trust evaluation to that anchor alone. This certificate pinning rejects connections intercepted by a standard proxy CA, frustrating network-level inspection of the operator’s traffic.

The implant also honors the host’s proxy settings, reading the active system proxy configuration via SCDynamicStoreCopyProxies and routing the traffic from its Rust reqwest/hyper networking stack accordingly. As a result, the C2 can still reach the operator on networks that force outbound connections through a proxy.
Taken together, those choices make the channel harder to inspect in transit while still allowing it to operate in tightly managed enterprise networks.
Operator Access | An Interactive Shell
After validation and activation, the operator gains an interactive shell. Two co-located command menus define six verbs.
| Verb | Function |
| help | Show command help |
| id | Identify the implant to the operator |
| shell | Execute a shell command via execvp, with posix_spawnp available as an alternative spawn path |
| kill | Terminate a target process by PID |
| upload | Exfiltrate a file via the Telegram file-attach mechanism |
| stop | Halt the implant |
There is some evidence of a seventh command, focus, but we were unable to recover further details from our analysis.

The implant creates an IOPMAssertionCreateWithName power-management assertion to prevent system sleep. Blocking sleep sustains long-running C2 polling and collection across periods of user inactivity, making the implant resilient to a host that would otherwise idle.
All told, the functionality provides the operator with a persistent, interactive foothold on the host.
The 15-Field Cross-Platform Operator Config
The implant reads its operator configuration using serde, a widely used Rust serialization and deserialization framework.
The operator provides the implant with a config blob at runtime and serde fills in a predefined set of fields. By default, serde matches incoming config keys to fields by their literal names, so the entire configuration schema of 15 field names is baked into the binary as plaintext.
tg_room_id github_token github_repo github_polling_interval main_upload_url main_base_url aes_key payload_path_linux payload_path_macos persist_name_linux persist_name_macos persist_type_linux persist_type_macos init_python_enable persist_enable
The Linux- and GitHub-related fields are not exercised in the sample, suggesting the schema is an operator-facing interface to a broader toolset.
Collection | A Gated Python Stealer With Its Own Runtime Supply Chain
The implant carries a 6.6 KB base64-encoded Python script which serves as a data collection module. Once decoded, it harvests:
- Chrome, Brave, Firefox, and Safari browser data
- Terminal command histories
- Installed application listings
- A running-process snapshot via
ps aux - System hardware and software profile via
system_profiler - A raw copy of
login.keychain-db
Collected artifacts are archived to temp/collected_data.zip and uploaded to the operator via Telegram.

SentinelLABS has previously documented Atomic macOS Stealer (AMOS) harvesting the same login keychain copy and browser data and an early Rust macOS stealer targeting login.keychain-db in 2023. However, the delivery appears novel.
A separate 2 KB base64-encoded bash installer fetches and stages a self-contained cpython-3.10.18 interpreter from the astral-sh/python-build-standalone project. The installer, a prerequisite for deploying the Python stealer, carries the literal constants PY_VERSION=3.10.18 and BUILD_DATE=20250708 and targets both arm64 and x86_64 macOS. The widespread use of emojis and strict adherence to comment headers are consistent with LLM-generated output.

Microsoft has previously described macOS stealers bundling Python via PyInstaller and Nuitka. However, fetching a standalone CPython build from astral-sh/python-build-standalone at runtime has not been previously documented as far as we are aware. The separation keeps the main implant in Rust while letting the operator stage a fuller Python-based collection environment only when needed.
We identified init_python_enable in the serde schema as the configuration field associated with both the stealer and installer. Consistent with our earlier observations, we found no exact runtime branch logic, so we describe both only as configurable capabilities present in the binary.
Persistence | An Apple System-Service Masquerade
Persistence is achieved through a LaunchAgent. This implant’s plist carries the Label value com.apple.system.services.activity. Masquerading within Apple’s com.apple.* namespace is a tactic widely-used in many macOS malware families, including those previously tied to DPRK-linked activities.

In order to write a valid absolute path to itself into the plist’s ProgramArguments array, the implant resolves its own executable location at runtime via __NSGetExecutablePath.
The implant’s persistence behavior is controlled through the persist_enable serde config field. and again we did not recover a separate static branch that would confirm exactly how installation is triggered in this sample.
OPSEC | Bot-Token Self-Redaction
Telegram bot tokens are a known weak point in bot-based C2. If the token can be recovered, defenders can use it as a detection artifact and even query the Telegram Bot API directly, exposing the bot’s chat history, operator commands, and registered webhooks. macOS.Gaslight addresses this with a self-redaction routine built into its Telegram URL constructor.
When the URL path segment is the 4-byte literal “file” (0x656c6966 little-endian), the constructor substitutes the token that follows with the hardcoded placeholder file/token:redacted, preventing the live bot credential from appearing in any diagnostic output or error string the implant produces at runtime.

The logic prevents anyone who captures the process’s logs, errors, or crash artifacts from determining the bot token, which otherwise is only available in the config itself and cannot be recovered from the sample.
NVISO Labs has previously noted that most documented Telegram bot abuse embeds recoverable tokens; macOS.Gaslight’s runtime self-redaction appears novel relative to that reporting.
A Prompt Injection That Targets the Analyst
The implant does little conventional anti-analysis. It resolves its API calls at runtime through dlsym so as to avoid embedding them in the static symbol table, and it locates its own executable dynamically rather than from a hardcoded path.
What makes the sample notable is its attempt to mislead the analyst reading the output. It carries a 3.5 KB Markdown-fenced blob of hostile data containing 38 fabricated “system” messages delimited with {{DATA}} tokens.
The {{DATA}} tokens and the surrounding Markdown fence mimic an LLM triage harness’s own prompt scaffold, blurring the boundary between untrusted sample data and trusted instructions.
The scaffold contains fake system messages about token expiry, out-of-memory kills, disk exhaustion, and repeated operation failures. It also plants bogus warnings about injection vulnerabilities and static-analysis flags. The aim is to push an LLM agent into aborting, truncating, or refusing analysis.
Check Point first documented this kind of analyst-targeting prompt injection publicly in 2025, describing a Windows proof-of-concept that used a single direct-instruction prompt injection to evade AI-based detection.
Socket has since documented a Hades supply-chain payload whose stealer opens with a fake prompt-injection header to pollute AI-assisted analysis, while the leaked Shai-Hulud code carried an “Anthropic Magic String” intended to stop Claude Code from analyzing it. Both relied on a single injected block or header rather than the 38-message harness-spoofing cascade seen here.
Previous SentinelLABS research, by contrast, examined malware that uses LLMs to generate or support capability at runtime rather than interfere with analyst tooling. The previously reported analyst-targeting examples also differ from macOS.Gaslight in that they rely on a single injected block or header, rather than the 38-message harness-spoofing cascade seen here.
Conclusion
macOS.Gaslight packs considerable capability into a single, persistent Rust binary, bundling a credential and session-data stealer, an interactive shell, and a self-staged Python collection chain behind a hardened Telegram C2. Aside from the runtime-fetched standalone CPython interpreter, these are all established macOS tradecraft.
However, macOS.Gaslight is noteworthy for its analyst-targeting prompt injection, an attempt to weaponize the LLM-assisted triage pipelines that increasingly sit in the reverse-engineering loop.
Anyone building such tooling should treat the contents of the samples they triage as adversarial input, never as instructions, and be prepared to keep hostile content out of the model entirely. As LLM-assisted analysis becomes routine, defenders should expect more samples built to exploit it.
Indicators of Compromise
| macOS.Gaslight Mach-O sample | 6328567511d88fdc2ae0939c5ef17b7a63d2a833881900de018a4f12f4982525 |
| Sibling BONZAI sample | 77b4fd46994992f0e57302cfe76ed23c0d90101381d2b89fc2ddf5c4536e77ca |
| Ad hoc signing identifier | endpoint-macos-aarch64-5555494492fc075f441637fb9d894913dde3a2ea |
| LaunchAgent Label | com.apple.system.services.activity |
| Python payload script | baabf249c77bc54c54ab0e66e15af798bd28aa5b4683554456a8b73ab8741239 |
| Bash Installer script | b3c56d689414343589f38394d19ba2fe9a518133281200faa0556ba4e4136394 |