task_for_pid Code Injection
Obtaining a Mach send right to a target's task port with task_for_pid, then writing and running code in its address space — the macOS analogue of CreateRemoteThread injection, gated by SIP and entitlements.
Every macOS process has a task port, a Mach IPC right that lets its holder
read, write and control the process's address space and threads. task_for_pid()
converts a PID into a send right to that port; with it, an injector can call
mach_vm_write to place code or data in the target and thread_create_running
(or thread_create + thread_set_state) to start a thread executing it — the
same capability CreateRemoteThread/WriteProcessMemory give an attacker on
Windows. The difference is how hard that first step is: since System Integrity
Protection (SIP) and hardened-runtime code-signing entitlements were
introduced, task_for_pid() against most system processes and any
hardened-runtime app fails outright unless the caller holds the
com.apple.security.cs.debugger entitlement (or is root and the target opts
in), which sharply narrows where this technique actually works compared to its
Windows counterpart.
How it works
task_t remote_task;
kern_return_t kr = task_for_pid(mach_task_self(), target_pid, &remote_task);
// kr == KERN_SUCCESS only if SIP/entitlements/privilege allow it
mach_vm_address_t remote_addr;
mach_vm_allocate(remote_task, &remote_addr, payload_size, VM_FLAGS_ANYWHERE);
mach_vm_write(remote_task, remote_addr, (vm_offset_t)payload, payload_size);
mach_vm_protect(remote_task, remote_addr, payload_size, FALSE,
VM_PROT_READ | VM_PROT_EXECUTE);
thread_act_t remote_thread;
thread_create_running(remote_task, x86_THREAD_STATE64, (thread_state_t)&state,
x86_THREAD_STATE64_COUNT, &remote_thread);Against an unprotected, non-hardened-runtime process run by the same user,
this succeeds cleanly. Against System Integrity Protection-covered system
daemons, or any app built with the Hardened Runtime and without the
get-task-allow/debugger entitlement, task_for_pid() itself returns an
error before any of the rest runs.
Detection & analysis
Static analysis:
- Check a suspect binary's entitlements with
codesign -d --entitlements :- <binary>forcom.apple.security.cs.debuggerorcom.apple.security.get-task-allow— a legitimate debugger or JIT-compiling app has a documented reason to hold these; most software does not. - A binary calling
task_for_pid,mach_vm_write, andthread_create_running/thread_set_statetogether, especially alongside hard-coded target-process names, is a strong static signal independent of whether it will succeed on a given target.
Dynamic analysis:
- The EndpointSecurity framework exposes
ES_EVENT_TYPE_NOTIFY_GET_TASK(and the older, deprecatedES_EVENT_TYPE_AUTH_GET_TASK); monitoring these shows exactly which process requested a task port for which target, which is the authoritative signal since a failedtask_for_pid()call still generates the event. - Correlate a
GET_TASKevent with an unexpected new thread appearing in the target (visible viatask_threads/Activity Monitor's thread count, or a debugger attached to the target) shortly afterward.
Detection rule hint:
Alert on ES_EVENT_TYPE_NOTIFY_GET_TASK where the requesting process is not a
known debugger, JIT runtime, or crash reporter, and especially where the
requesting process itself lacks a legitimate reason to hold
com.apple.security.cs.debugger — a get-task-allow request from an
ad-hoc-signed or unsigned binary against another user's process is high
confidence.