Executive Summary
- DPRK threat actors are utilizing Nim-compiled binaries and multiple attack chains in a campaign targeting Web3 and Crypto-related businesses.
- Unusually for macOS malware, the threat actors employ a process injection technique and remote communications via
wss
, the TLS-encrypted version of the WebSocket protocol. - A novel persistence mechanism takes advantage of SIGINT/SIGTERM signal handlers to install persistence when the malware is terminated or the system rebooted.
- The threat actors deploy AppleScripts widely, both to gain initial access and also later in the attack chain to function as lightweight beacons and backdoors.
- Bash scripts are used to exfiltrate Keychain credentials, browser data and Telegram user data.
- SentinelLABS’ analysis highlights novel TTPs and malware artifacts that tie together previously reported components, extending our understanding of the threat actors’ evolving playbook.
In April 2025, Huntabil.IT observed a targeted attack on a Web3 startup, attributing the incident to a DPRK threat actor group. Several reports on social media at the time described similar incidents at other Web3 and Crypto organizations. Analysis revealed an attack chain consisting of an eclectic mix of scripts and binaries written in AppleScript, C++ and Nim. Although the early stages of the attack follow a familiar DPRK pattern using social engineering, lure scripts and fake updates, the use of Nim-compiled binaries on macOS is a more unusual choice. A report by Huntress in mid-June described a similar initial attack chain as observed by Huntabil.IT, albeit using different later stage payloads.
SentinelLABS’ analysis of the payloads used in the April incidents shows the Nim stages contain some unique features including encrypted configuration handling, asynchronous execution built around Nim’s native runtime, and a signal-based persistence mechanism previously unseen in macOS malware.
In this post, we provide an overview of the attack chain and a technical analysis of the C++ and Nim-based components. We refer to this family of malware collectively as NimDoor, based on its functionality and development traits. Indicators of compromise and insights into the malware’s architecture are provided to aid defenders and threat hunters in identifying related activity.
Initial Access and Payload Delivery
The attack chain begins with a now-familiar social engineering vector: impersonation of a trusted contact over Telegram and inviting the target to schedule a meeting via Calendly. The target is subsequently sent an email containing a Zoom meeting link and instructions to run a so-called “Zoom SDK update script”.
An attacker-controlled domain hosts an AppleScript file named zoom_sdk_support.scpt
. Variants of this script can be found in public malware repositories through the seemingly unintentional typo in a code comment: - - Zook SDK Update
instead of - - Zoom SDK Update
. The file is heavily padded, containing 10,000 lines of whitespace to obfuscate its true function.

The script ends with three lines of malicious code that retrieve and execute a second-stage script from a command-and-control server hosted at support.us05web-zoom[.]forum
. This domain name format has been chosen for similarity to the legitimate Zoom meeting domain us05web.zoom[.]us
.
Our analysis found a number of parallel domains in use by the same actor.
support.us05web-zoom[.]pro support.us05web-zoom[.]forum support.us05web-zoom[.]cloud support.us06web-zoom[.]online

