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.[1]
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.
Below, researchers provide an overview of the attack chain and a technical analysis of the C++ and Nim-based components. Analysts 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 due to a 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 proper function.
The zoom_sdk_support.scpt is padded with 10,000 lines of whitespace; note the typo ‘Zook’ and the scroll bar at the top right.
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.
The analysis revealed several parallel domains in use by the same actor.
support.us05web-zoom[.]pro
support.us05web-zoom[.]forum
support.us05web-zoom[.]cloud
support.us06web-zoom[.]online
Other examples found in public repositories suggest a wider campaign, possibly with unique URLs for each target
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 a 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, as mentioned earlier, describe much the same initial attack chain. However, the second part of the attack chain is where things begin to diverge and become 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, 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, such as browser data and Telegram chat histories. All operations are staged from a folder created at ~/Library/DnsService.
The second execution chain begins with the installer binary, which is also a universal Mach-O executable compiled from Nim source code, and is responsible for setting up persistence. 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 the use of a C++-compiled binary, named a, which is deposited as a result of the initial infection through the fake Zoom update scripts described earlier. The 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 (also known as) 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 16 characters of the embedded base64 string.
The derived key and the base64 decoded encrypted data are passed to the AES-Encrypt 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 previous AesTrans call.
XORs the current encrypted data block with the current AesTrans result.
The AesTrans function is a wrapper of CCCrypt
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. The 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. Analysts 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. |
Binary Ninja’s Medium Level Interpreted Language (MLIL) representation of the command processing code
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
Targeted browsers in the upl script
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 tlgrm script targets the .tempkeyEncrypted file required for decryption
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"
Comparison of upl and tlgrm; the scripts use an almost identical function to exfiltrate user data
This investigation found related scripts in public malware repositories that may be tied to similar attacks. Analysts 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 installer binary prepares the file paths for later stages
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.
Boilerplate Nim code can look deceptively malicious
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.
Analysts 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).
Two versions of the installer binary are identical, save for the embedded config file path
In both cases, the 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.
Hardcoded data encrypted and written out to a hidden file /private/tmp/.config
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 LaunchAgent contains customized Client and Server keys for communication with the C2
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.
Execution chain once the persistence mechanism is activated by a login or reboot
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
The lookup table is hard-coded in the __const section
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 behavior 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 a user in the Terminal or by a program or script invokes the kill command.
Signal handlers 0x2 (SIGINT) and 0xf (SIGTERM) are set up to catch termination
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.
Persistence logic writes and sets execution permissions on the agent, trojan, and loader binaries.
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.
A string literal made up of hex characters is used to hide embedded AppleScript The embedded .ses script in the unstripped CoreKitAgent binary after decoding
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.
The embedded .ses script in the stripped CoreKitAgent binary after decoding
Conclusion – This 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 behavior 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, analysts saw threat actors utilizing Nim as well as Crystal, and Sentinel expects 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 |
This article is shared with permission at no charge for educational and informational purposes only.
Red Sky Alliance is a Cyber Threat Analysis and Intelligence Service organization. We provide indicators of compromise information via a notification service (RedXray) or an analysis service (CTAC). For questions, comments, or assistance, please get in touch with the office directly at 1-844-492-7225 or feedback@redskyalliance.com
- Reporting: https://www.redskyalliance.org/
- Website: https://www.redskyalliance.com/
- LinkedIn: https://www.linkedin.com/company/64265941
Weekly Cyber Intelligence Briefings:
REDSHORTS - Weekly Cyber Intelligence Briefings
https://register.gotowebinar.com/register/5207428251321676122
[1] https://www.sentinelone.com/labs/macos-nimdoor-dprk-threat-actors-target-web3-and-crypto-platforms-with-nim-based-malware/
Comments