macOS IPC - Inter Process Communication
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Mach messaging via Ports
Basic Information
Mach uses tasks as the smallest unit for sharing resources, and each task can contain multiple threads. These tasks and threads are mapped 1:1 to POSIX processes and threads.
Communication between tasks occurs via Mach Inter-Process Communication (IPC), utilising one-way communication channels. Messages are transferred between ports, which act kind of message queues managed by the kernel.
A port is the basic element of Mach IPC. It can be used to send messages and to receive them.
Each process has an IPC table, in there it's possible to find the mach ports of the process. The name of a mach port is actually a number (a pointer to the kernel object).
A process can also send a port name with some rights to a different task and the kernel will make this entry in the IPC table of the other task appear.
Port Rights
Port rights, which define what operations a task can perform, are key to this communication. The possible port rights are (definitions from here):
Receive right, which allows receiving messages sent to the port. Mach ports are MPSC (multiple-producer, single-consumer) queues, which means that there may only ever be one receive right for each port in the whole system (unlike with pipes, where multiple processes can all hold file descriptors to the read end of one pipe).
A task with the Receive right can receive messages and create Send rights, allowing it to send messages. Originally only the own task has Receive right over its port.
If the owner of the Receive right dies or kills it, the send right became useless (dead name).
Send right, which allows sending messages to the port.
The Send right can be cloned so a task owning a Send right can clone the right and grant it to a third task.
Note that port rights can also be passed though Mac messages.
Send-once right, which allows sending one message to the port and then disappears.
This right cannot be cloned, but it can be moved.
Port set right, which denotes a port set rather than a single port. Dequeuing a message from a port set dequeues a message from one of the ports it contains. Port sets can be used to listen on several ports simultaneously, a lot like
select
/poll
/epoll
/kqueue
in Unix.Dead name, which is not an actual port right, but merely a placeholder. When a port is destroyed, all existing port rights to the port turn into dead names.
Tasks can transfer SEND rights to others, enabling them to send messages back. SEND rights can also be cloned, so a task can duplicate and give the right to a third task. This, combined with an intermediary process known as the bootstrap server, allows for effective communication between tasks.
File Ports
File ports allows to encapsulate file descriptors in Mac ports (using Mach port rights). It's possible to create a fileport
from a given FD using fileport_makeport
and create a FD froma. fileport using fileport_makefd
.
Establishing a communication
As mentioned previously, it's possible to send rights using Mach messages, however, you cannot send a right without already having a right to send a Mach message. So, how is the first communication stablished?
For this, he bootstrap server (launchd in mac) is involved, as everyone can get a SEND right to the bootstrap server, it's possible to ask it for a right to send a message to another process:
Task A creates a new port, getting the RECEIVE right over it.
Task A, being the holder of the RECEIVE right, generates a SEND right for the port.
Task A establishes a connection with the bootstrap server, and sends it the SEND right for the port it generated at the beginning.
Remember that anyone can get a SEND right to the bootstrap server.
Task A sends a
bootstrap_register
message to the bootstrap server to associate the given port with a name likecom.apple.taska
Task B interacts with the bootstrap server to execute a bootstrap lookup for the service name (
bootstrap_lookup
). So the bootstrap server can respond, task B will send it a SEND right to a port it previously created inside the lookup message. If the lookup is successful, the server duplicates the SEND right received from Task A and transmits it to Task B.Remember that anyone can get a SEND right to the bootstrap server.
With this SEND right, Task B is capable of sending a message to Task A.
For a bi-directional communication usually task B generates a new port with a RECEIVE right and a SEND right, and gives the SEND right to Task A so it can send messages to TASK B (bi-directional communication).
The bootstrap server cannot authenticate the service name claimed by a task. This means a task could potentially impersonate any system task, such as falsely claiming an authorization service name and then approving every request.
Then, Apple stores the names of system-provided services in secure configuration files, located in SIP-protected directories: /System/Library/LaunchDaemons
and /System/Library/LaunchAgents
. Alongside each service name, the associated binary is also stored. The bootstrap server, will create and hold a RECEIVE right for each of these service names.
For these predefined services, the lookup process differs slightly. When a service name is being looked up, launchd starts the service dynamically. The new workflow is as follows:
Task B initiates a bootstrap lookup for a service name.
launchd checks if the task is running and if it isn’t, starts it.
Task A (the service) performs a bootstrap check-in (
bootstrap_check_in()
). Here, the bootstrap server creates a SEND right, retains it, and transfers the RECEIVE right to Task A.launchd duplicates the SEND right and sends it to Task B.
Task B generates a new port with a RECEIVE right and a SEND right, and gives the SEND right to Task A (the svc) so it can send messages to TASK B (bi-directional communication).
However, this process only applies to predefined system tasks. Non-system tasks still operate as described originally, which could potentially allow for impersonation.
Therefore, launchd should never crash or the whole sysem will crash.
A Mach Message
The mach_msg
function, essentially a system call, is utilized for sending and receiving Mach messages. The function requires the message to be sent as the initial argument. This message must commence with a mach_msg_header_t
structure, succeeded by the actual message content. The structure is defined as follows:
Processes possessing a receive right can receive messages on a Mach port. Conversely, the senders are granted a send or a send-once right. The send-once right is exclusively for sending a single message, after which it becomes invalid.
The initial field msgh_bits
is a bitmap:
First bit (most significative) is used to indicate that a message is complex (more on this below)
The 3rd and 4th are used by the kernel
The 5 least significant bits of the 2nd byte from can be used for voucher: another type of port to send key/value combinations.
The 5 least significant bits of the 3rd byte from can be used for local port
The 5 least significant bits of the 4th byte from can be used for remote port
The types that can be specified in the voucher, local and remote ports are (from mach/message.h):
For example, MACH_MSG_TYPE_MAKE_SEND_ONCE
can be used to indicate that a send-once right should be derived and transferred for this port. It can also be specified MACH_PORT_NULL
to prevent the recipient to be able to reply.
In order to achieve an easy bi-directional communication a process can specify a mach port in the mach message header called the reply port (msgh_local_port
) where the receiver of the message can send a reply to this message.
Note that this kind of bi-directional communication is used in XPC messages that expect a replay (xpc_connection_send_message_with_reply
and xpc_connection_send_message_with_reply_sync
). But usually different ports are created as explained previously to create the bi-directional communication.
The other fields of the message header are:
msgh_size
: the size of the entire packet.msgh_remote_port
: the port on which this message is sent.msgh_voucher_port
: mach vouchers.msgh_id
: the ID of this message, which is interpreted by the receiver.
Note that mach messages are sent over a mach port
, which is a single receiver, multiple sender communication channel built into the mach kernel. Multiple processes can send messages to a mach port, but at any point only a single process can read from it.
Messages are then formed by the mach_msg_header_t
header followed by the body and by the trailer (if any) and it can grant permission to reply to it. In these cases, the kernel just need to pass the message from one task to the other.
A trailer is information added to the message by the kernel (cannot be set by the user) which can be requested in message reception with the flags MACH_RCV_TRAILER_<trailer_opt>
(there is different information that can be requested).
Complex Messages
However, there are other more complex messages, like the ones passing additional port rights or sharing memory, where the kernel also needs to send these objects to the recipient. In this cases the most significant bit of the header msgh_bits
is set.
The possible descriptors to pass are defined in mach/message.h
:
In 32bits, all the descriptors are 12B and the descriptor type is in the 11th one. In 64 bits, the sizes vary.
The kernel will copy the descriptors from one task to the other but first creating a copy in kernel memory. This technique, known as "Feng Shui" has been abused in several exploits to make the kernel copy data in its memory making a process send descriptors to itself. Then the process can receive the messages (the kernel will free them).
It's also possible to send port rights to a vulnerable process, and the port rights will just appear in the process (even if he isn't handling them).
Mac Ports APIs
Note that ports are associated to the task namespace, so to create or search for a port, the task namespace is also queried (more in mach/mach_port.h
):
mach_port_allocate
|mach_port_construct
: Create a port.mach_port_allocate
can also create a port set: receive right over a group of ports. Whenever a message is received it's indicated the port from where it was.
mach_port_allocate_name
: Change the name of the port (by default 32bit integer)mach_port_names
: Get port names from a targetmach_port_type
: Get rights of a task over a namemach_port_rename
: Rename a port (like dup2 for FDs)mach_port_allocate
: Allocate a new RECEIVE, PORT_SET or DEAD_NAMEmach_port_insert_right
: Create a new right in a port where you have RECEIVEmach_port_...
mach_msg
|mach_msg_overwrite
: Functions used to send and receive mach messages. The overwrite version allows to specify a different buffer for message reception (the other version will just reuse it).
Debug mach_msg
As the functions mach_msg
and mach_msg_overwrite
are the ones used to send a receive messages, setting a breakpoint on them would allow to inspect the sent a received messages.
For example start debugging any application you can debug as it will load libSystem.B
which will use this function.
To get the arguments of mach_msg
check the registers. These are the arguments (from mach/message.h):
Get the values from the registries:
Inspect the message header checking the first argument:
That type of mach_msg_bits_t
is very common to allow a reply.
Enumerate ports
The name is the default name given to the port (check how it's increasing in the first 3 bytes). The ipc-object
is the obfuscated unique identifier of the port.
Note also how the ports with only send
right are identifying the owner of it (port name + pid).
Also note the use of +
to indicate other tasks connected to the same port.
It's also possible to use procesxp to see also the registered service names (with SIP disabled due to the need of com.apple.system-task-port
):
You can install this tool in iOS downloading it from http://newosxbook.com/tools/binpack64-256.tar.gz
Code example
Note how the sender allocates a port, create a send right for the name org.darlinghq.example
and send it to the bootstrap server while the sender asked for the send right of that name and used it to send a message.
Privileged Ports
There are some special ports that allows to perform certain sensitive actions or access certain sensitive data in case a tasks have the SEND permissions over them. This makes these ports very interesting from an attackers perspective not only because of the capabilities but because it's possible to share SEND permissions across tasks.
Host Special Ports
These ports are represented by a number.
SEND rights can be obtained by calling host_get_special_port
and RECEIVE rights calling host_set_special_port
. However, both calls require the host_priv
port which only root can access. Moreover, in the past root was able to call host_set_special_port
and hijack arbitrary that allowed for example to bypass code signatures by hijacking HOST_KEXTD_PORT
(SIP now prevents this).
These are divided in 2 groups: The first 7 ports are owned by the kernel being the 1 HOST_PORT
, the 2 HOST_PRIV_PORT
, the 3 HOST_IO_MASTER_PORT
and the 7 is HOST_MAX_SPECIAL_KERNEL_PORT
.
The ones starting from the number 8 are owned by system daemons and they can be found declared in host_special_ports.h
.
Host port: If a process has SEND privilege over this port he can get information about the system calling its routines like:
host_processor_info
: Get processor infohost_info
: Get host infohost_virtual_physical_table_info
: Virtual/Physical page table (requires MACH_VMDEBUG)host_statistics
: Get host statisticsmach_memory_info
: Get kernel memory layout
Host Priv port: A process with SEND right over this port can perform privileged actions like showing boot data or trying to load a kernel extension. The process need to be root to get this permission.
Moreover, in order to call
kext_request
API it's needed to have other entitlementscom.apple.private.kext*
which are only given to Apple binaries.Other routines that can be called are:
host_get_boot_info
: Getmachine_boot_info()
host_priv_statistics
: Get privileged statisticsvm_allocate_cpm
: Allocate Contiguous Physical Memoryhost_processors
: Send right to host processorsmach_vm_wire
: Make memory resident
As root can access this permission, it could call
host_set_[special/exception]_port[s]
to hijack host special or exception ports.
It's possible to see all the host special ports by running:
Task Special Ports
These are ports reserved for well known services. It's possible to get/set them calling task_[get/set]_special_port
. They can be found in task_special_ports.h
:
From here:
TASK_KERNEL_PORT[task-self send right]: The port used to control this task. Used to send messages that affect the task. This is the port returned by mach_task_self (see Task Ports below).
TASK_BOOTSTRAP_PORT[bootstrap send right]: The task's bootstrap port. Used to send messages requesting return of other system service ports.
TASK_HOST_NAME_PORT[host-self send right]: The port used to request information of the containing host. This is the port returned by mach_host_self.
TASK_WIRED_LEDGER_PORT[ledger send right]: The port naming the source from which this task draws its wired kernel memory.
TASK_PAGED_LEDGER_PORT[ledger send right]: The port naming the source from which this task draws its default memory managed memory.
Task Ports
Originally Mach didn't have "processes" it had "tasks" which was considered more like a container of threads. When Mach was merged with BSD each task was correlated with a BSD process. Therefore every BSD process has the details it needs to be a process and every Mach task also have its inner workings (except for the inexistent pid 0 which is the kernel_task
).
There are two very interesting functions related to this:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: Get a SEND right for the task por of the task related to the specified by thepid
and give it to the indicatedtarget_task_port
(which is usually the caller task which has usedmach_task_self()
, but could be a SEND port over a different task.)pid_for_task(task, &pid)
: Given a SEND right to a task, find to which PID this task is related to.
In order to perform actions within the task, the task needed a SEND
right to itself calling mach_task_self()
(which uses the task_self_trap
(28)). With this permission a task can perform several actions like:
task_threads
: Get SEND right over all task ports of the threads of the tasktask_info
: Get info about a tasktask_suspend/resume
: Suspend or resume a tasktask_[get/set]_special_port
thread_create
: Create a threadtask_[get/set]_state
: Control task stateand more can be found in mach/task.h
Notice that with a SEND right over a task port of a different task, it's possible to perform such actions over a different task.
Moreover, the task_port is also the vm_map
port which allows to read an manipulate memory inside a task with functions such as vm_read()
and vm_write()
. This basically means that a task with SEND rights over the task_port of a different task is going to be able to inject code into that task.
Remember that because the kernel is also a task, if someone manages to get a SEND permissions over the kernel_task
, it'll be able to make the kernel execute anything (jailbreaks).
Call
mach_task_self()
to get the name for this port for the caller task. This port is only inherited acrossexec()
; a new task created withfork()
gets a new task port (as a special case, a task also gets a new task port afterexec()
in a suid binary). The only way to spawn a task and get its port is to perform the "port swap dance" while doing afork()
.These are the restrictions to access the port (from
macos_task_policy
from the binaryAppleMobileFileIntegrity
):If the app has
com.apple.security.get-task-allow
entitlement processes from the same user can access the task port (commonly added by Xcode for debugging). The notarization process won't allow it to production releases.Apps with the
com.apple.system-task-ports
entitlement can get the task port for any process, except the kernel. In older versions it was calledtask_for_pid-allow
. This is only granted to Apple applications.Root can access task ports of applications not compiled with a hardened runtime (and not from Apple).
The task name port: An unprivileged version of the task port. It references the task, but does not allow controlling it. The only thing that seems to be available through it is task_info()
.
Thread Ports
Threads also have associated ports, which are visible from the task calling task_threads
and from the processor with processor_set_threads
. A SEND right to the thread port allows to use the function from the thread_act
subsystem, like:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
Any thread can get this port calling to mach_thread_sef
.
Shellcode Injection in thread via Task port
You can grab a shellcode from:
Introduction to ARM64v8Compile the previous program and add the entitlements to be able to inject code with the same user (if not you will need to use sudo).
For this to work on iOS you need the entitlement dynamic-codesigning
in order to be able to make a writable memory executable.
Dylib Injection in thread via Task port
In macOS threads might be manipulated via Mach or using posix pthread
api. The thread we generated in the previous injection, was generated using Mach api, so it's not posix compliant.
It was possible to inject a simple shellcode to execute a command because it didn't need to work with posix compliant apis, only with Mach. More complex injections would need the thread to be also posix compliant.
Therefore, to improve the thread it should call pthread_create_from_mach_thread
which will create a valid pthread. Then, this new pthread could call dlopen to load a dylib from the system, so instead of writing new shellcode to perform different actions it's possible to load custom libraries.
You can find example dylibs in (for example the one that generates a log and then you can listen to it):
macOS Dyld Hijacking & DYLD_INSERT_LIBRARIESThread Hijacking via Task port
In this technique a thread of the process is hijacked:
macOS Thread Injection via Task portTask Port Injection Detection
When calling task_for_pid
or thread_create_*
increments a counter in the struct task from the kernel which can by accessed from user mode calling task_info(task, TASK_EXTMOD_INFO, ...)
Exception Ports
When a exception occurs in a thread, this exception is sent to the designated exception port of the thread. If the thread doesn't handle it, then it's sent to the task exception ports. If the task doesn't handle it, then it's sent to the host port which is managed by launchd (where it'll be acknowledge). This is called exception triage.
Note that at the end usually if not properly handle the report will end up being handle by the ReportCrash daemon. However, it's possible for another thread in the same task to manage the exception, this is what crash reporting tools like PLCreashReporter
does.
Other Objects
Clock
Any user can access information about the clock however in order to set the time or modify other settings one has to be root.
In order to get info its possible to call functions from the clock
subsystem like: clock_get_time
, clock_get_attributtes
or clock_alarm
In order to modify values the clock_priv
subsystem can be sued with functions like clock_set_time
and clock_set_attributes
Processors and Processor Set
The processor apis allows to control a single logical processor calling functions like processor_start
, processor_exit
, processor_info
, processor_get_assignment
...
Moreover, the processor set apis provides a way to group multiple processors into a group. It's possible to retrieve the default processor set calling processor_set_default
.
These are some interesting APIs to interact with the processor set:
processor_set_statistics
processor_set_tasks
: Return an array of send rights to all tasks inside the processor setprocessor_set_threads
: Return an array of send rights to all threads inside the processor setprocessor_set_stack_usage
processor_set_info
As mentioned in this post, in the past this allowed to bypass the previously mentioned protection to get task ports in other processes to control them by calling processor_set_tasks
and getting a host port on every process.
Nowadays you need root to use that function and this is protected so you will only be able to get these ports on unprotected processes.
You can try it with:
XPC
Basic Information
XPC, which stands for XNU (the kernel used by macOS) inter-Process Communication, is a framework for communication between processes on macOS and iOS. XPC provides a mechanism for making safe, asynchronous method calls between different processes on the system. It's a part of Apple's security paradigm, allowing for the creation of privilege-separated applications where each component runs with only the permissions it needs to do its job, thereby limiting the potential damage from a compromised process.
For more information about how this communication work on how it could be vulnerable check:
macOS XPCMIG - Mach Interface Generator
MIG was created to simplify the process of Mach IPC code creation. This is because a lot of work to program RPC involves the same actions (packing arguments, sending the msg, unpacking the data in the server...).
MIC basically generates the needed code for server and client to communicate with a given definition (in IDL -Interface Definition language-). Even if the generated code is ugly, a developer will just need to import it and his code will be much simpler than before.
For more info check:
macOS MIG - Mach Interface GeneratorReferences
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Last updated