CrashStealer: C++ macOS infostealer posing as crash reporter
Jamf Threat Labs discovers and investigates CrashStealer, a C++ macOS infostealer that impersonates Apple's crash-reporting framework to harvest browser credentials, cryptocurrency wallets and keychain data, encrypting stolen files with AES-GCM before exfiltrating them to a remote command-and-control server.
By Thijs Xhaflaire
Introduction
In early May, a suspicious macOS sample uploaded to VirusTotal surfaced through our sample-processing pipeline, and Jamf Threat Labs began tracking it. It impersonated Apple's crash reporting framework and, at that point, looked like an infostealer still in development. By early July we were seeing in-the-wild detections of the payload matching one of our in-house rules, indicating the project had matured from development into active use. We track this malware under the name CrashStealer.
Unlike much of the commodity stealer activity on macOS, which is built on AppleScript droppers or thin Objective-C wrappers, CrashStealer is implemented in native C++ around an internal class the authors named MacOSData. It validates the victim's login password locally before harvesting, collects broadly across browsers, cryptocurrency wallets, password managers and the keychain, encrypts what it collects with AES-GCM before exfiltrating over libcurl, and persists by copying and re-signing itself. Although its objectives overlap with families such as Atomic (AMOS), MacSync and Phexia, its native C++ implementation and client-side encryption set it apart, and we track it as a distinct family rather than a variant.
We have since identified the stage that precedes the payload: a signed and Apple-notarized dropper, distributed as a disk image named "Werkbit Setup," that retrieves the CrashStealer payload from attacker infrastructure and launches it. Because the dropper carries a valid Developer ID and a stapled notarization ticket, it clears Gatekeeper on first launch, in contrast to the ad-hoc-signed payload it installs.
Throughout this post, we examine CrashStealer as we observed it and highlight the behaviors most relevant from a defender's perspective.
A signed, notarized dropper posing as an installer
Initial access is through a disk image named "Werkbit Setup," which mounts at /Volumes/Werkbit Setup and contains a single application bundle, Werkbit.app. Its executable is named veltod and carries the bundle identifier dev.golove.velto. Unlike the payload it eventually installs, the dropper is properly code signed and notarized: it is a universal (arm64 and x86_64) binary signed with the Developer ID Emil Grigorov (WWB7JA7AQV), has hardened runtime enabled, and carries a stapled notarization ticket. Notably, the disk image itself is signed as well, not just the application inside it, which is uncommon in malicious DMG delivery where the container is typically left unsigned.
On mount, the disk image presents a polished "installer" window branded as Werkbit Setup that walks the victim through opening the bundle, instructing them to right-click the app and choose Open. Right-click-to-open is the familiar convention for launching software past Gatekeeper; here the dropper is already notarized and would launch normally, so the instruction functions mainly as social engineering to get the victim to run it.
When the victim opens it, veltod first reaches out to GitHub, querying the GitHub API and then fetching raw content to retrieve a file named sys.cache from the repository mgothiclove/pkeys (hxxps://github.com/mgothiclove/pkeys/blob/main/sys.cache). That file supplies the curl command the dropper runs next. Staging the first hop on a trusted developer domain keeps the initial network activity inconspicuous. The retrieved command uses curl to pull a shell script from the delivery endpoint hxxps://endpoint-api-v1[.]com/d/f1b24e. The script is not stored on disk in readable form; its operational strings arrive as a series of Base64 blobs that are decoded at runtime (we observed three successive base64 -d invocations feeding a final tr -cd 'a-zA-Z0-9' cleanup) before the assembled script is piped to bash.
After decoding, the script is a self-contained downloader-installer that fetches and stages the payload:
The behavior is straightforward and explains several artifacts we had previously observed only at the endpoint. The script downloads the payload disk image over cleartext HTTP from hxxp://endpoint-api-v1[.]com/d/f1b24e/download, retrying up to three times, and saves it as CrashReporter.dmg in /tmp. It mounts the image without browsing or verification (hdiutil attach -nobrowse -noverify -noautoopen -quiet), copies the first .app bundle it finds into a hidden directory at /tmp/.CrashReporter, then detaches the image and deletes the downloaded .dmg. It clears extended attributes on the staged bundle with xattr -cr, makes the inner binary executable, strips the payload's existing signature and re-signs it ad-hoc (codesign --remove-signature followed by codesign -s - --force --deep --no-strict), registers it with Launch Services via lsregister -f, and finally launches it in the background with open -g -n.
Two points are worth flagging for defenders. First, the payload's ad-hoc signature and its execution from the hidden /private/tmp/.CrashReporter/ path; both noted below are produced by this dropper script rather than by the payload itself. Second, the initial access stage is signed and notarized under the Developer ID Emil Grigorov (WWB7JA7AQV) with the bundle identifier dev.golove.velto, and it reaches out first to the GitHub repository mgothiclove/pkeys and then to endpoint-api-v1[.]com. Each of these is a durable indicator for detection and hunting. The Werkbit Setup disk image itself originates from hxxps://werkbit[.]io, whose domain was registered in late June 2026, closely matching the build date of the dropper we analyzed. The download there is gated behind a meeting PIN, so the installer is served only to visitors who arrive with the right code rather than to any casual visitor. We have since identified additional domains and shared backend infrastructure tied to the same operation, which points to CrashStealer being one part of a larger, multi-platform campaign.
After confirming that the Developer Team ID was used to distribute malicious payloads, Jamf Threat Labs reported it to Apple.
The payload: a disk image impersonating a system component
The disk image the dropper downloads, CrashReporter.dmg, contains a single application bundle, CrashReporter.app, dressed up to impersonate Apple's built-in crash-reporting component: it carries the bundle identifier com.apple.crashreporter, the display name CrashReporter, and an application icon, so that a victim who mounts the image sees what appears to be a legitimate Apple utility. As noted above, the ad-hoc signature the bundle carries at execution is applied by the dropper, which strips the bundle's original signature and re-signs it locally before launch.
We have now traced how the payload reaches the host: it is downloaded and staged by the signed dropper described above. What is clear is that the packaging leans on the same Apple-component impersonation that runs through the rest of the malware, from the bundle identifier to the LaunchAgent label it later installs.
In-the-wild detections revealed that the bundle executed from a hidden directory under /private/tmp, specifically /private/tmp/.CrashReporter/CrashReporter.app/Contents/MacOS/CrashReporter, the exact staging path the dropper creates and launches from. This differs from where the sample later installs a copy of itself for persistence (under ~/Library/Caches/, covered below), and the leading dot on the .CrashReporter directory keeps the staging location hidden from a default Finder view. An application bundle launching from a hidden /private/tmp path is itself an unusual, high-signal indicator.
Intent revealed in Info.plist
Before examining runtime behavior, the bundle's Info.plist is worth a close look, because the authors placed a striking amount of operational detail in it. Rather than confining its configuration to the encrypted string blob, the malware declares parts of its intent openly in the property list.
Two categories of entries stand out. The first is a set of TCC usage-description strings that plainly reveal the data the malware intends to reach:
These strings pre-populate the text macOS shows in its permission prompts. Framing the request as being "for system administration" is social engineering aimed at coaxing the victim into granting full disk access, and the Desktop, Documents, Downloads and removable-volume descriptions line up directly with the locations the file searcher component later walks. The bundle also sets LSUIElement to true so it runs as a background agent with no Dock icon or window, and carries a low CFBundleVersion of 1.0.1, itself consistent with an early-stage project. Notably, later samples we observed omit the LSUIElement key entirely, one of several small build-to-build variations we have seen across the family.
The second, and more unusual, entry is an App Transport Security (ATS) exception that hardcodes the command-and-control address directly in the property list:
App Transport Security is the macOS policy that governs how an app may make network connections; by default it requires HTTPS with TLS 1.2 or later and forward secrecy. This exception relaxes those requirements for 179.43.166.242: it permits cleartext HTTP, drops the minimum TLS version to 1.0, and waives forward secrecy, with NSAllowsArbitraryLoadsbroadly disabling ATS as a fallback.
This is likely a byproduct of the authors' test setup, as it would let an operator reach the C2 regardless of how the server is configured. The reliable takeaway is for defenders: the C2 address sits in the property list in cleartext.
This ATS exception appears in the earlier samples we identified but not in the more recent ones, which omit it. The likely explanation is that the operator has since configured valid TLS on the command-and-control server, removing the need to relax App Transport Security for an insecure endpoint. That would fit the picture of a family maturing from development toward production, and it means this hardcoded-C2-in-the-Info.plist indicator applies to the earlier samples rather than the latest builds.
Execution flow
We detonated the sample in a sandboxed environment and captured the activity through dynamic analysis. The observed execution chain matches the static analysis and fills in the on-disk detail.
At a high level, execution proceeds through the following stages, each of which we examine below:
- The binary clears metadata, including its own quarantine attribute.
- It presents a password prompt and validates the entered credential locally.
- It unlocks the login keychain using the validated password.
- It enumerates installed security and analysis tooling.
- It collects browser data, cryptocurrency wallet extensions, password-manager data and keychain material.
- It encrypts the collected data as it stages it in hidden directories, then packages it into archives.
- It writes and re-signs a second copy of itself, then establishes persistence as a LaunchAgent.
- It removes its staging directories.
Quarantine self-removal
Immediately after launch, the binary shells out to clear the extended attributes on its own path:
xattr -cr strips all extended attributes in a single sweep, so both the com.apple.quarantine flag and the com.apple.lastuseddate#PS timestamp come off at once. This reduces the likelihood of a Gatekeeper prompt on subsequent launches. The com.apple.lastuseddate#PS timestamp that comes off alongside it is often useful to incident responders.
Password capture and local validation with dscl
As part of its run, the stealer displays a native password prompt styled to resemble a genuine macOS authorization request. As many macOS stealers do, it validates the entered credential locally by invoking dscl, a legitimate macOS directory-service utility, with the -authonly option before proceeding:
During dynamic analysis, this command returned successfully, confirming the entered password was valid. The routine is written to loop: an invalid password stalls the chain, and the stealer re-prompts until a correct credential is supplied. Validating the password with dscl -authonly before harvesting lets the operator keep only credentials that actually work rather than exfiltrating noise. The spawned dscl process is a useful behavioral signal for defenders. The validated password is written to a local cache file (observed as ~/.cache/.sys_auth, permissions set to 600) for reuse in the steps that follow. Its contents are not plaintext, but the low entropy points to simple obfuscation rather than real encryption.
Keychain unlock
With a validated password in hand, the stealer unlocks the login keychain, again by invoking a legitimate Apple binary:
The unlocked login.keychain-db is subsequently copied into the staging area, placing the victim's stored secrets within reach of the collection routine.
Reconnaissance of security tooling
Before and during collection, the stealer performs a distinctive reconnaissance sweep. For each application in a target list it issues a paired set of commands, first reading the application's version, then measuring its on-disk size:
The internal routine responsible is named collectSoftwareTargets in the binary's own debug strings, and it maintains counters for processed and skipped entries. It does not enumerate every installed application; it works from a specific embedded list. In our analysis the applications it queried were, with one exception, macOS malware analysis, endpoint security and EDR tooling rather than software of obvious exfiltration value. The one exception was Safari, which the stealer handles elsewhere as a collection target.
The defaults read and du -sh pairing is a lightweight way to check both whether a given tool is installed and which build is present. The skew toward analysis, endpoint security and EDR tooling suggests the routine is aimed at least in part at profiling the defensive environment. That said, what gets queried in any run depends on what is installed, and the target list is stored encrypted in the binary, so the full set is likely broader than what we observed.
Collection scope
The stealer's collection is broad. Its own debug strings name the responsible routines: collectBrowserData (which accepts a skipSafari toggle), collectExtensions, collectFirefoxExtensions, collectSoftwareTargets and runFileSearcher. The file system activity observed during dynamic analysis shows the corresponding directory structure being created under a hidden staging root at ~/.cache/com.apple.crashreporter/.
For browser data, the stealer targets Chromium-family browsers, with names including Chrome, Brave, Edge, Opera and Opera GX, Vivaldi, Chromium, and NAVER Whale appearing among its strings, and it handles profile directories such as Default and Profile. The binary links SQLite, including the sqlite3_backup API (the standard way to copy a database that is locked by a running browser), and produces artifacts named after Firefox's logins.json credential store and directories suffixed _IDB, corresponding to the IndexedDB stores used by browsers and their extensions.
The cryptocurrency and password manager targeting is extensive. During dynamic analysis, the stealer created a staging directory named for each target it looks for, giving us a direct view of the list: roughly 80 cryptocurrency wallet extensions, among them MetaMask, Phantom, Coinbase, Trust Wallet, Rabby, OKX Wallet, Exodus, Keplr, Solflare and Backpack; a long tail spanning the Solana, Cosmos, TON, Sui, Aptos and NEO ecosystems; and 14 password managers, including 1Password, Bitwarden, LastPass, Dashlane, Keeper, KeePassXC, NordPass, Enpass and RoboForm. The harvested data is written into hidden per-run directories beneath the staging root (for example ~/.cache/com.apple.crashreporter/.brw_/), where the collected items are stored as individually encrypted .cache files rather than in the clear. Within the browser and credential staging directory, the unlocked login keychain is also copied in at Keychain/login.keychain-db, and the captured account password is written to password.txt at the root of that directory.
A separate file searcher component, named runFileSearcher / FileSearcher::search in the binary's symbols and debug strings, recursively walks user directories such as ~/Documents and ~/Downloads looking for files worth collecting. Its filtering logic is worth calling out, because it reflects deliberate curation rather than a blind sweep. The searcher skips a hardcoded set of directories as it walks the file system, confirmed in the decompiled comparison logic: Caches, Logs, Trash and .Trash, CrashReporter, DiagnosticReports, WebKit, Saved Application State, nsurlsessiond, bird, node_modules, __pycache__ and .git. It also skips files by extension, checking each filename against a table (beginning at .dylib) and discarding anything that matches: executables and libraries (.dylib, .so, .o, .a, .framework, .app, .exe, .dll, .sys), disk images and installers (.dmg, .iso, .pkg, .deb, .msi), and bulky archive and media formats (.zip, .tar, .gz, .rar, .7z, .mp4, .mov, .png, and similar). Skipping large, unimportant binaries and installers this way is common practice among infostealers, aimed at keeping the collected set small and relevant. The routine writes its output to FileSearch_Results.txt and a GrabbedFiles directory.
Encryption and exfiltration
CrashStealer encrypts collected data before it leaves the host, and it does so as the data is staged: the collection routines write their output into the staging directories as individually encrypted .cache files, so the loot is not exposed in the clear on disk even before it is archived.
The encryption is authenticated and reasonably modern. Each item is encrypted with AES-256-GCM through Apple's CommonCrypto, following the standard sequence of creating a cryptor (CCCryptorCreateWithMode), setting an initialization vector (CCCryptorGCMSetIV), encrypting (CCCryptorGCMEncrypt), and finalizing the authentication tag (CCCryptorGCMFinal). The 32-byte key is derived with PBKDF2-HMAC-SHA256 over 10,000 iterations (CCKeyDerivationPBKDF), using a passphrase together with a salt. The salt is not random: it is decoded at runtime from a hardcoded constant that the authors label panel_salt_v1, and a nearby cleartext string, using fallback salt — set CONFIG_CRYPTO_SALT for production, indicates the developers intended the salt to be configurable and left a development default in place. The same routine also links the GCM decryption calls, so the operator side can reverse the process with the corresponding key. Examining staged .cache files bears this out: they carry no readable header or file type magic and measure close to the theoretical maximum entropy (roughly 8.0 bits per byte), consistent with strong encryption rather than encoding or compression.
Before upload, the stealer packages each staging directory into its own hidden ZIP archive by shelling out to the zip utility. In our analysis it produced one archive per collection category, each written into the staging root with a .zx_ prefix followed by eight hexadecimal characters, for example .zx_c67e4203.zip:
The eight-character hex value is randomized per run, so the specific names above will differ between infections, but the .zx_ prefix, the eight-hex-character pattern, the .zip extension and the location are stable. (The staging directories and the encrypted .cache files inside them use different, non-hex naming schemes, so the ZIP archives are the cleanest thing to key on.)
The .brw_ archive holds the browser and credential material, including the copied login.keychain-db and the password.txt; the .ext_ archive holds the wallet and password-manager extension data; and the .sw_ archive holds the software-reconnaissance output. These .zx_.zip files are the packaged loot, and they are the artifacts that defenders are most likely to find on disk. Notably, when the stealer cleans up after itself it removes only the staging directories (.brw_, .ext_and .sw_) with rm -rf; in the activity we captured it did not delete the .zx_.zip archives, so they remain under ~/.cache/com.apple.crashreporter/ after execution. Hunting for hidden .zx_-prefixed ZIP files in that directory, ~/.cache/com.apple.crashreporter/, is therefore a high-value detection and triage step. Because the staged files are already encrypted, these archives contain only ciphertext, so their presence confirms collection even though the data itself cannot be read at rest.
Exfiltration is performed with libcurl using its multipart form interface (the curl_mime family of functions). The command-and-control endpoint is 179.43.166.242, an address that is hardcoded in the sample's Info.plist as an ATS exception (discussed above). During dynamic analysis, we observed the sample generating network traffic to this IP shortly after launch, confirming it as the live C2 rather than a dormant configuration entry. At the time of analysis, the address carried 11 malicious detections across public threat intelligence sources. The full command-and-control URL is assembled at runtime from encrypted strings.
Duplication, re-signing and persistence
Rather than downloading a distinct second stage, CrashStealer persists by copying and re-signing itself. The running binary creates ~/Library/Caches/com.apple.crashreporter/CrashReporter.app, writes its executable into Contents/MacOS/ (initially as a .tmp file, then renamed into place and made executable), and writes an accompanying Info.plist. It then strips quarantine from the new bundle and re-signs it ad-hoc:
Ad-hoc re-signing rewrites the code-signature data in the copied binary, so both its file hash and its CodeDirectory hash (cdhash) change even though the code is identical.
Persistence is then established as a user LaunchAgent. The stealer writes ~/Library/LaunchAgents/com.apple.crashreporter.helper.plist and loads it:
The bootstrap call registers the label com.apple.crashreporter.helper, again impersonating an Apple component, so the re-signed copy relaunches at login. After archiving, the stealer removes its temporary staging directories with rm -rf.
The installed property list is shown below:
A few of these keys reinforce the family's priorities. RunAtLoad together with a KeepAlive that sets SuccessfulExit to falsemeans the agent starts at login and is relaunched by launchd whenever it exits with a failure, keeping the implant resident. ProcessType is Background and AssociatedBundleIdentifiers again names com.apple.crashreporter, continuing the Apple-component impersonation into the persistence layer. StandardOutPath and StandardErrorPath are both routed to /dev/null, discarding any output the process might otherwise leave behind, in keeping with the quiet execution seen elsewhere.
Obfuscation and anti-analysis
CrashStealer puts noticeable effort into resisting analysis, in three main ways.
Control-flow flattening. Functions are flattened so that, instead of a readable control-flow graph, execution runs through dispatch blocks that branch on compile-time constants via jump tables. It is applied broadly and significantly slows manual reverse engineering.
Encrypted strings. The sensitive strings, including the command-and-control URL, file-system paths, browser and wallet identifiers, and the software-reconnaissance target list, are stored as an encrypted blob in the __const section and decoded at runtime by a byte-substitution routine. Only development and debug strings remain readable, which is what let us enumerate the family's capabilities by name before decoding the operational strings.
Layered anti-debugging. The sample checks for a debugger at more than one point. A constructor that runs before main, during dynamic-linker initialization, uses sysctl with a KERN_PROC / P_TRACED query, the standard macOS debugger check, and terminates with exit code 45 if one is attached, before any malicious behavior runs. Patching out that first check is not enough on its own: a second check later in application initialization exits the same way. The sample runs normally when no debugger is attached, consistent with the checks being debugger-specific and defeating naive single-point patching.
A live operator panel on the delivery domain
The delivery domain also fronts a web-facing operator interface. Browsing to hxxps://endpoint-api-v1[.]com/login returns a dark-themed sign-in page labeled "Command Panel," with username and password fields and a "Remember me" option. What sits behind it is unknown, but its presence on the same host that serves the dropper script and payload indicates endpoint-api-v1[.]com is live operator infrastructure rather than a throwaway drop. The panel was independently surfaced by MalwareHunterTeam, who shared it on X. Based on its presence on the same delivery domain, we can to link it to CrashStealer.
This is not the only such panel. Additional operator interfaces tied to the same campaign have since been identified like cohezo[.]io, cohezo[.]com, cordinex[.]io and are listed in the Indicators of compromise section below.
Conclusion
CrashStealer's delivery chain shows real care: rather than a bare, unsigned lure, the operators front the attack with a signed and notarized dropper that clears Gatekeeper before quietly fetching, re-signing and launching the payload. The payload itself is a native C++ macOS infostealer that pairs a familiar set of objectives with a more deliberate implementation. What sets it apart from the commodity stealer crowd is less what it collects than how it is built: client-side AES-GCM encryption of the collected files, and an emphasis on analysis resistance through control-flow flattening, encrypted strings and layered anti-debugging.
Jamf Threat Labs continues to monitor this activity and track related infrastructure and variants. In Jamf for Mac, customers can configure threat prevention, advanced threat controls and web protection to Block and Report to help prevent the execution of similar threats.
Indicators of compromise
Read the latest research from Jamf Threat Labs