The follow-on script downloads an HTML file named check
, which includes a legitimate Zoom redirect link.
<a ref="https://us05web.zoom[.]us/j/4724012536?pwd=ADlAXdxkUclRhvYoJbpKQmizkQ1RV4.1">Temporary Redirect</a>
This HTML file is passed to curl
and executed via run script
, ultimately launching the attack’s core logic.
Researchers at Validin have also recently published extended indicators around this and associated infrastructure. The posts by Huntabil.IT and Huntress mentioned earlier describe much the same initial attack chain. However, the second part of the attack chain is where things begin to get both different and increasingly complex.
Execution Chain and File Deployment
The multi-staged infection process Huntabil.IT observed resulted in the download of two Mach-O binaries—a
and installer
—into /private/var/tmp
. These two binaries set off two independent execution chains.
In the first, the a
binary is a C++-compiled universal architecture Mach-O executable. It writes an encrypted embedded payload called netchk
to disk. The execution from here involves a complex chain of obfuscation and distraction which we describe in the following section. Ultimately, the aim is to fetch two Bash scripts used for data exfiltration. These include mechanisms for scraping general system data as well as application-specific data like browser data and Telegram chat histories. All operations are staged from a folder created at ~/Library/DnsService
.
The second execution chain starts with the installer
binary, which is also a universal Mach-O executable compiled from Nim source code, and is responsible for persistence setup. It drops two additional Nim-compiled binaries: GoogIe LLC
(where “GoogIe” is spelled using a deceptive capital “i” rather than a lowercase ‘L’) and CoreKitAgent
. These payloads orchestrate long-term access and recovery mechanisms for the threat actor.
Technical Analysis of a, netchk and trojan1_arm64
Both Huntabil.IT and Huntress describe use of a C++-compiled binary with the name a
being deposited as a result of initial infection through the fake Zoom update scripts described earlier.
The a
binary is ad hoc signed and carries the identifier InjectWithDyldArm64
. As reported by previous researchers, it can take a command line argument --d
, which results in the deletion of a
‘s current working directory, or a file name and password. In the Huntabil.IT post, this was reported as:
./a ./netchk gift123$%^
The InjectWithDyldArm64
(aka a
) binary uses Password-Based Key Derivation Function 2 (PBKDF2) with HMAC-SHA-256 to derive a 32-byte key from the password gift123$%^
, using 10000 iterations and a salt consisting of the first sixteen characters of the embedded base64 string.
The derived key and the base64 decoded encrypted data are passed to the AesEncrypt
function, which iterates through 16 byte blocks of the encrypted data. On each iteration it:
- calls
AesTrans
, a wrapper for CCCrypt, to perform an AES encryption in CBC mode with the derived key and a zero-filled initialization vector. In the first iteration the data to be encrypted is the key itself, but in subsequent iterations the input data is taken from the previousAesTrans
call. - XORs the current encrypted data block with the current
AesTrans
result.

SentinelLABS’ analysis shows that this process is used to decrypt two embedded binaries. The first carries an ad hoc signature and the identifier Target
. The second has an ad hoc signature with the identifier trojan1_arm64
. The Target
binary is benign and appears to do nothing other than generate random numbers.
However, Target
is spawned by InjectWithDyldArm64
in a suspended state via
posix_spawnattr_init(&attrp) && !posix_spawnattr_setflags(&attrp, POSIX_SPAWN_START_SUSPENDED) posix_spawn(&pid, filename, 0, &attrp, argv_1, environ)
and injected with the trojan1_arm64
binary’s code. After injection, the suspended Target
process is resumed via
kill(pid, SIGCONT)
and the code from the trojan1_arm64
binary is executed.
This kind of process injection technique is rare in macOS malware and requires specific entitlements to be performed; in this case, the InjectWithDyldArm64
binary has the following entitlements to allow the injection:
com.apple.security.cs.debugger com.apple.security.get-task-allow
After first negotiating an HTTP handshake, the injected code uses wss
to communicate with the C2 – another uncommon technique for macOS malware – at wss://firstfromsep[.]online/client
.
The malware uses multiple levels of RC4 encryption in combination with the base64 encoding and three different keys before the communication.
Our analysis found that the communication messages from the C2 use a JSON format of {"name":"","payload":"","target":""}
. The name field takes the value auth
or message
.
When the auth
value is used, the payload
field has the JSON structure {"uid":"","cipher":""}
, where the uid
field contains a generated uid value and the cipher
field contains the uid value encrypted using the key Ej7bx@YRG2uUhya#50Yt*ao
and then encoded in base64. We suspect the target
field is used for the victim identifier.
When the message
value is used, the payload
field value is encrypted using the key 3LZu5H$yF^FSwPu3SqbL*sK
. The payload has the JSON structure {"cmd":, "data":""}
where the cmd
field contains an int value for the command to be executed. Available commands we were able to identify in trojan1_arm64
were as follows:
Command | Code | Function |
execCmd | 12 | Execute the arbitrary command provided in the data field. |
setCwd | 34 | Change the Current Working Directory to the one given in the data field. |
getCwd | 78 | Get the Current Working Directory. |
getSysInfo | 234 | Get information about the system such as boot time, username, macOS version, machine name, platform and arch. |

