Skip to content
Tracker

Tracker

www.globenewswire.com • June 1, 2026

MacOS has long been considered inherently resistant to malware, but the threat landscape tells a different story. Attacks targeting the Apple ecosystem are more frequent and sophisticated than commonly assumed.

The Adex team encountered this reality first-hand: we detected, analyzed, and attributed an active XCSSET infection – one of the most persistent and evasive malware families targeting macOS developers. This case study breaks down how the attack works and what teams can do to defend against it.

XCSSET is a modular macOS malware family first discovered in the summer of 2020 that has since undergone significant evolution. It is distributed through compromised Xcode projects and triggered at compile time – the moment a developer builds the project, the malicious payload executes.

What sets XCSSET apart from more conventional malware is where it lives: not inside the compiled application, but embedded within the Xcode project itself. This distinction makes it particularly difficult to detect: there is nothing obviously malicious in the final app, and the infection is concealed in the build configuration files that most security tools never inspect.

Xcode is Apple’s official integrated development environment (IDE) for building applications for macOS, iOS, iPadOS, watchOS, and tvOS. It is the standard toolchain for every Apple platform developer: from independent creators to enterprise engineering teams.

Xcode projects are typically stored and shared via version control platforms such as GitHub, which makes them a natural vector for supply chain attacks: a developer clones what appears to be a legitimate project, opens it in Xcode, hits Build, and the malicious payload executes as a natural part of the compilation process. No suspicious downloads, no unusual permissions, no red flags from the system. The attack is entirely invisible within the standard developer workflow.

Every Xcode project contains a set of build phase instructions – a configuration that tells the IDE exactly what to do when a developer hits Build: which scripts to run, which resources to bundle, which tests to execute.

XCSSET exploits this mechanism by injecting an additional build phase script into the project. The insertion is silent and requires no special privileges: it runs under the developer’s own account, inheriting all of their system access.

Once triggered at compile time, the malicious script performs several actions in parallel: it harvests credentials and session tokens from browsers and cryptocurrency wallets, establishes persistence by registering itself as a login item so it survives reboots, and – most critically – scans the device for other Xcode projects and injects itself into each of them. In essence, the infection was designed to spread on its own.

From here on, the infection spreads through three paths:

No infected apps have reached the App Store, and direct binary injection remains undocumented (Intego, 2020). The risk to end users is indirect, but it’s real.

No infected apps have reached the App Store, and direct binary injection remains undocumented (Intego, 2020). The risk to end users is indirect, but it’s real.

This is a textbook supply chain attack: the target is not the end user directly, but the trusted link between them and the software: the developer, their tools, and their repositories.

XCSSET isn’t new territory: XcodeGhost (2015) also used Xcode as an attack surface. The difference: XcodeGhost corrupted the Xcode installer itself, which made it detectable. XCSSET doesn’t touch Xcode at all – it hides inside individual projects, where no one thinks to look.

XCSSET isn’t new territory: XcodeGhost (2015) also used Xcode as an attack surface. The difference: XcodeGhost corrupted the Xcode installer itself, which made it detectable. XCSSET doesn’t touch Xcode at all – it hides inside individual projects, where no one thinks to look.

Anyone in the Apple development ecosystem is a potential target.

The risk is highest for teams that regularly pull Xcode projects or SDKs from GitHub (more on that later) , independent developers who work with third-party templates and starter repositories, and any organization whose software is built on macOS. If a machine is used to build iOS or macOS apps, it is a viable entry point.

With unrestricted access to the infected machine, XCSSET operates across three vectors: data theft, system manipulation, and persistence.

Any BTC or ETH address copied to the clipboard is silently replaced with one controlled by the attacker. It means that the payment indeed goes through, but lands in the wrong wallet ( Microsoft, Sept 2025 ).

On top of that, XCSSET replaces Safari, Chrome, and Firefox with modified versions carrying an injected JavaScript backdoor, giving the attacker persistent access to everything the browser touches ( Trend Micro, 2020 ).

Every Xcode project on the machine gets infected: new injection methods were documented as recently as 2025, according to Microsoft . And from the very first report: XCSSET ships with a ransomware module capable of encrypting files ( Trend Micro, 2020 ).

One of Adex’s clients – an iOS app development studio – reached out with a specific concern: their infrastructure might already be exposed to XCSSET.

Adex began a hands-on investigation. The process broke down into four stages: spotting the anomaly, capturing the sample, dissecting its behavior, and tracing how it established persistence.

