preface

In previous articles in the assembly tutorial series, we explored a number of principles in user mode. In today’s blog, we’ll look into the iOS Jailbreak Adventurous ways and learn more about binary security Exploits.

Although the Exploit POCs provided by foreign bigwigs all have relatively detailed write-ups, these write-ups are often based on previous POCs without elaborating some specific principles, which makes it difficult for beginners to fully understand them. My Jailbreak Priciples article will integrate all the relevant POCs and write-ups and analyze them on the assumption that the reader is a kernel geek (and I am). The goal is to create a series of XNU bug analysis articles that everyone can read.

The nature of prison break

IOS only provides users with a limited Unix environment, and normally we can only interact with the kernel in user mode through legitimate system calls. MacOS for computers, by contrast, has a lot of freedom. They’re all based on Darwin XNU, but Apple put a lot of restrictions on the iPhoneOS, and jailbreaking by lifting those restrictions allowed us to get root privileges on the iPhoneOS and have a certain degree of freedom.

Apple has used Sandbox, Signature Checkpoints, and more to protect the system, making it incredibly difficult to beat these limitations.

Classification of jailbreak

Currently, jailbreaking mainly falls into two categories: BootROM exploits based on hardware vulnerabilities and Userland exploits based on software vulnerabilities.

BootROM Exploit

This kind of vulnerability is similar to IC decryption in single chip microcomputer. The vulnerability of iPhone itself is discovered from the hardware level, which makes the Secure Boot Chain of the whole system unreliable. Such vulnerability is highly lethal and can only be solved by updating the hardware. Recently checkM8 and checkRA1n developed based on it have realized the hardware debugging and jailbreaking of iPhone 5S ~ iPhone X series models;

Userland Exploit

These vulnerabilities were discovered in code audits of darwin-Xnu, and allowed us to send arbitrary executable code to the kernel in user mode. Sock Port Exploit we are about to introduce is the exploitation of a UAF vulnerability of socket options in XNU.

Sends user-mode data to the kernel

As we know from the above analysis, an important basis of Userland Exploit is that it can write arbitrary data into the heap area of the Kernel to make it an effective Kernel data structure, and then implement illegal control over the Kernel from the user mode. Unfortunately, we can’t manipulate the kernel’s memory data directly, because user-mode applications don’t have access to kernel_task and can’t manipulate the kernel’s stack directly through functions like vm_read and vm_write.

Since it is not possible to manipulate memory directly, we need to consider ways to manipulate memory indirectly. In fact, there are many ways to read and write kernel data indirectly. The most common ways are Socket, Mach Message and IOSurface. Then the vulnerability analysis of Sock Port will introduce the combination of these three methods.

Indirect kernel memory read and write based on Socket

Since Socket implementation is at the operating system level, the kernel will perform some memory allocation operations when creating sock through Socket functions in user mode, such as the following user mode code:

int sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
Copy the code

In the kernel state, a struct socket structure is created based on the parameters passed in:

/* * Kernel structure per socket. * Contains send and receive buffer queues, * handle on protocol and pointer to protocol * private data and error information. */
struct socket {
	int	so_zone;		/* zone we were allocated from */
	short	so_type;		/* generic type, see socket.h */
	u_short	so_error;		/* error affecting connection */
	u_int32_t so_options;		/* from socket call, see socket.h */
	short	so_linger;		/* time to linger while closing */
	short	so_state;		/* internal state flags SS_*, below */
	void	*so_pcb;		/* protocol control block */
	// ...
}
Copy the code

Here, we can control the memory in the kernel indirectly and in a limited way through the parameters passed into the socket. However, since the system only returns the sock handle to us, we cannot directly read the memory content of the kernel.

To read the kernel’s memory, we can use the socket options function provided by the kernel, which can modify some socket configurations. For example, the following code changes the Maximum Transmission Unit under IPV6:

// set mtu
int minmtu = - 1;
setsockopt(sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(*minmtu));

// read mtu
getsockopt(sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(*minmtu));
Copy the code

In kernel mode, the system reads so_PCB of struct socket and performs read and write operations from user mode. Therefore, we read and write part of socket structure in kernel through options related functions.

Use sockets to read and write arbitrary contents of the kernel

One of the obvious limitations of this approach is that we can only read and write memory within the control of the kernel, so there is no way to do anything with this approach alone. Imagine if we could try to assign a fake Socket structure to another section of the kernel, wouldn’t it be possible to read and write arbitrary memory using setsockopt and getsockopt?

Sock Port is a vulnerability that uses Socket function set to realize arbitrary read and write of kernel memory. It is mainly based on a vulnerability of Socket disconnect in kernel code of iOS 10.0-12.2. The kernel code is as follows:

