您的位置:首頁>正文

「漏洞分析」CVE

Larryxi@360GearTeam

0x00 問題背景

谷歌安全團隊對dnsmasq進行了測試並發現了多個漏洞, 其中的CVE-2017-14491是一個堆溢出漏洞, 存在RCE的風險。 不過其在相關博客中只給出了PoC腳本,

測試步驟和相關的報錯asan, 需要我們自己分析過程中的調用流程, 進一步有可能開發出RCE的exp腳本。

攻擊流程可分為三步:

1.攻擊者偽造成為dnsmasq的上游DNS伺服器, 即運行PoC腳本。

2.攻擊者在用戶端向dnsmasq發送PTR請求, dnsmasq不存在相應PTR記錄便向上游DNS查詢, 然後獲得PTR的查詢結果緩存並應答用戶端。

3.攻擊者再次在用戶端向dnsmasq發送PTR請求, dnsmasq便解析展開之前的PTR記錄, 由於資料包的構建都在堆上, 而且上游的惡意的PTR相應記錄的大小超過了堆上分配的記憶體空間, 最後造成了堆溢出。

攻擊面初步猜想就是攻擊者當控制上游DNS伺服器後, 通過配置特定的PTR回應和用戶端的反向查詢, 即可實現對dnsmasq主機的遠端代碼執行。

0x01 調試環境

作業系統:Ubuntu 14.04 x86_64

軟體版本:dnsmasq v2.75

PoC腳本:https://github.com/google/security-research-pocs

Debugger:peda-gdb

另外需要說明的是:

1.關於存在漏洞的軟體版本, 根據穀歌的博客公告, dnsmaqs全部版本都存在此CVE堆溢出漏洞, 並且早於2.76和用於此commit的版本堆溢出都沒有限制, 否則只能溢出兩個位元組。

2.軟體可直接下載tar壓縮包或clone下來checkout相應版本, 並在Makefile中加入-g選項編譯安裝,

方便後續調試。

3.

sudo gdb dnsmasq

啟動dnsmasq後使用

set args -p 53535 --no-daemon --log-queries -S 127.0.0.2 --no-hosts --no-resolv

設置啟動參數進行調試。

4.dns請求默認是有重傳機制, 而且在調試過程中會中斷程式, 重傳會導致程式重複執行某些代碼影響調試, 所以在dig時可指定不進行重傳:

0x02 流程追蹤

資料包的堆分配

穀歌給出的asan是基於2.78test2版本的dnsmasq, 其中堆的分配是在dnsmasq.c中的safe_malloc函數:

調試的2.75版本也同樣存在safe_malloc, 只不過其內部使用的是malloc分配堆:

其中的daemon是全域可以訪問的結構體, 而daemon->packet主要是用於存儲資料包內容的記憶體空間, 通過safe_malloc會為其在堆上分配空間, 為後續的資料包構建做準備。 在dnsmasq.c:96處下中斷點, 可以看到daemon->packet_buff_sz為0x1000大小:

malloc之後分配的堆空間起始位址為0x648f00:

接下來運行PoC看看程式崩潰時的環境:

其中有以下三點要注意:

1.PoC在執行第一次PTR查詢時, 程式沒有產生崩潰, 而是在在第二次崩潰。

2.在bt的輸出中, #1的answer_request函數的參數中header為0x648f00是最開始為資料包分配的堆位址, 同時limit為0x64900, 兩者相差正好0x1000, 可能是限制堆溢出的操作。

3.那麼問題來了:a.為什麼第一次不會崩潰;b.為什麼看似有限制但還是堆溢出了。

第一次查詢

通過下中斷點得知第一次PTR反向查詢過程中,首先也會調用dnsmasq.c:1004處的check_dns_listeners函數,然後將listener傳入dnsmasq.c:1515處的reccive_query函數,在其定義處,可以看到區域變數header指標指向的就是構建資料包的那塊堆的起始位址:

在後續的操作中,首先會在forward.c:1178處會接收udp請求,將請求資料包的內容存儲在堆中:

