No sooner do we have Big Sur and the first Apple Silicon Macs than talk turns to stripping unwanted code from Universal Apps and binaries. I’m very grateful to Rich Trouton at Der Flounder for reminding me that I looked at this in detail back in July, and he has recently provided additional help for sysadmins who need to test for Universal Apps.
One question that comes to the front of the mind when contemplating mutilating signed and notarized apps is what effects this might have on their acceptance by Big Sur’s tightened security. The simple answer is that it doesn’t affect it in the slightest: provided that you strip them properly, these apps will clear first run checks by Gatekeeper when they have a quarantine flag set, and will also pass subsequent executable code checks. The detail is a bit more complex, and provides further insights into what each of these checks involves.
I’ve previously stripped various Universal apps to remove support for one architecture, passed the lightened app via AirDrop, which attaches a quarantine flag, and shown that they still clear full first run checks by Gatekeeper. This is reliable so long as you’re careful not to damage the rest of the app in any other way. If you’re being really thorough, you can also strip unwanted architectures (‘slices’) from dylibs and other executable code.
What this doesn’t explain is which slices get checked when. To examine this, I deliberately changed a couple of characters in a string in one slice in individual copies of a signed and notarized app of mine. This gave me one copy of the app whose ARM slice broke its signature, but its Intel slice remained intact, and another copy in which it was the Intel slice which was broken.
With the quarantine flag set on the apps, for example by passing them through AirDrop, trying to open either damaged app on both Intel and Apple Silicon Macs resulted in Gatekeeper refusing, with the report that the app “is damaged and can’t be opened.”
I then tried two remedies for that (albeit improbable) situation: using
lipo to remove the damaged slice, and removing the quarantine flag to bypass full first run checks. In both circumstances, the app then opened without problems.
It has been a while since I first dissected what happens during the full first run checks which occur on an app when its quarantine flag is set, but you can find an account for Catalina here. It’s clear that the signature check performed in this case covers all slices, whether the app is being opened on an Intel or Apple Silicon Mac.
However, when a slice has been removed, that doesn’t result in failure: Big Sur doesn’t complain.
Executable code check
Without a quarantine flag set, regular first run executable code checks were passed, provided that the undamaged slice was run. On an Apple Silicon Mac, that means running ARM-native if the Intel slice was damaged, or forcing Rosetta 2 translation if the ARM slice was damaged.
There’s an obvious difference between what happens in these checks, and those performed when a quarantine flag is set. Instead of seeing a warning dialog, failure of an executable code check results in the abrupt crashing of the app, with a crash report containing an Exception Type appropriate for the signature error, and giving the architecture of the code which failed.
This demonstrates a fundamental difference in signature checking between full first run checks, performed when the quarantine flag is set, and the more limited checks when there’s no quarantine flag. With a quarantine flag set, all supported architectures are checked; without that, only the selected architecture is checked.
Before you wonder if that might provide a means of sneaking malicious code onto a Mac by hiding it as a slice, the moment anything tries to load and execute that code, executable code checks would immediately detect any irregularity with that slice’s signature.
When code is signed using the normal tool
codesign, it signs all slices the same, so it isn’t easy to produce a single Universal Mach-O file in which the slices have different signatures – a chimera. To do that, individual slices would have to be signed, then brought together using
lipo. Although this might appear reasonable, for example joining an unsigned Intel slice with an ARM slice signed using an ad hoc signature, it’s hard to see any benefits for genuine or malicious code.
Chimera or not, there doesn’t appear to be any trick which could evade the thorough all-slice signature check performed to clear quarantine, coupled with the quicker and more efficient single-slice check made when trying to load and run executable code which isn’t in quarantine.