dyld & Dynamic Linking
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
- Inject a dylib into the target process (via DYLD_INSERT_LIBRARIES or a custom dyld patch)
- The dylibβs
__attribute__((constructor))runs beforemain() - 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
dyldreplacement - 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
- Apple β dyld Source Code
- Apple
<mach-o/dyld.h>headers - Jonathan Levin β macOS and iOS Internals Vol. I (Dynamic Linking chapter)
- Mike Ash β dyld: Dynamic Linking On OS X
Exercises
- Trace dyld loading: On macOS, set
DYLD_PRINT_LIBRARIES=1and run a binary, observe the load order - Extract a dylib from the shared cache: Extract
UIKitorFoundation, open in IDA/Ghidra - Write an interpose dylib: Hook the
open()syscall, log all file access, inject into an app on macOS - Analyze lazy binding: Set a breakpoint at a stub, trace execution flow through stub_helper, dyld, then the real function
- Calculate ASLR slide: From
vmmapoutput, compute the slide for the main executable and shared cache