It started with a routine ps check – nothing exotic, just a standard look at what was running on the machine. But one line stood out immediately:

And then another. And another.

osascript is Apple’s built-in interpreter for AppleScript – a legitimate tool, but not one that should be spawning repeatedly from /tmp. That directory is for throwaway temp files; no well-behaved application drops executables there. The argument attached to each call was massive and looked encoded.

The processes themselves were short-lived, disappearing almost as fast as they appeared, yet they kept coming back, which meant something was relaunching them on a schedule.

The file at the center of it all, /tmp/jl, was even harder to pin down. It existed for only a few milliseconds at a time – long enough to be executed, gone before it could be read.

The fix didn’t require any specialized tooling. The file was too fast to catch by hand, so instead, a simple polling loop was set to watch for it:

Every 100 milliseconds, check if /tmp/jl exists. If it does, copy it to /tmp/jl.bak before it disappears. That’s it.

Within seconds, the trap worked. The copy landed, and for the first time, the file stayed still long enough to be examined.

With the copy in hand, the first step was simple identification:

The output: a compiled AppleScript – binary .scpt format. Not human-readable as-is, but that was expected. What wasn’t expected was what was layered inside.

The script’s logic followed a familiar pattern for obfuscated malware: extract a packed payload, base64-decode it, and execute it.

Unpacking those inner base64 blocks revealed a second layer, and this one was doing real work. A shell script, responsible for quietly building a detailed fingerprint of the machine:

Everything gets bundled into a JSON object and shipped out via a single curl call:

The -k flag tells curl to ignore TLS certificate errors. It’s a small detail, but a telling one: a well-resourced operation doesn’t skip TLS validation. This is the signature of a hastily configured C2.

The embedded config made the architecture even clearer: domain: “riggletoy.ru”, moduleName: “lbwbl”, entryPoint: “Terminal”, userLocale: “en_CY”.

This wasn’t a standalone piece of malware – it was one module inside a larger, modular framework.

The domain itself returned nothing in public threat intelligence feeds at the time of discovery. It wasn’t burned, wasn’t flagged – it was quietly operational.

The domain itself returned nothing in public threat intelligence feeds at the time of discovery. It wasn’t burned, wasn’t flagged – it was quietly operational.

Short-lived processes that keep reappearing don’t restart themselves; something is launching them. The step was to find it.

The usual suspects were checked one by one: LaunchAgents, LaunchDaemons, git hooks, .zshrc, Dock entries, Xcode toolchains. And there, buried deep in a cache directory, was a counterfeit Launchpad:

The real Launchpad lives at /System/Applications/Launchpad.app, protected by SIP – Apple’s system integrity layer that prevents modification of core system files. Anything calling itself Launchpad.app outside of that path isn’t Launchpad. The location alone was enough to confirm it.

This is precisely what Microsoft documented in March 2025 as the “dock method” : plant a fake Launchpad in the user’s space, then use dockutil to silently rewrite the Dock entry. From that point on, every click on the Dock icon launches two things at once: the real app and the malware. The user sees nothing unusual.

With persistence mapped, one question remained: how many Xcode projects had been touched? A single scan across the directory answered it:

Over 20 projects came back positive. Every single one had been modified at the exact same minute, not roughly the same time, the same minute. That’s not a user action. That’s one automated trigger sweeping the entire directory in a single pass, silently injecting itself into every Xcode project it could reach.

A full picture of how the malware worked was valuable: every mechanism XCSSET relied on could now be turned against it. The same behavior that made it dangerous: predictable persistence points, recognizable file patterns, consistent network calls, made it detectable.

The key here isn’t the sequence of steps – it’s the loop.

Every persistence mechanism has one job: survive. And it does this aggressively. Whenever the opportunity arises: a new shell session, a Dock click, a git operation – the dropper sweeps the entire directory and rewrites every Xcode project it can find.

That’s what makes a single-project cleanup pointless. Remove the injection from one .xcodeproj, reopen Xcode, hit Build, and the trigger puts it straight back. The infection doesn’t live in the projects. It lives in the persistence layer.

The only path to clean remediation, in order:

Microsoft and Loginsoft’s technical breakdown of the 2025 variant describes the full chain as four stages : a malicious build script triggers unpacking, which fetches a second-stage payload from C2, which then loads additional modules on demand.

What’s actually running on a given machine at a given moment depends entirely on what the C2 decides to deliver.

How can we be certain that our specimen is really XCSSET?

