macOS Thread Injection via Task port
Reading time: 8 minutes
tip
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
Code
- https://github.com/bazad/threadexec
- https://gist.github.com/knightsc/bd6dfeccb02b77eb6409db5601dcef36
1. Thread Hijacking
Initially, the task_threads()
function is invoked on the task port to obtain a thread list from the remote task. A thread is selected for hijacking. This approach diverges from conventional code-injection methods as creating a new remote thread is prohibited due to the mitigation that blocks thread_create_running()
.
To control the thread, thread_suspend()
is called, halting its execution.
The only operations permitted on the remote thread involve stopping and starting it and retrieving/modifying its register values. Remote function calls are initiated by setting registers x0
to x7
to the arguments, configuring pc
to target the desired function, and resuming the thread. Ensuring the thread does not crash after the return necessitates detection of the return.
One strategy involves registering an exception handler for the remote thread using thread_set_exception_ports()
, setting the lr
register to an invalid address before the function call. This triggers an exception post-function execution, sending a message to the exception port, enabling state inspection of the thread to recover the return value. Alternatively, as adopted from Ian Beer’s triple_fetch exploit, lr
is set to loop infinitely; the thread’s registers are then continuously monitored until pc
points to that instruction.
2. Mach ports for communication
The subsequent phase involves establishing Mach ports to facilitate communication with the remote thread. These ports are instrumental in transferring arbitrary send/receive rights between tasks.
For bidirectional communication, two Mach receive rights are created: one in the local and the other in the remote task. Subsequently, a send right for each port is transferred to the counterpart task, enabling message exchange.
Focusing on the local port, the receive right is held by the local task. The port is created with mach_port_allocate()
. The challenge lies in transferring a send right to this port into the remote task.
A strategy involves leveraging thread_set_special_port()
to place a send right to the local port in the remote thread’s THREAD_KERNEL_PORT
. Then, the remote thread is instructed to call mach_thread_self()
to retrieve the send right.
For the remote port, the process is essentially reversed. The remote thread is directed to generate a Mach port via mach_reply_port()
(as mach_port_allocate()
is unsuitable due to its return mechanism). Upon port creation, mach_port_insert_right()
is invoked in the remote thread to establish a send right. This right is then stashed in the kernel using thread_set_special_port()
. Back in the local task, thread_get_special_port()
is used on the remote thread to acquire a send right to the newly allocated Mach port in the remote task.
Completion of these steps results in the establishment of Mach ports, laying the groundwork for bidirectional communication.
3. Basic Memory Read/Write Primitives
In this section, the focus is on utilizing the execute primitive to establish basic memory read/write primitives. These initial steps are crucial for gaining more control over the remote process, though the primitives at this stage won't serve many purposes. Soon, they will be upgraded to more advanced versions.
Memory reading and writing using the execute primitive
The goal is to perform memory reading and writing using specific functions. For reading memory:
uint64_t read_func(uint64_t *address) {
return *address;
}
For writing memory:
void write_func(uint64_t *address, uint64_t value) {
*address = value;
}
These functions correspond to the following assembly:
_read_func:
ldr x0, [x0]
ret
_write_func:
str x1, [x0]
ret
Identifying suitable functions
A scan of common libraries revealed appropriate candidates for these operations:
- Reading memory —
property_getName()
(libobjc):
const char *property_getName(objc_property_t prop) {
return prop->name;
}
- Writing memory —
_xpc_int64_set_value()
(libxpc):
__xpc_int64_set_value:
str x1, [x0, #0x18]
ret
To perform a 64-bit write at an arbitrary address:
_xpc_int64_set_value(address - 0x18, value);
With these primitives established, the stage is set for creating shared memory, marking a significant progression in controlling the remote process.
4. Shared Memory Setup
The objective is to establish shared memory between local and remote tasks, simplifying data transfer and facilitating the calling of functions with multiple arguments. The approach leverages libxpc
and its OS_xpc_shmem
object type, which is built upon Mach memory entries.
Process overview
- Memory allocation
- Allocate memory for sharing using
mach_vm_allocate()
. - Use
xpc_shmem_create()
to create anOS_xpc_shmem
object for the allocated region.
- Allocate memory for sharing using
- Creating shared memory in the remote process
- Allocate memory for the
OS_xpc_shmem
object in the remote process (remote_malloc
). - Copy the local template object; fix-up of the embedded Mach send right at offset
0x18
is still required.
- Allocate memory for the
- Correcting the Mach memory entry
- Insert a send right with
thread_set_special_port()
and overwrite the0x18
field with the remote entry’s name.
- Insert a send right with
- Finalising
- Validate the remote object and map it with a remote call to
xpc_shmem_remote()
.
- Validate the remote object and map it with a remote call to
5. Achieving Full Control
Once arbitrary execution and a shared-memory back-channel are available you effectively own the target process:
- Arbitrary memory R/W — use
memcpy()
between local & shared regions. - Function calls with > 8 args — place the extra arguments on the stack following the arm64 calling convention.
- Mach port transfer — pass rights in Mach messages via the established ports.
- File-descriptor transfer — leverage fileports (see triple_fetch).
All of this is wrapped in the threadexec
library for easy re-use.
6. Apple Silicon (arm64e) Nuances
On Apple Silicon devices (arm64e) Pointer Authentication Codes (PAC) protect all return addresses and many function pointers. Thread-hijacking techniques that reuse existing code continue to work because the original values in lr
/pc
already carry valid PAC signatures. Problems arise when you try to jump to attacker-controlled memory:
- Allocate executable memory inside the target (remote
mach_vm_allocate
+mprotect(PROT_EXEC)
). - Copy your payload.
- Inside the remote process sign the pointer:
uint64_t ptr = (uint64_t)payload;
ptr = ptrauth_sign_unauthenticated((void*)ptr, ptrauth_key_asia, 0);
- Set
pc = ptr
in the hijacked thread state.
Alternatively, stay PAC-compliant by chaining existing gadgets/functions (traditional ROP).
7. Detection & Hardening with EndpointSecurity
The EndpointSecurity (ES) framework exposes kernel events that allow defenders to observe or block thread-injection attempts:
ES_EVENT_TYPE_AUTH_GET_TASK
– fired when a process requests another task’s port (e.g.task_for_pid()
).ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE
– emitted whenever a thread is created in a different task.ES_EVENT_TYPE_NOTIFY_THREAD_SET_STATE
(added in macOS 14 Sonoma) – indicates register manipulation of an existing thread.
Minimal Swift client that prints remote-thread events:
import EndpointSecurity
let client = try! ESClient(subscriptions: [.notifyRemoteThreadCreate]) {
(_, msg) in
if let evt = msg.remoteThreadCreate {
print("[ALERT] remote thread in pid \(evt.target.pid) by pid \(evt.thread.pid)")
}
}
RunLoop.main.run()
Querying with osquery ≥ 5.8:
SELECT target_pid, source_pid, target_path
FROM es_process_events
WHERE event_type = 'REMOTE_THREAD_CREATE';
Hardened-runtime considerations
Distributing your application without the com.apple.security.get-task-allow
entitlement prevents non-root attackers from obtaining its task-port. System Integrity Protection (SIP) still blocks access to many Apple binaries, but third-party software must opt-out explicitly.
8. Recent Public Tooling (2023-2025)
Tool | Year | Remarks |
---|---|---|
task_vaccine | 2023 | Compact PoC that demonstrates PAC-aware thread hijacking on Ventura/Sonoma |
remote_thread_es | 2024 | EndpointSecurity helper used by several EDR vendors to surface REMOTE_THREAD_CREATE events |
Reading these projects’ source code is useful to understand API changes introduced in macOS 13/14 and to stay compatible across Intel ↔ Apple Silicon.
References
- https://bazad.github.io/2018/10/bypassing-platform-binary-task-threads/
- https://github.com/rodionovd/task_vaccine
- https://developer.apple.com/documentation/endpointsecurity/es_event_type_notify_remote_thread_create
tip
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.