
A controlled exploration of dyld's page-in linking and chained fixup machinery as a PAC signing oracle, in the context of CVE-2026-20700.
On arm64e, every function pointer is hardware-authenticated. The goal here is to show that dyld itself can be directed — through a hand-crafted Mach-O — to produce PAC-valid pointers into attacker-chosen slots, using nothing but its own normal fixup mechanism.
Tested on: iPhone 14 (iOS 18.5, arm64e).
__DATA.fixupPage64 — malformed page_start / next values drive dyld out of the page boundary, proving branch reachability.dispatch_source_t timer handler and called naturally through the event loop, with no direct invocation from the PoC code.dyld-signing-oracle-poc/
├── Makefile ← orchestrates the full pipeline
├── src/
│ └── launcher.c ← iOS launcher (2 threads + dispatch chain-close)
├── generators/
│ ├── gen_exports.py ← generates exports.c (N dummy symbols)
│ ├── gen_client.py ← generates client.c (N imports, dyld gate stress)
│ └── gen_malformed_dylib.py ← generates libmalformed.dylib (hand-crafted Mach-O)
├── tools/
│ ├── scan_pointers.py ← classifies Mach-O pointer sections as W/R
│ └── inspect_fixups.py ← parses LC_DYLD_CHAINED_FIXUPS header
└── blog/
├── it/
│ └── dyld-signing-oracle.md ← write-up completo in italiano
└── en/
└── dyld-signing-oracle.md ← full write-up in English
xcrun --sdk iphoneos --show-sdk-path)# Default full build (arm64, 99k symbols)
make
# Fast build for iteration
make SYMBOLS=10000
# Deterministic crash in fixupPage64 (branch reachability proof)
make stress
# Stable intra-image write-what-where
make exploit
# dyld writes PAC-valid pointer → dispatch calls it naturally
make chain_close
# Verify generated chained-fixup blob layout
make verify
# Parse LC_DYLD_CHAINED_FIXUPS header of libmalformed.dylib
make inspect
# Scan pointer sections (GOT/non-lazy) across compiled binaries
make scan
# Ad-hoc signed (Sideloadly)
make chain_close
# drag PoCApp.ipa into Sideloadly
# Real certificate
make resign IDENTITY="iPhone Developer: ..." PROFILE=embedded.mobileprovision
# Monitor logs
idevicesyslog | grep "POC"
Runtime load order inside PoCApp:
libexports.dylib — loaded first (RTLD_GLOBAL), provides write_target_value and attacker_hook to any subsequent dlopen.libclient.dylib — loaded by Thread B (128KB stack); ~99k bind targets stress the dyld page-in linking gate.libmalformed.dylib — loaded by Thread A; hand-crafted chained-fixup chain makes dyld resolve and write _write_target_value (and optionally _attacker_hook) into __DATA+0x10 / +0x20.chain_close mode, main registers __DATA+0x20 as a timer handler — the event loop calls it 1 second later with no direct invocation from the PoC code.Full technical write-up in Italian and English. Covers: PAC hardware mechanics, dyld as a pointer producer, chained-fixup encoding from scratch, page-in linking gate mechanics, Mach-O engineering pitfalls (sizeofcmds, section count, stride), canary-validated data layout, the 99k symbol gate, crash proof of reachability, stable intra-image primitive, arm64e signing oracle concept, dispatch timer chain-close.
| File | Generated by |
|---|
exports.c | generators/gen_exports.py + Makefile sentinel symbols |
client.c | generators/gen_client.py |
libmalformed.dylib | generators/gen_malformed_dylib.py |
libexports.dylib | clang from exports.c |
libclient.dylib | clang from client.c |
PoCApp | clang from src/launcher.c |
PoCApp.ipa | Makefile package step |
| Variable | Default | Description |
|---|
SYMBOLS | 99000 | Bind target count in libclient (< 100k for pre-26.3 gate, < 64k for 26.3+) |
STACK_KB | 128 | Worker thread stack size in KB |
BURN_KB | 0 | Stack KB to consume before dlopen (0 = auto) |
MARGIN_KB | 24 | Auto-burn margin |
ARM64E | 0 | Use DYLD_CHAINED_PTR_ARM64E_USERLAND24 format |
MALFORM_PAGEIN | 0 | Enable malformed page-in chain |
MALFORM_TARGET_OFFSET | — | Target offset inside __DATA (e.g. 0x10) |
CHAIN_CLOSE | 0 | Second slot → _attacker_hook, enable dispatch demo |
dispatch_source_t