对BIND几个缺陷的分析
综述
现在随着Internet的日益普及,而Internet非常依赖于域名服务(DNS)。在RFC845中对域名服务作了如下定义:一个迭代的分布式数据库系统,它为Internet操作提供了基本的信息,例如:域名<-->IP地址的相互转换,邮件处理信息。BIND(Berkeley Inetnet Name Domain,伯克利Internet域名是一种使用最广的域名系统。它有安全缺陷对Internet无疑于是一场灾难。
2001年月29日,Network Associates of California发表了一个报告,指出了BIND最近出现的四个安全缺陷。其中有两个是关于缓冲区溢出,可以使攻击者关闭DNS或者获得root权限,一个叫做"TSIG bug",影响BIND8,另一个是叫“complain bug"的缓冲区溢出缺陷,影响BIND4。其余两个一个叫做"infoleak",影响BIND4和BIND8,另一个叫做"complain bug"格式化字符串缺陷,只影响BIND4。本文将着重讲述infoleak和TSIG bug。其中,infoleak bug不能直接用来进行攻击,但是它可以泄露栈的信息,甚至使攻击者得到BIND运行时的内存布局,为使用TSIG进行攻击创造了便利。这恐怕也是最近两个蠕虫:lion和adore都使用BIND的漏洞进行传播的主要原因之一。
细节
1.infoleak
infoleak bug是由Claudio Musmarra发现的,最早在CERT安全建议CA-2001-02对这个BUG进行了报道。它使攻击者能够直接得到named程序栈祯的信息,从而直接计算出进行单字节缓冲区溢出所需要的信息,大大增加了攻击的成功率。
程序执行时,在栈中保存了程序运行的内部变量和函数的局部变量,以及函数调用的返回地址等信息。infoleak bug可以使攻击者直接读出在栈中的这些信息,甚至程序运行时的内存布局。通过向运行有这个缺陷的BIND版本的DNS服务器发送一个特制的查询包,就可以达成上述目的。
所谓特制的查询包就是向一个合法的很大的IQUERY(反向查询)查询包。向一个运行BIND的DNS服务器发出一个合法的IQUERY请求,DNS服务器把应答记录放在这个查询包之后返回。应答包括一个域名、类型(type)、类别(class)和ttl(包的生存时间)。在构造这个反向查询包时,只要使域名对named程序的dn_skipname()函数是合法的就可以了。把这个反向查询包的数据长度设置为一个和很大的数值,就会是应答记录超出缓冲区的边界。named程序的req_iquery()函数会发现这个反向查询包非法,并且返回一个指示错误的字符串。不幸的是,它在检查是否有错误时,不管反向查询包的数据区有多长,首先把指向包尾的指针cp向后推,这样很可能使cp指针超出了缓冲区的边界。从req_iquery()函数返回后,ns_req()函数就会发出大小是cp-msg(指向缓冲区的头)个字节含有错误信息的应答包。如果这个应答包已经超出了缓冲区的大小,就会包含named程序当前栈祯的信息如ebp等等,然后攻击者就可以使用TSIG安全缺陷进行单字节缓冲区溢出攻击了。
因为相对于TSIG安全缺陷关于infoleak的分析资料较少,所以我将以bind-8.2为例对infoleak进行分析。BIND在查询包小于512个字节时,使用UDP/53端口接受数据(更详细的信息请参考TSIG部分),具体接受数据的函数就是datagram_read(),以下是datagram_read()函数的相关源代码
static void
datagram_read(evContext lev, void *uap, int fd, int evmask) {
interface *ifp = uap;
struct sockaddr_in from;
int from_len = sizeof from;
int n, nudp;
union {
HEADER h; /* Force alignment of @#buf@#. */
u_char buf[PACKETSZ+1];
} u;<--这就是named函数存放小于512个字节的查询包的缓冲区,后面对于查询的处理操作都是针对于这个缓冲区的,也就是说,datagram_read是使用传址方式把查询包传递给以后的处理函数*/
..................
dispatch_message(u.buf, n, PACKETSZ, NULL, from, fd, ifp);
if (++nudp < nudptrans)
goto more;
}
这时,栈的布局如下:
------------------
|参数 |
| |
| |
------------------
| |
| 返回地址 |
------------------
|ebp |
------------------
|各个局部变量 |
-----------------
|u.buff[513] |
-----------------
|u.buff[512] |
-----------------
| ..... |
-----------------
|u.buff[0] |<----缓冲区
-----------------
接着,dispatch_message函数调用ns_req()函数:
void
ns_req(u_char *msg, int msglen, int buflen, struct qstream *qsp,
struct sockaddr_in from, int dfd)
{
HEADER *hp = (HEADER *) msg;
u_char *cp, *eom;/*<---cp指向请求包的数据区*/
/*cp = msg + HFIXEDSZ*/
/*eom指向请求包的尾*/
/*eom = msg + msglen*/
................
if (error == NOERROR) {
switch (hp->opcode) {
case ns_o_query:
action = req_query(hp, &cp, eom, qsp,
&buflen, &msglen,
msg, dfd, from, in_tsig);
break;
case ns_o_iquery:
action = req_iquery(hp, &cp, eom, &buflen, msg, from);
break;
/*反向请求包由req_iquery函数处理*/
此时,栈如图所示:
------------------
|参数 |
| |
| |
------------------
| |
| 返回地址 |
------------------
|ebp |
------------------
|各个局部变量 |
-----------------<----eom
|u.buff[513] |
-----------------
|u.buff[512] |
-----------------
| ..... |
-----------------
|u.buff[12] |
-----------------<----cp
| ..... |
-----------------
|u.buff[0] |
-----------------<---msg
下面是req_iquery()函数:
static enum req_action
req_iquery(HEADER *hp, u_char **cpp, u_char *eom, int *buflenp,
u_char *msg, struct sockaddr_in from)
{
int dlen, alen, n, type, class, count;
char dnbuf[MAXDNAME], anbuf[PACKETSZ], *data, *fname;
nameserIncr(from.sin_addr, nssRcvdIQ);
if (ntohs(hp->ancount) != 1
@# @#ntohs(hp->qdcount) != 0
@# @#ntohs(hp->nscount) != 0
@# @#ntohs(hp->arcount) != 0) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery header counts wrong");
hp->qdcount = htons(0);
hp->ancount = htons(0);
hp->nscount = htons(0);
hp->arcount = htons(0);
hp->rcode = FORMERR;
return (Finish);
}/*构造包时,使其能够通过这些检查*/
/*
* Skip domain name, get class, and type.
*/
if ((n = dn_skipname(*cpp, eom)) < 0) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery packet name problem");
hp->rcode = FORMERR;
return (Finish);
}
/*dn_skipname函数接着调用ns_name_skip函数*/
*cpp += n;
/*使攻击程序构造的包数据区很大*/
假设这时,栈如图所示:
------------------
|参数 |
| |
| |
------------------
| |
| 返回地址 |
------------------
|ebp |
------------------
|各个局部变量 |
-----------------<----eom
|u.buff[512] |
-----------------
|u.buff[511] |
-----------------
| ..... |
-----------------
|u.buff[446] |
-----------------<----cp
| ... |
-----------------
|u.buff[12] |
-----------------
| ..... |
-----------------
|u.buff[0] |
-----------------<---msg
/*但是符合*cpp+3*INT16SZ+INT32SZ<=eom*/
if (*cpp + 3 * INT16SZ + INT32SZ > eom) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery message too short");
hp->rcode = FORMERR;
return (Finish);
}
/*named处理type,class*/
GETSHORT(type, *cpp);
GETSHORT(class, *cpp);
*cpp += INT32SZ; /* ttl */
GETSHORT(dlen, *cpp);
/*cpp已经接近缓冲区的边界了*/
此时,栈如图所示:
------------------
|参数 |
| |
| |
------------------
| |
| 返回地址 |
------------------
|ebp |
------------------
|各个局部变量 |
-----------------<----eom
|u.buff[512] |
-----------------
|u.buff[511] |
-----------------
| .... |
-----------------
|u.buff[458]=255|
-----------------<---cp
|u.buff[457]=0 |
-----------------
|u.buff[456]=255|<--假设dlen为255
-----------------
| ..... |
-----------------
|u.buff[446] |
-----------------
| ... |
-----------------
|u.buff[12] |
-----------------
| ..... |
-----------------
|u.buff[0] |
-----------------<---msg
*cpp += dlen;
/*攻击程序发出的反向查询包的dlen为一个很大的值*/
/*此时,再向后推dlen个字节*/
/*哈,越界了*/
if (*cpp != eom) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery message length off");
hp->rcode = FORMERR;
return (Finish);
}
接下来,就是由ns_req()将cp-msg个字节发送给攻击程序。攻击者就可以得到named栈的信息,为下一步的单字节缓冲区攻击作好准备。
2.TSIG bug
DNS域名服务器使用TISG(tranaction signature)来进行验证通讯。TSIG BUG因此而得名。在最近出现的四个BIND BUG中,TSIG BUG危害是最为严重的。一个TSIG是一个高层的DNS资源记录,在请求或者应答中是分别计算的,用完后丢弃,不能重复使用,也不应该保存在高速缓存中。TSIG是一个复杂的安全机制。它必须在消息的最后。如果在资源记录(RR)中有几个TSIG,或者位置不正确,BIND就会丢弃这个包并且送回一个错误信息。TSIG有几个验证机制,阻止了攻击者从网络上截取含有TSIG的包使用。
TSIG BUG影响的BIND版本有:8.2(any service pack),8.2.1,8.2.2(packs1-7),和所有的8.2.3beta版本。
当BIND接到一个请求,它会根据接受请求使用的传输机制,把请求放在栈或者堆中。如果DNS请求小于512个字节,它就使用UDP/53接受请求的数据,并将其放在栈区中;如果DNS请求大于512个字节,它就使用TCP/53接受请求的数据,并把请求数据放在堆中。
当请求小于或者等于512个字节时,由datagram_read()函数把请求放到栈中的一个513个字节缓冲区中,即u.buff;当收到一个TCP请求时,就由stream_getlen()函数把请求数据读到一个64K的缓冲区中,这个缓冲区叫做sp->s_buff,是在堆中为每个套接字分配的。其中,有一个很有意思的特征,无论是使用TCP传输协议还是UDP传输协议,BIND都是只在缓冲区中对数据进行操作,然后做出相应的响应。
BIND使用两个变量来跟踪缓冲区的使用情况:msglen保存缓冲区中现有数据的字节数;bufflen保存缓冲区没有使用的字节数。
当接到一个DNS消息,msglen被初始化为从网络上接到的数据的长度。对于UDP来说,就是由recvfrom()系统调用返回的数;而TCP消息的msglen是由客户端给出的。buflen被设置为读取消息的缓冲区的大小,UDP是512,TCP是64K。
通常情况下,在处理一个DNS查询时,BIND回在查询的后面加上应答、验证以及其它的记录信息。接着,BIND就会修改这个DNS查询的头来显示上面所做的修改,将其送出。在处理过程中,msglen将会反映构造应答信息的缓冲区使用情况,而buflen将跟踪缓冲区空闲区域的情况。在整个处理过程中,BIND都假设buflen+msglen等于缓冲区的长度。
根据消息头的设置,BIND会区分请求还是应答,分别对其进行处理。如果接到一个请求,它就区分这个请求是查询(query)、反向查询(iquery)、update还是notification。从BIND8.2开始,在处理请求数据之前,BIND首先要检验有没有TSIG,这个功能是由ns_find_tsig()来完成的,同时这个函数还会对TSIG的合法性进行基本的验证。如果有一个正确的TSIG而没有准确的security key,BIND就发出一个错误信号,并跳过正常的处理操作。此时,msglen和buflen也保留为其初始值,而不是被设置为其工作值。
因为有一个正确的TSIG但没有准确的security key,BIND就进行错误处理。在产生错误信息时,BIND会重新起用缓冲区并且在这个有问题请求之后加上一个TSIG。此时,BIND假定msglen+buflen等于缓冲区的大小,当然在通常情况下,这时对的,但是只是在通常情况下-:(,在一些特殊的情况下msglen+buflen几乎可以达到缓冲区大小的两倍。接下来,BIND就调用ns_sign()函数在查询之后加上一个TSIG,可能已经超出了缓冲区的范围。
下面是BIND处理请求的大体过程的示意图:(本来是应该使用HTML格式的,但----是我一直对制作页面缺乏兴趣,所以只要如此了,不过我会抓紧学一学的-:) )
----------------
|收到DNS请求 |
----------------
| |
| |
/ /
-------------- ---------------
|UDP请求由 | |TCP请求由 |
|datagram_read| |stream_getlen |
|放到 | |放到 |
-------------- ---------------
| |
| |
/ /
------------ -----------------
|栈 | |堆heap |
|u.buff[513]| | sp->s_buff |
| | | |
------------ ------------------
跟踪变量:msglen---数据长度
buflen---未使用缓冲区的长度
图2.1
|----------------------------------------------------------------|
| 接到一个DNS请求 |
| msglen设置为从网络上接受的数据的长度 |buflen被初始化为缓冲区的长度|
| UDP:recvfrom() |UDP:513 bytes |
| TCP:由客户端给出 |TCP:64K bytes |
|-----------------------------------------------------------------|
判断请求是:
query
iquery
update
notification
检查是否有TSIG
并检验其合法性
----------------------------------------------------------------------
通常情况下, 异常情况下,
TSIG和security key合法 正确的TSIG,而security key非法
处理请求 发出错误信号:
BIND跳过正常对请求的处理
进行错误处理
在查询信息之后 msglen和buflen保留为其原来的值
加上验证及其它
记录
修改查询包的头 重用缓冲区产生错误信息
msglen等于数据的长度 假设nsglen+buflen等于原来缓冲区的长度
buflen空闲缓冲区的长度
假设原来缓冲区的长度等于
msglen+buflen
发出应答 加上TSIG,溢出
------------------------------------------------------------------
例如,A公司的DNS使用的是BIND8.2.2-Patch5。一名攻击者通过端口扫描知道了相关的信息,接着攻击者向这台域名服务器发出了一个请求,而这个请求包有一个TSIG而security key非法。A公司的DNS收到这个请求进行处理,它发现一个TSIG但是没有合法的security key就发出了错误信号,而此时msglen和buflen都被锁定,不能在处理错误时准确反映缓冲区的情况。datagram_read()或者stream_getlen()函数返回之前,BIND在请求包之后加上了新的TSIG,就超出了缓冲区。
总结
针对这个BUG可以使用单字节缓冲区溢出和堆溢出exploit。由于这两个BUG和系统设置无关,所以应该赶快升级BIND系统。有一些途径可以用来对named进行保护:
1).不以root运行named;
2).使用chroot保护文件系统。
此外还可以使用其它一些方式保护自己的系统。