
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 浏览器漏洞利用示例:
The important part of the above diagram is the green box, corresponding to arrayBuffer1
, and 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
:
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:
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:
首先,我们需要两个管道,并分配缓冲区。这与上面的基本管道示例非常相似:
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 非常简单:
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
的对象。这包含两个非常有用的字段:
-
fg_ops
: a pointer to an array of function pointers. This is how the kernel knows to callpipe_read
rather thanvn_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! -
fg_data
: a pointer to thestruct 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?
MORE CORELLIUM MAGIC: HYPERVISOR HOOKS
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 的语法以单行形式执行的:
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
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:
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 功能创建符号缓存文件:
And then that file can be grepped to find the two symbols we need:
Now we simply locate the call to ipc_kobject_alloc_port
from within fileport_makeport
:
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:
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
.以下是完整的实现:
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
其中包含我们关心的所有字段:
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
:
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
字段:
Where prw
is a structure matching the layout of struct pipebuf:
其中 prw
的结构与布局 struct pipebuf:
相匹配
And then the write primitive works similarly:
然后 write 原语的工作方式类似:
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:
Putting it all together and running the exploit looks like this:
将它们放在一起并运行漏洞利用如下所示:
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 上):
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: 嗯,差不多:
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
区域的标志。
For more accuracy, here's the disassembly^7 with some notes:
为了更准确,这里是拆解 ^7 和一些注释:
In earlier versions of XNU, pipe buffers would be allocated by kalloc:
在早期版本的 XNU 中,管道缓冲区将由 kalloc:
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
子映射进行分配:
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 上找到。
- 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.
这些是逼真的基元吗?也许不是,但这里的目的是练习一种技术,所以潜在的错误(真实的或其他的)不那么重要。 - Corellium devices by default have KASLR disabled. Be sure to edit the settings before booting.
默认情况下,Corelliium 设备禁用了 KASLR。请务必在启动前编辑设置。 - 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 前设备。 - There are a bunch of other cool monitor commands exposed by Corellium, run process plugin packet monitor help for a list!
Corellium 还公开了一堆其他很酷的监视命令,运行进程插件数据包监视帮助以获取列表! - Perhaps Corellium will add a way to hook kernel_base+offset in the future which would make this much easier.
也许 Corellium 将来会添加一种钩住 kernel_base+offset 的方法,这将使这变得更加容易。 - 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 在用户空间中既可读又可写,但它们证明了这一点。 - 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
转载请注明:Exploring UNIX pipes for iOS kernel exploit primitives | CTF导航