Notes on Mach IPC
Introduction⌗
This blog is intended to be a dedicated effort to structure information in relation to Mach Messages in a way that I can remember and use as a reference for the future.
Often as a vulnerability researcher, attack-surfaces involving compromising other adjacent processes relative to a preliminar target are increasingly important with today’s enforced mitigations (specially in the SBX realm), and having a good understanding of the internals of the underlying technology which makes this inter-process communication mechanism possible is vital to understand and tackle adjacent attack surfaces.
Overview⌗
XNU is the computer operating system kernel in use on macOS operating system, and released as free and open-source software as part of the Darwin OS, which in addition to macOS is also the basis for the Apple TV Software, iOS, iPadOS, watchOS, and tvOS OSes. XNU is an abbreviation of X is Not Unix
.
XNU is a hybrid kernel, containing features of both monolithic and microkernel designs. An overview of the comparison of the distinct characteristics between monolithic and microkernel architctures are the following:
-
A microkernel is a kernel type that implements an operating system by providing methods, including low-level address space management, IPC, and thread management. On the other hand, a monolithic kernel is a type of kernel in which the complete OS runs in the kernel space.
-
Microkernels run user and kernel services in different address spaces. On the other hand, the monolithic kernel runs both kernel and user services in the same address space. In microkernels, only essential processes like IPC, memory management, and scheduling take place in kernel space.
-
The execution of a microkernel is slower because communication between the system’s application and hardware is established by message passing. On the other hand, the execution of the monolithic kernel is faster because the system call establishes the communication of the system’s application and hardware.
-
Microkernels use the messaging queues to achieve IPC. On the other hand, monolithic kernels use sockets and signals to achieve IPC. The microkernel size is small than the monolithic kernel because only the kernel services run in the kernel address space. On the other hand, the monolithic kernel size is larger because both user and kernel services run in the same address space.
-
Microkernels are more secure than the monolithic kernels because the operating system is unchanged if a service fails in a microkernel. On the other hand, if a service fails in a monolithic kernel, the entire system fails.
-
Microkernels are simple to extend as new services are added in user address space, which is separate from kernel space, and thus the kernel doesn’t need to be updated. On the other hand, the complete kernel must be updated if a new service is used in a monolithic kernel.
-
Microkernel designing needs less code that leads to fewer errors. In contrast, the monolithic kernel requires more code that leads to more errors.
Running the core of an operating system as separated processes, allows for great flexibility but this often reduces performance due to time-consuming kernel/user mode context switches and overhead stemming from mapping or copying messages between the address spaces of the kernel and that of the service daemons.
Given the challenges of a microkernel design, XNU uses a hybrid design. Much of the functionality lives in the kernel space, but also many subsystems are backed by user space daemons.
Mach messages are relevant in this context as it implements the core technology for IPC communication between kernel and user-space tasks in the Mach microkernel architecture.
Mach Messages⌗
Mach messages are a message queue mechanism based IPC mechanism, in which endpoints are required to receive and send messages through. In Mach, these endpoints are called mach ports. Mach ports are a kernel level abstraction and are not directly exposed to user-space, instead these mach ports can be accessed through port rights.
Each task has its own namespace of port rights, and these are represented as 32bit integers. Is due to this reason that many Mach port APIs take an explicit task argument in order to disambiguate the respective mach port name.
Is important to note that mach ports are not only used to establish a means of IPC communication for the kernel and user-space services, but to also serve as an abstraction for resources. As an example, in user-space all tasks, threads or even semaphores handles are used as port rights to ports representing the underlying resources.
Here for example are some typedefs from mach/mach_types.h
showcasing this abstraction:
/*
* If we are not in the kernel, then these will all be represented by
* ports at user-space.
*/
typedef mach_port_t task_t;
typedef mach_port_t thread_t;
typedef mach_port_t thread_act_t;
typedef mach_port_t ipc_space_t;
typedef mach_port_t host_t;
typedef mach_port_t host_priv_t;
typedef mach_port_t host_security_t;
typedef mach_port_t processor_t;
typedef mach_port_t processor_set_t;
typedef mach_port_t processor_set_control_t;
typedef mach_port_t semaphore_t;
typedef mach_port_t lock_set_t;
typedef mach_port_t ledger_t;
typedef mach_port_t alarm_t;
typedef mach_port_t clock_serv_t;
typedef mach_port_t clock_ctrl_t;
Mach Ports and Mach Port Rights⌗
Mach ports are the IPC primitives under Mach. A mach port is conceptually a message queue maintained by the kernel. Tasks and the kernel itself can enqueue and dequeue messages to/from a port via a port right to that port.
A port right is a handle to a port that allows either sending (enqueuing) or receiving (dequeuing) messages. There are the following kinds of port rights:
Receive right
: 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.
Send right
: allows sending messages to the port.Send-once
: allows sending one message to the port and then disappears.Port set
: 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
: This 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.
In addition, A port right name is a specific integer value a task uses to refer to a port right it holds, a lot like a file descriptor that a process uses to refer to an open file. * Sending a port right name, the integer, to another task does not allow it to use the name to access the port right, because the name is only meaningful in the context of the port right namespace of the original task.
Mach ports are an unidirectional communication channel by design. Therefore there can only exists one holder of a receive right to a port, however there can be multiple send right owners. Rights can be transfered over messages. There can be only one receive right, and this can only be moved (in case it wants to be send to a different task). However, send rights can either be moved or copied to other tasks.
Given a unidirectional channel a bidirectional communication can be established. The following describes how this process works:
- Alice is an owner of a receive right to a port
- Bob is an owner of a send right to the same port
- Bob then allocates a Mach port with a receive right
- Bob inserts a send right to this new mach port
- He transfers the send right to this port over a message to Alice
Now both Bob and Alice own a receive and send rights to ports they can leverage to communicate with one another. Syncing up the communication over different ports is not very convenient or efficient, therefore a mach message can optionally include a repy port which can be used to send a message response.
In addition to simply send and receive port rights, other port rights can be enforced. The following values denote port rights (obtained from this resource) :
-
MACH_MSG_TYPE_MAKE_SEND
The message will carry a send right, but the caller must supply a receive right. The send right is created from the receive right, and the receive right’s make-send count is incremented.
-
MACH_MSG_TYPE_COPY_SEND
The message will carry a send right, and the caller should supply a send right. The user reference count for the supplied send right is not changed. The caller may also supply a dead name and the receiving task will get
MACH_PORT_DEAD
. -
MACH_MSG_TYPE_MOVE_SEND
The message will carry a send right, and the caller should supply a send right. The user reference count for the supplied send right is decremented, and the right is destroyed if the count becomes zero. Unless a receive right remains, the name becomes available for recycling. The caller may also supply a dead name, which loses a user reference, and the receiving task will get
MACH_PORT_DEAD
. -
MACH_MSG_TYPE_MAKE_SEND_ONCE
The message will carry a send-once right, but the caller must supply a receive right. The send-once right is created from the receive right.
-
MACH_MSG_TYPE_MOVE_SEND_ONCE
The message will carry a send-once right, and the caller should supply a send-once right. The caller loses the supplied send-once right. The caller may also supply a dead name, which loses a user reference, and the receiving task will get
MACH_PORT_DEAD
. -
MACH_MSG_TYPE_MOVE_RECEIVE
The message will carry a receive right, and the caller should supply a receive right. The caller loses the supplied receive right, but retains any send rights with the same name.
The following msgt_name values in a received message indicate that it carries port rights:
-
MACH_MSG_TYPE_PORT_SEND
This name is an alias forMACH_MSG_TYPE_MOVE_SEND
. The message carried a send right. If the receiving task already has send and/or receive rights for the port, then that name for the port will be reused. Otherwise, the new right will have a new name. If the task already has send rights, it gains a user reference for the right (unless this would cause the user-reference count to overflow). Otherwise, it acquires the send right, with a user-reference count of one. -
MACH_MSG_TYPE_PORT_SEND_ONCE
This name is an alias forMACH_MSG_TYPE_MOVE_SEND_ONCE
. The message carried a send-once right. The right will have a new name. -
MACH_MSG_TYPE_PORT_RECEIVE
This name is an alias forMACH_MSG_TYPE_MOVE_RECEIVE
. The message carried a receive right. If the receiving task already has send rights for the port, then that name for the port will be reused. Otherwise, the right will have a new name.
If a message carries a send or send-once right, and the port dies while the message is in transit, then the receiving task will get MACH_PORT_DEAD
instead of a right.
Establishing a connection⌗
In Darwin, there is a centralised broker service, also known as the bootstrap server, which serves as an orchestrator to establish connections between tasks/services.
The role of this bootstrap server in XNU is fulfilled by launchd
(although before launchd, the init process and the bootstrap server where separated).
Each spawned service/task holds an implicit send right to the broker service. The bootstrap server is resposible for service registration and lookup, and allows to establish a connection between two parties. As an example to showcase the role of the bootstrap server, the following describes a scenario in which two tasks establish a connection via the broker server:
- Alice registers her service with the bootstrap server using some name. eg
com.test-service.alice
. - Registration in practice means that Alice grants the broker a send right to a port Alice holds a receive right to.
- Bob queries the bootstrap server for the
com.test-service.alice
name, and the broker server copies the send right registered for that tasks, which was sent by Alice previously. - Now Bob holds a send right to a port in which Alice holds a receive right.
Messages⌗
Mach messages are used to exchange data over Mach ports. The following is an overview of the structure of mach messages:
As shown in the diagram above, a mach message is composed of a header and a body. Mach message header conforms to a structure of type mach_msg_header_t
. This structure is defined as follows:
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
msgh_bits
- options and message metadata, such as disposition of port rights in the messagemsgh_size
- total message size, including headermsgh_remote_port
- remote Mach port, used as the destination when sending a message, or a reply port when receivingmsgh_local_port
- local Mach port, the port the message was received on, or a reply port when sending a messagemsgh_voucher_port
- port identifying a Mach Voucher, that’s an optional fieldmsgh_id
- user defined message identifier
When receiving a message, messages can optionally contain a field known as trailer at the end of the message. Trailers contain information about the message. Trailers will be covered in more detailed in a future section.
Unidirectional Message Example⌗
This section will describe a minimalist example of an unidirectional exchange between two parties over mach messages. This will be divided into two parts, being the server and client side roles.
Server Side⌗
The server side functionality is summerized in the following steps:
- Create a Mach port receive right
- Add a send right to the port for the bootstrap server to use
- Retrieving a Mach port to communicate with the broker
- Registering our service
- Message receiving
The full source code for the server is shown as follows:
// server.c
#include <bootstrap.h>
#include <mach/mach.h>
#include <mach/message.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
mach_msg_trailer_t trailer;
} MachMessage;
int main(void) {
mach_port_t task = mach_task_self();
mach_port_name_t recv_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if (mach_port_insert_right(task, recv_port, recv_port, MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if (bootstrap_register(bootstrap_port, "com.test-service.alice", recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
MachMessage message = {0};
while (true) {
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t *)&message, // msg
MACH_RCV_MSG, // option
0, // send size
sizeof(message), // receive size
recv_port, // recv_name
MACH_MSG_TIMEOUT_NONE, // timeout
MACH_PORT_NULL); // notify port
if (ret != MACH_MSG_SUCCESS) {
return ret;
}
printf("[+] Received message!\n");
printf("\tmsgh_id\t\t: %d\n", message.header.msgh_id);
printf("\tmessage.body\t: %s\n", message.body);
}
return EXIT_SUCCESS;
}
Create a Mach port receive right⌗
Defining our message structure composed of a mach_msg_header_t
header, a body and a default mach_msg_trailer_t
, although this last one is not neccesary for now.
typedef struct {
mach_msg_header_t header;
char body[4096];
mach_msg_trailer_t trailer;
} MachMessage;
mach_port_t task = mach_task_self();
mach_port_name_t recv_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
Add a send right to the port for the bootstrap server to use⌗
Inserting a new send right into our mach port
if (mach_port_insert_right(task, recv_port, recv_port, MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
Registering service to bootstrap server⌗
Retrieving the port of the bootstrap server
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if (bootstrap_register(bootstrap_port, "com.test-service.alice", recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
Wait to receive messages⌗
Wait in an infinite loop for messages to arrive
while (true) {
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t *)&message, // msg
MACH_RCV_MSG, // option
0, // send size
sizeof(message), // receive size
recv_port, // recv_name
MACH_MSG_TIMEOUT_NONE, // timeout
MACH_PORT_NULL); // notify port
if (ret != MACH_MSG_SUCCESS) {
return ret;
}
printf("[+] Received message!\n");
printf("\tmsgh_id\t\t: %d\n", message.header.msgh_id);
printf("\tmessage.body\t: %s\n", message.body);
}
Client Side⌗
The server side functionality is summerized in the following steps:
- Retrieve the bootstrap server port to query it for the service port
- Query bootstrap for the service port
- Setup message header
- Fill message body
- Send the message
The full source of the client is shown as follows:
// client.c
#include <bootstrap.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/message.h>
#include <mach/port.h>
#include <mach/task.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
int main(void) {
mach_port_name_t task = mach_task_self();
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t port;
if (bootstrap_look_up(bootstrap_port, "com.test-service.alice", &port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
MachMessage message = {0};
message.header.msgh_remote_port = port;
// Copy send right to the remote port
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND, // remote
0, // local
0, // voucher
0); // other
message.header.msgh_id = 4;
message.header.msgh_size = sizeof(message);
strcpy(message.body, "Some Message Body Data :)");
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t *)&message, // msg
MACH_SEND_MSG, // option
sizeof(message), // send size
0, // recv size
MACH_PORT_NULL, // recv name
MACH_MSG_TIMEOUT_NONE, // timeout
MACH_PORT_NULL); // notify port
if (ret != MACH_MSG_SUCCESS) {
printf("[-] Failed mach_msg: %d\n", ret);
return EXIT_FAILURE;
}
sleep(120);
return EXIT_SUCCESS;
}
Retrieve target service port⌗
Retrieve the bootstrap server prot
mach_port_name_t task = mach_task_self();
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t port;
if (bootstrap_look_up(bootstrap_port, "com.test-service.alice", &port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
Setup message header⌗
One important macro to cover is the macro MACH_MSGH_BITS_SET
. It serves to set appropiate bits in mach_msg_bits_t
representing a given port right disposition. This macro is defined in mach/message.h.auto.html
:
/* setter macros for the bits */
#define MACH_MSGH_BITS(remote, local) /* legacy */ \
((remote) | ((local) << 8))
#define MACH_MSGH_BITS_SET_PORTS(remote, local, voucher) \
(((remote) & MACH_MSGH_BITS_REMOTE_MASK) | \
(((local) << 8) & MACH_MSGH_BITS_LOCAL_MASK) | \
(((voucher) << 16) & MACH_MSGH_BITS_VOUCHER_MASK))
#define MACH_MSGH_BITS_SET(remote, local, voucher, other) \
(MACH_MSGH_BITS_SET_PORTS((remote), (local), (voucher)) \
| ((other) &~ MACH_MSGH_BITS_PORTS_MASK))
remote
, local
and voucher
, is setting bits on specific binary masks. These binary masks correspond to the following:
-
MACH_MSGH_BITS_REMOTE_MASK
:Encodes
mach_msg_type_name_t
values that specify the port rights in themsgh_remote_port
field.The value must specify a send or send-once right for the destination of the message.
-
MACH_MSGH_BITS_LOCAL_MASK
:Encodes
mach_msg_type_name_t
values that specify the port rights in themsgh_local_port
field.If the value doesn’t specify a send or send-once right for the message’s reply port, it must be zero and
msgh_local_port
must beMACH_PORT_NULL
. -
MACH_MSGH_BITS_COMPLEX
: The complex bit must be specified if the message body contains additional port rights or out-of-line memory regions.
Basically these mask, estipulate to which port the port right disposition is being applied (remote port, local port, or OOL port rights).
These bits applied to these binary masks correspond to a specified port disposition, which indicates the kernel what to do with the port specified in the mach message header.
Here we are usig the MACH_MSGH_BITS_SET
to specify that the send right should be copied to the remote port with the value MACH_MSG_TYPE_COPY_SEND
. This may seem a bit weird at first, however lets remember that port disposition serve to direct the kernel how to process port rights for each port. In this case, we are directing the kernel to copy the send right of the registered service shared by the bootstrap server to the remote port in order for the message to be delivered to the destination endpoint sucessfully.
MachMessage message = {0};
message.header.msgh_remote_port = port;
// Copy send right to the remote port
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND, // remote
0, // local
0, // voucher
0); // other
message.header.msgh_id = 4;
message.header.msgh_size = sizeof(message);
Send the message⌗
Here we simply populate the message body, and we proceed to call mach_msg
with the MACH_SEND_MSG
option
strcpy(message.body, "Some Message Body Data :)");
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t *)&message, // msg
MACH_SEND_MSG, // option
sizeof(message), // send size
0, // recv size
MACH_PORT_NULL, // recv name
MACH_MSG_TIMEOUT_NONE, // timeout
MACH_PORT_NULL); // notify port
if (ret != MACH_MSG_SUCCESS) {
printf("[-] Failed mach_msg: %d\n", ret);
return EXIT_FAILURE;
}
Bidirectional Message Example⌗
In this example we will extend the previous example to support reply messages.
The server code is the following:
Server⌗
// server.c
#include <bootstrap.h>
#include <mach/mach.h>
#include <mach/message.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
MachMessage message;
mach_msg_trailer_t trailer;
} ReceivedMachMessage;
int main(void) {
mach_port_t task = mach_task_self();
// allocating a new ephemeral mach port
mach_port_name_t recv_port;
if(mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// inserting a port send right to newly allocated port
if (mach_port_insert_right(task, recv_port, recv_port, MACH_MSG_TYPE_MAKE_SEND)
!= KERN_SUCCESS) {
return EXIT_FAILURE;
}
// retrieving the bootstrap server port
mach_port_t bootstrap_port;
if(task_get_bootstrap_port(task, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// register ourselves with to the bootstrap server
if (bootstrap_register(bootstrap_port, "com.service.alice", recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// waiting for messages
printf("[*] Waiting for messages\n");
while(true) {
// receiving message
ReceivedMachMessage received_message = {0};
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t*)&received_message,
MACH_RCV_MSG,
0,
sizeof(received_message),
recv_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
printf("[-] Failed to receive mach message");
}
printf("[+] Got message!\n");
printf("[+] \tremote port:\t%d\n", received_message.message.header.msgh_remote_port);
printf("[+] \tbody: \t\t%s\n", received_message.message.body);
if (received_message.message.header.msgh_remote_port == MACH_PORT_NULL) {
continue;
}
// crafting reply message
MachMessage response = {0};
// We can use msgh_bits from the received message as it is
response.header.msgh_bits = received_message.message.header.msgh_bits
& MACH_MSGH_BITS_REMOTE_MASK;
response.header.msgh_remote_port = received_message.message.header.msgh_remote_port;
response.header.msgh_id = 0;
response.header.msgh_size = sizeof(response);
strcpy(response.body, "A response to a sent message");
// sending message reply
ret = mach_msg(
(mach_msg_header_t *)&response,
MACH_SEND_MSG,
sizeof(response),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
printf("[+] Error sending message reply");
}
}
return EXIT_SUCCESS;
}
// crafting reply message
MachMessage response = {0};
// We can use msgh_bits from the received message as it is
response.header.msgh_bits = received_message.message.header.msgh_bits
& MACH_MSGH_BITS_REMOTE_MASK;
// we change the destination port to conform with the one sent on our intial message
response.header.msgh_remote_port = received_message.message.header.msgh_remote_port;
response.header.msgh_id = 0;
response.header.msgh_size = sizeof(response);
strcpy(response.body, "A response to a sent message");
// sending message reply
ret = mach_msg(
(mach_msg_header_t *)&response,
MACH_SEND_MSG,
sizeof(response),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
printf("[+] Error sending message reply");
}
Lets look at the clients to see what changes we need to make clients also conform to messages replies
Client⌗
The full client source is shown below:
// client.c
#include <bootstrap.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/message.h>
#include <mach/port.h>
#include <mach/task.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
MachMessage message;
mach_msg_trailer_t trailer;
} ReceivedMachMessage;
int main(void) {
mach_port_t task = mach_task_self();
// retrieving the bootstrap server port
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// retrieving port of destination service
mach_port_t port;
if (bootstrap_look_up(bootstrap_port, "com.service.alice", &port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// creating a bew epehemeral mach port dedicated to receive reply messages
mach_port_t reply_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &reply_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// inserting a send right to it
if (mach_port_insert_right(task, reply_port, reply_port, MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// crafting initial message
MachMessage message = {0};
message.header.msgh_remote_port = port;
message.header.msgh_local_port = reply_port;
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
// we direct the kernel to also make the send right for the local port
MACH_MSG_TYPE_MAKE_SEND,
0,
0
);
message.header.msgh_size = sizeof message;
strcpy(message.body, "This is a test message");
printf("[*] Sending message to port %d\n", message.header.msgh_remote_port);
// Sending message
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t*)&message,
MACH_SEND_MSG,
sizeof message,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL
);
if (ret != MACH_MSG_SUCCESS) {
printf("[+] Failed to send message\n");
return EXIT_FAILURE;
}
printf("[*] Waiting for replies to port %d\n", message.header.msgh_local_port);
// waiting for replies
ReceivedMachMessage response = {0};
ret = mach_msg(
(mach_msg_header_t*)&response,
MACH_RCV_MSG,
0,
sizeof response,
reply_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
printf("[-] Could not receive mach message");
return EXIT_FAILURE;
}
printf("[+] Got reply!\n");
printf("\tFrom port:\t%d\n", response.message.header.msgh_remote_port);
printf("\tbody:\t\t%s\n", response.message.body);
}
Allocating a new mach port for replies⌗
In order to receive replies, we need to allocate a new mach port for doing so.
In order to do this we do this as we did it in the server side, via mach_port_allocate
.
In addition, in order for the remote task to be able to send us messages, we need to also insert a send right to our newly created port:
// creating a bew epehemeral mach port dedicated to receive reply messages
mach_port_t reply_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &reply_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
// inserting a send right to it
if (mach_port_insert_right(task, reply_port, reply_port, MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
Crafting message with reply port with appropiate port disposition⌗
Now we have to craft the initial message we will be sending to the server. In order to do so we need to set the apporpiate disposition not only for the remote port, but also to our local port dedicated for the remote endpoit to send us replies.
In order to do this we have to use the macro MACH_MSGH_BITS_SET
again, setting the MACH_MSG_TYPE_COPY_SEND
disposition for the remote port’s port right to be able to send the message in the first place, but also the MACH_MSG_TYPE_MAKE_SEND
dispositioin for our local port, which is dedicated to receive message replies. With this, this message will send our local port send right to the remote endpoint, in order for it to be able to send us replies.
// crafting initial message
MachMessage message = {0};
message.header.msgh_remote_port = port;
message.header.msgh_local_port = reply_port;
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
// we direct the kernel to also make the send right for the local port
MACH_MSG_TYPE_MAKE_SEND,
0,
0
);
message.header.msgh_size = sizeof message;
strcpy(message.body, "This is a test message");
printf("[*] Sending message to port %d\n", message.header.msgh_remote_port);
// Sending message
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t*)&message,
MACH_SEND_MSG,
sizeof message,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL
);
Waiting for replies from remote task⌗
Once we have sent our local port send right to the remote endpoint, all we have to do now is to listen to replies in our local port
// waiting for replies
ReceivedMachMessage response = {0};
ret = mach_msg(
(mach_msg_header_t*)&response,
MACH_RCV_MSG,
0,
sizeof response,
reply_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
Complex Message Example⌗
In this section we will modify the previous example in order to showcase how can we use complex mach messages in order to send mach ports to a destinatary, instead of using the local_port field in the mach message header.
Server⌗
The full source of the server application can be shown below:
// server.c
#include <bootstrap.h>
#include <mach/mach.h>
#include <mach/message.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} PortMachMessage;
typedef struct {
PortMessage message;
mach_msg_trailer_t trailer;
} ReceivedPortMachMessage;
#define MSG_ID_PORT 1337
int main(void) {
mach_port_t task = mach_task_self();
mach_port_name_t recv_port;
if(mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if(mach_port_insert_right(task, recv_port, recv_port, MACH_MSG_TYPE_MAKE_SEND)
!= KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if(bootstrap_register(bootstrap_port, "com.service.alice", recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
while(true) {
ReceivedPortMachMessage received_message = {0};
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t*)&received_message,
MACH_RCV_MSG,
0,
sizeof received_message,
recv_port,
0,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
puts("[-] Failed to receive message");
return EXIT_FAILURE;
}
if (received_message.message.header.msgh_id != MSG_ID_PORT) {
printf("[-] Received invalid message id: %d\n", received_message.message.header.msgh_id);
continue;
}
printf("[*] Message received!\n\tPort %d received in message\n",
received_message.message.descriptor.name);
MachMessage response = {0};
response.header.msgh_bits = MACH_MSGH_BITS_SET(
received_message.message.descriptor.disposition,
0,
0,
0);
response.header.msgh_remote_port = received_message.message.descriptor.name;
response.header.msgh_size = sizeof response;
strcpy(response.body, "A sample response");
ret = mach_msg(
(mach_msg_header_t*)&response,
MACH_SEND_MSG,
sizeof response,
0,
0,
0,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
puts("[+] Error sending message reply");
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
mach_msg_size_t
field denoting the number of subsequent descriptors, and then an array of descriptors, which in our case for sake of simplicity, we only defined one descriptor of type mach_msg_port_descriptor_t
, although other descriptor types are supported such as OOL (out-of-line) descriptors which we will be covering next. For the sake of this example the following are the defined message types:
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} PortMachMessage;
typedef struct {
PortMessage message;
mach_msg_trailer_t trailer;
} ReceivedPortMachMessage;
PortMachMessage
type, we put a mach_msg_size_t
field called msgh_descriptor_count
right after the mach_msg_header_t
member. One could instead use the mach_msg_body_t
type to define the ‘body’ of the message, although it seems a bit misleading because this field only contains one member called msgh_descriptor_count
as shown below, but both of these declarations are valid.
typedef struct {
mach_msg_size_t msgh_descriptor_count;
} mach_msg_body_t;
Retrieving remote port disposition for message reply⌗
In the previous example we actually needed to handle the port disposition manually by applying a binary mask to the reply message message header msgh_bits field.
However, for this scenario since we are using a complex message with a port descriptor, we can obstain the port disposition directly from the port descriptor’s name field in the received message.
MachMessage response = {0};
response.header.msgh_bits = MACH_MSGH_BITS_SET(
received_message.message.descriptor.disposition,
0,
0,
0);
response.header.msgh_remote_port = received_message.message.descriptor.name;
response.header.msgh_size = sizeof response;
Client⌗
Now we will cover the changes we will have to make to support complex messages with port descriptors in our client.
The following is the full source of the client:
// client.c
#include <bootstrap.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/message.h>
#include <mach/port.h>
#include <mach/task.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} PortMachMessage;
typedef struct {
MachMessage message;
mach_msg_trailer_t trailer;
} ReceivedMachMessage;
#define MSG_ID_PORT 1337
int main(void) {
mach_port_t task = mach_task_self();
mach_port_t bootstrap_port;
if(task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t port;
if(bootstrap_look_up(bootstrap_port, "com.service.alice", &port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t reply_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &reply_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if(mach_port_insert_right(task, reply_port, reply_port, MACH_MSG_TYPE_MAKE_SEND)
!= KERN_SUCCESS) {
return EXIT_FAILURE;
}
PortMachMessage message = {0};
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
0,
0,
MACH_MSGH_BITS_COMPLEX
);
message.header.msgh_id = MSG_ID_PORT;
message.header.msgh_size = sizeof message;
message.header.msgh_remote_port = port;
message.msgh_descriptor_count = 1;
message.descriptor.name = reply_port;
message.descriptor.disposition = MACH_MSG_TYPE_MAKE_SEND;
message.descriptor.type = MACH_MSG_PORT_DESCRIPTOR;
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t*)&message,
MACH_SEND_MSG,
sizeof message,
0,
0,
0,
MACH_PORT_NULL
);
if (ret != MACH_MSG_SUCCESS) {
puts("[-] Failed to send mach message");
return EXIT_FAILURE;
}
ReceivedMachMessage response = {0};
ret = mach_msg(
(mach_msg_header_t*)&response,
MACH_RCV_MSG,
0,
sizeof response,
reply_port,
0,
MACH_PORT_NULL
);
if (ret != MACH_MSG_SUCCESS) {
puts("[-] Failed to receive mach message");
return EXIT_FAILURE;
}
printf("[*] Response received\n\tbody: %s\n", response.message.body);
return EXIT_SUCCESS;
}
Set the complex bit in the message header⌗
One thing that is easy to overlook to craft complex messages, is to set the appropiate bit in the mach message header to denote the message is indeed complex. We can do this with the macro MACH_MSGH_BITS_SET
as follows:
PortMachMessage message = {0};
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
0,
0,
MACH_MSGH_BITS_COMPLEX
);
Crafting A Complex Message⌗
In contrast with previous examples, appart from populating the header of our mach message with a msgh_id
(optional), msgh_size
and msgh_remote_port
fields, we need to also populate our port descriptor. In order to do this we need to put the port name under the descriptor’s name field, the port disposition under the descritor’s disposition field and lastly the type to reflect that our descriptor is of type MACH_MSG_PORT_DESCRIPTOR
. We can do all of this as follows:
message.header.msgh_id = MSG_ID_PORT;
message.header.msgh_size = sizeof message;
message.header.msgh_remote_port = port;
message.msgh_descriptor_count = 1;
message.descriptor.name = reply_port;
message.descriptor.disposition = MACH_MSG_TYPE_MAKE_SEND;
message.descriptor.type = MACH_MSG_PORT_DESCRIPTOR;
// server.c
#include <bootstrap.h>
#include <mach/mach.h>
#include <mach/message.h>
#include <dispatch/dispatch.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} PortMachMessage;
typedef struct {
PortMessage message;
mach_msg_trailer_t trailer;
} ReceivedPortMachMessage;
#define MSG_ID_PORT 1337
int main(void) {
mach_port_t task = mach_task_self();
mach_port_name_t recv_port;
if(mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if(mach_port_insert_right(task, recv_port, recv_port, MACH_MSG_TYPE_MAKE_SEND)
!= KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
if(bootstrap_register(bootstrap_port, "com.service.alice", recv_port) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
dispatch_source_t port_source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_MACH_RECV,
recv_port,
0,
dispatch_queue_create("com.service.MessageQueue", DISPATCH_QUEUE_CONCURRENT)
);
dispatch_source_set_event_handler(port_source, ^(void) {
ReceivedPortMachMessage received_message = {0};
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t*)&received_message,
MACH_RCV_MSG,
0,
sizeof received_message,
recv_port,
0,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
puts("[-] Failed to receive message");
return;
}
if (received_message.message.header.msgh_id != MSG_ID_PORT) {
printf("[-] Received invalid message id: %d\n", received_message.message.header.msgh_id);
return;
}
printf("[*] Message received!\n\tPort %d received in message\n",
received_message.message.descriptor.name);
MachMessage response = {0};
response.header.msgh_bits = MACH_MSGH_BITS_SET(
received_message.message.descriptor.disposition,
0,
0,
0);
response.header.msgh_remote_port = received_message.message.descriptor.name;
response.header.msgh_size = sizeof response;
strcpy(response.body, "A sample response");
ret = mach_msg(
(mach_msg_header_t*)&response,
MACH_SEND_MSG,
sizeof response,
0,
0,
0,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
puts("[+] Error sending message reply");
return;
}
});
dispatch_resume(port_source);
sleep(120);
}
Creating a dispatch source⌗
In order to create a GCD dispatch queue responsive to an event, the function dispatch_source_create
must be used to create a dispatch source. Many different types of dispatch sources exist. However for our particular use-case, we are interested in the DISPATCH_SOURCE_TYPE_MACH_RECV
dispatch queue type, however there are many other types, the following is a list of them:
-
DISPATCH_SOURCE_TYPE_DATA_ADD
,DISPATCH_SOURCE_TYPE_DATA_OR
Sources of this type allow applications to manually trigger the source’s event handler via a call to dispatch_source_merge_data(). The data will be merged with the source’s pending data via an atomic add or logic OR (based on the source’s type), and the event handler block will be submitted to the source’s target queue.
-
DISPATCH_SOURCE_TYPE_MACH_SEND
Sources of this type monitor a mach port with a send right for state changes.
-
DISPATCH_SOURCE_TYPE_MACH_RECV
Sources of this type monitor a mach port with a receive right for state changes.
-
DISPATCH_SOURCE_TYPE_PROC
Sources of this type monitor processes for state changes. The handle is the process identifier (pid_t) of the process to monitor and the mask may be one or more of the following:
DISPATCH_PROC_EXIT
The process has exited and is available to wait(2).DISPATCH_PROC_FORK
The process has created one or more child processes.DISPATCH_PROC_EXEC
The process has become another executable image via a call to execve(2) or posix_spawn(2).DISPATCH_PROC_REAP
The process status has been collected by its parent process via wait(2).DISPATCH_PROC_SIGNAL
A signal was delivered to the process.
The data returned by dispatch_source_get_data() indicates which of the events in the mask were observed.
-
DISPATCH_SOURCE_TYPE_READ
Sources of this type monitor file descriptors for pending data. The handle is the file descriptor (int) to monitor and the mask is unused and should be zero.
The data returned by dispatch_source_get_data() is an estimated number of bytes available to be read from the descriptor.
-
DISPATCH_SOURCE_TYPE_SIGNAL
Sources of this type monitor signals delivered to the current process. The handle is the signal number to monitor (int) and the mask is unused and should be zero.
The data returned by dispatch_source_get_data() is the number of signals received since the last invocation of the event handler block.
-
DISPATCH_SOURCE_TYPE_TIMER
Sources of this type periodically submit the event handler block to the target queue on an interval specified by dispatch_source_set_timer(). The handle and mask arguments are unused and should be zero.
-
DISPATCH_SOURCE_TYPE_VNODE
Sources of this type monitor the virtual filesystem nodes for state changes. The handle is a file descriptor (int) referencing the node to monitor, and the mask may be one or more of the following:
DISPATCH_VNODE_DELETE
The referenced node was removed from the filesystem namespace via unlink(2).DISPATCH_VNODE_WRITE
A write to the referenced file occurredDISPATCH_VNODE_EXTEND
The referenced file was extendedDISPATCH_VNODE_ATTRIB
The metadata attributes of the referenced node have changedDISPATCH_VNODE_LINK
The link count on the referenced node has changedDISPATCH_VNODE_RENAME
The referenced node was renamedDISPATCH_VNODE_REVOKE
Access to the referenced node was revoked via revoke(2) or the underlying fileystem was unmounted.
-
DISPATCH_SOURCE_TYPE_WRITE
Sources of this type monitor file descriptors for available write buffer space. The handle is the file descriptor (int) to monitor and the mask is unused and should be zero.
As shown above by the different types of dispatch sources, dispatch sources are versatile and are convenient to distribute code on specific events on dedicated dispatch queues.
On our server, all we have to do to create a dispatch source to monitor state changes on a mach port is the following:
dispatch_source_t port_source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_MACH_RECV,
recv_port,
0,
dispatch_queue_create("com.service.MessageQueue", DISPATCH_QUEUE_CONCURRENT)
);
Registering an event handler to the dispatch source⌗
Once we have created our dispatch source, we can then register a handler to the dispatch source as follows:
dispatch_source_set_event_handler(port_source, ^(void) { /* code to handle incoming mach messages */ });
Deploying the dispatch queue⌗
Lasly, all we have to do left is to resume the dispatch source so the event handler to our dispatch queue gets deployed. We can do this as follows:
dispatch_resume(port_source);
* thread #2, queue = 'com.service.MessageQueue', stop reason = breakpoint 1.1
frame #0: 0x0000000100003cdc server`__main_block_invoke(.block_descriptor=0x00000001002043a0) at server.mm:63:30
60
61 dispatch_source_set_event_handler(port_source, ^(void) {
62 ret = mach_msg(
-> 63 (mach_msg_header_t*)&received_message,
64 MACH_RCV_MSG,
65 0,
66 sizeof received_message,
Target 0: (complex_port_client) stopped.
(lldb)
OOL Complex Message Example⌗
The following example extends the previous example to support OOL messages. Is very similar to complex messages with port descriptors as we will see. For sake of simplicity the relevant part has been implemented in the server, although the code for the client will be also provided for testing purposes. Following is the source of the server:
Server⌗
// server.mm
#include <bootstrap.h>
#include <mach/mach.h>
#include <mach/message.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
MachMessage message;
mach_msg_trailer_t trailer;
} ReceivedMachMessage;
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_ool_descriptor_t descriptor;
} OOLMachMessage;
int main(void) {
mach_port_t task = mach_task_self();
mach_port_name_t recv_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &recv_port) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
if (mach_port_insert_right(
task, recv_port, recv_port, MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS) {
return EXIT_FAILURE;
}
dispatch_source_t port_source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_MACH_RECV,
recv_port,
0,
dispatch_queue_create("com.service.MessageQueue", DISPATCH_QUEUE_CONCURRENT)
);
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
if (bootstrap_register(bootstrap_port, "com.service.alice", recv_port) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
void *ool_buffer = NULL;
vm_size_t ool_buffer_size = vm_page_size;
if (vm_allocate(
mach_task_self(),
(vm_address_t *)&ool_buffer,
ool_buffer_size,
VM_PROT_READ | VM_PROT_WRITE) != KERN_SUCCESS) {
puts("[-] Failed to allocate memory");
return EXIT_FAILURE;
}
strcpy((char *)ool_buffer, "This is some OOL Data!");
dispatch_source_set_event_handler(port_source, ^(void) {
ReceivedMachMessage received_message = {0};
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t *)&received_message,
MACH_RCV_MSG,
0,
sizeof received_message,
recv_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
puts("[-] Could not receive mach message");
return;
}
puts("[+] Got incoming message!");
printf("\tbody: %s\n", received_message.message.body);
OOLMachMessage message = {0};
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
0,
0,
MACH_MSGH_BITS_COMPLEX);
message.header.msgh_remote_port = received_message.message.header.msgh_remote_port;
message.header.msgh_size = sizeof message;
message.msgh_descriptor_count = 1;
message.descriptor.address = ool_buffer;
message.descriptor.size = ool_buffer_size;
message.descriptor.copy = MACH_MSG_VIRTUAL_COPY;
message.descriptor.deallocate = false;
message.descriptor.type = MACH_MSG_OOL_DESCRIPTOR;
ret = mach_msg(
(mach_msg_header_t *)&message,
MACH_SEND_MSG,
sizeof message,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
puts("[-] Could not send mach message reply");
return;
}
});
dispatch_resume(port_source);
sleep(120);
}
Allocating OOL Buffer⌗
One important thing to highlight is that in order to send an OOL message, we need to first allocate the OOL buffer we will be sending to our client. We could use any standard APIs to do this (such as malloc), However, in our example we will be using vm_allocate
to directly allocate virtual memory.
Here we allocate a new memory region of a page size, with read/write permissions:
void *ool_buffer = NULL;
vm_size_t ool_buffer_size = vm_page_size;
if (vm_allocate(
mach_task_self(),
(vm_address_t *)&ool_buffer,
ool_buffer_size,
VM_PROT_READ | VM_PROT_WRITE) != KERN_SUCCESS) {
puts("[-] Failed to allocate memory");
return EXIT_FAILURE;
}
strcpy((char *)ool_buffer, "This is some OOL Data!");
Setting appropiate OOL Message MSGH_BITS⌗
Lets remember that we have to set the appropiate bit in msgh_bits
field in our mach message header to direct that our message is a complex message setting the MACH_MSGH_BITS_COMPLEX
bit:
OOLMachMessage message = {0};
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
0,
0,
MACH_MSGH_BITS_COMPLEX);
Crafting an OOL Message⌗
The type definition for our OOL Mach message is the following:
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_ool_descriptor_t descriptor;
} OOLMachMessage;
mach_msg_ool_descriptor_t
.
The following is the OOL descriptor structure:
typedef struct{
void* address;
mach_msg_size_t size;
boolean_t deallocate: 8;
mach_msg_copy_options_t copy: 8;
unsigned int pad1: 8;
mach_msg_descriptor_type_t type: 8;
} mach_msg_ool_descriptor_t;
address
: the out-of-line datasize
: size of the ool datadeallocate
: true if the memory page at the address wants to be removed from the sender’s address space once the message’s been sentcopy
: defines the way of copying the memorytype
: defines the type of the message descriptor, for the OOL descriptor, it’sMACH_MSG_OOL_DESCRIPTOR
In addition, there are two copy types, these are:
-
MACH_MSG_VIRTUAL_COPY
When sending a message, the kernel can choose how to exactly transmit data. For example, it can decide to actually copy physical memory, or make a virtual copy.When receiving this means that the kernel made a virtual copy.
-
MACH_MSG_PHYSICAL_COPY
When sending a message, this option instructs the kernel to perform an actual copy of the physical memory.
On the receiving end, this means there’s a new physical copy of the memory.
For the scope of this article, you’re only going to need the MACH_MSG_VIRTUAL_COPY
copy type.
Note that sending out-of-line data requires merely the data address and size, where both can be dynamic. This is unlike inline data, where indirection isn’t possible, and all data must be copied into the message buffer.
The following is how we populate the OOL message structures in our server:
message.header.msgh_remote_port = received_message.message.header.msgh_remote_port;
message.header.msgh_size = sizeof message;
message.msgh_descriptor_count = 1;
message.descriptor.address = ool_buffer;
message.descriptor.size = ool_buffer_size;
message.descriptor.copy = MACH_MSG_VIRTUAL_COPY;
message.descriptor.deallocate = false;
message.descriptor.type = MACH_MSG_OOL_DESCRIPTOR;
Client⌗
The client does not introduce any new changes to handle OOL messages in addition to what has been already introduced in the server. For sake of convenience the client source code is left here for testing:
// client.mm
#include <bootstrap.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/message.h>
#include <mach/port.h>
#include <mach/task.h>
#include <mach/mach_vm.h>
#include <mach/vm_map.h>
#include <mach/vm_region.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
mach_msg_header_t header;
char body[4096];
} MachMessage;
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_ool_descriptor_t descriptor;
} OOLMachMessage;
typedef struct {
OOLMessage message;
mach_msg_trailer_t trailer;
} ReceivedOOLMachMessage;
int main(void) {
mach_port_name_t task = mach_task_self();
mach_port_t bootstrap_port;
if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap_port) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t port;
if (bootstrap_look_up(bootstrap_port, "com.service.alice", &port) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
mach_port_t reply_port;
if (mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &reply_port) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
if (mach_port_insert_right(
task, reply_port, reply_port, MACH_MSG_TYPE_MAKE_SEND) !=
KERN_SUCCESS) {
return EXIT_FAILURE;
}
MachMessage message = {0};
message.header.msgh_remote_port = port;
message.header.msgh_local_port = reply_port;
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND,
MACH_MSG_TYPE_MAKE_SEND,
0,
0);
message.header.msgh_size = sizeof message ;
strcpy(message.body, "A sample message");
mach_msg_return_t ret = mach_msg(
(mach_msg_header_t *)&message,
MACH_SEND_MSG,
sizeof message,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
ReceivedOOLMachMessage response = {0};
ret = mach_msg(
(mach_msg_header_t *)&response,
MACH_RCV_MSG,
0,
sizeof response,
reply_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (ret != MACH_MSG_SUCCESS) {
return EXIT_FAILURE;
}
printf("[+] Received OOL messge!\n\tbuffer: %s\n",
(const char *)response.message.descriptor.address
);
return EXIT_SUCCESS;
}