Project Zero对 iOS 12.4 内核任意地址读写漏洞的深度分析(CVE-2019-8605) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

Project Zero对 iOS 12.4 内核任意地址读写漏洞的深度分析(CVE-2019-8605)

h1apwn 新闻 2020-01-15 10:00:00
620999
收藏

导语:在这篇文章中,我将记录作为iOS研究新手的经历。之前已经发布了许多高质量的iOS内核漏洞利用报告,但这些漏洞通常具有较弱的初始原语和很多聪明之处,因此很难确定哪些iOS内部机制是针对漏洞利用的,哪些是通用技术。

0x01 漏洞介绍

在这篇文章中,我将记录作为iOS研究新手的经历。之前已经发布了许多高质量的iOS内核漏洞利用报告,但这些漏洞通常具有较弱的初始原语和很多聪明之处,因此很难确定哪些iOS内部机制是针对漏洞利用的,哪些是通用技术。

在本文中,我们将研究CVE-2019-8605(这是iOS内核和macOS的漏洞)以及如何利用它来实现任意内核读/写。此问题最早在2013年受到XNU的影响,并于2019年3月由我报告给Apple。然后在2019年5月在iOS 12.3中进行了修补,我发布:https://bugs.chromium.org/p/project-zero/issues/detail?id=1806 了完整的详细信息,包括iOS的漏洞分析,名为“ SockPuppet”。 2019年7月,然后发现此问题在iOS 12.4中也存在,后来在2019年8月下旬在iOS 12.4.1中进行了修补。

SockPuppet中的原语比其他原语更强大:它提供了任意读取且几乎不需要任何工作即可释放的内容。由于我们可以跳过通常的工作来设置强大的初始原语,因此这使查看iOS规范利用变得更加容易。首先,我将描述如何发现的漏洞,然后在仅给出Linux和Windows开发背景的情况下,说明如何利用它。如果你有兴趣,我已经与LiveOverflow合作制作了一段解释此漏洞的视频。你可以在这里观看。

https://www.youtube.com/watch?v=YV3jewkUJ54

0x02  Bug Hunting

Why network fuzzing?

选择fuzzing目标的一种技术是枚举给定项目的先前漏洞报告,在源代码树中查找漏洞位置,然后选择独立的项目组件,其中包含各种漏洞子集。然后,通过创建一个相当通用但仍可以重现以前发现的模糊器,很可能会发现新的漏洞。当我开始研究模糊器时,我使用了两个漏洞报告来为我的研究做准备:Google Project Zero的Ian Beer 的mptcp_usr_connectx缓冲区溢出ICMP数据包解析缓冲区溢出由Semmle的Kevin Backhouse撰写。它们成为最佳选择的原因是,它们是同一子系统完全不同部分中的关键安全问题:一个是与网络相关的系统调用,另一个是解析远程数据包。如果我可以做一个模糊器来进行与网络相关的随机系统调用,并将随机数据包馈送到IP层,则我可能能够重现这些漏洞并找到新的漏洞。分别使用代码审计和静态分析发现了这些过去的漏洞。作为主要使用模糊来查找内存损坏漏洞的人,这些是我研究的非常有用的工件,因为它们来自行业中一些最佳的审计和静态分析从业人员。如果我无法重现漏洞或发现任何新漏洞,那么至少对我来说这将是一个很好的学习经历。成功将证明我的方法至少与最初用于发现这些漏洞的方法一样好,失败的话也说明我的方法存在一些缺陷。

https://bugs.chromium.org/p/project-zero/issues/detail?id=1558

https://lgtm.com/blog/apple_xnu_icmp_error_CVE-2018-4407

该模糊器通过可操作的ASAN报告发现了Ian和Kevin的漏洞。甚至更好的是,对于ICMP缓冲区溢出,它完全崩溃了,就像Imm在Semmle博客中描述的给Kevin的电子邮件中描述的那样。当我看到这是多么准确和有效时,我开始感到非常兴奋。更好的是,我的模糊测试器继续找到了一个ICMP漏洞的变种,我没有看到它被公开提及过,但幸运的是,Apple对该漏洞进行了全面修补。

从Protobuf到PoC

在以后的文章中将详细介绍模糊器的工作原理,但是需要一些上下文来了解如何发现此特定的漏洞。从总体上讲模糊器的设计与syzkaller的设计非常相似。它使用基于Protobuf的语法来对网络相关的系统调用及其参数类型进行编码。在每个模糊器迭代中,它执行一系列随机系统调用,将随机数据包的到达(作为伪系统调用)发送到网络层

https://github.com/google/syzkaller

https://github.com/google/syzkaller

例如,打开套接字的syscall是int socket(int domain,int type,int protocol)。表示此系统调用及其参数的protobuf消息为:

 message Socket {
   required Domain domain = 1;
   required SoType so_type = 2;
   required Protocol protocol = 3;
 }
 
 enum Domain {
   AF_UNSPEC = 0;
   AF_UNIX = 1;
   AF_INET = 2;
 ...
   AF_MAX = 40;
 }
 
 enum SoType {
   SOCK_STREAM = 1;
   SOCK_DGRAM = 2;
   SOCK_RAW = 3;
   SOCK_RDM = 4;
   SOCK_SEQPACKET = 5;
 }
 
 enum Protocol {
   IPPROTO_IP = 0;
   IPPROTO_ICMP = 1;
   IPPROTO_IGMP = 2;
   IPPROTO_GGP = 3;
 ...
 }

LibFuzzer和protobuf-mutator一起使用我定义的格式来生成和变异protobuf消息。然后,我使用这些消息并调用真正的C实现。模糊器可能会生成以下protobuf消息,作为表示syscall的消息序列的一部分:

 socket {
   domain: AF_INET6
   so_type: SOCK_STREAM
   protocol: IPPROTO_IP
 }

在输入syscall消息的循环中,我根据消息类型适当地调用syscall:

 std::set open_fds;
 
 // ...
 case Command::kSocket: {
   int fd = 0;
   int err = socket_wrapper(command.socket().domain(),
                            command.socket().so_type(),
                            command.socket().protocol(), &fd);
   if (err == 0) {
     assert(open_fds.find(fd) != open_fds.end());
     open_fds.insert(fd);
   }
   break;
 }

在这里,可以看到其中涉及的一些简单的手动工作:我跟踪打开的文件描述符,因此可以确保在一个模糊测试迭代结束时将其关闭。

