dyld (dynamic linker/loader) is the first program that runs when you exec() a binary. It loads the binary, resolves symbols, and transfers control to main(). The dyld shared cache is an optimization that contains most system frameworks.


Why You Need to Learn This

  • The dyld shared cache contains most system code, so analyzing targets requires extracting from the cache
  • Hook injection (Substrate, Ellekit, libhooker) works by hijacking dyld mechanisms
  • DYLD_INSERT_LIBRARIES is the simplest primitive for code injection (on macOS)
  • Understanding symbol resolution helps understand GOT/PLT attacks and lazy binding exploitation

dyld Loading Process

fork() + exec()
  β”‚
  β–Ό
Kernel:
  1. Parse Mach-O header
  2. Map segments into virtual memory
  3. Set up stack
  4. Transfer control to dyld (LC_LOAD_DYLINKER)
  β”‚
  β–Ό
dyld:
  1. Map dyld itself
  2. Load main executable segments
  3. Load dependent dylibs (LC_LOAD_DYLIB)
     └─→ Recursive: each dylib may load additional dylibs
  4. Rebase (fix internal pointers for ASLR)
  5. Bind (resolve external symbol references)
     β”œβ”€β†’ Non-lazy: resolve immediately
     └─→ Lazy: resolve on first call (via stubs)
  6. Run initializers
     β”œβ”€β†’ +load methods (ObjC)
     β”œβ”€β†’ __attribute__((constructor)) functions
     └─→ C++ static constructors
  7. Call main()

dyld Shared Cache

Concept

On iOS, most system frameworks (UIKit, Foundation, CoreGraphics, Security, …) are merged into a single large file called the dyld shared cache:

/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e
  • A single file ~1-2GB containing hundreds of dylibs
  • All processes share the same mapping, saving memory
  • Symbols are pre-linked, so loading is faster
  • ASLR: the cache gets a random slide each boot, but all processes use the same slide

Why this matters for RE

  • System frameworks do not exist as separate .dylib files on iOS
  • You must extract individual dylibs from the shared cache for analysis
  • Gadgets reside in the shared cache and share the same base address across all processes

Extracting dylibs from the Shared Cache

# Using dyld_shared_cache_util (Xcode tools)
dyld_shared_cache_util -extract output_dir dyld_shared_cache_arm64e

# Using jtool2
jtool2 -e UIKit dyld_shared_cache_arm64e

# Using IDA β€” load shared cache directly
# File β†’ Open β†’ select dyld_shared_cache β†’ choose the dylib to analyze

Symbol Resolution

Non-Lazy Binding (resolved at load time)

__DATA,__got (Global Offset Table)
  β”œβ”€β”€ slot 0: β†’ address of _objc_msgSend
  β”œβ”€β”€ slot 1: β†’ address of _NSLog
  └── slot 2: β†’ address of _malloc

dyld fills GOT slots with real addresses when loading the binary.

Lazy Binding (resolved on first call)

Code calls stub:
  __TEXT,__stubs:
    LDR X16, [GOT_slot]    ; load pointer from __la_symbol_ptr
    BR  X16                  ; jump to it

First call β†’ __la_symbol_ptr points to stub_helper:
  __TEXT,__stub_helper:
    LDR W16, lazy_bind_info  ; load bind info offset
    B   dyld_stub_binder     ; call dyld to resolve

dyld resolves β†’ updates __la_symbol_ptr β†’ subsequent calls go directly to real function

Symbol Interposing (Hook mechanism)

// DYLD_INTERPOSE β€” replace function implementation
#define DYLD_INTERPOSE(_new, _orig) \
    __attribute__((used, section("__DATA,__interpose"))) \
    static struct { void *new_func; void *orig_func; } \
    _interpose_##_orig = { (void *)&_new, (void *)&_orig }

int my_open(const char *path, int flags) {
    printf("Opening: %s\n", path);
    return open(path, flags);  // call original
}
DYLD_INTERPOSE(my_open, open);

Tweak Injection Mechanisms

Jailbreak tweak loaders use dyld mechanisms to inject code:

CydiaSubstrate / Substitute / Ellekit / libhooker

  1. Inject a dylib into the target process (via DYLD_INSERT_LIBRARIES or a custom dyld patch)
  2. The dylib’s __attribute__((constructor)) runs before main()
  3. Hook functions by:
    • Overwriting GOT entries
    • Patching function prologues (inline hook)
    • ObjC method swizzling

Rootless Jailbreak (iOS 15+)

  • Does not modify system dyld
  • Injects via custom environment variables or a custom dyld replacement
  • Ellekit: modern hooking framework used by Dopamine

ASLR (Address Space Layout Randomization)

  • Each process has a random slide for the main executable
  • The dyld shared cache has a random slide per-boot (shared across processes)
  • The kernel has KASLR – a random slide each boot
Base address (in Mach-O):  0x100000000
ASLR slide:                0x000012340000 (random)
Runtime address:           0x100012340000

Resources


Exercises

  1. Trace dyld loading: On macOS, set DYLD_PRINT_LIBRARIES=1 and run a binary, observe the load order
  2. Extract a dylib from the shared cache: Extract UIKit or Foundation, open in IDA/Ghidra
  3. Write an interpose dylib: Hook the open() syscall, log all file access, inject into an app on macOS
  4. Analyze lazy binding: Set a breakpoint at a stub, trace execution flow through stub_helper, dyld, then the real function
  5. Calculate ASLR slide: From vmmap output, compute the slide for the main executable and shared cache