繼續跟進在forward.c:1398~1415行中是先本地查詢,如果沒有結果向上游DNS伺服器查詢:

所以在第一次查詢中會進入dnamasq.c:1409的forward_query函數,在其內部對sendto函數或發送完資料包的522行處下中斷點,即可看到其在堆上構建的向上游伺服器查詢的資料包:

同樣的思路,在dnsmasq發送完向上游DNS的PTR請求後,肯定要接收回應資料,所以對recvfrom函數下中斷點,即可知道其在dnsmasq.c:1510中會調用reply_query函數,在其內部首先會接收上游伺服器的回應,資料包的存儲也還是用的daemon->packet,但是在這裡也使用recvfrom函數的參數來確定了接收資料包的長度和堆分配的長度一致,所以在存儲時沒有產生溢出:

緊接著reply_query函數會對資料包頭部進行整理,然後把得到的回應資料包通過send_from函數傳給用戶端,而且值得注意的是原始PoC中構造的DNS回應資料包的大小本身也是沒有超過daemon->packet_buff_sz,即分配的堆空間大小:

第二次查詢

第二次查詢的前半部分和第一次查詢類似,也是由check_dns_listeners進入receive_query函數,在dnsmasq接到用戶端的第二次PTR請求後,還是會進過先調用answer_request函數然後經過forward_query函數對用戶端回應。實際上和開頭提到的一樣,查詢在進入answer_request函數就崩潰了,崩潰附近的原始程式碼如下:

當產生崩潰時,查看bt full得知anscount的值為0x51,即迴圈了81次後,再次調用cache_find_by_addr造成非法的記憶體引用產生崩潰。這裡的原始程式碼邏輯就是通過迴圈,調用cache_find_by_addr將cache保存至crecp指標中,並通過cache_get_name獲取ptr記錄的name,再調用add_resource_record添加記錄,經過81此後在add_resource_record中產生堆溢出。

記錄裡的record cache應該是通過第一次查詢的結果向記憶體中保存了相關的資料結構,觀察cache.c檔後對cache_insert函數下中斷點,可得知第一次查詢後的函數堆疊:

對應的原始程式碼則是在第一次查詢中,在把回應發送給用戶端之前,調用process_reply函數,再其內部調用extract_addresses函數,通過遍歷迴圈回應中的ancount,把記錄中的name等資訊cache_insert至crec結構體構成的雙向鏈表中:

第一個cache_insert函數執行後,可知其crec位址為0x64a2d0,並且後續的crec結構體中都把PoC中向前引用的name給完全解析擴展開,這樣就一下增大了回應資料包的大小,造成後續的堆溢出:

溢出原因

具體跟進add_resource_record函數,可以定位到rfc1035.c:1440行的do_rfc1035_name函數,該函數類似於一個copy操作,就是把解析的功能變數名稱放入回應資料包的RDATA欄位,由於解析功能變數名稱後的資料包就擴展的很大,超出了分配的堆空間,所以造成了溢出:

再次下中斷點b rfc1035.c:1855 if anscount == 0x51 ,查看在即將產生崩潰時的上下文環境。由於PoC的資料包在擴展解析後直接溢出到了接近0x64a300的位置,而在cache_find_by_addr函數的內部會訪問到第一個crec結構體的位址0x64a2d0,由於該位址被Z字元溢出,所以最終造成了非法位址的引用:

但有趣的是,在add_resource_record函數的末尾是有對溢出的檢查:

不過溢出的與否只是影響了返回值,而且在返回後影響的只是anscount變數是否加1,並未其他的安全處理,所以這裡的安全檢查就形同虛設了。

分析補充

經過胡牛的提示,其實上圖中對於溢出的檢測也是有點作用的,關鍵點是1476行的*pp = p 會把寫入資料包的指標向後移動,以便後續的answer在資料包的寫入,但是一旦發生溢出這個指標就不會移動,我們就只能從同一個位置開始反復地進行越界寫,所以越界寫的機會只有一次。

