最近glibc有一個棧溢出的漏洞具體情況,漏洞的具體信息可以參考下面鏈接。
CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow
poc在github上:https://github.com/fjserna/CVE-2015-7547
操作系統:ubuntu15.04
glibc版本:glibc-2.2.0
在ubuntu系統下,只需要執行源碼和調試符的命令之后就可以使用gdb對glibc的跟蹤調試,安裝指令如下:
sudo apt-get install libc6-dbg
sudo apt-get source libc6-dev
但是因為系統自帶的glibc是發行版的,所以在編譯的是時候選用了優化參數 -O2
,所以在調試的過程中會出現變量被優化無法讀取以及代碼運行的時候與源碼的行數對不上的情況。
所以需要自己編譯一個可調式并且沒有過度優化的glibc來進行調試。
首先,從glibc的官網下載glibc的源碼。我選擇了2.20的版本。編譯安裝glibc的方法很容易可以在網上找到。需要注意的是在進行configure時需要設置一些特殊的參數。如果需要調試宏可以添加 -gdwarf-2,glibc無法使用-O0編譯,不過-O1也夠用了。
/opt/glibc220/configure --prefix=/usr/local/glibc220/ --enable-debug CFLAGS="-g -O1" CPPFLAGS = "-g -O1"
在configure
執行完成之后只需要簡單執行編譯與安裝就好了。
sudo make
sudo make install
在glibc編譯安裝成功后,系統默認的glibc還是原來的那個。所以需要選擇指定的glibc來編譯POC代碼。
gcc -o client CVE-2015-7547-client.c -Wl,-rpath /usr/local/glibc220
通過ldd指令可以看到,確實使用了剛編的glibc。
這個時候就可以用GDB調試glibc中的函數了。
運行poc的python服務器。修改/etc/resolv.conf
。將域名服務器改為127.0.0.1就好了。不過這樣一來這臺機器訪問網絡就會出問題了。
nameserver 127.0.0.1
使用gdb啟動客戶端直接運行,出現崩潰堆棧。
可以看到棧都被覆蓋為0x42424242,根據google提供的分析,出問題的是send_dg和send_vc函數。分別在send_vc和send_dg上下斷點,重新運行程序,會發現先調用send_dg函數再調用send_vc函數。
可以看出是在send_vc的時候發生了棧溢出。
因為根據google提供的分析可以知道是在讀取socket的時候發生的溢出,可以通過結合源碼調試來分析。剔除不需要看的代碼,核心代碼如下,總共干了四件事。
[1]選擇適當的緩存
[2]讀取dns包的長度
[3]讀取dsn包
[4]判斷是否需要讀取第二個數據包。
#!c
static int
send_vc(res_state statp,
const u_char *buf, int buflen, const u_char *buf2, int buflen2,
u_char **ansp, int *anssizp,
int *terrno, int ns, u_char **anscp, u_char **ansp2, int *anssizp2,
int *resplen2, int *ansp2_malloced)
{
const HEADER *hp = (HEADER *) buf;
const HEADER *hp2 = (HEADER *) buf2;
u_char *ans = *ansp;
int orig_anssizp = *anssizp;
[...] //這段干的事情可以無視。
read_len:
//----------------[2]-------------start----------------
cp = (u_char *)&rlen16;
len = sizeof(rlen16);
while ((n = TEMP_FAILURE_RETRY (read(statp->_vcsock, cp,
(int)len))) > 0) {
cp += n;
if ((len -= n) <= 0)
break;
}
if (n <= 0) {
[...] //出錯處理無視。
}
int rlen = ntohs (rlen16);
//----------------[2]-------------end----------------
//----------------[1]-------------start----------------
int *thisanssizp;
u_char **thisansp;
int *thisresplenp;
if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { //第一次從read_len開始讀取網絡包進入這個分支。
thisanssizp = anssizp; //第一次調用read時可用內存65536
thisansp = anscp ?: ansp; //第一次調用read時使用的緩存anscp
assert (anscp != NULL || ansp2 == NULL);
thisresplenp = &resplen;
} else {
if (*anssizp != MAXPACKET) {
[...] //重現流程中不會進入這塊。
} else {
/* The first reply did not fit into the
user-provided buffer. Maybe the second
answer will. */
*anssizp2 = orig_anssizp; //第二次調用時可用內存長度65536
*ansp2 = *ansp; //第二次調用read時使用的緩存ansp
}
thisanssizp = anssizp2;
thisansp = ansp2;
thisresplenp = resplen2;
}
//----------------[1]-------------end----------------
anhp = (HEADER *) *thisansp;
*thisresplenp = rlen;
if (rlen > *thisanssizp) {
[...] //重現流程中不會進入這塊。
} else
len = rlen;
if (__glibc_unlikely (len < HFIXEDSZ)) {
[...] //重現流程中不會進入這塊。
}
cp = *thisansp; //*ansp;
//---------------[2]--------------------start-----------------
while (len != 0 && (n = read(statp->_vcsock, (char *)cp, (int)len)) > 0){ //溢出點。
cp += n;
len -= n;
}
//---------------[2]--------------------start-----------------
if (__glibc_unlikely (n <= 0)) {
[...] //重現流程中不會進入這塊。
}
if (__glibc_unlikely (truncating)) {
[...] //重現流程中不會進入這塊。
}
/*
* If the calling application has bailed out of
* a previous call and failed to arrange to have
* the circuit closed or the server has got
* itself confused, then drop the packet and
* wait for the correct one.
*/
//---------------[4]--------------------start-----------------
if ((recvresp1 || hp->id != anhp->id) //不進。
&& (recvresp2 || hp2->id != anhp->id)) {
[...] //重現流程中不會進入這塊。
goto read_len;
}
/* Mark which reply we received. */
if (recvresp1 == 0 && hp->id == anhp->id) //第一次運行recvresp1=1 recvresp2=0
recvresp1 = 1;
else
recvresp2 = 1;
/* Repeat waiting if we have a second answer to arrive. */
if ((recvresp1 & recvresp2) == 0) // 調用goto,回到前面。
goto read_len;
//---------------[4]--------------------end-----------------
/*
* All is well, or the error is fatal. Signal that the
* next nameserver ought not be tried.
*/
return resplen;
}
根據源碼分析,從socket讀取網絡包數據的時候是溢出的地方,所以在這里下斷點。
gdb> b res_send.c:853
通過調用棧可以得知,read發生了兩次[4],而且第一次是正確的,在第二次read之后發生了溢出。通過[1]可以得知,在兩次調用read的時候cp指向的內存不同。
第一次調用read
函數時,緩沖區為anscp指向的內存。
第二次調用read
函數時,緩沖區為ansp指向的內存。這里暫時不用考慮二級指針的問題。
可以斷定,ansp指針索引的地址出現了問題。ansp是調用時從參數傳入的。所以需要通過分析send_vc的調用函數。
send_vc的調用函數如下:
#!c
int
__libc_res_nsend(res_state statp, const u_char *buf, int buflen,
const u_char *buf2, int buflen2,
u_char *ans, int anssiz, u_char **ansp, u_char **ansp2,
int *nansp2, int *resplen2, int *ansp2_malloced)
{
[...]
if (__glibc_unlikely (v_circuit)) {
/* Use VC; at most one attempt per server. */
try = statp->retry;
n = send_vc(statp, buf, buflen, buf2, buflen2, //statp狀態,buff,bufflen第一組發送數據,buff,2bufflen2第二組發送數據。
&ans, &anssiz, &terrno, //u_char **ansp, int *anssizp,int *terrno,
ns, ansp, ansp2, nansp2, resplen2, //int ns, u_char **anscp, u_char **ansp2, int *anssizp2,int *resplen2,
ansp2_malloced); //int *ansp2_malloced
if (n < 0)
return (-1);
if (n == 0 && (buf2 == NULL || *resplen2 == 0))
goto next_ns;
} else {
/* Use datagrams. */ //經過send_dg函數調用,ansp指向65536buff,ans指向2048buff。
n = send_dg(statp, buf, buflen, buf2, buflen2,
&ans, &anssiz, &terrno,
ns, &v_circuit, &gotsomewhere, ansp,
ansp2, nansp2, resplen2, ansp2_malloced);
if (n < 0)
return (-1);
if (n == 0 && (buf2 == NULL || *resplen2 == 0))
goto next_ns;
if (v_circuit)
// XXX Check whether both requests failed or Z
// XXX whether one has been answered successfully
goto same_ns;
}
[...]
}
因為在調用send_vc
之前程序先調用了send_dg
,且兩個函數參數基本相同,通過閱讀源碼會發現,send_dg
對參數進行修改及新內存的申請。
#!c
static int
send_dg(res_state statp,
const u_char *buf, int buflen, const u_char *buf2, int buflen2,
u_char **ansp, int *anssizp,
int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp,
u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced)
{
//ans指向大小為2048的緩沖器
//ansp指向ans
//anscp指向ans
const HEADER *hp = (HEADER *) buf;
const HEADER *hp2 = (HEADER *) buf2;
u_char *ans = *ansp;
int orig_anssizp = *anssizp;
struct timespec now, timeout, finish;
struct pollfd pfd[1];
int ptimeout;
struct sockaddr_in6 from;
int resplen = 0;
int n;
[...]
else if (pfd[0].revents & POLLIN) {
int *thisanssizp;
u_char **thisansp;
int *thisresplenp;
if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { //send_dg第一次進入這個分支。
thisanssizp = anssizp;
thisansp = anscp ?: ansp; //thisansp被賦值為anscp
assert (anscp != NULL || ansp2 == NULL);
thisresplenp = &resplen;
} else {
[...] //第一次調用不會進入。
}
if (*thisanssizp < MAXPACKET
/* Yes, we test ANSCP here. If we have two buffers
both will be allocatable. */
&& anscp
#ifdef FIONREAD
&& (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0
|| *thisanssizp < *thisresplenp)
#endif
) {
u_char *newp = malloc (MAXPACKET);
if (newp != NULL) {
*anssizp = MAXPACKET; //anssizp誰為65536
*thisansp = ans = newp; //anscp指向65536的buffer,但是ansp指向仍然指向原來的2048的buffer
if (thisansp == ansp2)
*ansp2_malloced = 1;
}
}
通過調試可以看出,ansp仍然指向大小為2048的緩沖區,而anscp指向了大小為65536的緩沖區。之后這兩個指針又被傳遞給了send_vc。
所以溢出的原因是,*anssizp
因為在之前的send_dg
中被賦值為65536,send_vc
中第二次調用read
函數時,認為ansp指向的緩沖區的大小為*anssizp
即65536,而實際上ansp指向了一塊只有2048大小的緩沖區。所以在從socket讀取大于2048個字節之后產生了棧溢出。
感謝分享:)
CVE-2015-7547 --- glibc getaddrinfo() stack-based buffer overflow
https://sourceware.org/ml/libc-alpha/2016-02/msg00416.html
Linux glibc再曝漏洞:可導致Linux軟件劫持
http://www.freebuf.com/news/96244.html
CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow
https://googleonlinesecurity.blogspot.com/2016/02/cve-2015-7547-glibc-getaddrinfo-stack.html
glibc編譯debug版本
http://blog.csdn.net/jichl/article/details/7951996
glibc的編譯和調試?
http://blog.chinaunix.net/uid-20786208-id-4980168.html
?