macOS does not run code it cannot attribute. A signature binds three things to the exact bytes on disk: a hash of every page (nothing was modified), an identity (who produced it — a team, or an ad-hoc marker), and often entitlements (what it may do). PE answers the same need with Authenticode — page hashes plus a certificate chain in the attribute certificate table — and ELF distributions are typically unsigned; Mach-O puts it all in one big-endian blob at the end of __LINKEDIT.
This is also the load command that changes the file's own arithmetic: our sample's signature covers every byte before it and stops exactly at its own start — so the header, load commands, and code are all inside the hash, while the signature's 23920 bytes sit outside, waiting to prove them.
Think of an envelope containing named documents:
Validation is then mechanical: re-hash pages 0..codeLimit with the declared algorithm, compare against the CodeDirectory, check the CMS over the CodeDirectory, apply requirements. Modern macOS enforces this policy at load — conceptually the same page-hash core as Authenticode, with Apple's identity model on top.
The model's missing piece: the signature cannot hash itself — codeLimit is defined as the file offset where the signature begins, and everything from byte 0 up to (not including) that offset is what gets hashed.
LC_CODE_SIGNATURE = 0x1d, a linkedit_data_command {cmd, cmdsize 16, dataoff, datasize} — the same 16-byte shape as LC_FUNCTION_STARTS, LC_DYLD_EXPORTS_TRIE, and LC_DYLD_CHAINED_FIXUPS: "a blob at a file offset, with a size".
The blob's magics (Apple's cs_blobs.h family, all big-endian): SuperBlob 0xFADE0CC0, CodeDirectory 0xFADE0C02, Requirements 0xFADE0C01 (leaf 0xFADE0C00), BlobWrapper 0xFADE0B01 (the CMS carrier), entitlements 0xFADE7171. The SuperBlob header is magic u32, length u32, count u32, then count × {type u32, offset u32} — type first, offsets absolute within the blob. The CodeDirectory opens: magic, length, version, flags, hashOffset, identOffset, nSpecialSlots, nCodeSlots, codeLimit, then four bytes hashSize/hashType/platform/pageSize. Flags you will actually meet: CS_VALID 0x1, CS_ADHOC 0x2, CS_HARD 0x100, CS_KILL 0x200, CS_RUNTIME 0x10000, CS_LINKER_SIGNED 0x20000. Hash algorithms (hashType): 1 SHA-1, 2 SHA-256, 3 SHA-256-truncated, 4 SHA-384.
FA DE 0C C0, reading them little-endian yields 0xC00CDEFA and nothing will parse. Second trap: index order — the SuperBlob entry is (type, offset), with the blob's own magic at the target offset serving as the ground truth for what it is; and nCodeSlots is ceil(codeLimit / page size), not the file's total page count (the signature's own pages are not in it).libasyncProfiler.dylib: LC_CODE_SIGNATURE dataoff 707360, datasize 23920 — and the x86_64 slice is 731280 bytes = 707360 + 23920, so the signature runs exactly to the slice's end. First 64 bytes (FAT slice starts at file offset 0x4000, so these sit at 0xB0B20):
000b0b20: fade 0cc0 0000 3a35 0000 0003 0000 0000 ......:5........ 000b0b30: 0000 0024 0000 0002 0000 1674 0001 0000 ...$.......t.... 000b0b40: 0000 1718 fade 0c02 0000 1650 0002 0500 ...........P.... 000b0b50: 0001 0000 0000 00b0 0000 0060 0000 0002 ...........`....
Decode: magic FADE0CC0, length 14901, count 3. Index: type 0 → offset 36 (CodeDirectory, magic FADE0C02, length 5712); type 2 → offset 5748 (Requirements, FADE0C01, length 164 — 36+5712=5748 ✓); type 0x10000 → offset 5912 (the CMS BlobWrapper, FADE0B01, length 8989 — 5748+164=5912 ✓; 5912+8989=14901 ✓). The remaining 23920 − 14901 = 9019 bytes of the declared datasize are zero padding out to the slice end. No entitlements document — three slots, none of type 5.
Inside the CodeDirectory: version 0x20500 (2.5), flags 0x00010000 — CS_RUNTIME; hashOffset 176, identOffset 96 → identifier "sign"; teamOffset field (byte 48) = 101 → team ID "2ZEFAR8TH3" (both strings as observed in this sample); nSpecialSlots 2, nCodeSlots 173, codeLimit 707360 — which equals dataoff itself: every byte from 0 to 707359 (header, load commands, all segments) is inside the hashed range, and 173 = ceil(707360 / 4096). Then hashSize 32, hashType 2 (SHA-256), platform 0, pageSize 12 (2^12 = 4096). The CodeDirectory's own length checks out: 176 + 173 × 32 = 5712.
$ xxd -s $((0x4000 + 707360)) -l 64 libasyncProfiler.dylib
000b0b20: fade 0cc0 0000 3a35 0000 0003 0000 0000 ......:5........
$ python3 -c "import struct; b=open('libasyncProfiler.dylib','rb').read(); s=b[0x4000+707360:]; print([hex(x) for x in struct.unpack_from('>3I', s)])"
['0xfade0cc0', '0x3a35', '0x3']What to look for: '>3I' — big-endian unpack is mandatory; the three values are magic, length 0x3a35 = 14901, count 3. Contrast with the file's first bytes CF FA ED FE (little-endian): one file, two byte orders, and the switch happens exactly at LC_CODE_SIGNATURE's dataoff. Then hunt the index: 12 + 8 × k lands on each (type, offset) pair; follow an offset and confirm the magic you find there.
Without looking back: the first four bytes of a Mach-O signature blob are FA DE 0C C0. What 32-bit magic is that, and why did you have to think about it?
A CodeDirectory reports codeLimit 707360, hashType 2, pageSize 12, nCodeSlots 173. Which statement is true?
The pattern: codeLimit is the fence — nCodeSlots = ceil(codeLimit / 2^pageSize), the algorithm is whatever hashType says, and the fence always lands exactly where LC_CODE_SIGNATURE.dataoff begins.
The superblob proves the bytes and the team; the UUID from the previous lesson proves which link produced them. Neither helps you read the program — for that you need debug information matched to that same identity.
Next: Debug Info — dSYM companion bundles, why full DWARF never ships inside the binary, and how LC_UUID is the join key between a crash and its symbols.