Exploiting macOS dynamic-linker and process-injection primitives during authorized engagements - DYLD_INSERT_LIBRARIES injection, @rpath/missing-dylib hijacking with re-export proxy libraries, dyld interposing and fishhook symbol rebinding, Objective-C method swizzling, and Mach task-port thread hijacking (task_for_pid) including arm64e PAC handling - using otool, codesign, install_name_tool, gcc/clang, and lldb, with EndpointSecurity detection notes.
Exploiting macOS dynamic-linker and process-injection primitives during authorized engagements - DYLD_INSERT_LIBRARIES injection, @rpath/missing-dylib hijacking with re-export proxy libraries, dyld interposing and fishhook symbol rebinding, Objective-C method swizzling, and Mach task-port thread hijacking (task_for_pid) including arm64e PAC handling - using otool, codesign, install_name_tool, gcc/clang, and lldb, with EndpointSecurity detection notes.
Exploiting macOS Dyld Hijacking & Process Injection
When to Use
You want to run code inside another process on macOS to steal secrets (passwords, messages), inherit its TCC/entitlements, or hook its functions.
A target binary loads libraries via @rpath/DYLD_INSERT_LIBRARIES without library validation, or you hold a task port (task_for_pid) for a get-task-allow process.
You need to choose between load-time injection (dyld) and runtime injection (thread hijack / fishhook) based on the target's protections.
Critical: Techniques Most Often Missed
DYLD_INSERT_LIBRARIES — the simplest primitive; a dylib __attribute__((constructor)) runs before main. Blocked on hardened/platform binaries, so check restrictions first.
How to CONFIRM: sudo log stream --style syslog --predicate 'eventMessage CONTAINS[c] "[+] dylib"' shows your constructor logging when loaded.
@rpath / missing-dylib hijack — find a dylib the app expects but doesn't ship, and that it loads without checking the signature (com.apple.security.cs.disable-library-validation).
How to CONFIRM: one @rpath candidate path doesn't exist on disk; dropping your dylib there and running the binary logs your constructor.
Re-export proxy dylib — keep the app working by re-exporting the legit library's symbols from your malicious one, matching current/compatibility versions.
How to CONFIRM: otool -l /tmp/lib.dylib | grep REEXPORT -A2 shows the absolute re-export path; the app runs normally and your code executes.
fishhook symbol rebinding (runtime) — already inside a process, hook an imported C function by rewriting its import slot; only affects calls that go through an import pointer.
How to CONFIRM: hooked close()/etc. logs each call. NOTE: on arm64e targets check __AUTH_CONST.__auth_got and temporarily make __DATA_CONST writable.
Thread hijack via task port — with a task port you can't thread_create_running (mitigated); instead task_threads() → thread_suspend() → set x0..x7/pc → resume. On arm64e you must sign pc with PAC (ptrauth_sign_unauthenticated) or chain existing gadgets.
How to CONFIRM: target executes your function (e.g., via threadexec); EndpointSecurity emits ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE.
Load-time, env: target allows DYLD_* (no hardened runtime / has allow-dyld-environment-variables, not a platform binary).
Load-time, disk: a missing/@rpath dylib + disable-library-validation → hijack on disk (survives relaunch; great for TCC theft).
Runtime, in-proc: you already execute inside the process → interpose (__DATA,__interpose), dyld_dynamic_interpose, fishhook, or ObjC swizzling.
Runtime, remote: you hold the task port of a get-task-allow/unprotected process → thread hijack.
Step 3: Exploit (concrete commands)
# (a) Interpose a libc function via __interpose section, injected with DYLD_INSERT_LIBRARIES# gcc -dynamiclib interpose.c -o interpose.dylib (DYLD_INTERPOSE(my_printf, printf))
DYLD_INSERT_LIBRARIES=./interpose.dylib ./hello
DYLD_PRINT_INTERPOSING=1 DYLD_INSERT_LIBRARIES=./interpose.dylib ./hello # debug interposing# (b) ObjC method_setImplementation to sniff a secret (store original first)# gcc -dynamiclib -framework Foundation sniff.m -o sniff.dylib# constructor: real = method_setImplementation(class_getInstanceMethod(cls,@selector(setPassword:keyFileURL:)), fake)log stream --style syslog --predicate 'eventMessage CONTAINS[c] "Password"'# (c) Persisting dylib injection in an app bundle via Info.plist LSEnvironment, then re-register# <key>LSEnvironment</key><dict><key>DYLD_INSERT_LIBRARIES</key><string>/Applications/App.app/Contents/malicious.dylib</string></dict>
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f /Applications/Application.app
# (d) Inspect a remote process before thread hijack
lldb -p <pid> # confirm get-task-allow / debuggability
Step 4: Persist / escalate / pivot
Inherit TCC/entitlements: code running inside a TCC-granted or entitled app gains its camera/FDA/etc. access with no prompt (e.g., the CVE-2023-26818 Telegram camera-TCC bypass via dylib hijack).
Persist the hijack: a dylib dropped at the hijacked @rpath location reloads every launch; bundle LSEnvironment + lsregister survives too. NOTE: on newer macOS, stripping a previously-run app's signature stops it launching, so prefer hijack over re-sign.
Deepen with remote R/W: after thread hijack, build read/write primitives (property_getName for read, _xpc_int64_set_value for write), set up Mach-port channels, and xpc_shmem shared memory — wrapped by threadexec for full control, then transfer Mach ports / fileports to pivot.
Key Concepts
Concept
Description
dyld
The dynamic linker; resolves @rpath/@loader_path/@executable_path and honors DYLD_* env vars (restricted on hardened/platform binaries).
DYLD_INSERT_LIBRARIES
Loads attacker dylib before main; constructor runs first. Blocked by hardened runtime / library validation / platform-binary status.
@rpath hijack
App loads a dylib from a search path; if a higher-priority path is missing/writable and signature isn't checked, swap it.
Re-export proxy
-reexport_library + install_name_tool -change so the malicious dylib forwards real symbols and the app still works.
Interposing
__DATA,__interpose tuples (or dyld_dynamic_interpose) replace functions between the process and loaded libs (not the shared cache).
fishhook
Runtime symbol rebinding of imported functions by rewriting lazy/non-lazy pointer slots; needs __DATA_CONST writable, PAC-aware on arm64e.
ObjC swizzling
method_exchangeImplementations / method_setImplementation swap selector IMPs to intercept Objective-C calls.
Task port / thread hijack
task_for_pid → task_threads → suspend → set registers/pc → resume; thread_set_special_port/mach_reply_port to build Mach-port channels.
PAC (arm64e)
Pointer Authentication signs return addresses/pointers; jumping to attacker memory needs ptrauth_sign_unauthenticated or gadget reuse.
A Developer-ID app with disable-library-validation expects @rpath/lib.dylib at a path that doesn't exist. The tester drops a re-export proxy dylib there; on launch it runs inside the app and uses the app's camera TCC grant (CVE-2023-26818 pattern).
Scenario 2: ObjC swizzle to sniff a password manager
The tester injects sniff.dylib via the app's Info.plistLSEnvironment and lsregister, swizzling setPassword:keyFileURL: with method_setImplementation, logging cleartext passwords to the unified log while calling the original method so nothing breaks.
Scenario 3: Task-port thread hijack with PAC
Holding the task port of a get-task-allow process on Apple Silicon, the tester suspends a thread, sets x0..x7 and a PAC-signed pc, and resumes — building read/write and Mach-port channels via threadexec for full remote control.
Output Format
## macOS Process Injection Finding
**Target**: macOS 14.4 (arm64e) ; App: com.vendor.app (Developer ID, disable-library-validation)
**Primitive**: @rpath dylib hijack with re-export proxy
**Severity**: High
**Finding**: App loads attacker dylib from a missing @rpath path without signature validation
**Evidence**:
- `codesign -d --entitlements :- App.app` -> com.apple.security.cs.disable-library-validation
- `otool -l binary | grep @rpath` -> expects @rpath/lib.dylib ; only lib2/lib.dylib exists
- dropped proxy -> log shows "[+] dylib hijacked"; app runs normally (re-export intact)
**Impact**: Code execution inside the app inheriting its TCC/entitlements (e.g., camera, FDA), enabling data theft.
**Recommendation**:
1. Enable Hardened Runtime + Library Validation; remove `disable-library-validation`/`allow-dyld-environment-variables`.
2. Load dylibs from signed absolute paths; verify loaded-library Team IDs.
3. Ship without `com.apple.security.get-task-allow` to block non-root task-port access.
4. Deploy EndpointSecurity monitoring for GET_TASK / REMOTE_THREAD_CREATE / THREAD_SET_STATE.