模糊测试器通过简单地将所有与网络相关的系统调用编码为具有每个参数正确类型的消息而开始。为了提高覆盖率,我改进了语法并对测试的代码进行了更改。因为要覆盖的代码太多,所以查找漏洞的最有效方法是通过审计手动识别看似可疑的代码。给定我们模糊测试的基础结构,可以查看覆盖率指标,以了解对一些可疑代码的测试情况,并调整模糊测试器以统一执行所需状态。这可能比仅代码覆盖率具有更高的抽象级别,但是覆盖率仍将帮助确定是否达到某个状态以及以何种频率到达某个状态。

现在,看看如何改进语法如何将我们从低质量的崩溃转移到一个可充分利用的PoC。触发CVE-2019-8605首次崩溃的测试用例仅影响原始套接字,因此仅是root用户。这是我提交给Apple的PoC:

 #define IPPROTO_IP 0
 
 #define IN6_ADDR_ANY { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
 #define IN6_ADDR_LOOPBACK { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 }
 
 int main() {
     int s = socket(AF_INET6, SOCK_RAW, IPPROTO_IP);
     struct sockaddr_in6 sa1 = {
         .sin6_len = sizeof(struct sockaddr_in6),
         .sin6_family = AF_INET6,
         .sin6_port = 65000,
         .sin6_flowinfo = 3,
         .sin6_addr = IN6_ADDR_LOOPBACK,
         .sin6_scope_id = 0,
     };
     struct sockaddr_in6 sa2 = {
         .sin6_len = sizeof(struct sockaddr_in6),
         .sin6_family = AF_INET6,
         .sin6_port = 65001,
         .sin6_flowinfo = 3,
         .sin6_addr = IN6_ADDR_ANY,
         .sin6_scope_id = 0,
     };
     connect(s, (const sockaddr*)&sa1, sizeof(sa1));
     unsigned char buffer[4] = {};
     setsockopt(s, 41, 50, buffer, sizeof(buffer));
     connect(s, (const sockaddr*)&sa2, sizeof(sa2));
     close(s);
 }

由于此C复制程序是在protobuf测试用例之后直接建模的,因此可以看到我的早期语法对sockaddr结构具有很多精度。但是,setsockopt的指定严重不足:它只占用了2个整数和一个随机的数据缓冲区。幸运的是,这足以让我们猜测41(IPPROTO_IPV6)和50(IPV6_3542RTHDR),正确设置了IPv6输出选项。

查看UAF后的ASAN报告,我们看到以下堆栈跟踪:

 #0 0x497a3d in free _asan_rtl_:3
 #1 0x7f8bbe5f42cd in in6_pcbdetach /src/bsd/netinet6/in6_pcb.c:681:3
 #2 0x7f8bbe6b06d0 in rip6_detach /src/bsd/netinet6/raw_ip6.c:829:2
 #3 0x7f8bbe6af680 in rip6_abort /src/bsd/netinet6/raw_ip6.c:837:9
 #4 0x7f8bbe6b0795 in rip6_disconnect /src/bsd/netinet6/raw_ip6.c:848:9
 #5 0x7f8bbe10132f in sodisconnectlocked /src/bsd/kern/uipc_socket.c:1792:10
 #6 0x7f8bbe1028dc in soconnectlock /src/bsd/kern/uipc_socket.c:1664:15
 #7 0x7f8bbe133e00 in connectit /src/bsd/kern/uipc_syscalls.c:954:10
 #8 0x7f8bbe133b25 in connect_nocancel /src/bsd/kern/uipc_syscalls.c:726:10
 #9 0x7f8bbe6f22b4 in connect_wrapper /src/fuzzing/syscall_stubs.c:125:7

查看实际调用free的函数,我们看到以下内容:

 void
 in6_pcbdetach(struct inpcb *inp)
 {
     // ...
         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; // in6p_outputopts); // 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
 •                }
 // ...

此处的罪魁祸首是对ip6_freepcbopts 的调用。在我的模糊器构建中,此函数内联到ipc6_pcbdetach中,该函数解释了我们在漏洞报告中看到的回溯。开发人员打算在某些情况下通过在释放每个指针后将其清空为空来重用套接字选项。但是因为in6p_outputopts 由指向另一个结构的指针表示,所以它们由辅助函数in6_freepcbopts 释放。该函数不知道的地址INP ,因此它不能清除与inp-> in6p_outputopts ,因为我们可以在这个片段中忽略确实看到代码。经检查,此漏洞确实简单明了,但ROUTE_RELEASE以下行是安全的,因为它修改in6p_route 内嵌存储在INP 并正确调用指针。较旧的XNU修订版没有使NULL无效,或者它们都是漏洞的,或者该代码最初并非旨在解决套接字的重用。

释放的缓冲区是通过调用setsockopt 创建的。这暗示我们可以通过对getsockopt 和setsockopt的更多调用来继续访问释放的缓冲区,这将分别代表读取和写入原语。最初的测试用例看起来像原始套接字中一个非常特殊的边缘情况,因此我认为它不容易被利用。每当我报告一个漏洞时,我都会为其创建一个本地补丁,以避免在随后的模糊测试中再次被击中。但是因为我想找到它的更多变体,所以我只是在Fuzzer中禁用了原始套接字,只对一行进行了枚举更改,而使该漏洞保持不变。

这将被证明是正确的想法。我很快找到了一个新的变体,可通过TCP套接字使用getsockopt读取UAF数据,因此它可在iOS应用沙箱中使用。经过一番尝试和漏洞后,我发现setockopt 无法用于已断开连接的套接字。我再次解决了无法利用的测试用例,并通过专门为getsockopt情况添加了解决方法来使漏洞保持完整:

 // HACK(nedwill): this prevents us from seeing the trivial read UaF case
 if (in6p->inp_state == INPCB_STATE_DEAD) {
     error = 0;
     break;
 }
 // Normal handler
 error = ip6_getpcbopt(in6p->in6p_outputopts, optname, sopt);