The result of an executed command is returned to the C2 in the payload
field, now having the form {"cmd":,"err":,"data":""}
, where cmd
contains the int value related to the command that was executed, err
contains an int value related to success or failure, and data
contains the results of the executed command. For example, when a getSysInfo
command is executed, the data
field will be populated with values in a JSON structure of the form {"boottime":,"username":"","version":"","comname":"","platform":"","arch":""}
.
The whole JSON message is encrypted using the key lZjJ7iuK2qcmMW6hacZOw62
.
Data Stealing Bash Scripts
The first part of the attack chain concludes with trojan1_arm64
downloading and executing two scripts, upl
and tlgrm
.
The upl
script is a credential-stealer designed to silently extract browser and system-level information, package it, and exfiltrate it. The script targets data from the following browsers:
- Arc
- Brave
- Firefox
- Google Chrome
- Microsoft Edge

Browser data is copied to
/private/var/tmp/uplex_<username>/<browser>/
The script also targets the following Keychain and shell files and directories:
/Library/Keychains/System.keychain ~/Library/Keychains/login.keychain-db ~/.bash_history ~/.zsh_history ~/.zsh/
The data is then compressed via ditto -ck
and posted to the C2 using curl
.
The tlgrm
script steals Telegram’s encrypted local database (postbox/db
) and the decryption key blob, .tempkeyEncrypted
, presumably for offline decryption or brute force attempts.

The Telegram data is exfiltrated to the same server used in the upl
script. The uploadData()
function in both scripts is identical save for one variable name used to specify the server address: hostName
in upl
and serverUrl
in tlgrm
.
upl: hostName="https[:]//dataupload[.]store/uploadfiles" tlgrm: serverUrl="https[:]//dataupload[.]store/uploadfiles"

Our investigation found related scripts in public malware repositories that may be tied to similar attacks. We list these in the Indicators of Compromise section at the end of this post.
Technical Analysis of installer, GoogIe LLC, and CoreKitAgent
Installer
The second part of the attack chain begins with the installer
binary dropped alongside a
by the initial access scripts. Compiled from Nim and weighing in at ~233KB, the installer
binary is a universal architecture Mach-O with an ad hoc signature and the identifier user_startup_installer_arm64
.
The installer
binary checks for the existence of a LaunchAgent at [~]/Library/LaunchAgents/com.google.update.plist
and creates folder paths at [~]/Library/CoreKit/
and [~]/Library/Application Support/GoogIe LLC/
for use by the later stages described in the following sections.

The misspelling of GoogIe LLC
(uppercase ‘i’, not lowercase ‘L’) is intended to help the malware blend in and avoid suspicion.
An interesting feature of this and the other compiled Nim binaries is the existence of code that at first blush could be mistaken for C2 command options.

Huntress researchers also reported observing a subset of these “po” commands in their analysis. Nim documentation reveals that these are part of Nim’s std/osproc module, used for executing OS processes, similar to the way Objective-C uses NSTask, and are not attacker-written code or malware artifacts.
We identified two versions of the installer
binary, identical except for the path used to set up the config file used by later stage payloads. One version of installer
uses /private/tmp/cfg
(06566eabf54caafe36ebe94430d392b9cf3426ba) while the other uses /private/tmp/.config
(08af4c21cd0a165695c756b6fda37016197b01e7).

In both cases, installer
checks that the file does not exist, then writes a 0 byte file to the path, setting write-only access (O_WRONLY
) on the file. The file path contents are populated by the next stage GoogIe LLC
and later read by CoreKitAgent
.
GoogIe LLC
Compiled from Nim and approximately 195KB, the GoogIe LLC
executable is a universal Mach-O bearing an ad hoc code signature with the identifier user_startup_loader_arm64
. Interestingly, only the filename for this stage uses the typo spoofing trick; the parent folder /Google LLC/
spells Google correctly with a lowercase “L”.
~/Library/Application Support/Google LLC/GoogIe LLC
The binary’s primary function is to set up a configuration file and launch the next stage, CoreKitAgent
. The GoogIe LLC
executable contains hardcoded data that is combined with local environmental data, encoded, and then written out to the config file in /private/tmp
.

The resulting config file contains a 298 byte string of hexadecimal characters. This is later read by CoreKitAgent
, which is responsible for writing the LaunchAgent to disk using com.google.update.plist
for the Label key and the GoogIe LLC
binary for the program argument. The data written to the config file is used as the value for the LaunchAgent’s CLIENT_AUTH_KEY key.

The first 47 characters of the value of CLIENT_AUTH_KEY are also identical to the first 47 characters (of the total 86) used for the value of SERVER_AUTH_KEY.
When the LaunchAgent is activated by a user login or reboot, GoogIe LLC
is launched, which in turn calls CoreKitAgent
and the rest of the payload logic.

