First noted by a Chinese blogger in July 2021, macOS.ZuRu is a backdoor that was initially delivered through poisoned web results on Baidu. Users searching for the popular Terminal emulator iTerm2 were redirected to a malicious site hosting a trojanized version of the app. Subsequent ZuRu variants used the same model, poisoning Baidu for other popular macOS utilities including SecureCRT, Navicat and Microsoft’s Remote Desktop for Mac. The selection of trojanized apps suggested the malware authors were targeting users of backend tools for SSH and other remote connections utilities.
In January 2024, researchers at JAMF discovered pirated macOS apps using similar technical indicators, but now leveraging the open-source Khepri C2 framework for post-infection operations. In late May 2025, a new sample trojanizing the cross‑platform SSH client and server‑management tool Termius came to light on social media.
This recent sample uses a new method to trojanize legitimate applications as well as a modified Khepri beacon. In this post, we provide a technical analysis of this latest version of the malware along with new technical indicators to aid detection engineers and threat hunters.
Trojanized Application Bundle
The malware is delivered via a .dmg
disk image and contains a hacked version of the genuine Termius.app. The legitimate version of Termius comes on a disk image of around 225MB, whereas the trojanized version is somewhat larger at 248MB due to the malicious binaries that have been added.

Since the application bundle inside the disk image has been modified, the attackers have replaced the developer’s code signature with their own ad hoc signature in order to pass macOS code signing rules.
Aside from the modified signature, the trojan adds two executables to the embedded Termius Helper.app
. The legitimate Termius Helper
binary of around 248KB is replaced and renamed .Termius Helper1
. On execution, the implanted Termius Helper
binary – a massive 25MB Mach-O – launches both the malware loader, .localized
, and .Termius Helper1
, to ensure the parent application behaves as expected for the user. In the background, .localized
downloads a further binary from download.termius[.]info
and writes it to /tmp/.fseventsd
. This download is a Khepri C2 beacon.
While the use of Khepri was seen in earlier versions of ZuRu, this means of trojanizing a legitimate application varies from the threat actor’s previous technique. In older versions of ZuRu, the malware authors modified the main bundle’s executable by adding an additional load command referencing an external .dylib
, with the dynamic library functioning as the loader for the Khepri backdoor and persistence modules.
Persistence via LaunchDaemon
On execution, the .localized
binary reaches out to download.termius[.]info
and requests elevated privileges from the user. If privileges are granted, it writes a persistence plist with the label com.apple.xssooxxagent
to the domain level folder /Library/LaunchDaemons/
.
The persistence module is hard-coded into the .localized
binary and is designed to execute a copy of .localized
, located at /Users/Shared/com.apple.xssooxxagent
, every hour.

To manage the malicious daemon after it has been written to disk, .localized
uses the function _writePlistAndStartDaemon()
to execute the following commands.
launchctl bootout system/com.apple.xssooxxagent; launchctl bootstrap system/com.apple.xssooxxagent; sleep 1
The malware also uses the Security framework’s deprecated API AuthorizationExecuteWithPrivileges to request privileges for the persistence module.

The function _copySelfToShare()
retrieves the current executable’s path using [NSBundle mainBundle].executablePath
and checks whether this contains the string /Users/Shared/
. If it does not, it copies itself to /Users/Shared/com.apple.xssooxxagent
, deleting any file that may already be at the path first.

The .localized Loader and md5 Updater Mechanism
Aside from installing persistence, the loader performs some checking and installation tasks.

The _LockManager()
function ensures that only one instance of the malware is running by writing a lock file to /tmp/apple-local-ipc.sock.lock
. Despite the name, the lock file is not related either to IPC or Sockets. _LockManager()
uses flock to obtain an exclusive lock on the file, exiting if another process already holds the lock. If the file lock is successful, the malware writes its own PID to the file.

The _checkFileAndDownloadIfNeeded()
function downloads, verifies and executes the second stage payload. It first checks to see if the payload already exists at /tmp/.fseventsd
. If not, it retrieves the payload from the C2 at http[:]//download.termius[.]info/bn.log.enc
, decrypting it using the hardcoded key my_secret_key
. We note that this URL pattern follows closely that seen in earlier versions of ZuRu, for example hxxp://download[.]finalshell[.]cc/bd.log
.
If the file does exist, the function calculates its md5 hash and checks the value against one received remotely from a call to http[:]//download.termius[.]info/bn.log.md5
. If the values fail to match, a new version of the file is downloaded from the C2. This is likely implemented as an update mechanism, allowing the malware to check if a newer or different version of the payload is available from the C2, but it could also function to ensure the payload hasn’t been corrupted or tampered with.
Interestingly, we see some artifacts in this binary potentially indicating that the malware source code could have been reused from earlier campaigns or at least had earlier development. The _startBackgroundProcess()
function, for example, is not called in the code and has almost identical functionality to the _runfile(_maziFilePath)
function. The latter, which is called by _main
, takes a file path argument, /tmp/.fseventsd
, then forks and executes the payload. The _startBackgroundProcess()
function does much the same thing, except it takes no argument and instead has the filepath /tmp/check_id
hardcoded into an otherwise identical function body. This filepath was not used when executing the malware and appears to have no cross references to it other than from the redundant _startBackgroundProcess()
function.
Similarly, the code contains an earlier version of the _writePlistAndStartDaemon()
function called _startLaunchDaemon
. This latter function is never called and uses the kickstart
argument rather than the bootout/bootstrap
arguments with launchctl
, as we noted in the previous section.
Modified ZuRu Payload Decryption Routine
We note that earlier versions of ZuRu used a custom XOR routine. JAMF researchers described a function which:
“takes an encoded byte, applies a combination of XOR and subtraction operations with the XOR key 0x7a, and produces a decrypted byte. The use of both XOR and subtraction makes the encryption slightly more complex than a simple XOR cipher. The masking with 0xff ensures that all operations stay within the valid range for a byte, accounting for any underflow during subtraction.”
In the newer version we analyzed, the decryption still uses XOR and combines it with both addition and subtraction. However, the single byte XOR key 0x7a has been replaced with the key string, my_secret_key
. The decryption function _decryptData()
iterates over each byte of the input data and applies four operations in sequence.