至此,我意识到setsockopt 是漏洞的重要来源。我通过限制更新的语法来更好的模型系统调用setsockopt的是从一个枚举只选择有效的值。可以在下面看到更改,其中SocketOptName 枚举现在从SO,TCP,IPV6和其他级别指定了各种实选项名称。

 message SetSocketOpt {
    optional Protocol level = 1;
 -  optional int32 name = 2;
 +  optional SocketOptName name = 2;
    // TODO(nedwill): structure for val
    optional bytes val = 3;
    optional FileDescriptor fd = 4;
 }
 
  enum SocketOptName {
 +  option allow_alias = true;
 +
 +  /* socket.h */
 +  SO_DEBUG = 0x0001;           /* turn on debugging info recording */
 +  SO_ACCEPTCONN = 0x0002;              /* socket has had listen() */
 +  SO_REUSEADDR = 0x0004;               /* allow local address reuse */
 +  SO_KEEPALIVE = 0x0008;               /* keep connections alive */
 +  SO_DONTROUTE = 0x0010;               /* just use interface addresses */
 +  SO_BROADCAST = 0x0020;               /* permit sending of broadcast msgs */
 ...

这些更改是导致高度可利用的测试用例的关键更改。通过允许模糊器更有效地探索setsockopt 空间,不久之后它就合成了一个写入已释放缓冲区的测试用例。当我查看崩溃的输入时,在解码后的protobuf数据中看到了这一点:

 set_sock_opt {
   level: SOL_SOCKET
   name: SO_NP_EXTENSIONS
   val: "\267\000\000\000\001\000\000\000"
   fd: FD_0
 }

那SO_NP_EXTENSIONS选项是什么?为何将这个syscall插入测试用例中,为什么会将它从内存泄漏变成可利用的内存损坏?快速浏览XNU中的SO_NP_EXTENSIONS处理,我意识到我们遇到了这个问题:

 #define SONPX_SETOPTSHUT 0x000000001 /* flag for allowing setsockopt after shutdown */

我认为每位漏洞研究人员都可以与他们意识到自己有一个重大bug的时刻有关。那是我的那一刻,该记录描述了我需要将use-after-free-read 转换为 a use-after-free-write。将完整的测试用例转为C会产生以下结果:

 int s = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
 
 // Permit setsockopt after disconnecting (and freeing socket options)
 struct so_np_extensions sonpx = {.npx_flags = SONPX_SETOPTSHUT, .npx_mask = SONPX_SETOPTSHUT};
 setsockopt(s, SOL_SOCKET, SO_NP_EXTENSIONS, &sonpx, sizeof(sonpx));
 
 // Initialize ip6_outputopts
 int minmtu = -1;
 setsockopt(s, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));
 
 // Free ip6_outputopts
 disconnectx(s, 0, 0);
 
 // Write to ip6_outputopts
 setsockopt(s, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));

实际上,模糊器设法猜测以下系统调用:

 struct so_np_extensions sonpx = {.npx_flags = SONPX_SETOPTSHUT, .npx_mask = SONPX_SETOPTSHUT};
 setsockopt(s, SOL_SOCKET, SO_NP_EXTENSIONS, &sonpx, sizeof(sonpx));

看到这一点我感到很惊讶,因为我完全希望以其他方式触发UAF。将基于语法的模糊测试与覆盖率反馈相结合非常酷的地方在于,指定代表级别和名称选项的枚举以及“ val”字段的原始缓冲区足以找到此选项并正确设置。这意味着模糊器猜测val的长度(8)和代表SONPX_SETOPTSHUT的数据(每个小尾数双字的低位设置)。我们可以推断出,模糊器在尝试发现SO_NP_EXTENSIONS选项很多次之后,发现在附加覆盖范围内长度明显为8。然后,此set_sock_opt消息与其他相关测试用例(包括触发我的原始漏洞的情况)混合时,在整个主体中传播。然后确保在val中设置了两位只是4分之一的猜测。相同setsockopt 调用设置了状态后再次调用以触发UAF,这是由protobuf-mutator进行的另一个浅层突变,只是克隆了syscall序列的一个成员。编写有效的模糊器涉及很多有关概率的问题,可以看到如何通过在适当的位置提供模糊器手动定义的结构来设法在抽象级别上进行探索,从而为该漏洞找到了一个不错的PoC。

我希望你喜欢这种对bug搜索过程的见解。有关使用这种模糊方法的更多背景知识,请阅读syzkaller和本教程

https://github.com/google/fuzzer-test-suite/blob/master/tutorial/structure-aware-fuzzing.md

https://lwn.net/Articles/677764/

0x03 Exploitation

UAF

我将要描述的漏洞利用了一个UAF的bug,来获得安全可靠的任意读取,绕过ASLR,进行任意释放,最终允许我们建立一个任意的读/写机制。对于一个漏洞,这是很多责任,因此值得为从未使用过漏洞的读者提供一些先后使用的背景知识。我清楚地记得当我阅读克里斯·埃文斯(Chris Evans)帖子时就在此博客上,这是“什么是'良好的'内存损坏漏洞?”,当我下载并运行Chris的规范使用,并且“利用”在笔记本电脑上进行了首次尝试时,我立即被震惊。简单。从那时起,我就写了几篇关于UAF漏洞利用后在现实世界中的用法,它们都是基于相同的见解:与你想像的那样,使用受控数据回收释放的缓冲区要容易得多。只要未从PartitionAlloc分配缓冲区,大小大约相同的对象将在malloc和free的不同调用者之间混合。如果可以从释放对象的大小类中导致大小相同的任意分配,则可以肯定地确定你将快速回收释放的数据。这是设计使然:如果内存分配器没有按照这种方式运行,则应用程序将由于不重用最近释放的分配中的缓存行而导致性能下降。而且,如果你可以判断是否成功回收了释放的缓冲区,则利用几乎是确定性的。那么,是什么导致一个好的UaF漏洞?如果你可以控制何时释放,何时使用释放的分配,并且可以安全地检查是否已回收,则开发将很简单。至少这是CTF队友向我解释的方式,并且今天它仍然针对实际目标。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。应用程序将不重新使用最近释放的分配中的缓存行而导致性能下降。而且,如果你可以判断是否成功回收了释放的缓冲区,则利用几乎是确定性的。那么,是什么导致一个好的UaF漏洞?如果你可以控制何时释放,何时使用释放的分配,并且可以安全地检查是否已回收,则开发将很简单。至少这是CTF队友向我解释的方式,并且今天它仍然针对实际目标。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。应用程序将不重新使用最近释放的分配中的缓存行而导致性能下降。而且,如果你可以判断是否成功回收了释放的缓冲区,则利用几乎是确定性的。那么,是什么导致一个好的UaF漏洞?如果你可以控制何时释放,何时使用释放的分配,并且可以安全地检查是否已回收,则开发将很简单。至少这是CTF队友向我解释的方式,并且今天它仍然针对实际目标。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。剥削几乎是确定性的。那么,是什么导致一个好的UaF漏洞?如果你可以控制何时释放,何时使用释放的分配,并且可以安全地检查是否已回收,则开发将很简单。至少这是CTF队友向我解释的方式,并且今天它仍然针对实际目标。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。剥削几乎是确定性的。那么,是什么导致一个好的UaF漏洞?如果你可以控制何时释放,何时使用释放的分配,并且可以安全地检查是否已回收,则开发将很简单。至少这是CTF队友向我解释的方式,并且今天它仍然针对实际目标。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。到今天仍然与实际目标背道而驰。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。到今天仍然与实际目标背道而驰。我们在这篇文章中看到的漏洞就是其中一个漏洞,因此,随着内存损坏的发生,它的确“不错”。

