From ClickFix to code signed: the quiet shift of MacSync Stealer malware

Learn how MacSync Stealer malware has evolved from drag-to-terminal tricks to sophisticated code-signed Swift applications.

December 22 2025 by

Jamf Threat Labs

A lit Christmas tree is in the foreground of a dark room. A present lays at its base, emblazoned with a malicious app's icon

By Thijs Xhaflaire

Introduction

While reviewing the detections of our in-house YARA rules, Jamf Threat Labs observed a signed and notarized stealer that did not follow the typical execution chains we have seen in the past. The sample in question looked highly similar to past variants of the increasingly active MacSync Stealer malware but was revamped in its design.

Unlike earlier MacSync Stealer variants that primarily rely on drag-to-terminal or ClickFix-style techniques, this sample adopts a more deceptive, hands-off approach. Delivered as a code-signed and notarized Swift application within a disk image named zk-call-messenger-installer-3.9.2-lts.dmg , distributed via https://zkcall.net/download, it removes the need for any direct terminal interaction. Instead, the dropper retrieves an encoded script from a remote server and executes it via a Swift-built helper executable.

Jamf Threat Labs has also observed the Odyssey infostealer adopting similar distribution methods in recent variants. Surprisingly, the familiar right-click open instruction is still present in this sample even though the executable is signed and does not require this step.

window with zk-call and messenger, telling the user to right click and then hit open.

Installation instructions

After inspecting the Mach-O binary, which is a universal build, we confirmed that it is both code signed and notarized. The signature is associated with the Developer Team ID GNJLS3UYZ4.

Installer details showing notarization

We also verified the code directory hashes against Apple’s revocation list, and at the time of analysis, none had been revoked.

SHA1, Symhash and CDHash hashes and details showing no revocation

Hashes, none of which are revoked

Another notable observation is the unusually large size of the disk image (25.5MB), which appears to be inflated by decoy files embedded within the app bundle. These include PDFs related to LibreOffice applications.

List of files in the disk image, including two large PDFs to inflate its size

Disk image containing decoy files to inflate its size

At the time of analysis, some of the samples uploaded to VirusTotal were detected by only one antivirus engine, while others were flagged by up to thirteen. Most engines classify them as generic downloaders associated with either the coins or ooiid malware families.

After confirming that the Developer Team ID was used to distribute malicious payloads, Jamf Threat Labs reported it to Apple. Since then, the associated certificate has been revoked.

Initial detection

Most payloads related to MacSync Stealer tend to run primarily in memory and leave little to no trace on disk. Earlier variants are often flagged by Jamf's advanced threat controls, as they typically rely on either a drag-to-terminal approach, where users drop a script file into Terminal, or a ClickFix-style technique that tricks users into pasting a base64 encoded command. In both cases, the payload is decoded using base64 -D, decompressed with gunzip, stored in a variable and executed using eval. This then results in the fetching of a second-stage payload via curl.

In this case, however, it was a threat prevention (YARA) rule monitoring for the execution of obfuscated bash scripts that alerted us, highlighting a subtle but important deviation from previously seen behavior.

Looking at the matched event showed a shell script running at /tmp/runner which raised our suspicions.

The matched event showing a script path in /tmp

Upon further inspection of the matched event, reviewing the responsible process object tied to the script execution becomes revealing, as we can clearly see it was launched from a signed application. The appPath also indicates it is being executed directly from within a mounted disk image, adding to the suspicion.

App path details; app path is in in /Volumes/

App path details

After analyzing the /tmp/runner payload, it becomes clear this is the same script previously seen in MacSync Stealer campaigns. In earlier variants, it was typically executed without being written to disk.

Obfuscated payload

Obfuscated payload

Once decoded, the base64 payload is a match to the usual MacSync Stealer. The same focusgroovy[.]com domain was used in previous payloads as well as an identical daemon_function().

Deobfuscated payload showing a daemon_function used in other MacSync Stealers

Decoded payload

A brief analysis of the Swift-based Mach-O

_main

This section will focus on the universal runtimectl Mach-O binary that comes packaged with the malicious application bundle to validate the earlier observed behavior. The _main function serves as the entry point for the binary. It sets up the application’s state and logging paths, performs a basic internet connectivity check, and if successful, retrieves the second-stage payload.

After resolving the user’s home directory using _NSHomeDirectory(), the application builds several paths for its operation. It creates a log file at ~/Library/Logs/UserSyncWorker.log to record activity and then creates a directory at ~/Library/Application Support/UserSyncWorker/. Within that directory, it maintains additional files such as last_up and gate, used to track execution timing and update state. These files are only created if they do not already exist from previous executions.

The _main function in the Mach-O binary