if(! (so->so_flags & SOF_PCBCLEARING)) {struct ip_moptions *imo;
	struct ip6_moptions *im6o;

	inp->inp_vflag = 0;
	if(inp->in6p_options ! =NULL) {
		m_freem(inp->in6p_options);
		inp->in6p_options = NULL; // <- good
	}
	ip6_freepcbopts(inp->in6p_outputopts); // <- bad
	ROUTE_RELEASE(&inp->in6p_route);
	/* free IPv4 related resources in case of mapped addr */
	if(inp->inp_options ! =NULL) {(void) m_free(inp->inp_options); 
		inp->inp_options = NULL; // <- good
	}
	// ...
}
Copy the code

In6p_outputopts () {options () {in6p_outputopts () {in6p_outputopts () {in6p_outputopts () {in6p_outputopts () {in6p_outputopts () {in6p_outputopts () {in6p_outputopts ();

Fortunately, with some setup, we can continue to read and write the dangling pointer indirectly through setsockopt and getsockopt after socket disconnect. As the system reallocates this area of memory, we can still access it through dangling Pointers, so the question becomes how to indirectly control the system’s Reallocation to that area.

This vulnerability is called UAF (Use After Free), and the common ways to indirectly control the Reallocation of systems are Heap Spraying and Heap feng-shui. The vulnerability utilization of Sock Port is relatively complicated, and we will explain it step by step in the next few articles. Here we only need to have a preliminary understanding of these concepts.

Use After Free

Through the above examples, we have a preliminary understanding of UAF. Now we refer to Webopedia to give a clear definition:

Use After Free specifically refers to the attempt to access memory after it has been freed, which can cause a program to crash or, in the case of a Use-After-Free flaw, can potentially result in the execution of arbitrary code or even enable full remote code execution capabilities.

Attempts to access freed memory can lead to program crashes, potentially arbitrary code execution, or even full remote control.

One of the keys to UAF is to obtain the memory address of the freed region, which is usually achieved through dangling Pointers. Dangling Pointers are caused by the memory region to which the pointer points being freed but not cleared. This problem is common in code written by developers with little knowledge of binary security.

In the case of cross-process, it is not possible to read and write execution memory only through dangling Pointers. Some IPC functions that can read dangling Pointers indirectly are needed, such as setsockopt and getsockopt mentioned above. In addition, effective control of Reallocation often requires a combination of indirect manipulation heap related techniques.

Heap Spraying

The definition of Heap Spraying is defined by Computer Hope:

Heap spraying is a technique used to aid the exploitation of vulnerabilities in computer systems. It is called “spraying the heap” because it involves writing a series of bytes at various places in the heap. The heap is a large pool of memory that is allocated for use by programs. The basic idea is similar to spray painting a wall to make it all the same color. Like a wall, the heap is “sprayed” so that its “color” (the bytes it contains) is uniformly distributed over its entire memory “surface.”

In user mode through system calls, and other ways in different areas of the kernel stack to distribute large amounts of memory, if the kernel heap to a wall, heap injection by means of a large amount of allocated memory will be the same color paint spilled (the same byte) on the heap, which leads to the color (the same bytes) uniform distribution in the whole memory of plane, That is, the areas that were previously released are almost all made into the same content by Reallocation.

In short, let’s say we alloc an 8B region, then release it, and then sooner or later alloc will reuse the previous region, and if it happens to be occupied when we alloc it, then content control will be achieved. With this technique we can indirectly control the Reallocation content on the heap.

Obviously, if we combine the Socket UAF with Heap Spraying, we have the opportunity to assign fake content to the Socket Options. Then we use setsockopt and getsockopt to read and write and validate. You have complete control over the kernel heap memory.

An example of UAF & Heap Spraying in a pure user state

Based on the above theoretical discussion, we have a preliminary understanding of the read and write of heap memory. In fact, the whole thing is not as simple as we imagined. The utilization of Sock Port is based on the combination of many vulnerabilities, which can not be completely understood in a few words or overnight. I’m going to simulate a UAF and Heap Spraying scenario in user mode to give you an engineering start on these two concepts.

Hypothetical vulnerability scenarios

The system will execute tasks according to their priority order. The priority of tasks depends on the VIP level of the user, which is recorded in the options of the task:

struct secret_options {
    bool isVIP;
    int vipLevel;
};

struct secret_task {
    int tid;
    bool valid;
    struct secret_options *options;
};
Copy the code

Under Mach Mach Message, tasks can be created using create_secret_task. By default, there is no VIP level for tasks:

std: :map<task_t, struct secret_task *> taskTable;

task_t create_secret_task() {
    struct secret_task *task = (struct secret_task *)calloc(1, sizeof(struct secret_task));
    task->tid = arc4random();
    while(taskTable.find(task->tid = arc4random()) ! = taskTable.end()); taskTable[task->tid] = task;struct secret_options *options = (struct secret_options *)calloc(1, sizeof(struct secret_options));
    task->options = options;
    options->isVIP = false;
    options->vipLevel = 0;
    return task->tid;
}
Copy the code

Outside the system, users can only create tasks, obtain VIP information, and obtain task priorities:

typedef int task_t;
#define SecretTaskOptIsVIP 0
#define SecretTaskOptVipLevel 1
#define SecretTaskVipLevelMAX 9

int get_task_priority(task_t task_id) {
    struct secret_task *task = get_task(task_id);
    if(! task) {return (~0U);
    }
    return task->options->isVIP ? (SecretTaskVipLevelMAX - task->options->vipLevel) : (~0U);
}

bool secret_get_options(task_t task_id, int optkey, void *ret) {
    struct secret_task *task = get_task(task_id);
    if(! task) {return false;
    }
    switch (optkey) {
        case SecretTaskOptIsVIP:
            *(reinterpret_cast<bool *>(ret)) = task->options->isVIP;
            break;
        case SecretTaskOptVipLevel:
            *(reinterpret_cast<int *>(ret)) = task->options->vipLevel;
            break;
        default:
            break;
    }
    return true;
}
Copy the code

Ideally, without reverse-engineering, we can only get the handle of the Task, not the address of the Task, so we cannot arbitrarily modify the VIP information.

Xiao Ming also provided an API for the user to cancel the task. He only released the options of the task and marked the task as invalid. Inexperienced, he forgot to clean the options pointer and introduced a UAF Exploit for the system:

bool free_task(task_t task_id) {
    struct secret_task *task = get_task(task_id);
    if(! task) {return false;
    }
    free(task->options);
    task->valid = false;
    return true;
}
Copy the code

Hypothetical attack scenario

Normally, we can only access the system through the public API:

// create task
task_t task = create_secret_task();

// read options
int vipLevel;
secret_get_options(task, SecretTaskOptVipLevel, &vipLevel);

// get priority
int priority = get_task_priority(leaked_task);

// release task
free_task(task);
Copy the code

Since the Task is non-VIP by default, we only get the lowest priority INTMAX. Task ->options UAF can forge the VIP level of a task as follows:

  1. Create a Task and release it via the free_task function, which constructs onetask->optionsDangling pointer;
  2. Continuous distribution withtask->optionsPoint to thestruct secret_optionsThe same size memory area untiltask->optionsThe area that the dangling pointer points to is Reallocation as our newly allocated memory, and the validation method can be forged for specific data and then passedsecret_get_optionsRead verification;
  3. At this timestruct secret_optionsThe Task Options can be modified by modifying the newly applied area.
struct faked_secret_options {
    bool isVIP;
    int vipLevel;
};
struct faked_secret_options *sprayPayload = nullptr;
task_t leaked_task = - 1;

for (int i = 0; i < 100; i++) {
    // create task
    task_t task = create_secret_task();
    // free to make dangling options
    free_task(task);
    
    // alloc to spraying
    struct faked_secret_options *fakedOptions = (struct faked_secret_options *)calloc(1, sizeof(struct faked_secret_options));
    fakedOptions->isVIP = true;
    // to verify
    fakedOptions->vipLevel = 0x123456;
    
    // check by vipLevel
    int vipLevel;
    secret_get_options(task, SecretTaskOptVipLevel, &vipLevel);
    if (vipLevel == 0x123456) {
        printf("spray succeeded at %d!!! \n", i);
        sprayPayload = fakedOptions;
        leaked_task = task;
        break; }}// modify
if (sprayPayload) {
    sprayPayload->vipLevel = 9;
}
Copy the code

Because it is pure user mode, synchronous operation in the same thread, this method has a very high success rate. Of course, this method can only give you a general understanding of UAF and Heap Spraying. In fact, such exploits are cross-process and require very complex operations, usually with the help of Mach Message and IOSurface, and Payload structure is very complex.

Next day forecast

In the next chapter, we will start to analyze the source code of Sock Port, understand the Kalloc series functions from Ian Beer, and the method and principle of Heap Spraying using IOSurface. The Kalloc family of functions requires an in-depth understanding of Mach messages, so in the next article we will also examine the design of Mach ports from an XNU source point of view.

The resources

  1. Andy Slye. What Is Jailbreaking? How a Jailbreak Works – www.youtube.com/watch?v=tYK…
  2. Webopedia. Use After Free – www.webopedia.com/TERM/U/use-…
  3. Computer Hope. Heap spraying – www.computerhope.com/jargon/h/he…
  4. Lot. Jakeajames/sock_port – github.com/jakeajames/…