How apps can check integrity better than macOS

Last week I looked in detail at the initial signature checks made when a notarized app is launched in Mojave 10.14.5. In normal use, the majority of apps being launched are well-known to the security system, which has already checked their signatures. In those circumstances, macOS is happy to accept its previous cached result, and launch the app without running those checks again.

This allows extensive modifications to be made to an app but for it to continue launching successfully. Those changes would otherwise be sufficient to cause errors on even the most basic of signature checks, such as changes being made to the app’s Info.plist file.

Although Apple doesn’t explain the signature checks normally performed by macOS, it does provide a wide range of static code validation flags for code to use in calls like SecStaticCodeCheckValidityWithErrors() which developers can use to perform their own signature checks. Some of those are explained in outline, but several are merely named and no information is provided. The list of these flags currently includes:

  • kSecCSDoNotValidateExecutable – which doesn’t evaluate the main executable code
  • kSecCSDoNotValidateResources – which doesn’t evaluate any bundle resources
  • kSecCSBasicValidateOnly – described as kSecCSDoNotValidateExecutable | kSecCSDoNotValidateResources, so it doesn’t evaluate either the main executable code or any bundle resources
  • kSecCSCheckNestedCode – which evaluates code in ‘standard locations’ but not elsewhere
  • kSecCSStrictValidate – which performs additional checks to ensure the bundle isn’t structured to allow tampering, and rejects any resource envelopes which might weaken the signature
  • kSecCSCheckAllArchitectures – which evaluates code for all architectures, not just the ‘native’ one
  • kSecCSFullReport – undocumented
  • kSecCSCheckGatekeeperArchitectures – undocumented
  • kSecCSRestrictSymlinks – undocumented
  • kSecCSRestrictToAppLike – undocumented
  • kSecCSRestrictSidebandData – undocumented
  • kSecCSUseSoftwareSigningCert – undocumented
  • kSecCSValidatePEH – undocumented, and leads to a crash if used!

When examined in the log, these checks follow one of two basic patterns. In the first, only one signature for the whole bundle is checked, with a log sequence such as
0.357287 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (Sigourney[10740]/0#-1 LF=0)
0.357314 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.358051 opendirectoryd UID: 0, EUID: 0, GID: 0, EGID: 0, PID: 10598, PROC: <private>
0.358074 opendirectoryd RPC: mbr_identifier_translate, Module: SystemCache, Type: <private>, Value: <private>, Requesting: <private>
0.358113 opendirectoryd SystemCache UUID: CFEEB10D-DECE-4A6D-A4DB-A40C0B21DA3E, user: 501@/Local/Default
0.358148 opendirectoryd mbr_identifier_translate completed, delivered 1 result

(times are in decimal seconds, with Sigourney as the name of the app performing the signature check.)

I don’t understand the OpenDirectory entries which seem to refer to the Master Boot Record, an odd thing to be examining in this context. However, they appear consistently after the first CRL check in each signature assessment of this kind.

In the other pattern of log entries, those are followed by a succession of additional checks working through the contents of the bundle, each consisting of paired entries such as
0.270438 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (Sigourney[10661]/0#-1 LF=0)
0.270463 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0

Total time taken for this extended sequence is around 0.05 second for a basic app bundle, while the simpler check normally takes significantly less than 0.01 second.

The following validation flags only result in the short check being carried out:

  • kSecCSBasicValidateOnly
  • kSecCSDoNotValidateResources
  • kSecCSCheckNestedCode | kSecCSDoNotValidateResources
  • kSecCSStrictValidate | kSecCSDoNotValidateResources

The following validation flags result in fuller checks:

  • kSecCSCheckNestedCode
  • kSecCSStrictValidate
  • kSecCSCheckNestedCode | kSecCSStrictValidate

There were no differences in log entries made by those three checks: all reported the same sequence, and the same number of CRL fetches, for an app with conventional bundle structure.

The validation flags inevitably have a marked difference on the errors detected by a static code check. kSecCSBasicValidateOnly tolerates changes being made to the bundle’s Resources folder, and doesn’t notice the presence there of data, such as a PDF document, or code, such as the executable from another app. Neither does it notice addition of executable code to the main code folder, MacOS. It does, though, return an error when the Info.plist file is changed.

When macOS checks the signature of an unquarantined app on launch, its detection behaviour resembles that of the kSecCSBasicValidateOnly flag for a static code check, and specifically excludes resources. This ensures that the overall code signatre is checked, but apps still have flexibility over data such as Help books. When the quarantine flag is set, signature checking is much more thorough and will detect any discrepancies, including resources, which more closely resembles the effect of the kSecCSStrictValidate flag.

It is simple for developers to add code to check the integrity of their own apps. Incorporating a section similar to the following early during the app’s launch should suffice:
let myURL = Bundle.main.bundleURL
var code: SecStaticCode? = nil
let err: OSStatus = SecStaticCodeCreateWithPath(myURL as CFURL, [], &code)
if (err == OSStatus(noErr)) && (code != nil) {
var checkFlags: SecCSFlags? = nil
checkFlags = SecCSFlags.init(rawValue: kSecCSCheckNestedCode)
let secReq: SecRequirement? = nil
var secErr: Unmanaged<CFError>?
let csErr: OSStatus = SecStaticCodeCheckValidityWithErrors(code!, checkFlags!, secReq, &secErr)
if csErr != errSecSuccess {
NSApplication.shared.terminate(self) }
if secErr != nil {
secErr?.release()
} } }

checksig

In that case, kSecCSCheckNestedCode is used to ensure that a normal app bundle is checked fully. Alternatively, the flag kSecCSStrictValidate would include coverage of structural changes which might otherwise be missed. There seems little point in combining the two, but adding kSecCSCheckAllArchitectures works around the potential for malicious code to be hidden as if it were for a different architecture.

One criticism levelled against this technique is that it only checks that the app bundle matches its current signature. It’s feasible for another app to make changes to that bundle and then apply a new signature to it. There are at least two ways of addressing that:

  • Check the secure timestamp of the signature, and require it to be, say, no more recent than a couple of hours after the original signing. Any subsequent re-signing of the bundle would then fail.
  • Check the code-signing entity, to ensure that only your signature is accepted.

No doubt there are other strategies available too.

Deciding what you do is a matter of balancing perceived risks. However, such static code checks never rely on cached assessments, and are an accurate assessment of the integrity of the app at the moment that the app is launched. Even for apps which are hardened and notarized, they add a valuable assurance.

Finally, for all users, it’s worth bearing in mind that the checks and protections discussed here are only available to apps (and other executable code) which are properly signed. While macOS will continue to run unsigned code and apps, without an Apple-provided developer signature none of these checks can be performed. App bundles can be thoroughly subverted, and there’s no way for macOS or the app to detect those changes. Just because most malware now gets signed doesn’t mean that signatures aren’t important.