macOS userland mitigations

macOS userland mitigations

TL;DR: Hardened Runtime, Library Validation, dyld_shared_cache, ASLR, NX, stack canaries, and PAC on ARM64e — each blocks a classic exploit primitive; you bypass them by stealing pointers, finding signed-but-vulnerable gadgets, or pivoting to scripting engines whose JIT regions are exempt.

What it is

Recent macOS userland binaries ship with a stack of mitigations Apple has layered on over the last decade. The relevant set for an exploit developer:

  • ASLR + NX — baseline. /usr/bin/otool -hv shows MH_PIE | MH_ALLOW_STACK_EXECUTION_FLIP flags.
  • Hardened Runtime — codesign flag; disables dyld env vars (DYLD_INSERT_LIBRARIES), forbids unsigned executable pages, requires entitlements for task_for_pid and JIT.
  • Library Validation — only libraries signed by Apple or the same TeamID can be loaded; blocks attacker-supplied dylibs unless an com.apple.security.cs.disable-library-validation entitlement is present.
  • dyld_shared_cache (DSC) — most system frameworks live in one giant pre-linked cache. ASLR slides the entire DSC as a unit.
  • PAC (Pointer Authentication) on Apple Silicon (ARM64e) — function pointers, return addresses, Obj-C isa, vtable slots are signed with a per-process key.
  • Stack canaries (-fstack-protector-strong), PIE, __DATA_CONST (read-only after dyld fixups), CFI on some Apple binaries.

Preconditions / where it applies

  • Userland process on macOS 12+ on Apple Silicon (most aggressive set) or x86_64 (no PAC, otherwise similar).
  • You have an initial primitive (info leak + write) and want to reach RCE.

Technique

Defeating Hardened Runtime + Library Validation.

  • If the target has the com.apple.security.cs.disable-library-validation or cs.allow-dyld-environment-variables entitlement (electron apps, dev tools, Adobe), DYLD_INSERT_LIBRARIES is back on the table. Scan with codesign -d --entitlements - /Applications/Target.app/Contents/MacOS/Target.
  • Otherwise pivot via Mach-O side loading: drop a dylib with the same name as a weakly-linked one and rely on dyld’s search order.

Defeating DSC randomisation.

  • One leak of any DSC-resident pointer reveals the slide for every library — _dyld_get_shared_cache_range() returns base+size for the calling process. Use ROP gadgets from the whole cache once you have it.
  • Extract gadgets with dyld-shared-cache-extractor or ipsw dyld extract; see dyld-shared-cache-extraction.

Defeating PAC on ARM64e.

  • Sign primitives that exist in the codebase (signing oracles): any function that takes user-controlled pointer and calls ptrauth_sign_* on it gives you a forged signed pointer.
  • Cross-context: PAC keys differ per-context (IA/IB/DA/DB); a leaked B-key pointer doesn’t help with A-key authentications.
  • Stripping: ptrauth_strip exists but is gated; in practice you target unsigned pointers (raw void* casts in C code) and signed wrappers.
  • See pac-arm64e-bypass for primitives and the public WebKit / kernel research.

JIT regions and scripting engines.

  • WebKit / V8 / SquirrelFish JITs map RWX regions (W^X is enforced via PROT toggles via pthread_jit_write_protect_np). A confused-deputy bug that flips the protection while attacker code is in the buffer is the classic JIT escape.
  • Hardened Runtime allows JIT only with com.apple.security.cs.allow-jit entitlement.

Stack canary + PIE.

  • Same defeats as Linux: leak the canary via formatted-output, then overflow precisely. __stack_chk_guard resolves through __DATA_CONST.

Detection and defence

  • For defenders: keep Hardened Runtime + Library Validation enabled; never request disable-library-validation unless absolutely required.
  • Monitor codesign --verify --deep --strict regressions in CI.
  • Endpoint Security framework (es_event_exec_t, ES_EVENT_TYPE_NOTIFY_MMAP) catches DYLD injection attempts and unsigned-page mappings.
  • Treat any entitlement that re-enables old behaviour as a privilege boundary worth auditing.

References