CoreKitAgent
Of the four Nim binaries observed, CoreKitAgent
is the most technically complex. It exists in both an unsigned stripped (~233KB) version and an ad hoc signed, unstripped (~340KB) version. VirusTotal telemetry indicates that the stripped version was uploaded from South Korea in October 2024. The unstripped version was observed in the wild in early April 2025. Although it is a universal binary, the ad hoc signature identifies the binary as user_startup_main_arm64
.
The CoreKitAgent
program operates as an event-driven application using macOS’s kqueue mechanism. The _main
function sets up Nim’s standard signal handlers and then hands control to an asynchronous continuation function _mainX20X28AsyncX29___user95startup95main_u45
, which initiates the malware’s core logic.
Control flow within this function is handled by a 10-case switch statement implemented as a table-driven state machine, with an execution state stored in memory and updated via a lookup table with the following values:
00 00 fe ff 03 00 00 00 fe ff fe ff fe ff fe ff

After each case completes, the code consults the lookup table to determine the next state depending on error conditions or specific flags set during execution of the previous case. For example, if certain error bits are set, the next state value might be negated, effectively creating alternate paths through the state sequence depending on runtime conditions.
From an analysis point of view, much of the interesting malicious behaviour occurs in Case 1, which overrides some of the earlier signal handlers for intercepting SIGINT and SIGTERM. SIGINT is sent to a process when a user attempts to kill a process (such as by pressing ctl-c in the Terminal). SIGTERM, in contrast, is a signal sent by the system or another process, such as when the kill
command is invoked by a user in the Terminal or by a program or script.

When triggered, CoreKitAgent
catches these signals and writes the LaunchAgent for persistence, a copy of GoogIe LLC
as the loader, and a copy of itself as the trojan, setting executable permissions on the latter two via the addExecutionPermissions_user95startup95mainZutils_u32
function.

This behavior ensures that any user-initiated termination of the malware results in the deployment of the core components, making the code resilient to basic defensive actions.
The state machine also contains a 10-minute sleep routine, likely as an anti-VM or sandbox countermeasure. The sleep routine is set up and called in Case 6 with a hard-coded value of 0x927c0 (600,000ms), as indicated in the following pseudocode.
void* rax_29 = _sleepAsync__user95startup95main_u73(0x927c0); // 600,000ms = 10min if (*r12 != 0) _eqdestroy___pureZasyncdispatch_u1229(rax_29); // Error cleanup else { _eqsink___pureZasyncdispatch_u7188(rsi_1 + 0x40, rax_29); // Store future if (*r12 == 0) { *(r15 + 8) = 7; // Transition to state 7 rsi_15 = *(r15 + 0x40); } }
The sleep function, _sleepAsync__user95startup95main_u73
, uses the operating system’s mach_absolute_time() and mach_timebase_info() to create an asynchronous sleep. Rather than just blocking execution for 10 minutes – a technique many sandboxes would detect and counter – it instead registers a wake-up time with a global dispatcher and continues execution of the main event loop. When the sleep timer expires, CoreKitAgent
calls Case 7 and continues execution.
AppleScript Beacon and Backdoor
The malware’s custom encryption and obfuscation routines involve multiple passes through several functions. One of these involves deobfuscating string literals made up of long sequences of hexadecimal numbers that are passed to a decrypt function, _fromHex__pkgZnimcryptoZutils_u257
.
In the unstripped version, one of the hexadecimal strings contains the template for the previously discussed LaunchAgent. In both versions, although the content differs, an AppleScript is decoded, written to disk at ~/.ses
, and launched via osascript
.


The embedded AppleScript fetches the current Unix timestamp via date
to create a unique ID and builds an HTTP header string. Throughout, the authors have broken strings down into character lists to help protect the script from simple scanning rules. The same trick is used to disguise two hardcoded C2 addresses, writeup[.]live
and safeup[.]store
.
On execution, the script beacons out every 30 seconds to one of the two hardcoded C2s, chosen at random, and attempts to post data obtained from listing all running processes on the victim machine. The script also executes any response received from the C2 via the run script
command, meaning this simple AppleScript functions both as a beacon and a backdoor.
The embedded AppleScript in the stripped version of CoreKitAgent
takes a different form and uses different embedded C2 server addresses but has similar functionality, including the 30 second delay interval.