在2.76版本中,daemon->packet_buff_sz為5131位元組,limit最大為4096位元組,當我們溢出後繼續寫入一個answer,4096+12(answer頭部大小)+1024(最大功能變數名稱長度)-5131+1(最後的補0位元組)=2,所以穀歌官方所說的越界2個位元組是這麼來的。

0x03 補丁分析

為了省事,這裡就以最新版本的dnsmasq補丁看一下修復的原理,首先是定義了CHECK_LIMIT函數,如果指標和要寫入的size超過了限制就直接跳轉:

跳轉之後直接返回,也就不能執行寫入操作了:

0x04 總結

1.開始時使用PoC測試2.78test2版本沒有產生崩潰,相關原因還有待測試探究。

2.調試的過程主要是關注函數的調用棧,在程式中下好中斷點,同時結合源碼分析程式碼的邏輯,積極思考探討找到問題所在。

3.該漏洞還需要根據堆溢出的環境來構建RCE的exp,但堆上臨近的區域都是大型的結構體無法找相關函數指標覆蓋,可能的思路是如果再次反向查詢一個ptr,使伺服器再次記錄就有可能引起cache_unlink操作,感興趣的同學可以探究一下。

4.關於此漏洞的防禦,可以直接使用yum或者apt-get進行安全更新,也可以去官網下載最新版本的dnsmasq構建安裝。

0x05 相關參考

https://security.googleblog.com/2017/10/behind-masq-yet-more-dns-and-dhcp.html

http://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=summary

https://yi-love.github.io/blog/node.js/javascript/dns/2016/11/11/dns-request.html

https://tools.ietf.org/html/rfc1035

http://bobao.360.cn/learning/detail/4515.html

可能是限制堆溢出的操作。

3.那麼問題來了:a.為什麼第一次不會崩潰;b.為什麼看似有限制但還是堆溢出了。

第一次查詢

通過下中斷點得知第一次PTR反向查詢過程中,首先也會調用dnsmasq.c:1004處的check_dns_listeners函數,然後將listener傳入dnsmasq.c:1515處的reccive_query函數,在其定義處,可以看到區域變數header指標指向的就是構建資料包的那塊堆的起始位址:

在後續的操作中,首先會在forward.c:1178處會接收udp請求,將請求資料包的內容存儲在堆中:

繼續跟進在forward.c:1398~1415行中是先本地查詢,如果沒有結果向上游DNS伺服器查詢:

所以在第一次查詢中會進入dnamasq.c:1409的forward_query函數,在其內部對sendto函數或發送完資料包的522行處下中斷點,即可看到其在堆上構建的向上游伺服器查詢的資料包:

同樣的思路,在dnsmasq發送完向上游DNS的PTR請求後,肯定要接收回應資料,所以對recvfrom函數下中斷點,即可知道其在dnsmasq.c:1510中會調用reply_query函數,在其內部首先會接收上游伺服器的回應,資料包的存儲也還是用的daemon->packet,但是在這裡也使用recvfrom函數的參數來確定了接收資料包的長度和堆分配的長度一致,所以在存儲時沒有產生溢出:

緊接著reply_query函數會對資料包頭部進行整理,然後把得到的回應資料包通過send_from函數傳給用戶端,而且值得注意的是原始PoC中構造的DNS回應資料包的大小本身也是沒有超過daemon->packet_buff_sz,即分配的堆空間大小:

第二次查詢

第二次查詢的前半部分和第一次查詢類似,也是由check_dns_listeners進入receive_query函數,在dnsmasq接到用戶端的第二次PTR請求後,還是會進過先調用answer_request函數然後經過forward_query函數對用戶端回應。實際上和開頭提到的一樣,查詢在進入answer_request函數就崩潰了,崩潰附近的原始程式碼如下:

