1. 程式人生 > >阿里DNS:用LibFuzzer照亮DNS程式碼的死角

阿里DNS:用LibFuzzer照亮DNS程式碼的死角

1 引言

2018年11月初,國內某雲解析服務提供商出現大規模服務不可用故障,在業界引起了不小的震動,以下是官方的故障覆盤公告:

cc767bdc04c1e1ad5c2533e422bdce8c3a1e6bdf

技術覆盤中很明確地說明了此次故障的起因:大量惡意請求報文攻擊觸發了軟體的bug或漏洞,導致解析服務不可用。

作為域名解析保有量亞洲第一、全球第二的權威域名託管服務商,【阿里DNS團隊】打造的阿里云云解析產品和服務(https://help.aliyun.com/product/29697.html),一直致力於通過技術的力量讓整個解析服務更加穩定、更加安全和更加快速,在使用者服務體驗上持續改進。

【阿里DNS團隊】在很久之前就已經認識到惡意報文防禦在整個DNS系統安全穩定保障工作中的重要性,也花了非常大的精力在這個方面做了很多的研究和實踐,下面就簡要介紹一下我們在DNS程式碼白盒Fuzzing測試方面的一些工作。

2 技術方案選型

惡意報文攻擊的核心在於攻擊者利用了軟體處理資料報文的漏洞或者bug,導致軟體執行異常或者異常退出,進而導致服務不可用。惡意報文攻擊防禦要解決的核心問題是儘可能早和儘可能全的發現軟體中存在的漏洞,因此需要系統的方法和工具來解決這個問題。

而軟體漏洞挖掘屬於軟體安全或者程式碼安全的範疇,而阿里巴巴集團安全團隊的同學在軟體漏洞挖掘領域有非常成熟的可工程落地的經驗,因此我們可以利用這些已有的工具和方法幫助我們高效地挖掘DNS軟體的漏洞。經過分析和比對,我們最終選定的方案是通過Libfuzzer對DNS軟體進行白盒Fuzzing測試。

那麼我們為什麼使用LibFuzzer呢?

LibFuzzer是一種支援持續執行,基於程式碼覆蓋率的引導式模糊測試引擎。LibFuzzer與被測試的程式碼連結,然後通過特定的模糊入口(也稱為“目標函式”)將模糊輸入的樣例集提供給LibFuzzer。模糊器跟蹤程式碼會執行到哪些程式碼塊並且收集程式崩潰,記憶體溢位,記憶體洩漏等錯誤,同時對輸入資料根據程式碼覆蓋率產生突變,以便最大化覆蓋程式碼達到模糊測試的目的。LibFuzzer的程式碼覆蓋資訊由LLVM的SanitizerCoverage指令執行提供。

LibFuzzer相比其他模糊測試平臺有如下幾個優勢:

  • 基於程式碼覆蓋率的引導式模糊測試引擎,相比其他黑盒模糊測試平臺如peach,codenomicon等商用工具,其成本低廉,並且程式碼路徑覆蓋更加自主可控。
  • LibFuzzer在對網路協議進行模糊測試時不會解析完成後退出,並且LibFuzzer的輸入為一串指定長度的位元組序列,模糊測試改造更簡單,更適合網路協議白盒fuzz測試的場景。
  • 原有單元測試用例改造簡單高效,可以按照統一框架生成新的單元測試,提高原有測試資產質量和問題發現概率。
  • 基於地址消毒劑技術進行記憶體監控,能過發現傳統測試方法很難檢測到的深層次記憶體溢位和少量記憶體洩露問題。

3 實戰案例

陣列越界異常是一種非常不好發現的執行時異常,程式碼編譯階段是不容易被發現的,一般隱藏的也比較深,bug重現的觸發條件一般也比較嚴格,測試輸入構造起來難度相對比較大,是一種典型的惡意報文攻擊的目標。那麼我們事先在DNS解析程式碼裡面構造了幾處陣列越界的漏洞,看看通過LibFuzzer大神是否能夠幫我們快速發現它們的藏身之處。

3.1 環境準備

原始碼編譯LibFuzzer很是麻煩,幸運的是Clang在6.0版本之後已經天然集成了LibFuzzer,這樣就只需下載新版本的Clang即可。如果覺得Clang原始碼安裝也很麻煩可以直接選擇編譯好的release版本。Clang下載連結http://releases.llvm.org/download.html

3.2 介面改造

環境配置完畢之後開始進行模糊測試改造,被改造的目標函式要遵循一下幾個要求:

  • 被測程式碼必須能接受任意輸入(大,小,非法)。
  • 被測程式碼不能有執行exit()的分支
  • 如果使用多執行緒,多執行緒要在被測函式退出時join。
  • 被測程式碼不應該修改任何全域性狀態。
  • 被測程式碼的執行速度要儘可能的快,避免高複雜度的運算,記日誌等。

我們選擇了DNS報文的入口函式,即解析DNS報文的介面進行模糊改造,改造程式碼如下:

f886a27b3723b6cdbed3a7c77254634038f05652


由於被測程式基於DPDK開發,為了提升模糊測試的效果,需要將DPDK也進行改造。DPDK原生支援Clang編譯,需修改dpdk/mk/toolchain/clang/rte.vars.mk

edaaa5421957190fc09bfe6e0fc4c22bcb406876

執行dpdk-setup.sh

d9189b7b0a370e69e0a028502cde41b207d32e88


選擇12使用Clang編譯DPDK。由於解析DNS報文的介面在業務流程上屬於偏上層的介面,對其他程式碼有依賴,為了單獨測試這一個介面,需要把其他依賴程式碼編譯成靜態庫供LibFuzzer呼叫。

準備工作都完成之後可以編譯fuzz taget了。

c06b1bc80e8ad1e87654082969f4461c13e5f1cb


3.3 開始測試

準備工作都就緒,現在可以測試了。

首先不指定語料庫直接執行編譯獲得的可執行程式:

59e3a570d6924fc7e47901db248e805762102ef7

運行了一會兒就發現了一處越界錯誤:

a02f89cf900b5bec97e3c5395347b45d4a850095
5f7d4c5f9c17fe3db3007c822b38619124f08dd4

3.4 分析異常

發現問題,解決問題,我們再來看程式碼,問題出現在函式adns_dname_wire_check,該函式的作用是檢查DNS query name和合法性,並且把query name都轉成小寫(DNS協議不區分大小寫),其中問題出現在這麼一段程式碼中:

74cffc703097537d23b6eabe7f50a829e63ff822


因為DNS協議規定一個域名wire format的最大長度為255位元組,因此lower_name是一個長度為255位元組的陣列,用來存轉小寫後的query name,在給lower_name陣列迴圈賦值的時候沒有判斷wire_len是否已經越界,如果遇到超長非法域名則lower_name陣列會寫越界。lower_name是一個區域性陣列,寫越界就會寫壞後面的棧空間,是一個高危風險。

3.5 測試結論

可見,LibFuzzer非常快速地幫我們發現了我們事先構建的陣列越界異常,讓我們在程式碼釋出前就可以及早發現程式碼異常。

為了提高模糊測試效率,也可以指定語料庫執行fuzz測試。語料庫可以使用CZNIC收集的DNS報文的集合,https://github.com/CZ-NIC/dns-fuzzing。

執行方法:

a57ccc91526b51fe76960822d0043034504bceb5


針對DNS報文解析介面的fuzz測試最終發現了全部的陣列越界異常。限於篇幅原因,本文不逐一分析。

4 寫在最後

(1) DNS穩定性大於天

DNS作為網路基礎服務,又是一種相對陳舊的網路協議,很容易受到黑客攻擊,這幾年針對DNS的攻擊層出不窮,而一旦DNS出現了問題,影響波及範圍廣,影響力大,因此DNS是我們必須堅守的陣地。打鐵還需自身強,在和攻擊者的博弈中,需要對開發運維各個環節多維度收斂問題,盡力將風險降到最低。

(2) 不積跬步,無以致千里

Fuzz測試就是一個很好的從軟體本身出發,查漏補缺的入口。雖然結合程式碼的白盒模糊測試雖然效果很好,但是需要按照介面函式逐個改造測試,工程量巨大,整個測試體系的建立是一個慢工出細活的過程,需要慢慢積累。

【阿里DNS團隊】始終將軟體可靠性放在第一位考慮,持續在軟體可靠性上進行資源投入,通過黑盒+白盒結合的方式,以更高性價比的方式持續挖掘漏洞。

5 參考資料

http://llvm.org/docs/LibFuzzer.html