No single indicator is conclusive on its own. An osascript process running from /tmp could be legitimate developer tooling. A base64-encoded AppleScript could be someone’s own automation script. A Chrome binary being accessed could be a routine extension manager.

But seven of these indicators appear: behavioral, structural, network, and persistence mechanisms, all matching documented XCSSET patterns – in the same binary, with the same architecture?

What the table above establishes is not just identity – it is an operational state. The sample wasn’t dormant or partially deployed; every layer of XCSSET’s known architecture was active simultaneously: obfuscation, persistence, browser targeting, the C2 channel, and the propagation mechanism.

That last point matters most. The 20+ modified Xcode projects mean this machine had already become a distribution node, silently embedding XCSSET into codebases that would eventually reach other developers. By the time behavioral detection flagged the sample, the infection had moved beyond the single host.

XCSSET requires no special privileges and leaves no obvious traces in the places most developers think to look. The checklist below is ordered by effort: the first three items take under five minutes and, together, cover the majority of known infection vectors.

Individual hygiene is not enough when developer machines are shared infrastructure. At the organizational level, XCSSET prioritizes supply-chain risk over endpoint risk: a single compromised workstation can quietly embed malicious build steps into repositories that are then cloned, built, and shipped downstream.

The common thread across both sections is timing. XCSSET’s effectiveness depends almost entirely on going unnoticed long enough to propagate: through your projects, through your team, and eventually through your users.

The behaviors described above are not difficult to detect once you know what to look for; the threat persists because most developers never think to look.

The most straightforward way to find XCSSET samples is not a malware repository or a threat intelligence feed – it is GitHub’s own code .

At the time of writing, authenticated dork queries return 24 repositories containing live payload chains, several of them committed within days of this investigation. Three examples are examined in detail below with fully decoded payloads; the complete list follows.

A React Native application with an iOS target. In ios/DemoForAuthFlow.xcodeproj/project.pbxproj , lines 337–381, there are six distinct payload blocks.

What makes this sample particularly notable is that both obfuscation variants are present simultaneously — base64 -D | base64 -D | base64 -D | sh and xxd -p -r | xxd -p -r | xxd -p -r | sh – with iteration depth reaching five rounds, specifically to defeat static analysis on the first pass.

The file demo/ABiteOfMouthFeastBook_5.28/ABiteOfMouthFeastBook.xcodeproj/project.pbxproj , lines 452–614, holds a dozen payload blocks chained as base64 -D × 4 | sh.

( Issue #6 , write-up by Mayank Joshi, September 2025.) Two infected files:

The logic is the same across all three examples: a long base64 string is decoded several times in sequence, yielding a short command that fetches the actual malware from a C2 server via curl and executes it silently in the background (>/dev/null 2>&1 &).

Seven repositories stand out from the full list of 24.

Each is notable for a specific reason: a C2 domain that links it to a known campaign, a commit timestamp within hours of this investigation, or context that makes the infection particularly hard to dismiss as someone else’s problem.

All queries below require an authenticated GitHub account. Run them as-is – no modification needed.

IoC domain list for SOC alerting and DNS-level filtering (compiled from Joshi 2025, our live sample, and the findings above):

riggletoy.ru is the link between the public repositories and our live sample. netcdndev.in is a new domain that had not appeared in any prior research before this investigation.

This investigation started with a single anomalous osascript call. It ended with 24 infected repositories, a live C2 domain absent from every public threat intelligence feed, and a confirmed link between one workstation and an active GitHub distribution campaign.

That escalation is not incidental – it is the design. XCSSET doesn’t need a broad reach. It needs one developer to clone one repository and hit Build. Everything after that is automatic. The 82 commits stacked on top of an undetected infection in NandaMocha/Reflect illustrate what that looks like in practice: the developer kept working, kept shipping, had no idea.

Two details from this investigation go beyond what prior research documented.

The threat persists not because it’s technically sophisticated, but because it hides in the one artifact developers routinely without reading: the build file. That’s the actual attack surface, and it’s been open for five years.

For Adex, this isn’t adjacent to the core work – it is the core work. Fraud and malware infrastructure, and the same compromised machine that spreads XCSSET through a developer’s repositories can silently drain ad platform accounts for months. We look for this because we have to. We publish it because others need to know.

Each repository below was inspected by hand: project.pbxproj was opened as plain text, the encoded string was extracted and decoded through the full iteration chain, and the resulting command was confirmed to contain a C2 call matching known XCSSET infrastructure. No automated scanner was used.