Demystifying iOS Code Signature

Taking apart a code signature and learning to reconstruct it

teryx
CSIT tech blog

--

Introduction

For iOS developers, code signing is an essential step in the app development and publishing process. It assures users of the app’s identity and integrity, with the Xcode IDE fully handling the process of generating and attaching the code signature to the app. In the past, Android had a v1 signature vulnerability which allowed the injection of contents into an APK without affecting the signature. Similarly, Microsoft also had a vulnerability in Authenticode which allowed unverified code and data to be inserted. As a security practitioner, I was curious about the specific techniques that Apple uses to ensure app’s identity and integrity. Hence, I did some studying and reverse engineering of the code signature in iOS to gain a deeper understanding of its structure and the data embedded within.

Code Signature

From references, this was how I pictured the structure of the code signature to be:

Figure 1 — Overview of Code Signature
Code signing your app assures users that it's from a known source and hasn’t been modified since it was last signed. Before your app can integrate app services, be installed on a device, or be submitted to the App Store, it must be signed with a certificate issued by Apple. https://developer.apple.com/support/code-signing/

Let’s start the analysis of the code signature by picking an executable on MacOS and inspecting its contents. I’m using the MachOView.app tool on the Safari executable in the screenshot below.

Figure 2— MachOView of Safari

We also do a hex dump of the executable and inspect it.

hexdump -C /Applications/Safari.app/Contents/MacOS/Safari | less
Figure 3— Hexdump of Safari

You can see from Figure 2 that the code signature is located at the end of the signed executable. Starting from “0xFA 0xDE 0x0C 0x01” on Figure 3, you may also observe that there are several instances of a hex pattern ‘0xfa 0xde’ in the hexadecimal output. These entries are herein referred to as the ‘data blob’.

Figure 4 — Mach-O Header

In a Mach-O executable, we know that load commands located after the mach_header specify its layout and linkage characteristics. Signed Mach-O executables in Figure 4 have an additional load_command of command type LC_CODE_SIGNATURE, which contains a reference to the aforementioned ‘data blob’.

(Mach-O is the file format for executables that run on Apple’s operating systems. You can look up articles on the Mach-O Executable Format to learn more about it.)

Before the executable is allowed to run, its code signature must be verified by the operating system. Two questions that I had asked myself were: 1) What is found within the code signature and how is it constructed? 2) Is there any private/sensitive information inside the code signature?

To answer the questions, I needed to further break down the code signature. Apple has an opensource portal where some of the relevant codes/libraries used in code signing are released (albeit not the latest version) and they give important clues. Below is a code snippet from the library libsecurity_codesigning.

