前言
最近分析了2个漏洞,分别是FreeBSD 内核中的CVE-2020-7454 ,Netgear路由器中的ZDI-20-709。虽然这2个漏洞所涉及的厂商设备不同,但是对其Root Cause 分析,可以得到这2个漏洞出现的根本原因是他们都使用了一种错误的编程范式—使用多个变量表示同一个状态。本文侧重对漏洞根因的分析,目的是能够举一反三,在别的厂商设备中找到根因相同的漏洞。PS:本文不包含对漏洞利用的详细叙述。
CVE-2020-7454
官方公布的漏洞的原因是libalias库中的函数对UDP包的header访问之前,并没有对其数据长度进行验证,最终导致了一个OOB的越界的读或者写漏洞。
libalias(3) packet handlers do not properly validate the packet length before
accessing the protocol headers. As a result, if a libalias(3) module does
not properly validate the packet length before accessing the protocol header,
it is possible for an out of bound read or write condition to occur.
这个漏洞出现在FreeBSD内核的libalias库中,这个库的主要作用是对IP包进行aliasing和dealiasing,以实现NAT功能,同时libalias库还实现了一些和协议转换相关的功能。具体包含漏洞的源码为
AliasHandleUdpNbtNS(...)
{
/*...省略....*/
/* Calculate data length of UDP packet */
uh = (struct udphdr *)ip_next(pip);
nsh = (NbtNSHeader *)udp_next(uh);
p = (u_char *) (nsh + 1);
pmax = (char *)uh + ntohs(uh->uh_ulen); /* <--- (1) */
/* ... 省略... */
if (ntohs(nsh->ancount) != 0) {
p = AliasHandleResource(
ntohs(nsh->ancount),
(NBTNsResource *) p,
pmax,
&nbtarg
);
}
/* ... 省略... */
}
AliasHandleResource(..., char *pmax, ...)
{
/* ... 省略... */
switch (ntohs(q->type)) {
case RR_TYPE_NB:
q = (NBTNsResource *) AliasHandleResourceNB(
q,
pmax,
nbtarg
);
break;
/* ... 省略... */
}
在注释1的地方,内核直接从payload中读取UDP包的header,获得UDP数据包的length字段。注意这个地方很重要,payload是攻击者可控的,UDP包的长度也是攻击者可以修改的,UDP header中的length字段标识了UDP的负载和头的总长度。
继续分析代码,如果满足了一定的限制条件就可以进入到AliasHandleResource这个子函数的处理分支,进而可以到达AliasHandleResourceNB函数,这个函数是直接触发了OOB漏洞的函数。下面是AliasHandleResourceNB的部分源码
AliasHandleResourceNB(..., char *pmax, ...)
{
/* ... 省略 ... */
while (nb != NULL && bcount != 0) {
if ((char *)(nb + 1) > pmax) { /* <--- (2) */
nb = NULL;
break;
}
if (!bcmp(&nbtarg->oldaddr, &nb->addr, sizeof(struct in_addr))) { /* <--- (3) /
/* ... snip ... */
nb->addr = nbtarg->newaddr; /* <--- (4) */
}
/* ... 省略 ... */
nb = (NBTNsRNB *) ((u_char *) nb + SizeOfNsRNB);
}
}
pmax的值就是从udp header中读取的UDP length与upd头的指针相加得到的偏移,简言之就是假设的udp的尾部,之所以说是假设的,因为有两种方式可以索引这个udp的尾部,第一种就是上述的方法,利用udp头的偏移和udp length之和索引。另一种方式就是通过ip包索引,ip header偏移与ip length之和也是索引的udp的尾部。如果udp包头的header length没有被恶意更改的情况下,这两种方式的索引都是可以的,但是如果udp 包的头部被恶意修改了,那么就会造成两种索引方式的不同步,udp包头的索引方式就是错误的。
而AliasHandleResourceNB这个子函数就是使用了错误的索引方式,以udp header偏移和udp length之和作为udp尾部的索引。标注2处是循环的终止条件,就是一直处理直到达到pmax位置,如果udp length被恶意改成很大的值,那么就会造成pmax位置已经超出了udp包payload,最终造成越界访问。标注3出现了一次越界读取,标注4则出现了越界写。
分析这个漏洞产生的根本原因就是有两种方式可以索引udp的尾部状态,如果对两种索引方式进行混用,那么一旦没有保持两种索引方式的同步,就会导致危险。
笔者分析了这个根因之后,也试图直接再在FreeBSD的源码中搜索采用两种方式索引UDP尾部的代码,期望能够找到类似的漏洞。经过搜索共发现了15个文件中34处出现通过UDP header索引的方式的代码。
经过简单分析,除了已经爆出的alias库中的漏洞,还有udp6_usrreq.c中出现了混用
/*
* Destination port of 0 is illegal, based on RFC768.
*/
if (uh->uh_dport == 0)
goto badunlocked;
plen = ntohs(ip6->ip6_plen) - off + sizeof(*ip6);
ulen = ntohs((u_short)uh->uh_ulen);
nxt = proto;
cscov_partial = (nxt == IPPROTO_UDPLITE) ? 1 : 0;
if (nxt == IPPROTO_UDPLITE) {
/* Zero means checksum over the complete packet. */
if (ulen == 0)
ulen = plen;
if (ulen == plen) ---------------->1
cscov_partial = 0;
if ((ulen < sizeof(struct udphdr)) || (ulen > plen)) {
/* XXX: What is the right UDPLite MIB counter? */
goto badunlocked;
}
if (uh->uh_sum == 0) {
/* XXX: What is the right UDPLite MIB counter? */
goto badunlocked;
}
}
虽然出现了混用,但是可以看到代码在一开始就对这两种索引方式进行了同步,所以这个地方也是不存在漏洞的。所以很遗憾笔者没有再找到别的漏洞,但是ZDI博文中的作者利用这种方法找到了FreeBSD中的另外一处相同原因的漏洞。
AliasHandleCUSeeMeIn(...)
{
/* ... 省略 ... */
end = (char *)ud + ntohs(ud->uh_ulen); /* <--- untrusted UDP header length */
if ((char *)oc <= end) {
/* ... 省略 ... */
if (ntohs(cu->data_type) == 101)
/* Find and change our address */
for (i = 0; (char *)(ci + 1) <= end && i < oc->client_count; i++, ci++)
if (ci->address == (u_int32_t) alias_addr.s_addr) { /* <--- OOBR */
ci->address = (u_int32_t) original_addr.s_addr; /* <--- OOBW */
break;
}
}
}
官方对此漏洞修复方式为:
即增加对UDP包头的验证,确保这两种对UDP尾部的索引方式是相同的。
ZDI-20-709
这个漏洞是一个无需认证的漏洞,发现者在追踪数据流的时候,发现在正常的认证逻辑之前存在一个无需认证的逻辑分支
这个无需认证的分支是一个文件上传的请求分支,这种无需认证的分支是安全审计的重点环节。在这个逻辑中存在一个漏洞,可以导致堆溢出。其原型就是
buffer = malloc(attacker_control_size)
memcpy(buffer, file_content, file_content_size)
这个漏洞根因也是由于有两种方式表示一个上传文件的长度,第一种方式是根据POST请求中文件字段的长度计算出来的,第二种是通过Content-Length字段计算出来的。一般情况下这二者是相同的,但是作者通过在URL中添加Content-Length字段,最终导致了二者的不同步。
当发现malloc的长度变量和memcpy中的长度变量并不是由一个变量而来的,而本来这两个变量应该是同步的,最好是同一个变量。
小结
除了这种常见的多个变量表示同一个长度信息造成的不同步,其他信息的不同步也同样可以导致漏洞,比如一个经典漏洞,OpenSolaris内核中的CVE-2008-568,它是由于设置了两种error状态的返回方式,一种是通过函数返回值返回错误信息,一种是通过全局变量返回错误信息,但是在一些逻辑中会造成二者的不同步,最终导致越界读写等。如果单纯的分析漏洞的表面原因,很难将这些漏洞联系起来,如果将这些漏洞原因深层思考,得到漏洞的Root Cause,就可以抽象出一种经常出现的漏洞原型-多个变量表示同一状态。