在以前的汇编教程系列文章中,咱们在用户态下探讨了诸多原理。从今天开始,咱们将详细分析历代 iOS Jailbreak Exploits,并由此深刻 XNU 内核,并学习更多二进制安全攻防的知识。html
虽然国外大佬提供的 Exploit PoC 都有较为详细的 write-up,但这些 write-up 经常以以前出现的 PoC 为基础,并不详细展开某些具体原理,这就致使初学者很难彻底读懂。笔者的 Jailbreak Priciples 系列文章会将全部相关的 PoC 和 write-up 进行整合,并以读者是内核小白(其实笔者也是)为假设展开分析,目标是打造人人能读懂的 XNU 漏洞分析系列文章。git
iOS 仅为用户提供了一个受限的 Unix 环境,常规状况下咱们只能在用户态借助于合法的系统调用来与内核交互。相反的,用于电脑的 macOS 则有着很高的自由度。它们都基于 Darwin-XNU,但 Apple 在 iPhoneOS 上施加了诸多限制,越狱即解除这些限制使咱们能够得到 iPhoneOS 的 root 权限,进而在必定程度上随心所欲。github
Apple 采用了 Sandbox, Signature Checkpoints 等手段对系统进行保护,使得突破这些限制变得极为困难。web
目前越狱主要分为两类,一类是以硬件漏洞为基础的 BootROM Exploit,另外一类则是基于软件漏洞的 Userland Exploit。安全
这类漏洞相似于单片机中的 IC 解密,从硬件层面发现 iPhone 自己的漏洞,使得整个系统的 Secure Boot Chain 变得不可靠,这类漏洞的杀伤力极强,只能经过更新硬件解决。最近出现的 checkm8 及基于它开发的 checkra1n 就实现了 iPhone 5s ~ iPhone X 系列机型的硬件调试与越狱;数据结构
这类漏洞每每是对开源的 Darwin-XNU 进行代码审计发现的,基于这些漏洞每每能使咱们在用户态将任意可执行代码送入内核执行,咱们即将介绍的 Sock Port Exploit 便是对 XNU 中 socket options 的一个 UAF 漏洞的利用。app
经过上文的分析咱们知道,Userland Exploit 的一个重要基础是能将任意数据写入内核的堆区,使之成为有效地 Kernel 数据结构,进而从用户态实施对内核的非法控制。遗憾的是,咱们没法直接操做内核的内存数据,这是由于用户态的应用程序没有办法获取 kernel_task,也就没法直接经过 vm_read
和 vm_write
等函数操做内核的堆栈。dom
既然没法直接操做内存,咱们就须要考虑间接操做内存的方式,事实上咱们有很是多的方式可以间接读写内核的数据,最多见方式有 Socket, Mach Message 和 IOSurface 等,这里咱们先介绍最好理解的 Socket 方式,随后对 Sock Port 的漏洞时分析会介绍其利用这三种方式打的组合拳。iphone
因为 Socket 的实现是操做系统层面的,在用户态经过 socket 函数建立 sock 时内核会执行一些内存分配操做,例以下面的用户态代码:socket
int sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
复制代码
在内核态会根据传入的参数建立 struct socket
结构体:
/* * 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 */
// ...
}
复制代码
这里咱们能经过传入 socket 的参数间接、受限的控制内核中的内存,但因为系统只会返回 sock 的句柄(handle)给咱们,咱们没法直接读取内核的内存内容。
要读取内核的内存,咱们能够借助于内核提供的 socket options 相关函数,他们可以修改 socket 的一些配置,例以下面的代码修改了 IPV6 下的 Maximum Transmission Unit:
// 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));
复制代码
在内核态,系统会读取 struct socket
的 so_pcb,并执行来自用户态的读写操做,由此咱们透过 options 相关函数读写了内核中 socket 结构体的部份内容。
上述方式有一个明显的限制,那就是咱们只能在内核受控的范围内读写内存,单单经过这种方式是玩不出幺蛾子的。设想若是咱们能尝试把一个伪造的 Socket 结构体分配到内核的其余区段,是否是就能经过 setsockopt
和 getsockopt
来读写任意内存了呢?
Sock Port 是一个利用 Socket 函数集实现内核内存任意读写的漏洞,它主要基于 iOS 10.0 - 12.2 的内核代码中 socket disconnect 时的一个漏洞,观察以下的内核代码:
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
}
// ...
}
复制代码
能够看到在清理 options 时只对 in6p_outputopts
进行了释放,而没有清理 in6p_outputopts
指针的地址,这就形成了一个 in6p_outputopts
悬垂指针。
幸运的是,经过某种设置后,咱们可以在 socket disconnect 后继续经过 setsockopt
和 getsockopt
间接读写这个悬垂指针。随着系统从新分配这块内存,咱们依然可以经过悬垂指针对其进行访问,所以问题转化为了如何间接控制系统对该区域的 Reallocation。
这类透过悬垂指针操做已释放区域的漏洞被称为 UAF(Use After Free),而间接控制系统 Reallocation 的常见方式有 堆喷射(Heap Spraying) 和 堆风水(Heap feng-shui),整个 Sock Port 的漏洞利用较为复杂,咱们将在接下来的几篇文章中逐步讲解,这里只须要对这些概念有个初步的认识便可。
透过上述例子咱们对 UAF 有了一个初步的认识,如今咱们参考 Webopedia 给出明确的定义:
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.
即尝试访问已释放的内存,这会致使程序崩溃,或是潜在的任意代码执行,甚至获取彻底的远程控制能力。
UAF 的关键之一是获取被释放区域的内存地址,通常透过悬垂指针实现,而悬垂指针是因为指针指向的内存区域被释放,但指针未被清零致使的,这类问题在缺少二进制安全知识的开发者写出的代码中家常便饭。
对于跨进程的状况下,只透过悬垂指针是没法读写执行内存的,须要配合一些能间接读取悬垂指针的 IPC 函数,例如上文中提到的 setsockopt
和 getsockopt
,此外为了有效地控制 Reallocation 每每须要结合间接操做堆的相关技术。
下面咱们参考 Computer Hope 给出 Heap Spraying 的定义:
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."
即在用户态透过系统调用等方式在内核堆的不一样区域分配大量内存,若是将内核的堆比做墙壁,堆喷射就是经过大量分配内存的方式将一样颜色的油漆(一样的字节)泼洒到堆上,这会致使其颜色(一样的字节)均匀的分布在整个内存平面上,即那些先前被释放的区域几乎都被 Reallocation 成了一样的内容。
简言之就是,好比咱们 alloc 了 1 个 8B 的区域,随后将其释放,接下来再执行 alloc 时早晚会对先前的区域进行复用,若是刚好被咱们 alloc 时占用,则达到了内容控制的目的。透过这种技术咱们能够间接控制堆上的 Reallocation 内容。
显然若是咱们将上述 Socket UAF 与 Heap Spraying 组合,就有机会为 Socket Options 分配伪造的内容,随后咱们经过 setsockopt
和 getsockopt
执行读写和验证,就能实现对内核堆内存的彻底控制。
综合上述理论探讨,咱们对堆内存的读写有了初步的认识,事实上事情没有咱们想象的那么简单,整个 Sock Port 的利用是基于许多漏洞组合而来的,并不是三言两语和一朝一夕可以彻底搞懂,所以本文先不展开具体漏洞的内容,而是在用户态模拟一个 UAF 和 Heap Spraying 的场景让你们先从工程上初步认识这两个概念。
设想小明是一个初级页面仔,他要开发一个任务执行系统,该系统根据任务的优先级顺序执行任务,任务的优先级取决于用户的 VIP 等级,该 VIP 等级被记录在 task 的 options 中:
struct secret_options {
bool isVIP;
int vipLevel;
};
struct secret_task {
int tid;
bool valid;
struct secret_options *options;
};
复制代码
小明参考了 Mach Message 的设计理念,在系统内部维护 Task 的内存结构,只对外暴露 Task 的句柄(tid),用户能够透过 create_secret_task
建立任务,任务的默认是没有 VIP 等级的:
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;
}
复制代码
在系统以外,用户能作的只是建立任务、获取 VIP 信息以及获取任务优先级:
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;
}
复制代码
在理想状况下,不考虑逆向工程的方式,咱们只能拿到 Task 的句柄,没法获取 Task 地址,所以没法任意修改 VIP 信息。
小明同时为用户提供了注销任务的 API,他只对任务的 options 进行了释放,同时将任务标记为 invalid,缺少经验的他忘记清理 options 指针,为系统引入了一个 UAF Exploit:
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;
}
复制代码
常规状况下,咱们只能透过公共的 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);
复制代码
因为 Task 默认是非 VIP 的,咱们只能拿到最低优先级 INTMAX。这里咱们经过 task->options
的 UAF 能够伪造 task 的 VIP 等级,方法以下:
task->options
的悬垂指针;task->options
指向的 struct secret_options
相同大小的内存区域,直到 task->options
悬垂指针指向的区域被 Reallocation 成咱们新申请的内存,验证方式能够伪造特定数据,随后经过 secret_get_options
读取验证;struct secret_options
已经指向了咱们新申请的区域,能够经过修改该区域实现对 Task Options 的修改。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;
}
复制代码
因为是纯用户态、同一线程内的同步操做,这种方式的成功率极高。固然这种方式只能让你们对 UAF 与 Heap Spraying 有一个大体认识,实际上这类漏洞利用都是跨进程的,须要很是复杂的操做,每每须要借助于 Mach Message 和 IOSurface,且 Payload 构造十分复杂。
在下一个章节中咱们将开始着手分析 Sock Port 的源码,了解来自 Ian Beer 大佬的 kalloc 系列函数以及利用 IOSurface 进行 Heap Spraying 的方式和原理。其中 kalloc 系列函数须要对 Mach Message 有深刻的认识,所以在下一篇文章中咱们也会从 XNU 源码角度分析 mach port 的设计。