typedef struct __SuperBlob { 
uint32_t magic; /* magic number */
uint32_t length; /* total length of SuperBlob */
uint32_t count; /* number of index entries following */
CS_BlobIndex index[]; /* (count) entries */ /* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob;

From the code, we can tell that the code signature is represented by a superblob which consists of several blobs (e.g code directory blob, entitlements blob, requirements blob, blob wrapper blob). Each of these blobs has its own magic header. We can learn more about their content by examining other code snippets.

Code Directory Blob

The code directory blob contains information such as offsets, hashtype, identifier, teamid:

class CodeDirectory: public Blob<CodeDirectory, kSecCodeMagicCodeDirectory> {
public:
Endian<uint32_t> version; // compatibility version
Endian<uint32_t> flags; // setup and mode flags
Endian<uint32_t> hashOffset; // offset of hash slot element at index zero
Endian<uint32_t> identOffset; // offset of identifier string
Endian<uint32_t> nSpecialSlots; // number of special hash slots
Endian<uint32_t> nCodeSlots; // number of ordinary (code) hash slots
Endian<uint32_t> codeLimit; // limit to main image signature range
uint8_t hashSize; // size of each hash digest (bytes)
uint8_t hashType; // type of hash (kSecCodeSignatureHash* constants)
uint8_t platform; // platform identifier; zero if not platform binary
uint8_t pageSize; // log2(page size in bytes); 0 => infinite
Endian<uint32_t> spare2; // unused (must be zero)
Endian<uint32_t> scatterOffset; // offset of optional scatter vector (zero if absent)
Endian<uint32_t> teamIDOffset; // offset of optional teamID string
Endian<uint32_t> spare3; // unused (most be zero)
Endian<uint64_t> codeLimit64; // limit to main image signature range, 64 bits
Endian<uint64_t> execSegBase; // offset of executable segment
Endian<uint64_t> execSegLimit; // limit of executable segment
Endian<uint64_t> execSegFlags; // exec segment flags
Endian<uint32_t> runtime; // Runtime version encoded as an unsigned int
Endian<uint32_t> preEncryptOffset; // offset of pre-encrypt hash slots
// works with the version field; see comments above
static const uint32_t currentVersion = 0x20500; // "version 2.5"
static const uint32_t compatibilityLimit = 0x2F000; // "version 3 with wiggle room"

static const uint32_t earliestVersion = 0x20001; // earliest supported version
static const uint32_t supportsScatter = 0x20100; // first version to support scatter option
static const uint32_t supportsTeamID = 0x20200; // first version to support team ID option
static const uint32_t supportsCodeLimit64 = 0x20300; // first version to support codeLimit64
static const uint32_t supportsExecSegment = 0x20400; // first version to support exec base and limit
static const uint32_t supportsPreEncrypt = 0x20500; // first version to support pre-encrypt hashes and runtime version
...
}

Figure 5 shows how a code directory blob looks like.

Figure 5— Code Directory Structure
'flags' is used to specify option flags that can be embedded in a code signature during signing and that govern the use of the signature. https://developer.apple.com/documentation/security/seccodesignatureflag 'hashOffset' points to the start of codeSlots. Depending on the version specified, it also enables different options (scatter, teamid, codelimit64, etc.) There could be other options supported in future as the version number increases.

After the list of attributes, slots containing specific types of hashes will follow. There are two types of slots as shown in the CodeDirectory class - specialSlots and codeSlots.

Hashes in specialSlots are used to verify the integrity of other blobs (e.g entitlements, requirements, resource directory, info.plist) in the code signature.

CSSLOT_INFOSLOT = 1, 
CSSLOT_REQUIREMENTS = 2,
CSSLOT_RESOURCEDIR = 3,
CSSLOT_APPLICATION = 4,
CSSLOT_ENTITLEMENTS = 5,
// More CSSLOT_* can be found here: https://developer.apple.com/documentation/kernel/2869934-anonymous/

As for hashes in codeSlots, they are used to verify the integrity of the Mach-O executable i.e. ensure that the executable has not been modified after being signed.

pageSize = 12 == log2(page_size_bytes), hence: 
page_size_bytes = 2^12 = 0x1000

The verification is done by first splitting the entire Mach-O file into chunks of equal size. This size is derived from the specified pageSize variable (12) as seen above. Therefore, for every 0x1000 bytes, the data bytes are hashed and the shasum is stored in the codeSlots.

Here is the list of possible hash types (https://github.com/apple-oss-distributions/dyld/blob/main/common/CodeSigningTypes.h):CS_HASHTYPE_SHA1 = 1, 
CS_HASHTYPE_SHA256 = 2,
CS_HASHTYPE_SHA256_TRUNCATED = 3,
CS_HASHTYPE_SHA384 = 4,

Multiple code directory blobs can also be present together, each storing different SHA hash types to be used for verification.

From CodeDirectory blob version 0x20500 onwards, there are optional entries called pre-encrypt hashes. From my observations, these hashes come before the hashes in the specialSlots and codeSlots and they seem to be identical to the hashes in the codeSlots. Currently, I’m not sure why the hashes are duplicated. You can generate these hashes using the codesign tool (provided by Apple to perform code signing on the command line) with the hidden --generate-pre-encrypt-hashes option.

Requirements Blob

A requirement blob wraps one or more distinct internal requirements in a requirement set. The requirement set defines the criteria that the code signature needs to be evaluated against. The code signature must meet the defined requirements before the executable is allowed to run. To view the requirements of a Mach-O executable, you can use the codesign tool as below:

codesign -d --requirements - /Applications/Safari.app/Contents/MacOS/Safari Executable=/Applications/Safari.app/Contents/MacOS/Safari designated => identifier "com.apple.Safari" and anchor apple

We can learn how different constants, variables, operators, match expressions and constraints are specified from Apple’s documentation on the requirement language:

https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html

Through studying the parser for the requirement language and further reverse engineering, we can derive the following structure in Figure 6 for the requirement set found in the requirements blob (if there are 2 individual internal requirements in it):

Figure 6— Requirements Set Structure

If you are interested, with Security.framework on MacOS, you can also generate the internal requirement from a string that describes the requirement using the requirement language, illustrated by the code snippet below.

char *requirement_str = "<StringInRequirementLang>"; 
static SecRequirementRef requirement_ref = nil;
CFDataRef req_data;
CFStringRef req_ref = CFStringCreateWithCString(NULL, requirement_str, kCFStringEncodingUTF8);
SecRequirementCreateWithString(req_ref, kSecCSDefaultFlags, &requirement_ref);
SecRequirementCopyData((SecRequirementRef)requirement_ref, kSecCSDefaultFlags, &req_data);

Entitlements Blob

Apple defines an entitlement as a right or privilege that grants an executable particular capabilities to access and use system functions. Entitlements are configured as property list (plist) in binary/xml format and can be extracted in the following manner:

codesign -d --entitlements :- /Applications/Safari.app/Contents/MacOS/Safari

Here is an example of entitlement on iOS/MacOS:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>platform-application</key>
<true/>
<key>com.apple.private.skip-library-validation</key>
<true/>
<key>com.apple.private.security.no-container</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>com.apple.cfnetwork</string
<string>com.apple.safari.credit-cards</string>
<string>com.apple.safari.history</string>
<string>com.apple.ProtectedCloudStorage</string
<string>com.apple.webinspector</string>
<string>com.apple.passd</string>
<string>com.apple.sharing.safaripasswordsharing</string>
<string>com.apple.webkit.webauthn</string>
<string>lockdown-identities</string>
<string>com.apple.password-manager</string>
</array>
</dict>
</plist>

And Figure 7 shows how an entitlement blob looks like:

Figure 7— Entitlements Structure

A little fun experiment. Try adding or removing some of these entitlements from the plist and see if it results in a different behaviour. On a jailbroken device, an executable having the “com.apple.private.security.no-container” entitlement is allowed to read and write from typically any directory that a mobile user is allowed to; but not able to without it. Be sure to resign the executable after making any changes; you have to update the corresponding code signature after changing the entitlements. (see ‘Wrapping it up’ of article for an example to code sign the executable).

There also exists the ASN.1 (DER) encoded form, which is a requirement on iOS15. This encoded form begins with with a different magic (0xFADE7172 instead of 0xFADE7171) and can be generated using the --generate-entitlement-der option.

Miscellaneous trivia. In 2020, a researcher previously found a vulnerability in the parsing of entitlements and wrote an article about it.

Blob Wrapper (CMS) Blob

Lastly, let’s look at the CMS (Cryptographic Message Syntax) signature within the blob wrapper blob that is generated using an Apple signed certificate.

To install your own iOS app onto an iPad/iPhone through Xcode, you will be asked to sign into your Apple Developer Account to create Apple-signed certificates for code signing based on your AppleID identity. These certificates are subsequently added to the Keychain.app on your MacOS, and your AppleID identity can now be used for code signing, as shown below:

security find-identity -p codesigning Policy: Code Signing Matching identities
1) 01234567890ABCDEF01234567890ABCDEF012345 "Apple Development: yourname@email.com (0123456789)"
2) 01234567890ABCDEF01234567890ABCDEF012344 "Apple Development: yourname2@email.com (0123456788)"
2 identities found
Valid identities only
1) 01234567890ABCDEF01234567890ABCDEF012345 "Apple Development: yourname@email.com (0123456789)"
2) 01234567890ABCDEF01234567890ABCDEF012344 "Apple Development: yourname2@email.com (0123456788)"
2 identities found

During code signing, the chosen identity will have its certificate retrieved and used to generate the CMS signature. Figure 8 illustrates the flow:

Figure 8 — Code Signing Diagram

With further reverse engineering, I was able to parse the CMS signature. Below is the result of parsing the CMS signature of the Safari app:

/Applications/Safari.app/Contents/MacOS/SafariBlobWrapper: CMS (RFC3852) signature (4450 bytes)
Subject: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Code Signing Certification Authority
Issuer: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA
Not Before: Oct 24 17:39:41 2011 GMT
Not After: Oct 24 17:39:41 2026 GMT
Subject: /C=US/O=Apple Inc./OU=Apple Software/CN=Software Signing
Issuer: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Code Signing Certification Authority
Not Before: Oct 29 18:32:38 2020 GMT
Not After: Oct 24 17:39:41 2026 GMT
Subject: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA
Issuer: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA
Not Before: Apr 25 21:40:36 2006 GMT
Not After: Feb 9 21:40:36 2035 GMT
Found 5 attributes in signer_info
NID: 50 contentType (1.2.840.113549.1.9.3)
NID: 52 signingTime (1.2.840.113549.1.9.5)
signingTime: Jun 3 23:32:45 2021 GMT
NID: 51 messageDigest (1.2.840.113549.1.9.4)
NID: 0 1.2.840.113635.100.9.2 (1.2.840.113635.100.9.2)
NID: 0 1.2.840.113635.100.9.1 (1.2.840.113635.100.9.1)

It contains the certificate’s chain of trust, the validity and expiring timestamp of the certificates used, and the time at which the executable was signed.

Application signed using a developer’s certificate will have “Apple Worldwide Developer Relations Certificate Authority” as the issuer. Otherwise executables shipped by Apple will have “Apple Code Signing Certificate Authority” as the issuer, as seen in the example above. Note that Apple updates the intermediate certificates from time to time (as explained in here).

There is a different variant of the CMS signature generated, called an ad-hoc signature, if the executable is signed with the kSecCodeSignatureAdhoc flag switched on. This can be done by specifying a dash for the identity parameter when using the codesign tool. The resulting signature will consist of zeroes and cannot be used as cryptographic proof of the executable’s identity and integrity for verification.

Instead ad-hoc signed binaries are checked by comparing the SHA-1 hash value against a list of “known good” hash values stored in the static trust cache inside the kernel. This allows trusted Apple executables with only ad-hoc signature to be allowed to run, despite not having the cryptographic proof.

An interesting trivia. The static trust cache inside the kernel has been abused by public jailbreaks on older Apple phones to enable users to run their own executables that are only ad-hoc signed.

Wrapping it up

Now that you know how a code signature looks like, do try to code sign the executable yourself and get it to run on the iOS device.

// To codesign an executable with an identity/adhoc + entitlements
codesign --force -s <IDENTITY/e.g 01234567890ABCDEF01234567890ABCDEF012345 or – for adhoc signing> --entitlements <path_to_your_plist> <path_to_your_executable>
// For more information on how you can use the codesign tool
man codesign
// You can also 'strings' codesign to see the possible arg options available.

Side note: You may wish to save the new executable in a different location from the old executable, or delete the old executable before replacing it with the new. If not, you may get a “Killed: 9” error even after code signing due to the presence of the previous code signature blob in the kernel.

My two key takeaways from the research are:

  • Code signing does ensure the identity and integrity of the executable itself, and other important data, such as entitlements and requirements, that enforces the security model of iOS.
  • Since the requirements are defined by a proprietary language and the language needs to be parsed safely, it will be interesting to dive deeper and to understand more about it.

I’ve also tried to (partially) implemented the functionalities of the codesign tool. Feel free to play around with the code and extend as you see fit. Currently, the implementation provides more verbose information than codesign when dumping out the CMS signature from iOS executables (such as the certificate’s validity period).

Also, a shout out here to Jonathan Levin and his MacOS and *OS Internals trilogy. These are invaluable resources to learn about iOS internals. I strongly recommend getting his books if you’re interested in mobile security research (Android too).

If you would like to learn more about code signing, you may refer to the resources below as well (on top of those that are already mentioned above).

Happy learning!

References

[Jailbreak]

https://coolstar.org/electra/
https://meridian.sparkes.zone/
https://unc0ver.dev/
https://checkra.in/
https://github.com/LinusHenze/Fugu14
https://en.wikipedia.org/wiki/IOS_jailbreaking

[Apple Documentations/Articles/Sources]

https://opensource.apple.com/
https://developer.apple.com/support/code-signing/ https://developer.apple.com/support/expiration/ https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Introduction/Introduction.html https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html https://developer.apple.com/documentation/xcode/using-the-latest-code-signature-format

[Community Articles/Projects]

https://blog.siguza.net/psychicpaper/ http://www.newosxbook.com/tools/jtool.html https://gitlab.com/opensource-saurik/ldid

--

--