當產生崩潰時,查看bt full得知anscount的值為0x51,即迴圈了81次後,再次調用cache_find_by_addr造成非法的記憶體引用產生崩潰。這裡的原始程式碼邏輯就是通過迴圈,調用cache_find_by_addr將cache保存至crecp指標中,並通過cache_get_name獲取ptr記錄的name,再調用add_resource_record添加記錄,經過81此後在add_resource_record中產生堆溢出。

記錄裡的record cache應該是通過第一次查詢的結果向記憶體中保存了相關的資料結構,觀察cache.c檔後對cache_insert函數下中斷點,可得知第一次查詢後的函數堆疊:

對應的原始程式碼則是在第一次查詢中,在把回應發送給用戶端之前,調用process_reply函數,再其內部調用extract_addresses函數,通過遍歷迴圈回應中的ancount,把記錄中的name等資訊cache_insert至crec結構體構成的雙向鏈表中:

第一個cache_insert函數執行後,可知其crec位址為0x64a2d0,並且後續的crec結構體中都把PoC中向前引用的name給完全解析擴展開,這樣就一下增大了回應資料包的大小,造成後續的堆溢出:

溢出原因

具體跟進add_resource_record函數,可以定位到rfc1035.c:1440行的do_rfc1035_name函數,該函數類似於一個copy操作,就是把解析的功能變數名稱放入回應資料包的RDATA欄位,由於解析功能變數名稱後的資料包就擴展的很大,超出了分配的堆空間,所以造成了溢出:

再次下中斷點b rfc1035.c:1855 if anscount == 0x51 ,查看在即將產生崩潰時的上下文環境。由於PoC的資料包在擴展解析後直接溢出到了接近0x64a300的位置,而在cache_find_by_addr函數的內部會訪問到第一個crec結構體的位址0x64a2d0,由於該位址被Z字元溢出,所以最終造成了非法位址的引用:

但有趣的是,在add_resource_record函數的末尾是有對溢出的檢查:

不過溢出的與否只是影響了返回值,而且在返回後影響的只是anscount變數是否加1,並未其他的安全處理,所以這裡的安全檢查就形同虛設了。

分析補充

經過胡牛的提示,其實上圖中對於溢出的檢測也是有點作用的,關鍵點是1476行的*pp = p 會把寫入資料包的指標向後移動,以便後續的answer在資料包的寫入,但是一旦發生溢出這個指標就不會移動,我們就只能從同一個位置開始反復地進行越界寫,所以越界寫的機會只有一次。

在2.76版本中,daemon->packet_buff_sz為5131位元組,limit最大為4096位元組,當我們溢出後繼續寫入一個answer,4096+12(answer頭部大小)+1024(最大功能變數名稱長度)-5131+1(最後的補0位元組)=2,所以穀歌官方所說的越界2個位元組是這麼來的。

0x03 補丁分析

為了省事,這裡就以最新版本的dnsmasq補丁看一下修復的原理,首先是定義了CHECK_LIMIT函數,如果指標和要寫入的size超過了限制就直接跳轉:

跳轉之後直接返回,也就不能執行寫入操作了:

0x04 總結

1.開始時使用PoC測試2.78test2版本沒有產生崩潰,相關原因還有待測試探究。

2.調試的過程主要是關注函數的調用棧,在程式中下好中斷點,同時結合源碼分析程式碼的邏輯,積極思考探討找到問題所在。

3.該漏洞還需要根據堆溢出的環境來構建RCE的exp,但堆上臨近的區域都是大型的結構體無法找相關函數指標覆蓋,可能的思路是如果再次反向查詢一個ptr,使伺服器再次記錄就有可能引起cache_unlink操作,感興趣的同學可以探究一下。

4.關於此漏洞的防禦,可以直接使用yum或者apt-get進行安全更新,也可以去官網下載最新版本的dnsmasq構建安裝。

0x05 相關參考

https://security.googleblog.com/2017/10/behind-masq-yet-more-dns-and-dhcp.html

http://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=summary

https://yi-love.github.io/blog/node.js/javascript/dns/2016/11/11/dns-request.html

https://tools.ietf.org/html/rfc1035

http://bobao.360.cn/learning/detail/4515.html

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示