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:

  1. Alice is an owner of a receive right to a port
  2. Bob is an owner of a send right to the same port
  3. Bob then allocates a Mach port with a receive right
  4. Bob inserts a send right to this new mach port
  5. 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 getMACH_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 for MACH_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 for MACH_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 for MACH_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:

  1. Alice registers her service with the bootstrap server using some name. eg com.test-service.alice.
  2. Registration in practice means that Alice grants the broker a send right to a port Alice holds a receive right to.
  3. 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.
  4. 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;
A brief description of these fields is the following:

  • msgh_bits - options and message metadata, such as disposition of port rights in the message
  • msgh_size - total message size, including header
  • msgh_remote_port - remote Mach port, used as the destination when sending a message, or a reply port when receiving
  • msgh_local_port - local Mach port, the port the message was received on, or a reply port when sending a message
  • msgh_voucher_port - port identifying a Mach Voucher, that’s an optional field
  • msgh_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:

  1. Create a Mach port receive right
  2. Add a send right to the port for the bootstrap server to use
  3. Retrieving a Mach port to communicate with the broker
  4. Registering our service
  5. 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;
Allocating a new mach port
  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;
  }
Register mach port to service name via the bootstrap server
  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:

  1. Retrieve the bootstrap server port to query it for the service port
  2. Query bootstrap for the service port
  3. Setup message header
  4. Fill message body
  5. 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;
  }
Retrieve the target service port via the bootstrap server
  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))
Here we can see that for 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 the msgh_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 the msgh_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 be MACH_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;
}
### Using msg_bits to fetch remote reply port The only thing that is worth to highlight in contrast to the previous example is that, once we have received a mach message, we can use the msg_bits from the initial received message header, and acquire the destination port as the endpoint to send our message to. This can be seen in the following snippet:

// 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");
}
With these changes in the server we will be already supporting message replies.

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);

}
Changes that are worth to highlight are as follows:

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
  );
Once we do that we can send the message, and our local_port field in the mach message header will be converted to the remote_port field when the remote endpoint receives our message.

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 message layout for complex messages On important thing to higlight is that the type definitions of our mach messages have to be defined slightly different to support complex messages. Lets remember that for complex messages, we will have the mach message header, an additional 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;
Note that in our type definition for 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;
# Complex Message Server with DispatchQueue Example Although not entirely relevant for the subject in question of this post, I also wanted to document how we can handle mach messages with a dedicated GCD dispatch queue. Is very simple to implement, and provides handling of concurrent/serial, aync/sync connections much easier. This design pattern is also followed by many large projects which has to handle a high volume of mach messages for IPC communication. As an example, WebKit implements a similar but much more elaborated approach to send/receive mach messages to WebKit XPC services. In any case, to showcase the concept, the server of previous examples its been rewritten to handle incoming mach messages in an dedicated dispatch queue. The source code of this server is the follwing:

// 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 occurred
    • DISPATCH_VNODE_EXTEND The referenced file was extended
    • DISPATCH_VNODE_ATTRIB The metadata attributes of the referenced node have changed
    • DISPATCH_VNODE_LINK The link count on the referenced node has changed
    • DISPATCH_VNODE_RENAME The referenced node was renamed
    • DISPATCH_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);
We can see that now when new incomming messages arrive, they will be processed in a dedicated concurrent queue:

* 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;
We can notice that a new descriptor type is introduced, which is of type 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;
The fields of this descriptor are:

  • address: the out-of-line data
  • size: size of the ool data
  • deallocate: true if the memory page at the address wants to be removed from the sender’s address space once the message’s been sent
  • copy: defines the way of copying the memory
  • type: defines the type of the message descriptor, for the OOL descriptor, it’s MACH_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;
After this, all there is left is to send the message to the client.

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;
}
# References I want to show my special gratitude to Damian Malarczyk, as the content of this post was derived from his series on XNU IPC.

  1. Damian Malarczyk
  2. macOS and *OS Internals
  3. Mach Overview
  4. Exchanging port rights
  5. About Mach Ports
  6. Reaching the mach layer