更好的原语引导

通常,二进制利用的最终目标是执行任意代码,当该任意代码在目标系统上生成shell时,有时也称为“ shellcode”。对于iOS,添加PAC会使情况稍微复杂一些,这在内核内存R / W和内核代码执行之间引入了安全边界。这意味着我们的漏洞将作为使用基于数据的攻击来获取内核内存R / W的入口点,而代码执行则留给了另一层利用。

首先,我想看看不知道Mach细节就可以构建哪些原语会很有趣。Mach和BSD是XNU的阴和阳,代表了许多基本内核对象的双重视图。例如,一个进程在内核中表示两次:一次作为Mach 任务,一次作为BSD proc 。我的漏洞发生在BSD的一半中,大多数漏洞最终都可以控制一个特权较高的Mach端口。这意味着我们需要弄清楚如何从BSD端的损坏开始操纵Mach数据结构。在这一点上,我仍然只熟悉内核的BSD部分,因此我从这里开始了研究。

这是包含悬空的inp6_outputopts 指针的inpcb :

微信图片_20200114175958.png

spacer.gif通过[get / set] sockopt 查看这些选项的getter和setter ,我们很快发现,为minmtu 和preferred_tempaddr 字段获取整数很简单,这将使我们直接从释放的缓冲区中读取数据。如果我们设法回收它,我们也可以从in6po_pktinfo 指针中自由读取20个字节。自己看一下ip6_getpcbopt 实现中的以下代码片段:

 case IPV6_PKTINFO:
     if (pktopt && pktopt->ip6po_pktinfo)
         optdata = (void *)pktopt->ip6po_pktinfo;
     else {
         /* XXX: we don't have to do this every time... */
         bzero(&null_pktinfo, sizeof (null_pktinfo));
         optdata = (void *)&null_pktinfo;
     }
     optdatalen = sizeof (struct in6_pktinfo); // 20 bytes
     break;
 
 case IPV6_USE_MIN_MTU:
     if (pktopt)
         optdata = (void *)&pktopt->ip6po_minmtu;
     else
         optdata = (void *)&defminmtu;
     optdatalen = sizeof (int);
     break;
 
 case IPV6_PREFER_TEMPADDR:
     if (pktopt)
         optdata = (void *)&pktopt->ip6po_prefer_tempaddr;
     else
         optdata = (void *)&defpreftemp;
     optdatalen = sizeof (int);
     break;

ip6po_minmtu 和ip6po_prefer_tempaddr 彼此相邻且qword对齐,因此,如果我们设法与其他包含指针的对象一起回收此释放的结构,我们将能够读出指针并击败ASLR。我们也可以通过将它们用作堆喷射成功的预告片来利用这些字段。我们在重叠的位置喷洒含有任意指针值,我们选择的对象in6po_pktinfo 现场和魔法值mintmu 场。这样,我们可以重复读出minmtu字段,因此,如果我们看到魔术值,便知道可以安全地取消引用in6po_pktinfo中的指针。通常阅读inp6_outputopts 是安全的因为我们知道它已经被映射,但是in6po_pktinfo 却没有,因为它可能已经被其他一些指向未映射或无法读取的内存的垃圾回收。在讨论我们要喷射哪个对象以泄漏指针以及如何喷射任意数据之前,让我们快速确定可以从setsockopt 损坏中构建什么原语。

不幸的是,与getsockopt路径不同,setsockopt 路径并不像它最初出现时那样易于使用。大多数相关选项仅是root用户或受到严格限制。这仍然使IPV6_2292PKTINFO / IPV6_PKTINFO 仍然是最佳选择,但是在测试和读取代码时,除了高度受约束的值之外,似乎什么都写不到。该ipi6_addr必须将其设置为0才能通过检查未指定的字段,该字段看起来非常适合写入任意数据。接口索引必须有效,这将我们限制在较低的值。如果接口为0,则释放选项。这意味着我们只能在内存中的任何地方写入16个空字节以及一个小的非零4字节整数。对于剥削来说,这当然足够了,但是免费案例呢?只要你传递包含20个空字节的pktinfo结构,ip6_setpktopt 就会为你调用ip6_clearpktopts ,最终调用FREE(pktopt-> ip6po_pktinfo,M_IP6OPT)。记住,in6po_pktinfo是我们的受控指针,所以这意味着我们有一个任意的空闲空间。更好的是,它是一个空的空闲空间,这意味着我们可以释放任何对象而无需知道其区域。这是因为FREE 是kfree_addr 的包装,kfree_addr 代表你查找区域。为了保持后期开发的通用性,我选择了受约束的写基元上的任意自由基元。

实施和测试堆喷

现在我们有了一个攻击计划,只需要找出一种方法即可向堆中注入受控数据。对我们来说幸运的是,已经有一种通过IOSurface做到这一点的著名方法,甚至更好的是,Brandon Azad(@_bazad)已经有一些代码可以做到这一点!经过一些调试并将其集成到我的漏洞中之后,我有了一个有效的“阶段1”抽象,可以通过如上所述回收和检查minmtu magic值来读取并释放任意地址。IOSurface技术早在2016年就已被用作10.0.1-10.1.1 的野生漏洞利用链的一部分。

在不同的iOS设备和版本上进行测试时,我发现喷涂行为有所不同。一台设备上快速可靠的设备在另一台设备上不可靠。幸运的是,对一台设备的改进通常会使所有平台和版本受益,因此我不必担心在每个平台上维护多个喷雾模式。这里涉及的一些参数是每次尝试喷涂对象的数量,重试多少次,以及分配喷涂和(先使用后释放)插槽选项的顺序。了解跨版本和设备的堆分配器内部是理想的,但是我发现进行实验足以满足我的目的。这是CTF的见解;我曾经通过阅读glibc并仔细计划了纸上的漏洞来解决Linux堆问题。几年后 流行的方法(至少对我而言)已经转变为使用工具来检查堆的状态,并快速迭代以检查对漏洞利用的高级修改将如何改变堆的布局。当然,我在iOS上没有这样的工具。但是通过检查minmtu值,我确实拥有一个安全的预言机来测试喷雾性能和可靠性,因此可以快速手动进行迭代。当iOS 12.4退回并重新引入此漏洞时,我针对iPhone XR测试了此漏洞,并发现喷雾经常失败。但是在更改了我进行喷涂的顺序之后(每次尝试之后都创建一个新的悬空指针,而不是一开始就一次创建一次),成功再次变得快速而可靠。我毫不怀疑,对内部结构有很好的了解,

