← All insights
Vulnerability Research

Rotten Apples Returns — macOS Codesigning Translocation Revisited

In 2021, OSec’s vulnerability research team discovered a flaw in amfid that allowed transplanting codesigning entitlements from one binary to another. We called it “Rotten Apples.” Apple’s M1 release inadvertently patched this specific bug, though Apple never acknowledged it as a vulnerability, deeming it normal behavior.

The original 2021 Python implementation was notably simple, which Apple dismissed as “normal behavior.” The M1 transition killed it incidentally, but a new variant has emerged that proves harder to dismiss.

The original 2021 RottenApples.py using lief to copy a signature blob from one binary onto another

amfid — heavy is the head that wears the crown

amfid is the userland daemon at the center of macOS code signing enforcement. It works alongside AMFI.framework, the shared library handling entitlement policy, and AppleMobileFileIntegrity.kext, the kernel extension performing actual enforcement. Together they form a triumvirate intended to guarantee what runs on your Mac matches its claims.

Diagram of the AMFI triumvirate: amfid, AMFI.framework, and AMFI.kext, with the whitelist gap in the framework

The vulnerability resides in AMFI.framework, specifically within a small group of whitelisted entitlements. The logic: if a code signature blob contains only entitlements from the unrestrictedEntitlements list (including the com.apple.security.* prefix and specific exact entries) it’s considered unrestricted. No provisioning profile is required; any Developer-ID certificate holder can use those entitlements. The framework permits this blob to be transplanted onto any binary not anchored by Apple. Third-party and Developer-ID signed binaries qualify; Apple-signed system binaries do not.

unrestrictedEntitlements — 4 entries

MATCHENTITLEMENTNOTES
EXACTcom.apple.private.signing-identifierOverride the signing identifier string
PREFIXcom.apple.security.*Entire security namespace
EXACTcom.apple.developer.hardened-processHardened runtime opt-in
EXACTcom.apple.developer.hardened-process.Trailing-dot variant

softRestrictedEntitlements — 2 entries

MATCHENTITLEMENTNOTES
EXACTcom.apple.application-identifierCannot be transplanted
PREFIXcom.apple.security.application-groupsCannot be transplanted

The com.apple.security.* prefix in unrestrictedEntitlements covers the full hardenedRuntimeEntitlements list: all 7 entries beginning with com.apple.security.cs.*: cs.disable-library-validation, cs.automator-plugins, cs.allow-unsigned-executable-memory, cs.disable-executable-page-protection, cs.allow-relative-library-loads, cs.allow-dyld-environment-variables, and cs.allow-jit. Several are directly useful for malware tactics.

“If a code signature blob contains only entitlements from the unrestrictedEntitlements list—and none from softRestrictedEntitlements—AMFI will permit it to be transplanted onto any non-Apple-anchored binary.”

The attack: signature transplant + dyld race

You have two donor signature options: steal one from a real application with suitable entitlements (Obsidian’s Electron.framework works well), or craft your own. The transplant process is straightforward. Locate the LC_CODE_SIGNATURE load command in the donor’s Mach-O, copy the blob, append it to your malicious binary’s __LINKEDIT segment, and add a new load command pointing to it.

C code locating the LC_CODE_SIGNATURE load command and appending the donor blob to the target binary

On its own, a transplanted signature isn’t sufficient. CS_REQUIRE_LV (library validation, cleared via csops(CS_OPS_CLEAR_LV)) would still catch mismatched dylibs. The key flag to clear is CS_REQUIRE_LV using csops(CS_OPS_CLEAR_LV). Once cleared, the transplanted identity and all its entitlements travel with the binary, and library validation no longer applies.

Attack chain diagram: extract signature, transplant it, win the RENAME_SWAP dyld race, execute inside a signed process

Before the race executes, two prerequisites must be satisfied. First, the attacker needs write access to the target application’s bundle directory, specifically the Frameworks/ subtree inside the .app package. On macOS, apps installed by the current user (common for tools like Obsidian) are owned by that user, requiring no privilege escalation; admin group membership suffices for /Applications installs. Second, the malicious dylib and backup copy must be written before the race starts, and both operations require the same write access. Neither step demands root.

