對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保護文件系統。
此外還可以使用其它一些方式保護自己的系統。