Written by Cole Herzog, Engineering Manager
Everyone’s heard. It’s the trending water cooler discussion. Apple is removing embedded bitcode in Xcode 15. The king is dead; long live the king.
Embedded bitcode originally seemed intended to allow developers to submit one application that would work on all of Apple’s products. Bitcode covers up much of the pain involved with handling different processor architectures. While it’s been clear for some time that Apple intended to standardize their entire product line on arm64, embedded bitcode could be used to enable easier cross-platform support and gives Apple quite a bit of control over the compilation process for applications distributed in the app store. It was a feature that was not as widely adopted as Apple would have liked, but the impact of its removal has sent shockwaves throughout its extended development ecosystem.
There’s been a few mentions, bordering on fear mongering, by individuals in the security community around Apple moving away from LLVM entirely. This change slightly lessens Apple’s reliance on LLVM as a whole, but that doesn’t necessarily mean Apple intends to migrate the Swift open-source community away from LLVM. That would be a major change, and a death sentence for build-time bitcode protections. Swift is listed as the first open-source project on Apple’s website, and their developers continue to contribute to it regularly. Their newest Operating System, xrOS (call it an iMask), has already been added to their LLVM fork. While Apple is not afraid to make sweeping changes, there are no additional indicators to suggest a move away from LLVM. While it’s possible, it doesn’t feel like a short-term reality, and any claims otherwise read as disingenuous. Current solutions that rely on build-time bitcode are not going away, and we continue to actively support and develop our current App Protection for Apple products. That said, protection tools that integrate at build time are significantly harder to integrate and are fragile when exposed to toolchain changes.
Advantages of Post-Build vs. Build-Time
Apple’s original ability, and recommendation, to embed bitcode within a binary was an impactful shift for bitcode protection tools. Build-time integrations are unstable, require major Xcode-related maintenance, and cause organizational heartburn. Build-time protection tooling tends to negatively impact, or even prohibit, an organization’s ability to change or upgrade its build system. Post-build protection is the future, but that future no longer has bitcode in it. That leaves assembly-level protection. While assembly can be complex and difficult to modify, the end result is a more flexible protection that isn’t limited by specific toolchains and provides a wider range of protection capabilities.
Advantages of Bitcode vs. Assembly
One of the major strengths of bitcode is its (mostly) processor-independent representation. That strength is not particularly relevant now that Apple is standardizing all of its products around the arm64 instruction set. That shift enables all Apple-based protection tooling to focus on arm64 assembly exclusively. The arm64 instruction set is a strong, well-understood standard, and that standard allows for a more targeted approach to defeat static and dynamic analysis. While it’s more difficult to apply protections to Mach-O binaries, mostly due to imperfect analysis, the end result is stronger protection. Our ARM-based protection tooling successfully obfuscates and adds dynamic environmental protections to IPA and Xcarchive app packages. Protecting the native assembly enables a more comprehensive security strategy. All binaries in the application package can be protected in the same one-call process. Protecting frameworks built with Swift Package Manager becomes trivial rather than painfully complex or impossible.
The arm64 instruction set (specifically the armv8 version) is not overly complex and is impressively efficient. It has some major advantages over x86 assembly regarding analysis and obfuscation. Most importantly, all arm64 instructions are exactly four bytes long. Also, Data in Code is seemingly not very common (except for switch tables). These two things seriously simplify binary analysis. It’s just like the saying goes: “A comprehensive binary analysis is the first branch on the tree of a resilient obfuscation strategy.”
One of our more meaningful obfuscations for defeating static analysis is chopup. Deconstructing a single subroutine into sometimes hundreds of pieces and spreading them out can majorly hamper control flow analysis. While moving those around is difficult, properly connecting them is even harder. We, internally, call those connections “leaps”; unfortunately, not all leaps are made equal. Predictability is the enemy of security, so making use of all types of leaps is critical. A B (Branch) instruction, for example, only has a +/-128MB range, while a BR (Branch to Register) instruction has a binary wide range. However, a BR instruction requires a register containing the address to branch to. Not all registers are safe to write to at all points in a binary’s execution, and some, like x18, are never safe to use. All control flow instructions have pros and cons, and finding the right one for the job is a challenging ask. Once chopup is paired with tens of thousands of instruction substitution obfuscations, attackers have to jump around in Ghidra like a Pogopalooza World Champion just to read a single subroutine.
Enabling Hybrid Protections
The hybrid app scene does a fair bit of jumping around itself, and thankfully, assembly-level protections also enable easier and more comprehensive hybrid protections. Flutter applications cannot leverage bitcode protection solutions, as Flutter’s build system doesn’t have a path to bitcode. Likewise, Microsoft’s .NET 7+ with MAUI also limits the ability to output bitcode. Flutter and other hybrid frameworks do, however, use AoT compilation to build arm64 binaries. Post-build ARM protections can obfuscate and add active guarding to these binaries like any other iOS application. Protections “just work” for almost all iOS app packages. Enabling development teams to protect all binaries in their application package with one call is what a true shift left looks like.
How Post-Build Can Integrate into CI
A CI/CD DevSecOps pipeline is now a well-known industry standard, and nothing is more frustrating than debugging build-time issues in that pipeline. Adding a single call to a CI/CD system just after the build, but right before testing, is a painless integration and keeps development, testing, and DevOps teams running at full speed. Post-build protection can be part of the normal CI/CD pipeline. That means the protection tooling doesn’t need to be installed and licensed on every developer machine or executed during every development build. We’ve seen countless issues caused by build-time protections in well-built CI/CD environments. Swapping to a post-build security solution lets teams own and modify their build process while keeping CI/CD runs green, and development teams happy. Our teams like the color green; it’s in the logo after all. Finally, always remember: a 16-byte aligned stack pointer keeps the EXC_BAD_ACCESS away.
Further explore the future of iOS app security with ARM Protection and how it can safeguard your apps against evolving threats in our blog ‘Introducing ARM Protection: A Game-Changer for iOS App Security’