The race exploits a time-of-check to time-of-use (TOCTOU) window in how dyld loads frameworks. Using the raw renamex_np syscall with the RENAME_SWAP flag, the real framework binary and malicious dylib are atomically swapped. The path never becomes absent, eliminating gaps where a missing file would raise suspicion. A racer thread hammers this swap thousands of times per second while the target application launches and terminates repeatedly.

C code driving the renamex_np RENAME_SWAP racer thread against dyld's framework load

Against Electron.framework inside Obsidian, the race succeeded 100% across 2,000 iterations.

Callout explaining why Electron.framework is an ideal donor: qualifying entitlements, wide install base, dyld-loaded at launch

Addendum: the sudo chain

A compounding issue deserves separate mention, though its precise role needs a clear statement: this is a persistence mechanism, not privilege escalation. Writing to /private/etc/sudo.conf requires root. An attacker already possessing root can use this chain to ensure their payload re-executes with euid=0 on every subsequent sudo invocation, surviving reboots without visible user indicators.

The mechanism: sudo on macOS clears CS_REQUIRE_LV (via csops(CS_OPS_CLEAR_LV)) when plugin loading fails for a readable, validly signed binary, including ad-hoc signed binaries. The failure path retries dlopen with library validation disabled, and upon success, does not re-enable it.

sudo holds the com.apple.private.security.clear-library-validation entitlement specifically enabling this code path. By writing a plugin entry to /etc/sudo.conf pointing at an ad-hoc signed dylib, a root-level attacker can cause sudo to silently load their payload as euid=0 on every subsequent execution. The payload runs inside a setuid-root, Apple-signed binary with no library validation enforced.

Cleanup requires only root, not a SIP bypass. Both /private/etc/sudo.conf and the plugin directory /usr/local/libexec/sudo/ are explicitly excluded from SIP protection; a privileged user can remove both without bypassing SIP. The persistence survives as long as the attacker retains root and the victim does not audit /private/etc/sudo.conf, a file most users never inspect. The payload executes silently on every sudo invocation without visible indicators unless the user runs network monitors like Little Snitch detecting outbound payload connections.

PoC code writing a plugin entry to sudo.conf so sudo loads an ad-hoc signed payload as euid=0

“Once you have root, sudo ensures you keep it—silently, on every invocation, with no visible indicator to the user.”

Where does this go?

The practical applications are significant. With CS_REQUIRE_LV cleared and a transplanted identity, you possess a viable implant deployment primitive. The com.apple.security.* namespace includes hypervisor entitlements, so spinning up your own hypervisor falls within scope. The transplanted cs.* entitlements modify the signed binary’s own runtime behavior: disabling library validation on itself, permitting JIT mappings, allowing dyld environment variables, and similar capabilities. Cross-process debugging requires separate com.apple.security.get-task-allow on the target or dedicated debugger entitlements; the transplanted cs.* list does not grant it. What the transplant provides is a process AMFI treats as legitimately entitled, which underpins the dylib injection described and any capability gated on those specific cs.* flags.

The longer trajectory proves more intriguing. macOS slowly migrates AMFI’s responsibilities to the TXM (Trusted Execution Monitor), which runs at a separate privilege level isolated from XNU, SEPROM, and userland, a model resembling how the iPhone handles this. M5-class Macs with eMTE increasingly resemble jailbroken iPhones rather than traditional Macs. Once migration completes, codesigning vulnerabilities will either persist as logical bugs or require complex chains bridging ARM privilege levels and compromising the full chain of trust.

That benefits defense. But it won’t halt research. Apple cuts off the branch; another grows. Such is the game.

Summary of the entitlement transplant, RENAME_SWAP race, sudo persistence chain, and TXM migration outlook

More in Vulnerability Research
Pip Dreams and Security Schemes, Part II: The Interpreter in the MachineJun 4, 2026 · 11 minPip Dreams and Security Schemes: Chaos in your Configuration FilesSep 18, 2024 · 1 minWhat's Up, Doc? An OSec Approach to the Looney Tunables Bug (CVE-2023-4911)Oct 11, 2023 · 6 min
Talk to an expert