是什么使SockPuppet可以快速利用?其他漏洞利用通常依赖于垃圾回收,以使其释放的对象跨区域重新分配。因为我使用的所有对象都在同一个基于大小的通用区域中,所以我需要更少的分配才能成功,而且不必触发和等待垃圾回收。

了解tfp0

在这一点上,我已经将最初的漏洞扩展到其极限,这使我可以任意读取和任意释放。有了这个,我们现在可以免费使用后在原始代码中从来没有漏洞的地方创建新的用途。在接受了Mach开发路径为内核开发提供了一些不错的工具之前,我环顾了所有可能在内核树的BSD部分中其他人可能忽略的任何可爱的小技巧,因此是时候学习它了。

如果你随便跟随iOS内核开发,你可能听说过“ tfp0”。那么究竟是什么呢?这是task_for_pid的缩写,它会向你返回一个Mach端口,并带有给定pid的任务的发送权。当你使用pid 0调用它时,这将为你提供内核任务端口。端口是Mach的基本原语之一。就像一个用来描述消息队列的文件描述符。内核中的每个此类消息队列都有一个接收者,并可能有多个发送者。给定一个端口名,例如task_for_pid 返回的端口名,你可以根据访问该队列的权限来向该队列发送或接收Mach消息。该kernel_task 就像马赫任何其他的任务,因为它暴露了一个任务端口。

访问内核任务端口有什么好处?只需看一下XNU源代码中的osfmk / mach / mach_vm.defs 。它具有mach_vm_allocate ,mach_vm_deallocate ,mach_vm_protect 和mach_vm_read_overwrite之类的调用。如果我们具有任务端口的发送权,则可以在该过程中读取,写入和分配内存。XNU支持kernel_task的这种抽象,这意味着你可以使用此干净的API来操纵内核地址空间中的内存。我禁不住觉得每部iPhone都有一个“作弊”菜单,因此你必须通过严格的技能测试才能解锁。你可以看到为什么这是如此吸引人的剥削,以及为什么我如此兴奋地尝试抓住它!当然,我们不能只从无特权的沙盒应用程序调用task_for_pid(0)。但是,如果我们可以根据内存破坏原语来实现此函数调用,那么我们将能够假装自己做到了!

为了了解模拟合法tfp0调用所需的操作,让我们看一下从任务发送到另一个任务(也许是kernel_task)的消息的样子,从用户区的端口名(等效于文件描述符)开始消息传递。

让们从代表消息头的结构开始:

 typedef struct {
   mach_msg_bits_t    msgh_bits; // "disposition", e.g. MACH_MSG_TYPE_COPY_SEND
   mach_msg_size_t    msgh_size;
   mach_port_t        msgh_remote_port; // destination port name
   mach_port_t        msgh_local_port;
   mach_port_name_t   msgh_voucher_port;
   mach_msg_id_t      msgh_id;
 } mach_msg_header_t;

我已在上面标记了重要字段。msgh_remote_port 包含目标端口名称,如果我们可以访问它,它将是内核任务端口名称。该msgh_bits 指定多个标志,其中之一是对我们发送的不同端口名称的消息的“性格”。例如,如果我们拥有内核任务端口的发送权,我们将设置msgh_bits 告诉内核将IPC空间中的发送权复制到消息中。如果这听起来很棘手,请不要担心。要记住的主要事情是,我们在标头中命名消息的目的地,并且还标记了如何使用存储在IPC名称空间(马赫文件描述符表)中的消息的功能。