Conclusion
SentinelLABS’ analysis of NimDoor shows how threat actors are continuing to explore cross-platform languages that introduce new levels of complexity for analysts.
North Korean-aligned threat actors have previously experimented with Go and Rust, similarly combining scripts and compiled binaries into multi-stage attack chains. However, Nim’s rather unique ability to execute functions during compile time allows attackers to blend complex behaviour into a binary with less obvious control flow, resulting in compiled binaries in which developer code and Nim runtime code are intermingled even at the function level.
At the same time, the attackers take full advantage of macOS’s built-in scripting capabilities. Leveraging AppleScript to perform duties like beaconing is a novel approach that removes the need for a traditional post-exploitation framework and the detection ‘noise’ such implants can create. In addition, the use of wss
for communications and signal interrupts to trigger persistence logic provide yet further evidence of active development in new ways to defeat security measures.
Earlier this year, we saw threat actors utilizing Nim as well as Crystal, and we expect the choice of less familiar languages to become an increasing trend among macOS malware authors due both to their technical advantages and their unfamiliarity to analysts. As ever in the cat-and-mouse game of threat and threat detection, when one side innovates, the other must respond, and we encourage other analysts, researchers, and detection engineers to invest effort in understanding these lesser-known languages and how they will eventually be leveraged.
Indicators of Compromise
Domains
dataupload[.]store | upl/tlgrm C2 |
firstfromsep[.]online | netchk C2 |
safeup[.]store | CoreKit C2 |
support[.]us05web-zoom[.]pro | zoom_sdk_support.scpt C2 |
writeup[.]live | CoreKit C2 |
FilePaths
~/Library/Application Support/Google LLC/GoogIe LLC
~/Library/LaunchAgents/com.google.update.plist
~/.ses
~/Library/CoreKit/CoreKitAgent
~/Library/DnsService/a
~/Library/DnsService/netchk
/private/tmp/.config
/private/tmp/cfg
/private/var/tmp/uplex_
Binaries | SHA-1
027d4020f2dd1eb473636bc112a84f0a90b6651c | trojan1_arm64 (x86_64) |
0602a5b8f089f957eeda51f81ac0f9ad4e336b87 | GoogIe LLC (universal) |
06566eabf54caafe36ebe94430d392b9cf3426ba | installer (universal) |
08af4c21cd0a165695c756b6fda37016197b01e7 | installer (universal) |
16a6b0023ba3fde15bd0bba1b17a18bfa00a8f59 | GoogIe LLC (arm64) |
1a5392102d57e9ea4dd33d3b7181d66b4d08d01d | CoreKitAgent (x86_64) |
2c0177b302c4643c49dd7016530a4749298d964c | CoreKitAgent (arm64) |
2d746dda85805c79b5f6ea376f97d9b2f547da5d | netchk (arm64) |
2ed2edec8ccc44292410042c730c190027b87930 | trojan1_arm64 (arm64) |
3168e996cb20bd7b4208d0864e962a4b70c5a0e7 | GoogIe LLC (x86_64) |
5b16e9d6e92be2124ba496bf82d38fb35681c7ad | a (universal) |
7c04225a62b953e1268653f637b569a3b2eb06f8 | installer (arm64) |
945fcd3e08854a081c04c06eeb95ad6e0d9cdc19 | CoreKitAgent (universal) |
a25c06e8545666d6d2a88c8da300cf3383149d5a | CoreKitAgent (universal) |
c9540dee9bdb28894332c5a74f696b4f94e4680c | GoogIe_LLC (universal) |
e227e2e4a6ffb7280dfe7618be20514823d3e4f5 | installer (x86_64) |
ee3795f6418fc0cacbe884a8eb803498c2b5776f | netchk (x86_64) |
Scripts
Observed
023a15ac687e2d2e187d03e9976a89ef5f6c1617 | zoom_sdk_support.scpt |
bb72ca0e19a95c48a9ee4fd658958a0ae2af44b6 | tlgm |
4743d5202dbe565721d75f7fb1eca43266a652d4 | upl |
Related
1e76f497051829fa804e72b9d14f44da5a531df8 | expl (upl variant) |
79f37e0b728de2c5a4bfe8fcf292941d54e121b8 | upl (upl variant) |