Skip to content

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

c
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> for com.apple.security.cs.debugger or com.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, and thread_create_running/thread_set_state together, 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, deprecated ES_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 failed task_for_pid() call still generates the event.
  • Correlate a GET_TASK event with an unexpected new thread appearing in the target (visible via task_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.

Votes

Comments(0)