当我们想从用户区发送消息时,我们进行了一个mach陷阱,它相当于系统调用的mach,称为mach_msg_overwrite_trap 。让我们看一下MACH_SEND_MSG的情况并进行后续操作,因此我们发现需要为tfp0在内核内存中进行的安排:

 mach_msg_return_t mach_msg_overwrite_trap(
     struct mach_msg_overwrite_trap_args* args) {
   // ...
   mach_msg_return_t mr = MACH_MSG_SUCCESS;
   vm_map_t map = current_map();
 
   if (option & MACH_SEND_MSG) {
     ipc_space_t space = current_space();
     ipc_kmsg_t kmsg;
 
     mr = ipc_kmsg_get(msg_addr, send_size, &kmsg);
     // ...
     mr = ipc_kmsg_copyin(kmsg, space, map, override, &option);
     // ...
     mr = ipc_kmsg_send(kmsg, option, msg_timeout);
 // ...

如果要将消息传递到内核任务端口,则只需要了解ipc_kmsg_get ,ipc_kmsg_copyin 和ipc_kmsg_send的工作方式即可。ipc_kmsg_get 只是将消息从调用任务的地址空间复制到内核内存中。ipc_kmsg_copyin 实际上可以完成有趣的工作。让我们看看它如何通过调用ipc_kmsg_copyin_header 来摄取消息头。

 mach_msg_return_t ipc_kmsg_copyin_header(ipc_kmsg_t kmsg, ipc_space_t space,
                                          mach_msg_priority_t override,
                                          mach_msg_option_t *optionp) {
   mach_msg_header_t *msg = kmsg->ikm_header;
   mach_msg_bits_t mbits = msg->msgh_bits & MACH_MSGH_BITS_USER;
   mach_port_name_t dest_name = CAST_MACH_PORT_TO_NAME(msg->msgh_remote_port);
   mach_port_name_t reply_name = CAST_MACH_PORT_TO_NAME(msg->msgh_local_port);
 
   mach_msg_type_name_t dest_type = MACH_MSGH_BITS_REMOTE(mbits);
   ipc_object_t dest_port = IO_NULL;
   ipc_port_t dest_soright = IP_NULL;
   ipc_entry_t dest_entry = IE_NULL;
 
   if (dest_name != reply_name) {
     // nedwill: this converts name to ipc_entry_t
     dest_entry = ipc_entry_lookup(space, dest_name);
     if (dest_entry == IE_NULL) {
       goto invalid_dest;
     }
 
     // nedwill: this converts ipc_entry_t to ipc_port_t (and checks capability)
     kr = ipc_right_copyin(space, dest_name, dest_entry, dest_type, FALSE,
                           &dest_port, &dest_soright, &release_port, &assertcnt);
     if (kr != KERN_SUCCESS) {
       goto invalid_dest;
     }
 
     // ...
   }
 
   // ...
   msg->msgh_bits =
       MACH_MSGH_BITS_SET(dest_type, reply_type, voucher_type, mbits);
   msg->msgh_remote_port = (ipc_port_t)dest_port;
 
   // ...
 }

ipc_kmsg_copyin_header 用于将远程端口名称转换为端口对象,更新msg-> msgh_remote_port 指向实际对象,而不是存储特定于任务的名称。BSD / Linux等效于将文件描述符转换为它所引用的实际内核结构。消息头有几个名称字段,但是我已经简化了代码以突出显示目标案例,因为我们希望将kernel_task 端口作为目标端口。该ipc_space_t空间参数表示当前运行的任务,这是马赫相当于文件描述符表的IPC空间。首先,我们在IPC空间中查找dest_name 以获得ipc_entry_t代表它。每个ipc_entry_t 都有一个名为ie_bits 的字段,其中包含我们的任务必须与所涉及的端口进行交互的权限。IPC条目结构如下所示:

 struct ipc_entry {
   struct ipc_object *ie_object; // pointer to the ipc_port_t
   ipc_entry_bits_t ie_bits; // our rights (receive/send/send-once/etc.)
   mach_port_index_t ie_index;
 ...
 };

请记住,我们发送的消息的标题具有用于目的地的“处置”,它描述了我们希望消息利用对远程端口名称的能力来做什么。这是实际得到验证和使用的地方:

 kern_return_t ipc_right_copyin(ipc_space_t space, mach_port_name_t name,
                                ipc_entry_t entry,
                                mach_msg_type_name_t msgt_name, boolean_t deadok,
                                ipc_object_t *objectp, ipc_port_t *sorightp,
                                ipc_port_t *releasep, int *assertcntp) {
   ipc_entry_bits_t bits;
   ipc_port_t port;
 
   *releasep = IP_NULL;
   *assertcntp = 0;
 
   bits = entry->ie_bits;
 
   switch (msgt_name) {
     case MACH_MSG_TYPE_COPY_SEND: {
       if (bits & MACH_PORT_TYPE_DEAD_NAME) goto copy_dead;
 
       /* allow for dead send-once rights */
       if ((bits & MACH_PORT_TYPE_SEND_RIGHTS) == 0) goto invalid_right;
 
       port = (ipc_port_t)entry->ie_object;
 
       if ((bits & MACH_PORT_TYPE_SEND) == 0) {
         assert(IE_BITS_TYPE(bits) == MACH_PORT_TYPE_SEND_ONCE);
         assert(port->ip_sorights > 0);
 
         ip_unlock(port);
         goto invalid_right;
       }
 
       port->ip_srights++;
       ip_reference(port);
       ip_unlock(port);
 
       *objectp = (ipc_object_t)port;
       *sorightp = IP_NULL;
       break;
     }
 
     default:
     invalid_right:
       return KERN_INVALID_RIGHT;
   }
 
   return KERN_SUCCESS;
 }

在这里,我转载了MACH_MSG_TYPE_COPY_SEND 案例的代码。你可以看到IPC条目中的ie_bits 用于检查我们拥有的权限的位置。如果要利用此消息中的发送权限,可以将权限复制到该消息中,并且此代码在更新相关引用计数并最终使我们能够访问端口对象以检查是否拥有ie_bits 中的权限之前,我们可以将消息放入队列。如果根据entry-> ie_bits 没有适当的权限,则发送消息的尝试将失败。

现在我们的消息已被复制,验证和更新以包含真实的内核对象指针,ipc_kmsg_send 继续进行,仅将我们的消息添加到目标队列中:

 mach_msg_return_t ipc_kmsg_send(ipc_kmsg_t kmsg, mach_msg_option_t option,
                                 mach_msg_timeout_t send_timeout) {
   ipc_port_t port;
   thread_t th = current_thread();
   mach_msg_return_t error = MACH_MSG_SUCCESS;
   boolean_t kernel_reply = FALSE;
 
   port = (ipc_port_t)kmsg->ikm_header->msgh_remote_port;
   assert(IP_VALID(port));
   ip_lock(port);
 
   if (port->ip_receiver == ipc_space_kernel) {
     port->ip_messages.imq_seqno++;
     ip_unlock(port);
 
     kmsg = ipc_kobject_server(kmsg, option);
     if (kmsg == IKM_NULL) return MACH_MSG_SUCCESS;
 
     /* restart the KMSG_INFO tracing for the reply message */
     port = (ipc_port_t)kmsg->ikm_header->msgh_remote_port;
     assert(IP_VALID(port));
     ip_lock(port);
     /* fall thru with reply - same options */
     kernel_reply = TRUE;
     if (!ip_active(port)) error = MACH_SEND_INVALID_DEST;
   }
 
   if (error != MACH_MSG_SUCCESS) {
     ip_unlock(port);
   } else {
     // ...
     error = ipc_mqueue_send(&port->ip_messages, kmsg, option, send_timeout);
   }
   // ...
   return error;
 }

从上面可以看到,如果目标端口的ip_receiver 是内核IPC空间,则调用ipc_kobject_server 作为处理内核消息的特殊情况。内核任务端口将内核IPC空间作为其ip_receiver ,因此在安排tfp0时,请确保将其复制。

现在我们已经看到了消息发送背后的基本知识,已经准备好设想我们的目标状态,即我们希望内核内存看起来像我们成功调用了tfp0一样。我们想向我们的IPC空间添加一个IPC条目,ie_object 指向内核任务端口,ie_bits 指示我们具有发送权限。如下:

spacer.gif1576433145313306.png

绕过ASLR并伪造数据结构

IPC系统通常需要一种序列化文件描述符并通过管道发送它们的方法,内核需要了解此约定以进行正确的free。诸如文件描述符之类的端口可以由一个进程发送到具有附加发送权的另一个进程。可以使用包含mach_msg_ool_descriptor_t 的特殊消息将一个进程的离线端口发送到另一个进程。如果想在一条消息中发送多个端口,则可以发送mach_msg_ool_ports_descriptor_t ,这是存储在行外(OOL)的端口的数组,表示在消息头本身之外。与许多其他工具一样,我们将在漏洞利用程序中使用OOL端口描述符。

使OOL端口阵列如此有用的原因在于,可以完全控制阵列的大小。当传递一个端口名称数组时,内核将为任意数量的指针分配空间,每个指针都充满了指向我们要发送的ipc_port结构的指针。可以将此技巧用作ASLR绕过,因为我们可以将端口指针的OOL描述符数组与大小为192的释放缓冲区重叠,并且只需通过getsockopt 从释放的结构中读取两个相邻的int字段。在这一点上,可以开始通过任意读取遍历内核数据结构。

许多漏洞利用将损坏的bug变成读取的原语。在执行任何破坏操作之前,我们拥有获得可靠的读取原语的罕见特权,因此我们将其与此指针公开结合使用,以泄漏所有相关指针以完成漏洞利用,包括在已知地址处建立手工设计的数据。我们继续进行所有必要的遍历,如下所示。

spacer.gif1576433158652768.png

方的绿色节点代表我们的探索种子值,橙色节点代表我们试图寻找的值。通过使用包含指向表示主机端口的ipc_port 结构的指针的OOL端口描述符数组喷射消息,我们找到其ipc_port ,它将通过接收器字段为我们提供ipc_space_kernel 。

我们重复相同的初始技巧,以找到适合自己任务的ipc_port 。在这里,我们找到任务的文件描述符表,并使用它来找到套接字选项和管道缓冲区的vtable。vtable将为我们提供指向kernelcache二进制文件的指针。因为内核进程的BSD表示形式kernproc 是在bsd / kern / bsd_init.c中全局分配的,所以我们可以使用socketops表中的已知偏移量来找到它并查找kernel_task 的地址。

管道缓冲区是通过调用pipe()syscall创建的,它分配了一个缓冲区,我们可以通过文件描述符对该缓冲区进行写入和读取。这是在已知地址获取已知数据的众所周知的技巧。为了制作要注入到IPC空间中的伪造ipc_port ,我们创建一个管道并将数据发送给它。管道将排队的数据存储到内核堆上的缓冲区中,该缓冲区是通过基于大小的通用区域分配的。通过读写相关的管道文件描述符,我们可以从用户空间中重复读写该缓冲区,并且数据存储在内核内存中。通过知道管道缓冲区的地址,我们可以在其中存储受控数据并创建指向它的指针。需要用它来制作ipc_port 用于内核任务。

现在,可以创建伪造的ipc_port 并将其指向kernel_task和ipc_space_kernel ,即使我们可以调用task_for_pid(0)并获取kernel_task 端口,也无法向其发送消息。当内核将任务的ipc_port 转换为任务结构时,将阻止任何试图将消息发送到kernel_task的userland任务。这是在task_conversion_eval中实现的:

 kern_return_t
 task_conversion_eval(task_t caller, task_t victim)
 {
         /*
          * Tasks are allowed to resolve their own task ports, and the kernel is
          * allowed to resolve anyone's task port.
          */
         if (caller == kernel_task) {
                 return KERN_SUCCESS;
         }
 
         if (caller == victim) {
                 return KERN_SUCCESS;
         }
 
         /*
          * Only the kernel can can resolve the kernel's task port. We've established
          * by this point that the caller is not kernel_task.
          */
         if (victim == TASK_NULL || victim == kernel_task) {
                 return KERN_INVALID_SECURITY;
         }
 // ...

我使用了许多其他工具所使用的技巧,只是创建了kernel_task 对象的副本,因此他们使用的指针比较不会检测到我正在向伪造的kernel_task 对象发送消息。并不是真正的kernel_task,因为使用假的kernel_task 支持mach_vm_ * 函数很简单;我们只需要复制内核的kernel_map 并初始化其他一些字段。可以在上图中看到,我们可以简单地从kernel_task提取地址,该地址我们已经知道。我们将伪造的内核任务存储在伪造的ipc_port旁边在管道缓冲区中。

注入kernel_task端口

现在,我们将OOL端口描述符数组用于其他目的。我们向自己发送一条消息,其中包含一个OOL数组,该数组包含我们的任务端口名称的副本,我们有权将其发送。发送权限验证最初是在发送消息时进行的,因此,如果在等待发送时编辑数组,则可以覆盖ipc_port 之一以指向伪造的kernel_task ipc_port 。该技巧改编自Stefan Esser 在该主题上的介绍,并已用于多种漏洞利用中。注意,ipc_port 本身没有发送或接收权限的概念。这些权利将作为ipc_entry的一部分进行跟踪,并在 ipc_port 。因为端口封装了给定的消息队列,发送或接收到该队列的权限特定于每个进程,因此我们可以了解为什么该信息独立存储在每个进程的表中。

尽管在OOL端口描述符数组中重写指针的技巧是一种已知的利用技术,但要由利用开发人员来弄清楚如何真正使这种破坏发生。我们有任意阅读和任意自由。OOL端口描述符数组和管道缓冲区分配在全局区域之外。我们可以结合这些事实!之前我们记下了管道缓冲区的地址。因此,我们只需释放管道缓冲区的实际缓冲区地址并喷射OOL端口描述符数组。然后,我们读取管道缓冲区以查找任务的ipc_port,并使用指向虚假端口的指针覆盖它。然后,我们将消息传递给自己,并检查是否设法注入了伪造的内核任务端口。

至此,我们有了tfp0。像voucher_swap和其他漏洞利用一样,我们想使用带有管道缓冲区结构的此临时tfp0来引导更稳定的tfp0。为此,我们使用内核任务端口分配专用于存储数据的内核内存页面,然后使用write原语在其中写入伪造的任务端口和kernel_task 。然后,我们更改IPC空间条目以指向此新ipc_port 。

我们仍然有一个管道结构,带有指向释放缓冲区的悬空指针。我们不希望在关闭fd时将其释放两次,因此我们使用了新的稳定tfp0功能来使该指针无效。实际上,我们执行了两个破坏内存的操作:释放该指针,并使用新的释放后使用管道缓冲区覆盖单个ipc_port指针,因此跟踪清理非常简单。

如果你想自己阅读和测试该漏洞利用程序,可以在此处获取。

https://bugs.chromium.org/p/project-zero/issues/detail?id=1806#c13

评估PAC和MTE

由于此漏洞利用是基于内存损坏漏洞的,因此存在一个挥之不去的问题,即它如何受到不同缓解措施的影响。苹果公司使用A12芯片将PAC(指针身份验证)引入了iOS,这似乎是为了限制内核代码的执行而设计的,前提是要实现其他目标的任意内核读写。这听起来像是一个非常有力的缓解措施,并且没有任何实际经验,我不确定剥削的结果如何。我当时在A9芯片上进行测试,所以我只是希望自己在漏洞利用方面不做任何事情,以至于PAC可以缓解这种情况。就是这种情况。因为我的攻击仅针对数据结构,并且不涉及任意代码执行,所以没有要伪造的代码指针。

iOS 13开始为某些数据指针引入保护,因此值得研究我需要伪造哪些指针才能使此漏洞在数据PAC上下文中起作用。PAC通过用私有密钥签名它们免受损坏堆栈上的返回地址和指针本身的堆栈上下文值上的位置。但是,其他代码指针没有上下文值进行签名。同样,数据PAC的有效性可能取决于Apple选择使用上下文值的方式。

让我们考虑所有数据指针都受到保护但没有基于位置的上下文进行签名的情况。在这种情况下,只要我们设法泄漏它们,我们就可以将它们从一个位置复制到另一个位置。布兰登(Brandon)在有关PAC的博客文章中对此进行了描述,这就是众所周知的“指针替换攻击” 。

https://googleprojectzero.blogspot.com/2019/02/examining-pointer-authentication-on.html

我们的读取原语在数据PAC的上下文中仍然有效,因为悬空指针仍然是带符号的。我们最终需要伪造或替代一些源自攻击者的指针:ipc_space_kernel ,kernel_map ,&fake_port 和&fake_task ,以及找到它们所需的所有中间读取。回想一下&fake_port 和&fake_task 是指向管道缓冲区的指针。对于我们的初始入口点,pktinfo 指针是否受保护并不重要,因为无论如何我们都必须通过OOL端口spray 泄漏真实的ipc_port 指针。这意味着我们可以收集签名的ipc_port,并进行我们已经进行的所有前期数据结构遍历,因此复制PAC数据指针没有问题。ipc_space_kernel 和kernel_map 已经签名,如果管道缓冲区已签名,我们可以简单地将假端口和任务分配到两个管道缓冲区中,并获得指向每个缓冲区的签名指针。无论如何,该漏洞利用都无法立即使用,因为我们确实在文件描述符表中建立了一个指针以查找任意fd结构,并且某些查找可能需要读取20多个字节的数据。但是,读取原语的功能足够强大,可以在不花费大量精力的情况下解决这些差距。

实际上,iOS 13仅保护某些数据指针,这反常可能会提高最终用户的安全性。例如,如果管道缓冲区不受保护,则仅泄漏一个地址是不太可能让我们使用该指针来表示伪造的ipc_port如果指向端口的指针已签名。检查苹果17B5068e的内核缓存后发现,IPC端口指针的确没有受到保护,但是我确实认为它们计划这样做(或者已经在非Beta版本中这样做),这是根据今年早些时候苹果公司在BlackHat上发表的演讲。就像任何缓解措施以及提供强大初始原语的bug一样,这只是设计替代利用技术的问题。在不考虑应该保护或不保护指针的麻烦的情况下,我希望在将来,尽可能多的指针以该位置为背景进行签名,以帮助减轻指针替换攻击的影响。从思想实验中可以看出,如果仅使用上下文对数据指针进行签名,那么基于良好的基于释放后使用的读取原语将无济于事。

要考虑的另一个缓解措施是ARM的内存标记扩展(MTE),我相信Apple会尝试实现它的即将推出的CPU功能。这里这里都有一个很好的高层缓解摘要。本质上,内存分配器将由内存分配器分配一个随机标签,该标签将是指针的高位未使用位的一部分,例如在PAC中。正确的标记值将被离线存储,类似于ASAN异常存储堆元数据的方式。当处理器取消对指针的引用时,它将检查标签是否匹配。MTE可以缓解此漏洞,因为我们在漏洞利用程序中释放了许多次之后便触发了使用,并且每次访问释放的指针时,都会将其标记与释放范围或分配范围的新标记进行比较。回收缓冲区。识别不匹配的标记时,取决于配置为CPU或内核执行的操作,这将影响漏洞利用的进行方式。我希望苹果根据他们的PAC LLVM文档,考虑到它们努力触发PAC违规的数据中止,请配置在标签检查失败期间发生的同步或异步异常:“虽然ARMv8.3的aut *指令本身不会陷入失败,编译器只按会捕获的顺序发射它们。”

使用攻击者伪造的损坏代码指针的情况很少见,但是无效堆访问在实际代码中经常发生。我怀疑使用MTE会发现许多漏洞,并期待在iPhone中看到它的使用。如果将其与当前的PAC实施相结合,将极大地提高最终用户的安全性。

The iOS Exploit Meta

我发现有趣的是,我使用了哪些技术是iOS漏洞利用Meta的一部分,也就是说,这些技巧在公共漏洞利用中很常见,而在在野攻击中也经常看到。这些都是我遇到的所有技术,以及如何以及是否将它们整合到漏洞利用程序中。如前所述,有关我使用的方法的最接近公开记录的变体,请参阅Stefan Esser 关于该主题的演示,这似乎是第一个使用这种技术的方法。

https://www.slideshare.net/i0n1c/cansecwest-2017-portal-to-the-ios-core

A bug’s life

在较旧的手机上测试漏洞利用程序时,我注意到我的32位iPhone 5仍在运行iOS 9.2。出于好奇,我测试了高度可利用的Disconnectx PoC,它允许通过释放的缓冲区破坏内存,并且震惊地发现PoC立即开始工作。访问释放的内存时内核崩溃(崩溃发生时寄存器之一中存在0xDEADBEEF)。经过更多测试之后,我发现PoC可以在引入了Disconnectx的第一个XNU版本上工作:macOS 10.9.0发行版中包含的Mavericks内核。iOS 7 beta 1内核是在Mavericks之后不久推出的,因此,直到iOS 12.2 / 12.4的iOS 7 beta 1可能已受到此漏洞的影响。iOS 7的正式发布日期为2013年9月18日。

0x04 结论

如此令人惊讶的是,具有如此强大的初始原语的bug一直存在于iOS中。自iPhone问世以来,我一直在关注公开的iOS安全研究,直到最近我才意识到自己也许能够在内核中发现一个漏洞。当我阅读iOS 7时代的漏洞利用记录时,我发现有大量逻辑漏洞与内存损坏结合在一起。但是我从我在Chrome上的工作得知,模糊工具近来已经非常有效,以至于可以再次发现攻击表面的内存损坏漏洞,这些漏洞被认为是经过良好审核的。我们可以看到,这对于iOS来说是正确的(与其他平台(如Chrome等被认为很难破解的平台一样)),即使使用大型链时,也存在足以提升特权的单个漏洞。攻击者方的内存损坏研究现在太简单了:我们需要MTE或其他动态检查来开始发现这一问题。

我要感谢@_bazad对我对Mach问题的耐心配合。SockPuppet受其voucher_swap漏洞利用的启发很大,而voucher_swap漏洞利用又受到了之前许多漏洞利用技术的启发。这确实证明了在许多漏洞利用中出现的某些漏洞利用技巧的力量。借助PAC对关键数据指针的保护,我们可能会看到元转移,因为注入虚假任务端口的主要方法已经在Apple的雷达范围内,并且针对它的缓解措施已经到来。

最后,如果苹果公司更频繁地提供XNU源代码,我可以自动将模糊器与源代码合并,并且我们可以立即捕获iOS 12.4回归。Chromium和OSS-Fuzz已经在此模型上取得成功。我提交给Chrome的模糊测试程序的一个模糊测试程序最初仅发现3个漏洞,但自提交以来,现已发现95个稳定性和安全性问题

漏洞奖励计划:https://www.google.com/about/appsecurity/chrome-rewards/#fuzzerprogram

本文翻译自:https://googleprojectzero.blogspot.com/2019/12/sockpuppet-walkthrough-of-kernel.html?m=1如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务