本文系原創,轉載請說明出處:from 信安科研人
本文將詳細介紹一款基於python編寫的,能夠將程式碼插樁與覆蓋率運用到對程式進行模糊測試的FUZZER: EPF。
該工具對應的論文發表在CCF-B級會議PST上,將從實驗和原理分析兩部分介紹此工具。實驗部分將介紹epf的安裝以及對工控協議IEC104庫的fuzz,原理部分將依據論文介紹epf的實現。
實驗
工具的安裝
從原始碼的目錄結構中可以發現EPF的覆蓋率主要基於AFL++的插樁工具afl-clang-fast,對於AFL++的介紹請參考我這篇關於AFL++的原理介紹的文章。
因此:
1、安裝AFL++
安裝步驟可參考:https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/INSTALL.md
這裡,我選擇的是在本機安裝而非使用docker方式,我的作業系統是Ubuntu21.03
首先,安裝依賴:
sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake cmake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
# try to install llvm 11 and install the distro default if that fails
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-dev
sudo apt-get install -y ninja-build # for QEMU mode
接著,從github上下載AFLPlusPlus,因為github在部分地區被限制訪問,這裡推薦使用映象網址,也就是 https://github.com.cnpmjs.org/,步驟如下:
git clone https://github.com.cnpmjs.org/AFLplusplus/AFLplusplus
cd AFLplusplus
make distrib
sudo make install
這裡需要注意一點,如果對二進位制檔案的模糊測試不感興趣,就可以將make distrib這一步改為:
make source-only
2、安裝epf
首先,安裝依賴:
sudo apt-get update && sudo apt get install python3 python3-pip python3-venv
接著,安裝epf:
git clone https://github.com.cnpmjs.org/rhelmke/epf.git # clone
cd epf # workdir
python3 -m venv .env # setup venv
source .env/bin/activate # activate venv
pip3 install -r requirements.txt # dependencies
這裡可能會出現matplotlib庫安裝問題,解決辦法見我的另一篇文章
安裝之後,輸入:
python3 -m epf --help
若出現:
$ python3 -m epf --help
`-:-. ,-;"`-:-. ,-;"`-:-. ,-;"`-:-. ,-;"
`=`,'=/ `=`,'=/ `=`,'=/ `=`,'=/
y==/ y==/ y==/ y==/
,=,-<=`. ,=,-<=`. ,=,-<=`. ,=,-<=`.
,-'-' `-=_,-'-' `-=_,-'-' `-=_,-'-' `-=_
- Evolutionary Protocol Fuzzer -
positional arguments:
host target host
port target port
optional arguments:
-h, --help show this help message and exit
Connection options:
-p {tcp,udp,tcp+tls}, --protocol {tcp,udp,tcp+tls}
transport protocol
-st SEND_TIMEOUT, --send_timeout SEND_TIMEOUT
send() timeout
-rt RECV_TIMEOUT, --recv_timeout RECV_TIMEOUT
recv() timeout
Fuzzer options:
--fuzzer {iec104} application layer fuzzer
--debug enable debug.csv
--batch non-interactive, very quiet mode
--dtrace extremely verbose debug tracing
--pcap PCAP pcap population seed
--seed SEED prng seed
--alpha ALPHA simulated annealing cooldown parameter
--beta BETA simulated annealing reheat parameter
--smut SMUT spot mutation probability
--plimit PLIMIT population limit
--budget TIME_BUDGET time budget
--output OUTPUT output dir
--shm_id SHM_ID custom shared memory id overwrite
--dump_shm dump shm after run
Restart options:
--restart module_name [args ...]
Restarter Modules:
afl_fork: '<executable> [<argument> ...]' (Pass command and arguments within quotes, as only one argument)
--restart-sleep RESTART_SLEEP_TIME
Set sleep seconds after a crash before continue (Default 5)
則代表安裝成功,當然,這裡需要注意的是,每次啟動epf都需要輸入
source .env/bin/activate
以進入python虛擬環境,從而執行epf。
對IEC104協議庫進行fuzz
實驗準備
下載IEC104協議庫:
git clone https://github.com/mz-automation/lib60870.git
cd lib60870/lib60870-C
下載IEC104協議PCAP資料包:
具體連結:https://github.com/automayt/ICS-pcap/raw/master/IEC 60870/iec104/iec104.pcap
使用AFL++中的編譯器插樁
首先,本文測試IEC104協議實現庫漏洞的樣例是上文提到的IEC104協議庫中的cs104_server_no_threads樣例,該樣例位於:lib60870-C/examples/cs104_server_no_threads/cs104_server_no_threads.c
接著,將編譯器改為AFL++中的afl-clang-fast編譯器:
echo "CC=~/AFLplusplus/afl-clang-fast" >> make/target_system.mk
這裡需要注意的是,在lib60870庫中,編譯器已經在其make目錄中的target_system.mk中指定,因此,我們只需要將mk中指定編譯器程式碼寫為你主機中AFL++中的afl-clang-fast的地址即可,這裡的命令僅供參考。
make後得到程式,並將其複製到epf的資料夾下,同時也將iec104.pcap檔案移入。
開始fuzz
在命令列依次輸入:
cd ~/epf
source .env/bin/activate # activate virtualenv
python -m epf 127.0.0.1 2404 -p tcp --fuzzer iec104 --pcap iec104.pcap --seed 123456 --restart
afl_fork "./cs104_server_no_threads" --smut 0.2 --plimit 1000 --alpha 0.99999333 --beta 1.0 --budget 86400
上文中的命令引數的具體意義可以輸入 python3 -m epf --help,在epf的初始介面檢視
開始fuzz,效果甚好。
原理
問題提出
核心問題:不同的領域不同場景帶來的不同難度的FUZZ
工具框架
框架圖如上,EPF主要分為兩個階段,預處理和動態分析,預處理階段針對待測協議處理。依據框架圖依次介紹:
III-A:目標程式插樁
EPF中的插樁功能主要是呼叫AFL++的編譯外掛對程式進行編譯,具體覆蓋率矩陣計算詳見AFL++或者AFL baseline 的實現細節,在我的另一篇AFL++的原理文章也有講解。
論文中說到,EPF選擇AFL++中的編譯鏈作為程式插樁的實現的好處主要是能夠繼承AFL++的功能以及後續的擴充功能。
III-B:資料包建模
資料包建模是指FUZZER構建網路協議的輸入的模型構建。
網路協議的模糊測試領域中的一個挑戰是B/S兩端可能會拒絕不符合該協議規則的資料包,這就意味著如果想要有效的對協議進行模糊測試,應是以符合協議規則佈局的資料包為主要的FUZZ輸入,從而能夠改變產生協議的程式的狀態,進而對整個協議的實現進行漏洞挖掘。
EPF通過python中的Scapy庫實現資料包建模,主要原因是scapy支援的協議實在是太多^ _ ^,並且一直在更新擴充套件協議型別。
III-C:狀態轉換建模
網路協議的fuzz中,因為協議的通訊是雙向的,所以不僅僅需要產生與協議結構相似的資料,還需要考慮到協議狀態的轉換,從而能發掘協議的狀態邏輯等更深層次的漏洞。EPF做出來一個API,主要使用無向有環圖對協議狀態圖構建。
每一條路徑包含一個特殊的FUZZ節點以呼叫特定的資料包型別(可以參考下圖IEC104協議的狀態圖),然後,對於每一個測試用例,EPF都會開一個連線,並遍歷協議狀態圖且傳送所有預先設定好的payload到PUT模組(該模組相當於一個執行者),直到這個測試用例到達相對應的節點。在這個測試用例到達相應節點前,EPF還會繼續傳送下一個測試用例並遍歷一直到下一個測試用例達到其對應的節點,終止訊號是路徑結束。
III-C部分的建模所得出的是協議的狀態轉換圖,例如IEC608705-104協議的狀態圖如下,簡要概括一下就是,傳送兩個U型payload才能讓I型和S型對應的節點接收FUZZ的輸入。
III-D 遺傳種群構造器
這個遺傳種群構造器是整個預處理流程的最後一步,整合前兩個步驟所得到的包佈局定義和FUZZ狀態轉移圖,製造種子資料包語料庫,這個語料庫由追蹤目標協議網路流產生。
種群是指同一時間生活在一定自然區域內,同種生物的所有個體,因此這裡的種群就是指例如I型、U型、S型包含其對應其型別的資料包,以分別成組的群體。
具體的內容介紹如下圖,依舊是以IEC104為例:
當使用者將一種協議的PCAP資料包輸入到這個元件中,對於每個資料包,這個元件都會啟發式(基於經驗)的區於Scapy庫中的資料模型匹配,例如,我輸入一個IEC104的資料包,如果我識別了其特徵,與Scapy中的IEC104資料包模型相匹配,那麼全域性FUZZ模型就被認定為是IEC104,並進行例項化也就是按照這個模型產生資料包。
然後圖中說到的協議過濾器,其過濾規則需要人工定義,例如定義IEC104的資料包型別,即S-,I-,U-APDU格式,這些規則在文章裡被叫做白名單,然後這個過濾器就會把那些重複和不符合規則的的資料包。
接著,過濾出來的資料包將會按照型別(S,I,U)進行分類,分出來的組被稱為種群,所謂的遺傳就是在種群中變異出該型別資料包。正如圖中所示,大圓圈(藍色)是種群,小圓圈(例如綠色圓圈)是個體,chromosomes指的是具備相同的頭部。
III-E 執行引擎
執行引擎是個與主迴圈互動的併發模組,分為兩個部分,也就是如上圖所示的覆蓋率API和PUT模組,意如其名,API是獲取程式覆蓋率的介面,PUT可以從圖中看出來是執行引擎中的核心,也就是執行者。
覆蓋率API通過建立一個64KiB的共享記憶體,與基於AFL的插樁程式相對接。在執行時,PUT 將此記憶體對映到虛擬地址空間並插入覆蓋資料。在執行時,PUT 將此記憶體對映到虛擬地址空間並插入覆蓋資料。API 在傳送每個測試用例的前後,對記憶體對映進行快照並轉義覆蓋率資訊。 EPF中的覆蓋率是對映中非零位元組的總數。 這些位元組的所代表的釋義取決於使用者選擇的指標和插樁。 例如, 純版AFL利用定向(directed)分支覆蓋,而 N-Gram(一款fuzzer) 擴充套件引入了更敏感的路徑覆蓋。
執行器監控 PUT 的執行狀態。 它通過 execve 方法 fork 出一個子程式。 執行器在每次模糊測試迭代中輪詢子程式的狀態。 當 PUT 進入故障狀態時,將會獲取返回碼並記錄崩潰。 故障狀態是指意外的殭屍程式、失效和停止狀態。 EPF將其fork到一個新的例項來恢復模糊測試。
基於 execve 的方法有缺點。 首先,如果目標在每次迭代中重新啟動,每次fork都會帶來初始化開銷,從而減慢模糊測試的速度。 但是,EPF假設網路伺服器應用程式通過繼續執行來發出有效訊號。 因此,EPF 僅在 PUT 崩潰時才會fork。
III-F 主迴圈
分塊介紹,首先介紹各個操作對應的意義
Continue
就是說,t^budget 是EPF的計算預算,簡潔的說就是,預期設定一次模糊測試迭代的秒數。那麼這裡的continue是指如果一輪模糊測試將這個時間用完,那麼動態分析這部分就終止。在這個工具裡對應的引數是:
Schedule:
這部分對應下圖中的Schedule Population,population在上文已說過是不同型別資料包的種群。設C 是種子語料庫中的種群集合,按它們在種子檔案中的出現順序排列。 EPF 在 C 中分配 t^budget,從而提高覆蓋率。 如果 C 種群發現新程式碼的潛力耗盡,EPF會調整並嘗試另一個種群。 然而,EPF 面臨著一個multi-armed bandit問題 ,可以使用模糊測試配置排程 (FCS) 演算法 來解決這個問題。
EPF 使用模擬退火演算法對 C 上的預算分佈進行動態建模。模擬退火演算法(Simulate Anneal Arithmetic,SAA)是一種通用概率演演算法,用來在一個大的搜尋空間內找尋命題的最優解。它是基於Monte-Carlo迭代求解策略的一種隨機尋優演算法。 模擬退火演算法是S.Kirkpatrick, C.D.Gelatt和M.P.Vecchi等人在1983年發明的,1985年,V.Černý也獨立發明了此演演算法。模擬退火演算法是解決商旅問題(TSP)的有效方法之一。
TSP問題(Traveling Salesman Problem,旅行商問題),由威廉哈密頓爵士和英國數學家剋剋曼T.P.Kirkman於19世紀初提出。問題描述如下: 有若干個城市,任何兩個城市之間的距離都是確定的,現要求一旅行商從某城市出發必須經過每一個城市且只在一個城市逗留一次,最後回到出發的城市,問如何事先確定一條最短的線路已保證其旅行的費用最少?
而對於模擬退火演算法,這裡引用資料魔術師的帖子中的一段話:
模擬退火演算法以一定的概率來接受一個比當前解要差的解,因此有可能會跳出這個區域性的最優解,達到全域性的最優解。模擬退火演算法在搜尋到區域性最優解B後,會以一定的概率接受向右的移動。也許經過幾次這樣的不是區域性最優的移動後會到達BC之間的峰點D,這樣一來便跳出了區域性最優解B,繼續往右移動就有可能獲得全域性最優解C。這裡也有一個有趣的比喻:
普通貪心演算法:兔子朝著比現在低的地方跳去。它找到了不遠處的最低的山谷。但是這座山谷不一定最低的。
模擬退火:兔子喝醉了。它隨機地跳了很長時間。這期間,它可能走向低處,也可能踏入平地。但是,它漸漸清醒了並朝最低的方向跳去。
設∈c (i) ∈ [0, 1] 為迴圈第 i次迭代中種群 c 的能量或潛力。 EPF將潛能的發展過程,建模為一系列與覆蓋率相關的指數增長和衰減:
這裡,α為損失值,範圍為[0,1[,β為增長因子,範圍為]0,1]。
- 一開始,EPF 選擇第一個配置 c0 並設定∈c0 (0) = 1。FCS 返回並且對 c0 進行模糊測試。
- 對於下一次迭代 i + 1,FCS 繼續選擇 c0 進行模糊測試,並根據前一次迭代的結果重新計算∈c0 (i + 1):如果前一次迭代增加了覆蓋率,則 c0 獲得潛力,即: ∈c0 ( i + 1) ≥ ∈c0 (i)。 否則 c0 失去潛力,即:∈c0 (i + 1) < ∈c0 (i)。
- 一旦 ∈c0 (i) 達到閾值 ∈thresh範圍為]0, 1[,潛力就會耗盡。 EPF 然後選擇下一個集合c1(上文已經說到c是一個種群集合),設定∈c1 (i) = 1 並繼續該過程。
- 當演算法迴圈遍歷所有 c ∈ C 時,排程程式重新啟動,直到 t^budget 耗盡。
上圖的不同顏色代表不同種群,隨著時間的推移,覆蓋率的不變,預示這個種群的潛力下降,當覆蓋率升高,則這個種群的潛力上升。
Generate Input
在排程流程後,輸入生成器選擇父母用例並使用測試用例重組和測試用例變異建立子用例。
種群中的個體被組織在一個按其適應度排序的連結串列中。輸入生成器使用兩個概率函式隨機選擇兩個父母節點:假設父用例 A 來自指數分佈函式的選擇,母用例 B 來自均勻分佈函式的選擇。
與 AFL的拼接演算法類似,父母用例通過單點交叉進行復制:它們的資料包結構在左右部分隨機切片。然後,A 的一部分與 B 的對應部分合並(見上圖右部分)。哪個父母級別貢獻哪個部分是隨機選擇的。這個想法是結合結構上有效的資料包的特徵,這些特徵在連線在一起時可能會或可能不會變得無效。
單獨的重組不會產生遺傳多樣性。因此,EPF使用隨機點突變。每次用例片段交叉後,EPF 隨機選擇具有使用者定義概率 p^mut 的子資料包欄位(如下圖),並使用型別感知隨機位元組生成器改變其值。
Evaluate Input
從上一步的生成器中拿去生成的子用例作為輸入:
- 如果輸入到PUT的用例引起了PUT的crash,那麼,這個用例就會被接上exit碼,也就是被標為可能引起bug的用例。
- 如果覆蓋率圖的快照發生了變化,那麼,這個用例就會被接上coverage increase的標籤。
Update Population
該模組依據上一步生成的用例評估結果來更新種群集合。
如上圖,從左往右看,右邊為連結串列的末端,左邊為連結串列的頭端。如果沒有覆蓋率沒有增加,則這個測試用例被認為是壞用例,並以p^add=∈c(i) (也就是上文Schedule部分說到的潛力值)的概率,新增到種群集合中。因此,每一輪迭代的種群潛力值決定了弱個體在基因庫中生存的機會,這就是EPF標題中所謂的Evolution。
如果C用例增加了覆蓋率,那麼,它個人會被新增到連結串列首部,然後它的父母節點會被向前移動一位。如果沒有就如上文所述按照概率插入到連結串列中,父母節點往後移動一位。這也意味著,當一個父母用例被指數分佈選中時,選擇最近增加覆蓋率的個體的概率很高。
需要注意的是,當連結串列達到P^limit後,將會刪除連結串列最後一個用例。這就是自然法則。
框架流程總結
介紹完EPF的所有小部分的實現,我再總結一下整個測試框架的流程,依舊是那幅圖,即框架總圖:
這幅圖要從左往右看,分為兩個部分即對應兩個FUZZ階段看。
預處理階段:
- 對被測程式插樁
- 對使用者輸入的pcap資料包進行結構提取並與Scapy中的庫匹配,確定好協議型別後,按照scapy中定義的協議資料佈局,準備生成協議資料。
- 狀態轉移圖生成
- 按照人工定義的規則對使用者輸入的pcap包中的種子用例進行種群劃分
動態分析階段:
總體上使用基於種群的模擬退火實現資源分佈和覆蓋最大化。
- 對劃分好的種群進行排程,每個種群生成一個連結串列,使用模擬退火演算法對每個種群中的個體分配t^budget以限制計算資源。
- 對每個種群劃分潛力值,每一次迭代,種群潛力值將按照設定好的潛力值公式變化
- 按照兩種分佈函式選取同一種群中兩個連結串列節點用例作為父母用例,拼接或者變異用例。
- 將生成的用例輸入到執行引擎中執行
- 將覆蓋率的變化情況分類並標籤到測試用例的末尾
- 按照上一步的標籤對種群進行用例的更新,一般是連結串列順序更新
- 迴圈直到t^budget值用盡,也就是使用者設定的fuzz時間