First, it adds a byte to the input by taking a byte from the key, cycling through the 13 bytes of my_secret_key
in turn, and performing a modulo 3 operation. This ensures that the byte to be added will effectively be 0, 1 or 2. Next, to ensure that the result stays within the valid range of 0-255 (i.e., can be stored as a single byte), it adds 0x100 (256) then takes the remainder after dividing again by 0x100. The third step subtracts one of 0, 1 or 2 (modulo 3 again) depending on the byte’s position in the data. Finally, it XORs the result with the original key byte. We can represent the logic more simply in pseudocode as follows.
for (i = 0; i < inputLength; i++) { byte = input[i]; // Step 1: Add a pseudo-random byte from the key byte += key[i % keyLength] % 3; // Step 2: Normalize with modulo 256 byte = (byte + 0x100) % 0x100; // Step 3: Subtract an offset based on i % 3 byte -= (i % 3); // Step 4: XOR again with the same key byte byte ^= key[i % keyLength]; output[i] = byte; }
The symmetrical nature of the XOR routine ensures that the same code can be used both to encode and decode the data being transmitted. However, roll-your-own routines like this do not provide true encryption and serve primarily to obfuscate data from automated analysis and detection engines.
Modified Khepri Command & Control Implant
The payload obtained from the C2 is a universal Mach-O around 174KB carrying an ad hoc signature with the identifier beacon_arm64_x86_64
. The executable requires Sonoma 14.1 (released in October 2023) or later, indicating that the attackers do not expect their intended targets to be running older versions of macOS.
The payload is a modified Khepri C2 beacon first seen on VirusTotal on 05 December 2024. The embedded strings suggest that the original open source version of Khepri hosted on GitHub has been customized for use specifically with the Termius trojan.
Khepri is a full-featured C2 implant with capabilities for:
- File transfer
- System reconnaissance
- Process execution and control
- Command execution with output capture

The beacon can be launched with either -s
or -bd
as a flag, or neither. If -s
is not used, the code tries to acquire a lock on a file at /tmp/my_unique_process.lock
. If the lock fails, the beacon exits; otherwise, it looks for the -bd
flag. If -bd
is supplied on launch, the beacon calls the runInBackground()
function to daemonize the process. If -s
is used, the beacon skips checking for the lock file and continues to check for -bd
.

The beacon sets a constant heartbeat interval of 5 seconds, somewhat more rapid than the 10 second default in the open source version, and sets the C2 to port 53, a common port used for DNS. It uses the legitimate domain www.baidu[.]com
as a decoy while in fact reaching out to ctl01.termius[.]fun
. The C2, which resolves to the Alibaba Cloud IP 47[.]238.28[.]21
, follows a pattern seen in earlier ZuRu malware artifacts, specifically ctl01.macnavicat[.]com
, resolving to 47[.]242.144[.]113
and reported in 2024 by previous researchers.
SentinelOne Singularity Detects macOS.ZuRu
SentinelOne Singularity detects and protects against macOS.ZuRu. When the Protect policy is enabled, all malicious components are prevented from executing on the device. If the policy is set to Detect, SentinelOne Singularity detects macOS.ZuRu’s attempt to install persistence and immediately kills all related processes and quarantines its malicious components.
Conclusion
The latest variant of macOS.ZuRu continues the threat actor’s pattern of trojanizing legitimate macOS applications used by developers and IT professionals. The shift in technique from Dylib injection to trojanizing an embedded helper application is likely an attempt to circumvent certain kinds of detection logic. Even so, the actor’s continued use of certain TTPs - from choice of target applications and domain name patterns to the reuse of file names, persistence and beaconing methods - suggest these are offering continued success in environments lacking sufficient endpoint protection.
SentinelOne customers are protected from macOS.ZuRu; organizations without that protection are advised to review the indicators provided below and the technical details presented above.
Indicators of Compromise
/Library/LaunchDaemons/com.apple.xssooxxagent.plist
/Users/Shared/com.apple.xssooxxagent
/private/tmp/Termius
/tmp/.fseventsd
/tmp/apple-local-ipc.sock.lock
Files
SHA-1 | Name | Description |
a7a9b0f8cc1c89f5c195af74ce3add74733b15c0 | .fseventsd | Khepri C2 Beacon |
ace81626924c34dfbcd9a485437cbb604e184426 | Termius Helper | Trojan Mach-O |
de8aca685871ade8a75e4614ada219025e2d6fd7 | Termius9.5.0.dmg | Trojan Disk Image |
fa9b89d4eb4d47d34f0f366750d55603813097c1 | .localized Termius com.apple.xssooxxagent |
Malware Loader |
Network Communications
http[:]//download.termius[.]info/bn.log.enc
http[:]//download.termius[.]info/bn.log.md5
ctl01.termius[.]fun
47[.]238.28[.]21