The _main function in the Mach-O binary

A log message indicating the start of execution is written to ~/Library/Logs/UserSyncWorker.log, and a minimum interval of approximately 3600 seconds is defined, likely to prevent the executable from running multiple times within a short period.

Next, the application performs a conditional check for internet access by calling checkInternet(). Only if connectivity is confirmed does it proceed to execute runInstaller().

A check for internet access and the logging of a string

Checking for internet access

After calling runInstaller() (discussed later), the string Starting update... is logged using log().

If no internet connection is available, another message preflight: internet=false is logged, and the process exits cleanly using _exit(1).

Regardless of the path taken, the application retrieves the current date and time, formats it into a localized string and prints it to the console using print(). It then creates or updates the log file at ~/Library/Logs/UserSyncWorker.log.

This conditional execution logic, tied directly to network availability, reflects an effort to avoid execution in offline or sandboxed environments.

_runInstaller()

A closer look at therunInstaller() function sheds light on how the dropper executes its second-stage payload.

The runInstaller() function implements the full second stage execution logic and acts as a downloader and execution routine. It begins by enforcing a rate limit by reading a previously stored timestamp from a file located in ~/Library/Application Support/UserSyncWorker/last_up. If the file does not exist or if the last recorded execution was more than approximately 3600 seconds ago, the routine proceeds. Otherwise, it logs a message indicating that execution is being deferred due to rate limiting and exits. Below, example log entries indicate an execution has been rate limited.

Once the timing conditions are met, the application invokes checkInternet() again and logs the result. It then removes any previously dropped files from /tmp, such as /tmp/runner. Below are example log entries showing that the preflight internet check was successful.

Next, the function prepares a conditional HTTP request to: https[:]//gatemaden.space/curl/985683bd660c0c47c6be513a2d1f0a554d52d241714bb17fb18ab0d0f8cc2dc6 with a specific user agent UserSyncWorker/1.0 (macOS).

A conditional HTTP request

HTTP request

This request is built using /bin/zsh -lc through NSTask, and the payload is written to /tmp/runner along with headers for validation in /tmp/runner.headers.

Notably, the curl command used to retrieve the payload shows clear deviations from earlier variants. Rather than using the commonly seen -fsSL combination, the flags have been split into -fL and -sS, and additional options like --noproxy have been introduced. These changes, along with the use of dynamically populated variables, point to a deliberate shift in how the payload is fetched and validated, likely aimed at improving reliability or evading detection.

The Curl command for retrieving the payload

Curl command for retrieving the payload

Before launching the payload, the function removes the com.apple.quarantine attribute via removeQuarantine(at:) and ensures the file is executable using _NSFilePosixPermissions with permissions 750 being set.

function clearing the com.apple.quarantine extended attribute

Prior to payload execution, the function clears the com.apple.quarantine extended attribute

Basic validation is performed by first checking if the payload is a script. This is done by shelling out to /usr/bin/file --mime-type -b flags on /tmp/runner and confirming the returned value matches text/x-shellscript. It also runs a separate /usr/bin/file -b command to validate that the output contains the expected string, such as Paul Falstad's zsh script text executable, ASCII text to ensure it's a zsh script. A Gatekeeper check is then performed using spctl -a -v to ensure the downloaded file passes Apple’s security policy.

Finally, after execution completes, the /tmp/runner payload is deleted from disk and the current timestamp is written back to disk at ~/Library/Application Support/UserSyncWorker/last_update to enforce the minimum interval before the next run.

Overall, runInstaller() implements a layered, stateful and evasive dropper routine. It combines environment checks, throttling logic and network requests with conditional updates, Gatekeeper evasion and lightweight validation, all wrapped in a native Swift executable for stealth and persistence. Once the payload has been executed, the typical osascript dialog appears, followed by other behaviors commonly associated with MacStealer activity.

Conclusion

While MacSync Stealer itself is not entirely new, this case highlights how its authors continue to evolve their delivery methods. We have not previously observed this specific dropper, which arrives as a Swift-based, code-signed and notarized application that silently retrieves and executes a second-stage payload.

This shift in distribution reflects a broader trend across the macOS malware landscape, where attackers increasingly attempt to sneak their malware into executables that are signed and notarized, allowing them to look more like legitimate applications. By leveraging these techniques, adversaries reduce the chances of being detected early on.

Jamf Threat Labs will continue to track these developments as threat actors refine their tactics and explore new ways to deliver macOS malware.

We strongly recommend that customers ensure threat prevention and advanced threat controls are enabled and set to block mode in Jamf for Mac to stay protected against these latest infostealer variants.

Indicators of compromise

Indicators of compromise (IoCs) are listed below. You can also explore the full collection on VirusTotal.