A NULL pointer is address 0. For decades the exploit playbook started by mapping page 0, so the program's "nothing" landed in attacker-owned memory. The second trick: find a page that is writable and executable and drop shellcode there. The third: hard-code target addresses because nothing ever moves. Every one of those tricks dies before your code runs — because Mach-O declares its defenses in load commands, in bytes you can read with otool.
This is also where the numbers from earlier lessons stop being abstract. initprot 0x00000005 is not trivia: it is 4 plus 1, which is EXECUTE plus READ, which is why __text runs and cannot be overwritten. Read these fields like a security policy, because that is what the kernel enforces.
Four defenses, four places to look:
The model's missing piece: declaring is not enforcing. The segment protections are enforced by the kernel's VM system, the slide is applied by dyld, and the bytes being executed were attested by the code signature from the previous concept. One posture, three different enforcers — a defense only holds when all three agree.
Protections, decoded with the Mach-O values VM_PROT_READ = 1, VM_PROT_WRITE = 2, VM_PROT_EXECUTE = 4:
| Segment | maxprot / initprot | Decode | What it means |
|---|---|---|---|
| __PAGEZERO | 0 / 0 | no access | any read, write, or fetch faults — the NULL trap |
| __TEXT | 5 / 5 | r-x (1|4) | code runs; nothing can overwrite it in memory |
| __DATA | 3 / 3 | rw- (1|2) | mutable globals; never executable |
| __LINKEDIT | 1 / 1 | r-- | fixups, symbols, signature — readable, sealed |
In prog64.macho maxprot equals initprot on all four segments: the ceiling and the starting state are the same. segment_command comments the two fields "maximum VM protection" and "initial VM protection" — maxprot is the ceiling a later remap may raise up to, initprot is what the segment gets at map time.
Now the flags word, 0x00200085, against the hardening-relevant bits in loader.h:
| Flag | Value | Set in 0x00200085? | loader.h says |
|---|---|---|---|
| MH_NOUNDEFS | 0x1 | yes | "the object file has no undefined references" |
| MH_DYLDLINK | 0x4 | yes | "input for the dynamic linker and can't be staticly link edited again" |
| MH_TWOLEVEL | 0x80 | yes | "the image is using two-level name space bindings" |
| MH_PIE | 0x200000 | yes | "the OS will load the main executable at a random address" |
| MH_ALLOW_STACK_EXECUTION | 0x20000 | no | "all stacks in the task will be given stack execution privilege" |
| MH_NO_HEAP_EXECUTION | 0x1000000 | no | "run the main executable with a non-executable heap even on platforms (e.g. i386) that don't require it" |
| MH_DYLIB_IN_CACHE | 0x80000000 | no (dylib-only) | "the dylib is part of the dyld shared cache, rather than loose in the filesystem" |
The whole posture fits in the header's last eight bytes and four segment commands. Header bytes 16–31 of prog64.macho:
00000010: 0e00 0000 c803 0000 8500 2000 0000 0000 ........
ncmds = 14, sizeofcmds = 968, then the flags word 85 00 20 00 read little-endian = 0x00200085 = 0x1 | 0x4 | 0x80 | 0x200000 — NOUNDEFS, DYLDLINK, TWOLEVEL, PIE, and nothing else. The reserved word after it is zero.
Pair it with the first load command, the one that gives the lesson its name:
segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000100000000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000
Zero address, 4 GiB, zero bytes on disk, zero permissions — a region that exists only to fail. loader.h gives it the title it deserves: "the pagezero segment which has no protections and catches NULL references for MH_EXECUTE files."
Any Mach-O works — on macOS use otool, on Linux llvm-otool. First the guard page, then the flag bits:
$ llvm-otool -l prog64.macho | grep -A7 __PAGEZERO
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
$ python3 -c "f=0x00200085; print('PIE', bool(f & 0x200000)); print('STACK_EXECUTION', bool(f & 0x20000))"
PIE True
STACK_EXECUTION FalseWhat to look for: PAGEZERO is all zeros with filesize 0 — pure reserved address space; and the mask test splits 0x200000 from 0x20000 so you never confuse ASLR's bit with stack execution's. Then check your own binary's initprot values against the READ/WRITE/EXECUTE table above.
Without looking back: catching NULL dereferences is __PAGEZERO's fame — what is its second job?
An auditor greps the flags of prog64.macho, sees 0x00200085, and asks: is MH_ALLOW_STACK_EXECUTION set — are stacks executable here?
The pattern: decode protections as bit values, never as appearances. 0x200000 and 0x20000 differ by one nibble — and so do ASLR and stack execution.
You have read the defense posture straight from the file: a guard page at zero, a random base, W^X segments, policy flags — declared in bytes, enforced by kernel and dyld, attested by the code signature. But someone has to actually perform that map: choose the slide, walk the dependencies, fix every pointer.
Next: How dyld Loads a Mach-O — seven stages from execve to main, each one anchored to a load command you have already met.