0x00 引言
sockaddr
是xnu内核中一个很普通的数据结构,用于描述socket地址的基本属性,包括地址长度及其所属family类型。结构体具体定义如下:
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
由于xnu支持多种socket类型,不同类型的socket使用的sockaddr
长度可能不同,xnu中为每种sockaddr
都有具体定义。例如,下面分别是sockaddr_in
、sockaddr_in6
、sockaddr_un
、sockaddr_ctl
的结构。
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct sockaddr_in6 {
__uint8_t sin6_len; /* length of this struct(sa_family_t) */
sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */
in_port_t sin6_port; /* Transport layer port # (in_port_t) */
__uint32_t sin6_flowinfo; /* IP6 flow information */
struct in6_addr sin6_addr; /* IP6 address */
__uint32_t sin6_scope_id; /* scope zone index */
};
struct sockaddr_un {
unsigned char sun_len; /* sockaddr len including null */
sa_family_t sun_family; /* [XSI] AF_UNIX */
char sun_path[104]; /* [XSI] path name (gag) */
};
struct sockaddr_ctl {
u_char sc_len; /* depends on size of bundle ID string */
u_char sc_family; /* AF_SYSTEM */
u_int16_t ss_sysaddr; /* AF_SYS_KERNCONTROL */
u_int32_t sc_id; /* Controller unique identifier */
u_int32_t sc_unit; /* Developer private unit number */
u_int32_t sc_reserved[5];
};
每种sockaddr_*
的头部结构都是sockaddr
,其中第一个字节即sa_len
表示该结构的长度,第二个字节sa_family
表示地址类型。内核使用struct sockaddr*
指针类型时,需要根据sa_family
将其转换成struct sockaddr_in6*
、struct sockaddr_in*
等具体类型。可以看到,当内核处理由用户态提交的sockaddr
数据时,如果对sa_family
或者sa_len
检查不严格时,就可能导致安全漏洞。尤其是sa_len
,描述了数据长度,如果检查不当,就可能引起内存越界访问等问题。
0x01 漏洞介绍
近些年xnu中陆续披露了一些与sockaddr
相关的安全漏洞,其中最为著名的,是Google Project 0团队Ian Beer在mptcp模块中发现的一个漏洞。这里,我们先详细介绍一下这个漏洞。理解这个漏洞的成因对挖掘新漏洞很有帮助。
漏洞回顾
Ian Beer发现的mptcp漏洞位于mptcp_usr_connectx
函数中。mptcp_usr_connectx
在处理用户态传入的sockaddr
数据时,认为其类型只可能是AF_INET
或者AF_INET6
。mptcp_usr_connectx
严格检查了sockaddr
是这两种类型时的sa_len
字段。然而,这里的逻辑缺陷是,一旦传入不是AF_INET
或者AF_INET6
类型的sockaddr
,sa_len
字段就没有检查。mptcp_usr_connectx
使用sa_len
字段调用memcpy
时发生堆溢出。更详细的漏洞分析见链接:Issue 1558: XNU kernel heap overflow due to bad bounds checking in MPTCP
// verify sa_len for AF_INET:
if (dst->sa_family == AF_INET &&
dst->sa_len != sizeof(mpte->__mpte_dst_v4)) {
mptcplog((LOG_ERR, "%s IPv4 dst len %u\n", __func__,
dst->sa_len),
MPTCP_SOCKET_DBG, MPTCP_LOGLVL_ERR);
error = EINVAL;
goto out;
}
// verify sa_len for AF_INET6:
if (dst->sa_family == AF_INET6 &&
dst->sa_len != sizeof(mpte->__mpte_dst_v6)) {
mptcplog((LOG_ERR, "%s IPv6 dst len %u\n", __func__,
dst->sa_len),
MPTCP_SOCKET_DBG, MPTCP_LOGLVL_ERR);
error = EINVAL;
goto out;
}
// code doesn't bail if sa_family was neither AF_INET nor AF_INET6
if (!(mpte->mpte_flags & MPTE_SVCTYPE_CHECKED)) {
if (mptcp_entitlement_check(mp_so) < 0) {
error = EPERM;
goto out;
}
mpte->mpte_flags |= MPTE_SVCTYPE_CHECKED;
}
// memcpy with sa_len up to 255:
if ((mp_so->so_state & (SS_ISCONNECTED|SS_ISCONNECTING)) == 0) {
memcpy(&mpte->mpte_dst, dst, dst->sa_len); <== 当sa_family为非AF_INET和AF_INET6时,没有对sa_len进行长度校验,所以sa_len可以最大为0xff,导致堆溢出。
}
Ian Beer对这个漏洞的利用技巧也非常精彩。我们暂不关心漏洞的利用过程,再分析一下这个漏洞特征。可以看到在这个漏洞代码里,开发者虽然有意识的检查了sockaddr
数据,但只检查了特定类型和相应长度的匹配关系;这导致如果传入的sockaddr
数据是别的类型,其sa_len
字段并没有有效检查。
漏洞1 ==>inctl_ifdstaddr
看过Ian Beer这个漏洞后,我们开始思考,xnu中是否还存在类似的问题:对传入的sockaddr
仅做了部分类型和长度匹配检查,对其它类型的sockaddr
未作检查而继续使用?
带着这个问题,我们继续审计xnu代码。很快我们就在ioctl的处理函数(in_control
函数)里发现了一个新的信息泄漏漏洞。
该漏洞原因是inctl_ifdstaddr
函数在处理SIOCSIFDSTADDR命令时,只处理了family为AF_INET
时的sin_len
,因此当family为其他值(比如AF_INET6
)的时候,sin_len
未被检查,可以为任意值。
如下所示,ifr
指向用户可控的数据,当inctl_ifdstaddr
函数在处理SIOCSIFDSTADDR
命令时,先将用户可控的结构体ifr
全部拷贝到ia
里,然后在a处,处理family为AF_INET
的情况:将ia->ia_dstaddr.sin_len
设置为sockaddr_in
的结构体大小。
但是,当family为其他值,比如为AF_INET6
时,inctl_ifdstaddr
函数没有做任何处理,所以ia->ia_dstaddr.sin_len
就仍是从ifr
里面拷贝过来的用户控制的length
,范围为0~0xff。
static __attribute__((noinline)) int
inctl_ifdstaddr(struct ifnet *ifp, struct in_ifaddr *ia, u_long cmd,
struct ifreq *ifr){
//...
case SIOCSIFDSTADDR: /* struct ifreq */
VERIFY(ia != NULL);
IFA_LOCK(&ia->ia_ifa);
dstaddr = ia->ia_dstaddr;
bcopy(&ifr->ifr_dstaddr, &ia->ia_dstaddr, sizeof(dstaddr));
if (ia->ia_dstaddr.sin_family == AF_INET) {
ia->ia_dstaddr.sin_len = sizeof(struct sockaddr_in); <== a:只在family为AF_INET时检查sin_len
}
//...
}
到这里,我们可以在ia->ia_dstaddr
填入一个非AF_INET
类型的sockaddr
并任意设定sin_len
。接下来的问题是,这个ia->ia_dstaddr
在哪里会被使用?
我们继续审计代码,在sysctl_iflist
函数中找到了对ia->ia_dstaddr
的使用。下面代码中,ifa->ifa_dstaddr
就是inctl_ifdstaddr
里设置的ia->ia_dstaddr
。在b处这个sockaddr
被存入到rti_info
里,然后传入到rt_msg2
函数中。
static int
sysctl_iflist(int af, struct walkarg *w)
{
while ((ifa = ifa->ifa_link.tqe_next) != NULL) {
//...
info.rti_info[RTAX_IFA] = ifa->ifa_addr;
info.rti_info[RTAX_NETMASK] = ifa->ifa_netmask;
info.rti_info[RTAX_BRD] = ifa->ifa_dstaddr; <== b: 之前设置的sockaddr
//...
len = rt_msg2(RTM_NEWADDR, &info, <== c:
caddr_t)cp, NULL, &cred);
//...
}
我们来看rt_msg2
的实现。rt_msg2
就循环遍历rtinfo
数组,当遍历到RTAX_BRD时,sa
就是ifa->ifa_dstaddr
。那么如e处所示,dlen
就是之前用户可控的length
,最大可达到0xff。rt_msg2
调用bcopy
函数做内存复制时,发生内存越界读,最大可拷贝出255字节的数据,这些泄漏出来的数据里可能包含函数指针,导致内存泄漏。
static int
rt_msg2(int type, struct rt_addrinfo *rtinfo, caddr_t cp, struct walkarg *w,
kauth_cred_t* credp){
for (i = 0; i < RTAX_MAX; i++) {
//...
if ((sa = rtinfo->rti_info[i]) == NULL) { <== d:当i遍历到RTAX_BRD时,sa就是ifa->ifa_dstaddr
continue;
}
//...
rtinfo->rti_addrs |= (1 << i);
dlen = sa->sa_len; <== e: 当i遍历到RTAX_BRD时, dlen为用户可控。
rlen = ROUNDUP32(dlen);
if (cp) {
bcopy((caddr_t)sa, cp, (size_t)dlen); <== f: cp最后会被拷贝到用户态
if (dlen != rlen) {
bzero(cp + dlen, rlen - dlen);
}
cp += rlen;
}
len += rlen;
}
//...
}
我们POC运行结果如下。越界读取函数指针后,即可计算kernel slide。

