Damaged apps can run normally: signature checks are complex

I’ve remarked before how, when the circumstances are right, apps which no longer match their code signatures can run perfectly normally, even though macOS is aware of the error. Since then, Apple has made changes in Mojave 10.14.5 which bring it closer to Catalina, particularly with respect to its handling of notarized apps. This article looks at those changes.

A typical basic app consists of a top-level bundle (folder) with the extension .app, within which there is a single folder named Contents. Inside that are four folders and three files:

  • _CodeSignature is a folder containing the file CodeResources, a compilation of hashes and other data for all the files within the app, as a dictionary in a property list;
  • CodeResources is a small file found in notarized apps, and includes the ‘stapled’ notarization ‘ticket’ issued by Apple;
  • Frameworks contains the .dylib frameworks used by the app;
  • Info.plist is a key property list containing a lot of information about the app, including its ID and version number;
  • MacOS is a folder containing the main executable code;
  • PkgInfo is a tiny file of 8 bytes listing the CLassic file information, normally APPL???? indicating it’s an application;
  • Resources is a folder which contains all non-executable resources, such as app icons, Interface Builder’s Storyboard for the app, Help book, and so on.

When an app is signed in depth, for example in preparation for its notarization by Apple, each item of executable code is signed individually, the signature being attached to that item, working from the outermost code files into the app bundle itself. Note that only executable code is signed: data in the Resources folder isn’t, but every file has its hash or checksum recorded in the CodeResources file. When a signature is checked thoroughly, that involves checking individual hashes for each item, as well as the whole tree of signatures.

Such thorough checks appear rare, even in macOS 10.14.5, occurring only at first run when the quarantine flag is set. Any changes made to the app, either to executable code or to the contents of the Resources folder, are then detected, and reported to the user in a dialog which advises the app should be placed in the Trash.

More commonly, signature checks ignore the Resources folder and its contents. Provided that an app isn’t in quarantine, tampering with the contents of the Resources folder therefore doesn’t prevent the app from being run normally, without any report of damage. This is even true of sandboxed and notarized apps, and by all accounts remains true in Catalina.

Signature checks also vary in their response to tampering with the Info.plist file and executable code. Even minor change in the Info.plist file should result in the app being crashed with a signing error, when such checks are carried out, which appears to be more frequently than in the past. Checks are normally called when an app is first run by LaunchServices from a path which has not previously been known to LaunchServices, but they can also be performed when an app is run from its normal location in /Applications.

Minor changes in a non-executable section of a signed Mach-O executable file are detected less commonly. I was able to make those changes to a normal signed app which then ran fine afterwards, as did a notarized app. However, making any change to executable code in a sandboxed app results in it being crashed with its sandbox registration invalid because its signatures doesn’t match.

Although the sandboxed app was more sensitive to changes in the Mach-O executable, no differences in behaviour were apparent between the ordinarily signed app and the app which had been notarized. These tests don’t, of course, assess any of the protection afforded by the hardened environment which is a pre-requisite for notarization.

The notarized app which I used for test purposes is my own Cirrus, which like many of my apps now performs its own code integrity check each time it’s opened. In order not to delay the opening of the app unnecessarily, this is currently performed using SecStaticCodeCheckValidityWithErrors() on the whole app bundle, with the check flags set to kSecCSCheckNestedCode with no additional requirements. This successfully detected all attempts at tampering with the contents of the app, including those limited to the Resources folder, and small changes to non-executable parts of the Mach-O executable.

Apple doesn’t currently make clear the precise checks which are performed by different options available for SecStaticCodeCheckValidityWithErrors(), nor how these equate to results returned by the spctl and codesign command tools. My next task is to correlate those so as to detail exactly what the different types of signature check actually do.

If you want any app to undergo the fullest signature check, which includes all its hashes, checksums, and the signatures of each of its executable code files, the most reliable way to do this is to attach a quarantine flag to it. If you don’t fancy doing that in Terminal, my free app xattred will do that at the click of a button. Successful first run after that is a reliable indicator that the entire contents of the app match the signatures given, and the checksums and hashes of each file listed in the CodeResources file. Of course the app may have been re-signed, and the signatures it’s now matching may not be the originals.