CVE-2015-7547簡單分析與除錯
0x00 漏洞資訊
最近glibc有一個棧溢位的漏洞具體情況,漏洞的具體資訊可以參考下面連結。
CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow
poc在github上:https://github.com/fjserna/CVE-2015-7547
0x01 環境準備
作業系統:ubuntu15.04
glibc版本:glibc-2.2.0
1.1 glibc原始碼編譯
在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
1.2 使用除錯版本glibc編譯POC
在glibc編譯安裝成功後,系統預設的glibc還是原來的那個。所以需要選擇指定的glibc來編譯POC程式碼。
gcc -o client CVE-2015-7547-client.c -Wl,-rpath /usr/local/glibc220
透過ldd指令可以看到,確實使用了剛編的glibc。
這個時候就可以用GDB除錯glibc中的函式了。
1.3 配置本地dns伺服器
執行poc的python伺服器。修改/etc/resolv.conf
。將域名伺服器改為127.0.0.1就好了。不過這樣一來這臺機器訪問網路就會出問題了。
nameserver 127.0.0.1
0x02 漏洞分析
2.1 執行POC
使用gdb啟動客戶端直接執行,出現崩潰堆疊。
2.2 尋找溢位函式
可以看到棧都被覆蓋為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的呼叫函式。
2.3 記憶體分配錯誤
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。
2.4 溢位原因
所以溢位的原因是,*anssizp
因為在之前的send_dg
中被賦值為65536,send_vc
中第二次呼叫read
函式時,認為ansp指向的緩衝區的大小為*anssizp
即65536,而實際上ansp指向了一塊只有2048大小的緩衝區。所以在從socket讀取大於2048個位元組之後產生了棧溢位。
0x03 參考&感謝
感謝分享:)
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
相關文章
- Kdevelop的簡單使用和簡單除錯2019-01-26dev除錯
- 簡單就是易於除錯2019-12-19除錯
- Node.js 簡單除錯2019-12-07Node.js除錯
- 簡單的沙箱反除錯2021-04-19除錯
- Google Chrome 除錯JS簡單教程2019-03-20GoChrome除錯JS
- Laravel 一個簡單的除錯工具2018-11-05Laravel除錯
- VS斷點除錯簡單筆記2020-12-21斷點除錯筆記
- Linux—gdb除錯簡單實現2020-12-24Linux除錯
- 除錯篇——斷點與單步2022-03-03除錯斷點
- 原來 Java 遠端除錯如此簡單2020-09-30Java除錯
- javascript除錯效能的兩種簡單方式2017-03-29JavaScript除錯
- 如何除錯javascript程式碼簡單介紹2017-04-14除錯JavaScript
- 頁面除錯神器Reveal的簡單使用2017-12-17除錯
- NASM 與 GDB 簡易除錯指南2024-08-27ASM除錯
- 除錯篇——除錯物件與除錯事件2022-03-02除錯物件事件
- redux簡單實現與分析2018-04-12Redux
- 逮蝦戶!Android程式除錯竟簡單如斯2018-12-07Android除錯
- 簡單實用的js除錯logger元件2010-11-23JS除錯元件
- Sentry 官方 JavaScript SDK 簡介與除錯指南2021-11-17JavaScript除錯
- rxjs Observable of 操作符的單步除錯分析2022-05-24JS除錯
- C++簡單日誌/debug除錯資訊輸出2024-09-04C++除錯
- 如何分析 SAP Spartacus 路由問題之 CheckoutAuthGuard 單步除錯2021-06-16路由除錯
- NgRx Store createSelector 的單步除錯和原始碼分析2021-09-25除錯原始碼
- gdb除錯命令小結_與多檔案除錯_遠端除錯2013-10-11除錯
- 除錯核對清單2015-03-27除錯
- eclipse單點除錯2005-05-26Eclipse除錯
- [譯] 使用 VS Code 除錯 Node.js 的超簡單方法2019-05-05除錯Node.js
- 除錯Kubernetes工作負載的最簡單方法 - Martin2021-05-28除錯負載
- 九個Console命令,讓js除錯更簡單2016-09-06JS除錯
- 日誌與除錯2017-03-21除錯
- 測試與除錯2015-06-20除錯
- Linux SNAT/DNAT簡單理解與案例分析。2018-07-25Linux
- JAVA反序列化漏洞完整過程分析與除錯2020-08-19Java除錯
- 使用GDB與QEMU除錯核心時的問題分析(轉)2007-08-16除錯
- ExplosionField簡單分析2017-03-05
- 使用谷歌瀏覽器進行斷點除錯簡單介紹2017-04-08谷歌瀏覽器斷點除錯
- 9 個讓 JavaScript 除錯更簡單的 Console 命令2016-08-11JavaScript除錯
- python如何單步除錯2021-09-11Python除錯