13661832872?profile=RESIZE_192XIn 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.

13661833095?profile=RESIZE_584xThe 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


13661833460?profile=RESIZE_584xOther 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.

13661833300?profile=RESIZE_710xThe 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.

     


13661833859?profile=RESIZE_584xBinary 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

13661833685?profile=RESIZE_710xTargeted 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.

13661833879?profile=RESIZE_710xThe 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"

13661834073?profile=RESIZE_710xComparison 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.

13661833894?profile=RESIZE_710xThe 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.

13661834453?profile=RESIZE_710xBoilerplate 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).

13661834274?profile=RESIZE_710xTwo 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.

13661834488?profile=RESIZE_710xHardcoded 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.


13661835054?profile=RESIZE_584xThe 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.


13661834680?profile=RESIZE_584xExecution 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

13661835087?profile=RESIZE_710xThe 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.

13661834901?profile=RESIZE_710xSignal 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.

13661835279?profile=RESIZE_710xPersistence 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.

13661835479?profile=RESIZE_710xA 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    

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/

E-mail me when people leave their comments –

You need to be a member of Red Sky Alliance to add comments!