Recently, I presented “ Practical Ransomware Detection on macOS (via Maths, Not AI) ” at both MacSysAdmins and Virus Bulletin .
The talk’s abstract describes the presentation well:
Ransomware remains a serious threat to macOS users, especially as Apple devices continue to gain traction in the enterprise. Despite its many variants, most ransomware shares a fundamental behaviour: rapidly transforming user files into encrypted data. This talk presents a practical, maths-driven approach to generically detecting and stopping ransomware on macOS. While this approach builds on established ideas, it is now far more practical with Apple’s Endpoint Security framework. By leveraging Endpoint Security and entropy-based analysis, we can distinguish benign file activity from encryption and further differentiate encrypted data from compressed data using simple mathematical heuristics. This approach avoids signatures and machine learning, instead relying on observable behaviour and measurable properties of data. We will walk through the implementation in a free, open-source tool, including real-world testing, and demonstrate the approach against macOS ransomware samples. We will also acknowledge limitations and possible avenues for evasion. The result is a lightweight, explainable, and practical method for detecting ransomware on macOS using maths, not AI.
Ransomware remains a serious threat to macOS users, especially as Apple devices continue to gain traction in the enterprise. Despite its many variants, most ransomware shares a fundamental behaviour: rapidly transforming user files into encrypted data.
This talk presents a practical, maths-driven approach to generically detecting and stopping ransomware on macOS. While this approach builds on established ideas, it is now far more practical with Apple’s Endpoint Security framework. By leveraging Endpoint Security and entropy-based analysis, we can distinguish benign file activity from encryption and further differentiate encrypted data from compressed data using simple mathematical heuristics. This approach avoids signatures and machine learning, instead relying on observable behaviour and measurable properties of data.
We will walk through the implementation in a free, open-source tool, including real-world testing, and demonstrate the approach against macOS ransomware samples. We will also acknowledge limitations and possible avenues for evasion.
The result is a lightweight, explainable, and practical method for detecting ransomware on macOS using maths, not AI.
Practical Ransomware Detection on macOS (via Maths, Not AI)
The talk revisited my earlier research, “ Towards Generic Ransomware Detection ,” which led to the original release of RansomWhere? , a free, open-source tool designed to protect macOS users from, well, ransomware!
The central idea remains just as relevant today: by monitoring for the rapid creation or modification of encrypted files by untrusted processes, we can generically detect most—perhaps even all—macOS ransomware:
Though it is not exactly the most sophisticated heuristic, it has held up remarkably well over the years! Best of all, it gave me an excuse to incorporate maths into the presentation:
So what was new in my talk? First, thanks to Apple’s Endpoint Security framework, I showed how RansomWhere? can now implement this heuristic far more efficiently and comprehensively. Even cooler, IMHO, was a discussion of Heartbreaker: a previously unknown and undetected ransomware sample targeting macOS that attempts to metamorphically rebuild itself.
In this blog post, we’ll analyze Heartbreaker, recover files encrypted by it, explore its unusual self-rebuilding machinery, and finally pit it against our generic ransomware-detection heuristic.
Analyzing Heartbreaker
Heartbreaker (password: infect3d ).
Heartbreaker was originally uploaded to VirusTotal in May 2025. As of this writing, it remains undetected by all of the antivirus engines on the site:
It was discovered there by Megan Carney, who will present her hunting approach in “ Lessons from Threat Hunting for Malware Triage ” at the upcoming Objective by the Sea v9 conference!
Initial triage reveals an ad hoc-signed ARM64 (Apple silicon) Mach-O binary named heartbreaker :
, if we extract strings embedded in the heartbreaker binary, we encounter the following:
Interesting! From the strings alone, the binary certainly smells like ransomware. The Rust dependencies also reveal its implementation language, while the mimicore dependency and METAMORPHIC_MARKER_* strings immediately piqued my interest.
Of course, one should never base an analysis solely on embedded strings. Instead, think of them as signposts that guide further investigation. With that in mind, let’s move into an isolated virtual machine and observe the binary at runtime.
Running FileMonitor alongside the sample lets us observe its filesystem activity:
Specifically, Heartbreaker recursively processes existing files in the user’s Downloads directory. For each file, it truncates and overwrites the original contents before renaming the file with a .heartbreaker extension.
It also writes a ransom note named README.txt to the user’s Desktop and displays a suitably scary warning confirming that the files have been encrypted and demanding payment:
Back to static analysis. In the malware’s source code—wait, how did we obtain that? We’ll answer that shortly—we find its core encryption logic:
For each file, Heartbreaker:
Reads the entire file into memory.
Generates a fresh 12-byte nonce.
Encrypts the file’s contents using AES-256-GCM and a hard-coded 32-byte key.
Truncates the original file.
Writes the nonce followed by the ciphertext and 16-byte GCM authentication tag.
Renames the file with a .heartbreaker extension.
In other words, each encrypted file has the following layout:
There’s nothing wrong with its choice of AES-GCM. The problem is that Heartbreaker uses the same hard-coded key for every file—and presumably every victim. As we’ll show, this makes it possible to create a decryptor and recover the files without paying the ransom!
Interestingly, Heartbreaker also attempts to persist. This is somewhat unusual for ransomware, as most ransomware simply encrypts the victim’s files, displays its ransom demand, and calls it a day.
Heartbreaker, however, begins its main ransomware routine with a call to mimicore::add_to_startup() :
Though the original mimicore source is no longer publicly available (more on this shortly!), strings recovered from the compiled component reveal the persistence mechanism. Specifically, it creates a LaunchAgent at:
The embedded property-list template contains the following:
At runtime, the ProgramArguments is populated with the path of the currently executing Heartbreaker binary. As the Launch Agent specifies RunAtLoad , macOS will attempt to execute that binary when the agent is loaded, such as during a subsequent user login.
And why would ransomware want to run again? Existing .heartbreaker files are skipped, but persistence would give the malware another opportunity to encrypt any new, restored, or previously inaccessible files added to ~/Downloads .
One implementation detail is worth noting: Heartbreaker ignores the return value from mimicore::add_to_startup() . Thus, a failure to create the Launch Agent does not prevent the malware from continuing with file encryption.
Decrypting Heartbreaker
With an understanding of Heartbreaker’s encryption format, we can now write a decryptor.
As we saw in the recovered source, the ransomware uses the following static, hard-coded key:
Because AES-GCM is a symmetric authenticated-encryption algorithm, and because every encrypted file stores its required nonce, recovery is straightforward. The core logic of our decryptor is as follows:
The GCM authentication tag provides another useful property: if the key is incorrect or the encrypted file has been damaged, authentication fails instead of silently producing corrupted plaintext.
Let’s run our decryptor against the file encrypted earlier, Downloads/file.txt.heartbreaker :
Neat! No ransom payment required.
Up to this point, Heartbreaker is not especially sophisticated—particularly because its victims’ files can be recovered using a key embedded in the malware. Now, however, comes an interesting twist: metamorphism.
Metamorphism: A technique in which malware rewrites its own code so that each new generation looks different while retaining substantially the same behavior.
Recall that the embedded strings included "Saving new executable" , "Transforming source code" , "METAMORPHIC_MARKER_START" , and "METAMORPHIC_MARKER_END" .
The relevant logic begins in a function named rewrite :
The first surprise is that the malware contains its own source code. Specifically, Rust’s include_str! macro embeds the contents of main.rs and build.rs directly into the compiled Mach-O at build time:
This explains how we recovered the malware’s original Rust source from the compiled binary: it was not reconstructed through decompilation, but deliberately stored inside the Mach-O as data.
The rewrite function also reconstructs a Cargo.toml containing the dependencies required to rebuild the malware, then passes it—along with the embedded main.rs and build.rs —to mimicore::morph() .
Heartbreaker reaches this logic automatically during normal execution. It first attempts persistence, encrypts files, writes its ransom note, and displays the ransom window. When that window closes, control returns to main() , which checks whether Rust is installed and then invokes rewrite() . If there were no eligible files to encrypt, the GUI is skipped and the rebuilding attempt begins immediately.
If Cargo is unavailable, Heartbreaker even attempts to install Rust before continuing. Subtle it is not.
Unfortunately, the referenced mimicore repository is no longer publicly available. However, another repository belonging to the same author, metacore , remains online:
As described by its author, metacore is “a dynamic and extensible framework for generating and transforming Rust source code with built-in support for randomization and junk code insertion with helper functions.”
Both operate on Rust source enclosed by identical “METAMORPHIC_MARKER_START” and “METAMORPHIC_MARKER_END” delimiters, remove previously generated junk code, randomize identifiers, insert new junk functions, construct a temporary Cargo project, and compile the transformed source using: cargo build –release.
They also expose matching APIs and contain near-identical build and executable-replacement logic. Most tellingly, the ostensibly generic metacore source retains an error message suggesting that the user rerun sudo ./heartbreaker.
Still, because the original mimicore repository is no longer available, the surviving public Git history does not definitively prove a formal rename or direct lineage.
Artifacts recovered from the compiled mimicore component, together with the closely related metacore source, give us a pretty good idea of how Heartbreaker’s metamorphic rebuilding was designed to work.
Again, metacore is not necessarily the exact version of mimicore compiled into Heartbreaker. Still, the shared APIs, markers, diagnostic messages, and overall design make its source quite useful for understanding the process.
First, the framework locates the portion of main.rs enclosed by the metamorphic markers:
Code outside this region is preserved, while the code between the markers becomes the framework’s metamorphic playground.
Before generating a new variant, the framework first removes artifacts inserted during the transformation. For example, the following regular expression identifies generated functions whose names begin with func_ :
The current metacore source also removes previously inserted control-flow constructs and randomly generated _var* variables. In other words, each generation is first stripped back toward its original functional code before a fresh collection of mutations is applied. This prevents the source from continually ballooning with junk from every build.
comes identifier randomization. The current metacore implementation looks for specially prefixed variables and replaces them with newly generated names:
Heartbreaker’s compiled version of mimicore uses the prefix r_ rather than the w_ found in the public metacore source. This explains the rather ugly randomized names found throughout Heartbreaker’s embedded source:
The framework then generates new junk functions. The implementation is refreshingly straightforward:
Heartbreaker asks mimicore to create 15 such functions during each transformation. This is why the recovered source ends with functions such as:
These functions don’t perform any ransomware-related work. Their purpose is simply to make the source—and (well for the malware) hopefully the resulting binary—look different from the generation.
The public metacore code also applies control-flow transformations before combining the real and junk functions. Finally, it shuffles their order:
Once the source has been transformed, the framework creates a temporary Cargo project. It writes the modified source to src/main.rs , then adds the reconstructed Cargo.toml and embedded build.rs :
From there, building the new variant is as subtle as you might expect:
Assuming compilation succeeds, the framework locates the newly built executable and attempts to swap it with the currently running binary. It first renames the original executable to a temporary path:
It then copies the newly compiled variant into the original location and restores its executable permissions:
This isn’t necessarily a one-time transformation. The modified source retains Heartbreaker’s original include_str! calls, so compiling it embeds the newly modified main.rs and build.rs inside the replacement Mach-O. That new generation therefore carries the source needed to rewrite and rebuild itself again the time it runs.
Once the build completes, the framework renames the original executable with a .tmp extension and copies the newly compiled binary into its place. Finally, it removes the old .tmp executable. If replacement fails after the original was renamed, the framework attempts to restore it.
Of course, Heartbreaker’s actual behavior remains largely unchanged. It still encrypts the same files, uses the same key, displays the same ransom demand, persists in the same manner, and attempts to rebuild itself again.
One especially amusing artifact remains in the current metacore source. If replacement repeatedly fails, it prints the following supposedly generic error message:
Did the author forget to clean that up during the transition from mimicore to metacore ?
RansomWhere? vs. Heartbreaker
Heartbreaker is a useful test of RansomWhere? precisely because the tool’s detection logic contains no Heartbreaker-specific signatures or prior knowledge of the sample. Moreover, Heartbreaker was designed to change its static binary identity through recompilation.
RansomWhere?, however, is behavior-based. It monitors for an untrusted process rapidly creating or modifying files whose contents appear encrypted. Using Endpoint Security’s modified-file close events means the heuristic also covers ransomware such as Heartbreaker that truncates and rewrites existing files in place rather than simply creating new encrypted copies.
So, how does RansomWhere? fare?
No surprises—at least not to me! RansomWhere? observes Heartbreaker rapidly transforming files into high-entropy, statistically random-looking data, suspends the process, and alerts the user.
Because the detection is based on the malware’s impact rather than its hash, the metamorphic rebuilding is largely irrelevant. Heartbreaker may change how its binary looks, but to ransom a victim it must still encrypt the victim’s files.
Before wrapping up, however, it is important to reiterate that no security tool is a panacea. In my talks—and in the original post—I sought to clearly articulate the limitations of this approach:
RansomWhere?’s heuristic is inherently reactive: a small number of files may be encrypted before the threshold is reached and the process is suspended. Legitimate software can also create encrypted data, so process trust and user-approved exceptions remain important for controlling false positives.
A determined attacker who specifically targets RansomWhere?’s implementation will also likely find a bypass. For example, ransomware might encrypt files slowly enough to avoid a time-based threshold, transform ciphertext before writing it, operate through a trusted child process, target files too small for reliable statistical classification, or exploit gaps in process attribution.
Such evasions generally impose costs of their own: slower encryption, additional code and complexity, larger output, or new process and filesystem activity. Those side effects may create other opportunities for detection, but they do not eliminate the need for layered defenses, least privilege, and reliable offline backups.
Heartbreaker is unlikely to pose a major real-world threat in its current form. It only targets files under ~/Downloads , lacks awareness of macOS privacy protections, embeds a universal recovery key, depends on a now-unavailable build framework, and performs an exceptionally noisy Rust installation and compilation workflow. Its disabled “Decrypt Files” button does not actually recover anything, either.
As a research specimen, however, it is pretty interesting (wellll, at least to me!).
First, Heartbreaker demonstrates an unusual approach to metamorphism on macOS. Rather than decrypting or rewriting its machine code in memory, it carries its own Rust source, transforms that source, invokes the native build toolchain, and attempts to replace itself with the result. Because each generation embeds its newly transformed source, the design can—in principle—continue across executions.
Second, it illustrates the limitations of relying on hashes and overly brittle static signatures. A successful rebuild changes the bytes of the Mach-O, and therefore its cryptographic hash, without meaningfully changing what the program does. Detection strategies that depend solely on one known sample will always struggle with this kind of mutation.
Third, it also illustrates the limits of metamorphism. Heartbreaker can rename variables, generate junk functions, shuffle source, and produce a different binary, but none of that changes the behavior required to achieve its objective. It must still open user files, overwrite them with encrypted data, rename them, establish persistence, and display its ransom demand. Those actions remain visible to Endpoint Security and other behavioral monitoring systems.
Finally, Heartbreaker validates the central premise behind RansomWhere?: ransomware families, implementations, and binary identities may change, but the fundamental impact behavior remains remarkably stable. By focusing on the rapid creation or modification of encrypted files by an untrusted process, defenders can detect previously unknown ransomware without first requiring a sample-specific signature.
Of course, this doesn’t mean signatures are useless, nor that a single heuristic can stop every attack. As always, the best defense is a layered one.
Still, Heartbreaker can rewrite its source, rebuild its binary, and change its hash—but to ransom a Mac, it ultimately has to encrypt the user’s files. And that’s where we catch it.
If you’re into macOS security research like this, I’d love to invite you to “ Objective by the Sea ,” our Apple security conference. The talks have been announced and tickets are now on sale! Best of all, it’s in Hawaii! 🌴
Objective by the Sea (#OBTS) v9
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