漏洞2 ==>flow_divert_is_sockaddr_valid
上面的信息泄漏不是孤例。很明显,开发者犯了mptcp里同样的错误。我们再把漏洞特征放宽一些,看看其它xnu模块中对sa_len
字段的检查。
很快,在flow_divert_is_sockaddr_valid
函数中,我们看到了下面的代码。
static boolean_t,
flow_divert_is_sockaddr_valid(struct sockaddr *addr)
{
switch (addr->sa_family) {
case AF_INET:
if (addr->sa_len < sizeof(struct sockaddr_in)) { <==应该是!=
return FALSE;
}
break;
#if INET6
case AF_INET6:
if (addr->sa_len < sizeof(struct sockaddr_in6)) {<==应该是!=
return FALSE;
}
break;
#endif /* INET6 */
default:
return FALSE;
}
return TRUE;
}
通过函数名字,不难推测flow_divert_is_sockaddr_valid
就是用来验证sockaddr
是否合法的。flow_divert_is_sockaddr_valid
明确限定了sockaddr
只能是AF_INET
或者AF_INET6
。然而,在长度检查中,flow_divert_is_sockaddr_valid
犯了一个低级错误: flow_divert_is_sockaddr_valid
函数只检查了addr->sa_len
不要小于结构体的实际大小,但是却没考虑到sa_len
可能大于结构体实际大小的情况。
因此,只要传入的sockaddr
类型是AF_INET
或者AF_INET6
,攻击者就可以设置过长的sa_len
,导致flow_divert
后继使用sockaddr
的时候发生内存越界访问。感兴趣的朋友可以尝试一下自行构造POC代码。
0x02 修复
针对第一个泄漏,Apple在最新的iOS 13.6版本中已经修复。在已经开源的xnu-6153.141.1中,我们可以对比发现补丁信息如下。

在上面的代码中,inctl_ifdstaddr
函数在处理SIOCSIFDSTADDR命令时,强制把ia->ia_dstaddr
的family
和sin_len
字段设置为AF_INET
类型。
针对第二个漏洞,Apple在iOS 13.5中已经修复。Apple并没有直接更改函数flow_divert_is_sockaddr_valid
, 而是在调用这个函数外层,增加了长度检查。

0x03 总结
这篇文章里,我们分享了我们如何在Ian Beer公布mptcp漏洞后,分析漏洞成因、总结漏洞特征、到根据漏洞特征挖掘新漏洞的过程。漏洞挖掘很考验研究者“举一反三”的能力。在大量代码中针对性的快速定位疑似漏洞代码会大大提高漏洞挖掘的效率。而从历史漏洞中总结分析,对定位疑似漏洞代码大有毗益。此外,
sockaddr
一个如此简单的数据结构,但在大量的类型转换过程中,一旦类型和长度检查逻辑不完备,就可能导致更严重的安全问题。在我们分享的这两个漏洞之外,我相信也能找到其它相似问题。
Credit:漏洞由盘古实验室迟欣茹、王铁磊发现,提交Apple修复。