首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第23期->技术专题
期刊号: 类型: 关键词:
Bind 8.2.x堆栈信息泄漏以及TSIG单字节溢出分析

作者:warning3 < mailto: warning3@nsfocus.com >
主页:http://www.nsfocus.com
日期:2001-07-17

Bind 8.2.x存在两个比较严重的安全问题。一个是堆栈信息泄漏问题,另外一个
是TSIG单字节溢出问题。这两个问题都是相当有意思的,所以我详细地解释一下。

这里的例子是以BIND 8.2.2-P5为蓝本解释的,其他版本是类似的。

一.

首先让我们来一下,BIND处理DNS请求的基本流程:

(1) bin\named\ns_main.c 524行 main() 524行附近:

  <...>
while (!main_needs_exit) {
evEvent event;

ns_debug(ns_log_default, 15, "main loop");
if (needs != 0) {
/* Drain outstanding events; handlers ~block~. */
while (evGetNext(ev, &event, EV_POLL) != -1)
INSIST_ERR(evDispatch(ev, event) != -1);
INSIST_ERR(errno == EINTR || errno == EWOULDBLOCK);
handle_need();
} else if (evGetNext(ev, &event, EV_WAIT) != -1) {
/* evDispatch()函数来处理所有的event */
INSIST_ERR(evDispatch(ev, event) != -1);
} else {
INSIST_ERR(errno == EINTR);
}
}
  <...>
  
  (2) lib\isc\Eventlib.c 446行附近 evDispatch():
  
  int
  evDispatch(evContext opaqueCtx, evEvent opaqueEv) {
evContext_p *ctx = opaqueCtx.opaque;
evEvent_p *ev = opaqueEv.opaque;
<...>
switch (ev->type) {
<...>
[01] case File: {
evFile *this = ev->u.file.this;
int eventmask = ev->u.file.eventmask;

evPrintf(ctx, 5,
"Dispatch.File: fd %d, mask 0x%x, func %#x, uap %#x\n",
this->fd, this->eventmask, this->func, this->uap);
[02] (this->func)(opaqueCtx, this->uap, this->fd, eventmask);
#ifdef EVENTLIB_TIME_CHECKS
func = this->func;
#endif
break;
    }
[03]     case Stream: {
evStream *this = ev->u.stream.this;

evPrintf(ctx, 5,
"Dispatch.Stream: fd %d, func %#x, uap %#x\n",
this->fd, this->func, this->uap);
errno = this->ioErrno;
[04] (this->func)(opaqueCtx, this->uap, this->fd, this->ioDone);
#ifdef EVENTLIB_TIME_CHECKS
func = this->func;
#endif
break;
    }
<...>   
#endif
ctx->cur = NULL;
[05] evDrop(opaqueCtx, opaqueEv);
return (0);
}


[01] 判断是否ev->type为"File"
[02] 如果是"File",则认为是UDP报文,去调用datagram_read()函数
[03] 判断是否ev->type为"Stream"
[04] 如果是"Stream",则认为是TCP报文,去调用stream_getlen()函数
[05] 再调用完相应函数后,就调用evDrop()来丢弃event,然后返回。

(3) bind在处理TCP和UDP报文时都会为输入数据分配一个buffer,将输入数据拷
贝到buffer中,以后所有的操作都在这个buffer中进行。对于UDP报文,分配了
一个513字节的局部buffer: u.buf

bin\named\ns_main.c 949行 datagram_read()附近:

static void
datagram_read(evContext lev, void *uap, int fd, int evmask) {
<...>
union {
HEADER h; /* Force alignment of 'buf'. */
[01] u_char buf[PACKETSZ+1];
} u;
<...>

[01]: PACKETSZ = 512字节,所以"u.buf"大小为513字节

对于TCP报文,stream_getlen()函数动态分配了一个64k的内存区:"sp->s_buf"

bin\named\ns_main.c 837行附近stream_getlen():

static void
stream_getlen(evContext lev, void *uap, int fd, int bytes) {
   <...>
if (!(sp->flags & STREAM_MALLOC)) {
sp->s_bufsize = 64*1024-1; /* maximum tcp message size */
sp->s_buf = (u_char *)memget(sp->s_bufsize);
  <...>
  
(4) 然后执行顺序为dispatch_message()-->ns_req()-->req_query()或者req_iquery()
   等。
   
bind使用了两个关键的变量来跟踪为输入请求开辟的buffer的情况:msglen和buflen。
msglen 代表该buffer的实际长度
buflen 代表该buffer的剩余(未用)长度

当BIND收到一个DNS信息后,msglen被初始化成从网络中接收到的数据长度。buflen
被初始化成用来读取这个消息的缓冲区的大小。(对于UDP报文为512字节,对TCP
报文为64k)。正常情况下,当BIND处理一个请求时,它会将回复记录附加到请求中
。然后它会编辑DNS头,使其反映出这种变化,并发送此响应报文。在此过程中,
msglen也会随之改变。

需要注意的是:
在处理报文过程中,BIND假设msglen加上buflen的大小始终等于缓冲区的原长度!

二. TSIG缓冲区溢出漏洞

这个所谓的缓冲区溢出实际上顶多是溢出一个字节,而且只能用0来覆盖。所以
要进行攻击是比较受局限的。

从BIND 8.2开始,在BIND处理一个DNS请求之前,它会检查DNS信息的附加区域,
检查是否有TSIG资源记录。函数ns_find_tsig()被用来进行这个检查[01]。如果一
个有效的TSIG标记被找到,但相应的安全字(security key)[02]却没有找到,BIND
将会报错[03],并绕过了正常的请求处理过程[04]。结果,msglen和buflen都仍然保持
它们的初始值。

/*
* Process request using database; assemble and send response.
*/
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;
enum req_action action;
msglen_orig = msglen;
siglen = sizeof(sig);
    
    <...>
    
[01] tsigstart = ns_find_tsig(msg, msg + msglen);
if (tsigstart == NULL)
has_tsig = 0;
else {
char buf[MAXDNAME];

has_tsig = 1;
ns_name_ntop(tsigstart, buf, sizeof(buf));
[02] key = find_key(buf, NULL);
if (key == NULL) {
[03] error = ns_r_badkey;
ns_debug(ns_log_default, 1,
"ns_req: TSIG verify failed - unknown key %s",
buf);
}
if (has_tsig && key != NULL) {
     <...>
} else if (has_tsig) {
action = Finish;
in_tsig = memget(sizeof(struct tsig_record));
if (in_tsig == NULL)
ns_panic(ns_log_default, 1, "memget failed");
in_tsig->key = NULL;
in_tsig->siglen = 0;
tsig_size = msg + msglen - tsigstart;
msglen = tsigstart - msg;
}

     <...>
     
[04]   if (error == NOERROR) {
switch (hp->opcode) {
     <...>
     
}
     <...>
    
     if ((hp->tc || error != NOERROR) && has_tsig > 0) {
hp->ancount = htons(0);
hp->nscount = htons(0);
hp->arcount = htons(0);
cp = msg + HFIXEDSZ;
cp += ns_skiprr(cp, msg + msglen, ns_s_qd, ntohs(hp->qdcount));
sig2len = sizeof(sig2);
[05] buflen += (msglen - (cp - msg));
[06] msglen = cp - msg;
[07] n = ns_sign(msg, &msglen, msglen + buflen, error, key,
    sig, siglen, sig2, &sig2len, tsig_time);
if (n != 0) {
INSIST(0);
}
cp = msg + msglen;

}

[05]  这时候buflen并不等于sizeof(buf) - msglen,而是几乎等于sizeof(buf)

BIND将此请求看作时一个错误请求,它使用原来的请求缓冲区,在问题域中
增加一段TSIG信息。这时候,BIND假设请求缓冲区的大小仍然是msglen+buflen
.正常情况下,这是正确的,然而,在这种特殊情况下,msglen+buflen几乎是
实际缓冲区大小的两倍!

这样,当BIND使用ns_sign()函数添加TSIG信息时,它们将被填充在缓冲区之外。
由于有效的安全字没有被发现,ns_sign()将只会增加很少的一些字节,而且字节
的内容也是有限的。[07]

\lib\nameserv\ns_sign.c 第46行附近:

#define BOUNDS_CHECK(ptr, count) \
do { \
[01] if ((ptr) + (count) > eob) { \
errno = EMSGSIZE; \
return(NS_TSIG_ERROR_NO_SPACE); \
} \
} while (0)

<...>
int
ns_sign(u_char *msg, int *msglen, int msgsize, int error, void *k,
const u_char *querysig, int querysiglen, u_char *sig, int *siglen,
time_t in_timesigned)
{
HEADER *hp = (HEADER *)msg;
DST_KEY *key = (DST_KEY *)k;
[02] u_char *cp = msg + *msglen, *eob = msg + msgsize;
u_char *lenp;
u_char *name, *alg;
int n;
time_t timesigned;

dst_init();
if (msg == NULL || msglen == NULL || sig == NULL || siglen == NULL)
return (-1);

/* Name. */
if (key != NULL && error != ns_r_badsig && error != ns_r_badkey)
n = dn_comp(key->dk_key_name, cp, eob - cp, NULL, NULL);
else
n = dn_comp("", cp, eob - cp, NULL, NULL);
if (n < 0)
return (NS_TSIG_ERROR_NO_SPACE);
name = cp;
cp += n;

/* Type, class, ttl, length (not filled in yet). */
[03] BOUNDS_CHECK(cp, INT16SZ + INT16SZ + INT32SZ + INT16SZ);

<...>  
if (error != ns_r_badtime)
[04] PUTSHORT(0, cp); /* Other data length */
else {
PUTSHORT(INT16SZ+INT32SZ, cp); /* Other data length */
BOUNDS_CHECK(cp, INT32SZ+INT16SZ);
PUTSHORT(0, cp); /* Top 16 bits of time */
PUTLONG(timesigned, cp);
}

<...>

[01] BOUNDS_CHECK是一个宏定义,用来检查是否越界,所谓"界",指得是eob
[02] eob 指向msg + msgsize.正常情况下,msgsize = 512 ,然而,如果
     前面的条件满足,这里的msgsize = msglen+buflen几乎等于512的两倍了。
[03] 因此,在进行类似这样的边界检查时,总是不会越界!所以会覆盖到buffer
     以外的地方。

在设置了TSIG标记,但是没有key的情况下,ns_sign()会使用固定大小的数据
来签名。通常大小为28字节。

对于TCP请求,请求缓冲区在heap区中。攻击者可以使用一些固定的值来覆盖
malloc()动态分配时的一些边界字节,这样下一个边界信息就可以从攻击者
控制的缓冲区中读取,这可能导致一个恶意的指针覆盖,攻击者也可能执行
任意代码。这个可能比较复杂,有时间的话我会分析一下。

对于UDP请求,请求缓冲区在堆栈中,所以可以进行单字节溢出攻击。

datagram_read()所保存的栈幀在buf+533字节处。

而我们所能提供的数据,最长也就是
508 + 4 = 512字节

这里的4字节为TSIG资源记录长度。而ns_sign在重新设置签名时,从原来的
TSIG资源记录开始处填充,也就是说,从第509字节开始填充,因此最长可以
508+28=536字节。也就是说,我们最多可以覆盖datagram_read()保存的ebp
,但是问题是,尽管我们能覆盖它,这几个字节的内容可不是我们能控制的。
所以,唯一的方法是只覆盖ebp的最后一个字节,也就是第533字节。

[04] 可以看到最后填充字节是0,因此,我们可以将ebp的最后一个字节清成0

这就意味着,如果原来保存的ebp为0xbffffec0,覆盖后变成0xbffffe00.

如果要刚好覆盖533字节,我们的数据长度就只能是
533 - 28 = 505字节
因此,我们实际构造的UDP报文是这样的:

                 问题记录部分
| DNS首部 | ...shellcode....伪造栈幀....| TSIG记录 |
+---------+-----------------------------+----------+
| 12字节  |     (505 -12 )=493字节     | 4字节    |
            

如果伪造栈幀地址(例如,0xbffffe00)刚好在buf中(如上图所示),我们就可以
在那个地址放一个伪造的栈幀:
| ebp | shellcode_addr | arg1 | arg2 |

这个栈幀要伪造成evDispatch()的栈幀。arg1和arg2的参数也必须是有效的值,
这是因为evDispatch()中,在从datagram_read()返回后,还要调用evDrop()函
数,见一. (1)中[05]:
[05] evDrop(opaqueCtx, opaqueEv);
我们必须保证evDrop能够正常返回,这样再从evDispatch()返回时才能跳到我们
的shellcode地址去执行。

如果我们发现伪造栈幀地址不在buf内,那么我们就不能进行攻击了。如果保存的ebp
最后一个字节比较大,比如大于0x70,那么将其清零也就是往堆栈低址方向移动了0x70
字节,通常这就在我们的buf中了。而过此字节比较小,比如0x09,那么也就是移动了
几个字节,这个地址的内容我们通常是无法控制的。

|   0x201 B |0x13|        0x44 B    |
+-----------+----+------------------+-------------------------------+
|header|....|....| ebp | eip |......| ebp1 | eip1 | arg1 | arg2 |...|    
^                ^                  ^                                          
|_ buf           |_fp               |_fp1                                      
                 datagram_read()    Dispatch()的栈幀                          
                 的栈幀                    

所以,攻击是否能成功,取决于datagram_read()保存的ebp的数值,因此并不是
所有情况下都可以被攻击。另外,由于ns_sign()只能覆盖28字节,如果datagram
_read()的局部变量有变化,使得不可能覆盖saved ebp的最后一个字节时,攻击
也会失效。redhat 7.0使用rpm安装版本就是这样的例子。

另外,在SPARC平台下,只能覆盖ebp的高地址,这块区域通常我们也不能控制,因此
也不能被攻击。

对于单字节溢出的问题,月刊里有过介绍,这里就不多说了,有兴趣的可以去看看。

攻击技术难点:

1. 准确得到保存的ebp的值以及evDispatch()的两个参数值。这个可以利用
   iquery堆栈信息泄漏来完成。
   
2. 将伪造的栈幀正确的放在我们的DNS请求中,使其刚好在修改后的ebp(最后一个
   字节清零)处
   
3. 在DNS请求中放shellcode.由于每个域名段最长是63字节。因此必须在每个记录
  末尾增加个'jmp'指令以跳过无用数据。



三. iquery导致堆栈信息泄漏

bind调用ns_req()来处理收到的报文:

ns_req()函数:
          cp = msg + HFIXEDSZ;
        eom = msg + msglen;
        buflen -= HFIXEDSZ;
           <...>
  switch (hp->opcode) {
           <...>
case ns_o_iquery:
[01] action = req_iquery(hp, &cp, eom, &buflen, msg, from);
break;
   <...>
[02]   n = doaddinfo(hp, cp, buflen);
cp += n;
buflen -= n;
   <...>
[03]             if (sendto(dfd, (char*)msg, cp - msg, 0,
   (struct sockaddr *)&from,
   sizeof(from)) < 0)
   
[01] 首先根据hp->opcode确定是不是iquery请求,如果是,就调用req_iquery()
     进行处理,并返回action.
     cp : 当前指向数据区:msg+12(12字节是DNS头)
     eom: 指向当前buffer中有效数据的末尾
     
[02] 增加附件信息到cp处,调整buflen
[03] 将msg到cp之间的数据作为回复报文发送。

正常情况下,req_iquery()在返回后,cp的值不会超过eom(msg+msglen),然而,
由于req_iquery()在处理时存在一个漏洞,导致cp的值在返回时可以远远大于
eom.这样,在后面发送时,就可能把一些更高地址的堆栈内容也发送回去,包
括环境变量部分的数据。

下面我们再来看看req_iquery()中具体出错的地方:

Ns_req.c 1339行附近:

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->rcode = ns_r_formerr;
return (Finish);
}

/*
* Skip domain name, get class, and type.
*/
[01] if ((n = dn_skipname(*cpp, eom)) < 0) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery packet name problem");
hp->rcode = ns_r_formerr;
return (Finish);
}
*cpp += n;
if (*cpp + 3 * INT16SZ + INT32SZ > eom) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery message too short");
hp->rcode = ns_r_formerr;
return (Finish);
}
GETSHORT(type, *cpp);
GETSHORT(class, *cpp);
*cpp += INT32SZ; /* ttl */
[02] GETSHORT(dlen, *cpp);
[03] *cpp += dlen;
[04] if (*cpp != eom) {
ns_debug(ns_log_default, 1,
"FORMERR IQuery message length off");
hp->rcode = ns_r_formerr;
return (Finish);
}

[01] 跳过前面的域名部分
[02] 取出dlen,这个值是资源记录数据的长度。
[03] 将*cpp加上dlen
[04] 如果*cpp不等于eom(数据末尾),则认为格式错误,返回。

   注意:这里*cpp的值并没有恢复成加dlen以前的状态!
   所以,如果我们将dlen设得足够大,理论上我们就可以让*cpp指向堆栈的高地址
   甚至可以到达环境变量区。
     
所以攻击者所要做的只是:
(1) 构造一个包含一个回答记录的iquery请求
(2) 类型,类,TTL随便设置,将资源数据长度域设置为一个较大的值evil_size

这个漏洞最直接的效果是攻击者可以获取堆栈中环境变量部分的内容,
从而可以了解远程主机的一些系统信息,比如bind是由什么用户启动的,系统类型
等等。另外,有了这个漏洞,可以精确得获得datagram_read(),evDispatch()函
数的栈幀地址,使得TSIG溢出的可行性大大增加了。

<完>

版权所有,未经许可,不得转载