:從靜態(tài)數(shù)據(jù)流分析到fuzz harness的自動化漏洞挖掘)
做漏洞挖掘這些年我最大的一個體會是fuzz 本身不缺速度缺的是一個聰明的入口。拿到一個陌生目標(biāo)傳統(tǒng)工作流往往是先花大半天讀代碼、找輸入點、手寫 harness、再開始跑樣本等真正跑到崩潰可能已經(jīng)過去兩三天。靜態(tài)分析工具倒是快能掃出一大片“疑似風(fēng)險點”可誤報率也高得讓人絕望每一條都去人工確認(rèn)的話時間開銷比直接人工審計還大。pasta 框架要解決的恰好就是這個斷層它用靜態(tài)數(shù)據(jù)流分析幫你鎖定從外部輸入到危險函數(shù)之間的候選路徑再自動為這些 source-sink 路徑生成可執(zhí)行的 fuzz harness最后交給 fuzz 引擎和符號執(zhí)行工具去動態(tài)驗證。一句話總結(jié)就是它把“這個參數(shù)可能有 bug”推進到了“這個參數(shù)真的能打崩程序”。這篇文章是我最近在一個模擬項目上從零跑通 pasta 的完整實戰(zhàn)記錄包含環(huán)境搭建、profile 配置、harness 生成、崩潰驗證以及一堆翻車之后才總結(jié)出來的避坑細(xì)節(jié)。適合正在做二進制漏洞挖掘、安全評估或者想把手頭“目標(biāo)多、人手少”的審計工作自動化一部分的人。1. 項目概述與核心思路1.1 一個老問題代碼審計和 fuzz 之間為什么永遠(yuǎn)差一截拿代碼審計舉例。人工審計的優(yōu)點是能理解語義知道哪些調(diào)用是真正可控的缺點是慢、累、依賴個人經(jīng)驗自動化靜態(tài)工具恰好相反它跑得快一條規(guī)則下去能掃出幾百個候選但這些候選里大頭都是不可達(dá)或者根本不可利用的路徑需要人工二次過濾。fuzz 則站在另一端它只能證明一個入口是否有問題前提是你要給它一個能正確調(diào)用目標(biāo)邏輯的 harness而 harness 恰恰是自動化流程里最難被泛化的一環(huán)。這種割裂導(dǎo)致一個很常見的尷尬場景靜態(tài)掃描報告里列著十個“高?!蹦闳プ粉櫚l(fā)現(xiàn)五個在 Release 版本里根本沒有調(diào)用鏈三個被上游白名單校驗擋死剩下的兩個又不清楚外部輸入到底能不能到達(dá)。另一邊f(xié)uzz 報告顯示“跑了二十四小時零崩潰”但你不知道是不是因為 harness 根本沒覆蓋到真正的處理邏輯。pasta 這類框架出現(xiàn)的原因就是有人想把這套流程用工程化方式串起來讓靜態(tài)分析的結(jié)果直接變成動態(tài)驗證的輸入。1.2 pasta 解決痛點的思路從 source 到 sink 自動打通整個框架的核心流程可以拆成五個階段我實際跑下來感覺每一步的定位都很清晰配置階段在 profile 里聲明目標(biāo)項目路徑、語言類型、構(gòu)建方式、需要關(guān)注的 source輸入點和 sink危險函數(shù)。靜態(tài)分析階段pasta 基于類似 semgrep 風(fēng)格的規(guī)則對源碼做數(shù)據(jù)流掃描找出“外部輸入能夠流到危險函數(shù)”的候選路徑。harness 生成階段針對每一條候選路徑自動生成一個最小驅(qū)動程序把模糊測試引擎的輸入喂給目標(biāo)代碼。動態(tài)執(zhí)行階段調(diào)用 fuzz 引擎或者符號執(zhí)行工具對目標(biāo)做覆蓋率引導(dǎo)的變異測試和路徑約束求解。結(jié)果輸出階段收集崩潰樣本、調(diào)用棧和報告供人工復(fù)現(xiàn)確認(rèn)。這個閉環(huán)的意義在于它改變了“疑似漏洞”的置信度。沒有動態(tài)驗證的靜態(tài)結(jié)論只是方向方向可以被證偽而一旦某個候選路徑真的在 fuzz 階段觸發(fā)了 ASAN 報錯你手里拿到的就是一條帶崩潰樣本的證據(jù)鏈。從工程效率看這也把人工從“從零找入口”中解放出來只需要去分析框架篩出來的 TopN 候選。1.3 這條流水線里的符號執(zhí)行到底扮演什么角色很多人提到符號執(zhí)行就頭大覺得它路徑爆炸、跑不動確實如果你把整個程序丟進去做完整符號執(zhí)行基本都會卡死。但 pasta 這類框架里符號執(zhí)行的定位不是“全程序勘探”而是“定向解鎖關(guān)鍵路徑”。我舉個具體場景。很多解析類程序入口都有 magic bytes 校驗比如要求前兩個字節(jié)必須是0xCA 0xFE普通 fuzz 要靠隨機變異偶然碰對概率是 1/65536如果要連續(xù)過五六個字節(jié)的校驗基本就等于是不可能事件。符號執(zhí)行遇到這類分支時會把條件記錄下來變成約束比如data[0]0xCA data[1]0xFE然后調(diào)用約束求解器直接解出一組滿足條件的輸入。這就是為什么我說pasta 這類流水線真正的聰明之處是先用靜態(tài)分析把路徑規(guī)??s小再讓符號執(zhí)行去處理那些 fuzz 很難越過的高門檻分支最后交給 fuzz 繼續(xù)做深度變異。在這個組合里每一個工具都在干自己最擅長的事靜態(tài)分析負(fù)責(zé)找方向符號執(zhí)行負(fù)責(zé)過魔法校驗fuzz 負(fù)責(zé)大范圍覆蓋人工負(fù)責(zé)最后拍板。2. 環(huán)境準(zhǔn)備與安裝先把輪子跑起來2.1 依賴清單與推薦環(huán)境pasta 本身不是一個單一二進制它更像一套把多個安全工具編排起來的框架所以安裝前最好先確認(rèn)下列組件在系統(tǒng)里是齊全的。我推薦的實驗環(huán)境是一臺干凈的 Linux 機器可以是物理機也可以是虛擬機關(guān)鍵是不要混入太多無關(guān)依賴。組件作用備注Linux 環(huán)境基礎(chǔ)運行環(huán)境建議主流的發(fā)行版內(nèi)核不需要特殊配置gcc / clang編譯目標(biāo)代碼和 harnessclang 對插樁支持更友好建議裝make / cmake構(gòu)建目標(biāo)項目很多目標(biāo)項目依賴這些工具鏈Docker可選隔離依賴目標(biāo)依賴很臟時強烈推薦Python / Rubypasta 可能有腳本依賴按倉庫 README 準(zhǔn)備即可fuzz 引擎動態(tài)執(zhí)行階段的核心AFL、libFuzzer 都可以符號執(zhí)行工具可選處理魔法校驗例如 angr、KLEE按需接入sanitizer崩潰復(fù)現(xiàn)檢測ASAN 是必備能直接定位內(nèi)存錯誤這里要額外說一句ASAN 一定要裝好而且要確認(rèn) clang 或者 gcc 能正常生成帶地址消毒的二進制。很多新手拿到 pasta 報告里的崩潰樣本后跑本地的時候沒有開 ASAN結(jié)果什么都沒發(fā)生直接懷疑工具出了問題。其實不是崩潰已經(jīng)在 fuzz 階段被攔截了你本地復(fù)現(xiàn)必須用同樣的 sanitizer 配置才看得見。2.2 安裝流程與目錄結(jié)構(gòu)因為不同版本安裝方式略有差異我把我實際用的方式寫出來你對照本地倉庫的 README 調(diào)整即可git clone 倉庫地址 pasta cd pasta ./scripts/setup.sh裝完后先確認(rèn)主入口是不是在 PATH 里pasta --help如果你看到輸出里有run、report之類的子命令就說明安裝基本完成了。我見過一個常見問題是 setup 腳本默認(rèn)安裝的 Python 依賴和系統(tǒng)全局環(huán)境沖突尤其是反復(fù)切換過多個 Python 版本的環(huán)境特別容易翻車。建議給 pasta 單獨建一個虛擬環(huán)境不要直接裝在系統(tǒng) site-packages 里。pasta 的目錄結(jié)構(gòu)大體上包含這樣幾個重要模塊規(guī)則目錄存放 source/sink 掃描規(guī)則、配置目錄存放 profile 文件、harness 模板目錄決定生成出來的驅(qū)動代碼樣式、報告輸出目錄存放崩潰樣本和 HTML/JSON 報告。跑熟悉了以后你會發(fā)現(xiàn)70% 的定制化需求都落在規(guī)則目錄和配置目錄里這是你真正要長期維護的資產(chǎn)。2.3 自帶樣例驗證一條必須走的“冒煙測試”不要上來就拿真實目標(biāo)跑第一次使用務(wù)必先跑一遍倉庫自帶的樣例項目。這就像開新車先熱車一樣能幫你把“環(huán)境問題”和“配置問題”分開。自帶樣例通常是一個故意留下漏洞的小程序流程上你只需要找到它的示例 profile執(zhí)行pasta run --config examples/example_profile.yaml正常的現(xiàn)象是日志里先出現(xiàn)靜態(tài)分析階段掃描到的 sink 列表然后是 harness 生成成功的提示再往后是 fuzz 引擎開始跑樣本的輸出最后在崩潰目錄里出現(xiàn) crash 文件或者一份自動生成的報告。我第一遍跑的時候卡在了 dependency 檢查上提示缺一個系統(tǒng)庫裝上之后再跑就一路順暢了。踩過一次坑之后我才意識到自帶樣例的價值不只是驗證安裝它還給了一個很好的“最低可復(fù)現(xiàn)基線”以后你改了任何配置或者升級了引擎先跑一遍這個樣例如果它都過不了就別急著怪目標(biāo)項目有問題。3. 核心配置解析profile、source 與 sink 的設(shè)計3.1 一個最小可用的 profile 長什么樣pasta 的入口配置我習(xí)慣叫 profile實際文件名在不同的版本里可能叫 config 或者 manifest本質(zhì)都是 YAML 或 JSON 來描述一個測試任務(wù)。下面這個例子是一個簡化版字段名可能略有差異但思路是大致一致的project_name: mini_demo target_path: ./demos/mini_demo language: c build_command: make mini_demo sources: - id: FileReadSource pattern: fread(...) sinks: - id: Sink_Memcpy type: memory_copy pattern: memcpy(...) - id: Sink_Strcpy type: string_copy pattern: strcpy(...) fuzz_engine: name: aflpp sanitizer: address execution: timeout: 300 input_seeds: ./seeds/我不建議照抄這個字段因為不同版本的 pasta 對配置項的命名確實有調(diào)整。正確的做法是先打開倉庫自帶的 example profile確認(rèn)你自己這個版本的字段叫什么再改寫成自己的目標(biāo)。這個提醒不是敷衍我是真吃過虧照著網(wǎng)上的舊教程填字段結(jié)果框架直接報解析失敗白白浪費一小時。3.2 source 和 sink 定義選對位置比選多更重要source 的選擇原則是“越靠外越好”。fd 的 read、網(wǎng)絡(luò)包接收、命令行參數(shù)、環(huán)境變量、配置文件解析入口這些都要優(yōu)先放進 source 列表。你如果把它定義得太深入比如某個解析函數(shù)內(nèi)部才讀到數(shù)據(jù)污點分析的起點就晚了前面可能漏掉好幾層可控性判斷。sink 的選擇要按“家族”覆蓋不要只盯著一個函數(shù)名。內(nèi)存拷貝族至少要有 memcpy、strcpy、strncpy注意這個也有坑、sprintf、vsprintf命令執(zhí)行族要有 system、popen、exec 系列SQL 安全評估還要考慮 query 拼接這在二進制漏洞挖掘里相對少見但在 Web 類目標(biāo)里是核心。每個項目都有自己的內(nèi)部危險接口比如某個內(nèi)部日志函數(shù)存在格式化字符串問題你必須在規(guī)則庫里補上這個函數(shù)名否則 pasta 根本不知道它值得關(guān)注。我實際跑下來的一個經(jīng)驗是不要把靜態(tài)分析規(guī)則做太多。規(guī)則越寬垃圾候選越多harness 生成的量也越大反而拖慢整個流程。先覆蓋最關(guān)鍵的危險函數(shù)族跑完一輪看結(jié)果再決定要不要加規(guī)則比一次性堆滿幾十條規(guī)則高效得多。3.3 污點標(biāo)簽、約束條件與符號執(zhí)行的配合在靜態(tài)數(shù)據(jù)流分析里污點從 source 點開始打標(biāo)簽一路沿著數(shù)據(jù)流傳播直到到達(dá) sink 點。真正重要的不是“有沒有到達(dá)”而是“到達(dá)時經(jīng)過了多少約束”。如果污點數(shù)據(jù)在中間被一個無條件長度檢查限制住了路徑的安全性就相對高如果污點數(shù)據(jù)經(jīng)過了幾層復(fù)雜的變換和校驗?zāi)沁@些變換和校驗本身就是 fuzz 的難點也是符號執(zhí)行能介入的地方。我拿一個典型場景來說明。目標(biāo)函數(shù)里有一句if (data[0] 0xCA data[1] 0xFE) { /* 進入核心處理 */ }普通 fuzz 引擎在這個分支前的覆蓋率卡住因為隨機變異很難同時猜中兩個魔數(shù)。符號執(zhí)行工具看到這個分支時會把條件壓進求解器直接返回一組滿足條件的具體輸入。這就是我前面說的“定向解鎖”它不求解整個程序的路徑只求解從當(dāng)前執(zhí)行點到關(guān)鍵分支的約束所以速度和可用性都大大提升。污點分析到這個層面還有個問題標(biāo)簽傳播要足夠保守但不能過于保守。比如污點數(shù)據(jù)經(jīng)過哈希函數(shù)再到達(dá) sink理論上標(biāo)簽應(yīng)該繼續(xù)傳播但實際可利用性已經(jīng)很低了。pasta 這類框架通常會保持“寧可多報不可漏報”的策略把這種路徑繼續(xù)報出來由你在人工驗證階段判斷。理解了這一點你就不容易因為報告里有大量“低可利用性”候選而失望了。4. 實戰(zhàn)記錄pasta 跑通一個模擬目標(biāo)的完整過程4.1 目標(biāo)代碼長什么樣我埋了哪些坑為了復(fù)現(xiàn)整個流程我寫了一個極小的模擬目標(biāo)mini_demo它不是真實漏洞只是用來展示框架工作流的 Demo。這個程序的邏輯非常簡單從文件讀入一段字節(jié)前兩個字節(jié)必須是0xCA 0xFE隨后兩個字節(jié)作為 name 長度接著把數(shù)據(jù)復(fù)制到棧上一個固定大小的緩沖區(qū)里。#include stdio.h #include stdint.h #include string.h #define NAME_BUF_SIZE 16 int process_config(const uint8_t *data, size_t size) { char name[NAME_BUF_SIZE]; if (size 18) return -1; if (data[0] ! 0xCA || data[1] ! 0xFE) return -1; size_t name_len data[2] | (data[3] 8); /* 此處是故意保留的脆弱點 name_len 未與 NAME_BUF_SIZE 做比較后續(xù) memcpy 會發(fā)生棧溢出。 */ memcpy(name, data 4, name_len); name[name_len] \0; if (strcmp(name, default) 0) { return 1; } return 0; } int main(int argc, char **argv) { FILE *fp; uint8_t buf[256]; size_t file_size; if (argc 2) return -1; fp fopen(argv[1], rb); if (!fp) return -1; file_size fread(buf, 1, sizeof(buf), fp); fclose(fp); return process_config(buf, file_size); }這個 Demo 里有兩個典型難點第一個是 0xCA 0xFE 的魔法校驗直接靠 fuzz 碰撞很困難第二個是memcpy復(fù)制長度完全可控但沒有邊界檢查一旦繞過魔法校驗就能觸發(fā)內(nèi)存越界。我選擇這樣的目標(biāo)就是因為它能同時展示 fuzz 的優(yōu)勢和瓶頸也方便理解符號執(zhí)行在流程中的真實價值。4.2 運行 pasta 的完整過程與階段輸出把上面的代碼放進demos/mini_demo/目錄寫一個對應(yīng)的 profile比如configs/mini_demo.yaml然后執(zhí)行pasta run --config configs/mini_demo.yaml第一階段的日志會顯示靜態(tài)分析掃描到了 fread 這個 source 和 memcpy 這個 sink并生成一條從fread - buf - process_config - name - memcpy的可疑數(shù)據(jù)流。到這里你已經(jīng)能看出 pasta 不只是找函數(shù)名它還做了變量粒度的傳播追蹤。第二階段日志會顯示它生成了一份 harness 代碼內(nèi)部大致是這樣#include stdint.h #include stddef.h #include mini_demo.h int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { process_config(data, size); return 0; }這份 harness 直接使用 fuzz 引擎?zhèn)魅氲淖止?jié)流作為輸入繞過了真實程序里讀文件的步驟讓 fuzz 可以針對核心函數(shù)做密集測試。這是整個框架最省時間的地方人工寫這么一份驅(qū)動通常不難但框架可以同時生成幾十個不同 source-sink 路徑的驅(qū)動這個規(guī)模是手寫做不到的。第三階段是動態(tài)執(zhí)行。我配置的引擎是 AFL日志里開始出現(xiàn)執(zhí)行速率、路徑數(shù)量、覆蓋率的滾動統(tǒng)計。因為我故意在 seeds 目錄放了一個滿足0xCA 0xFE前綴的種子文件動態(tài)執(zhí)行很快就跑到了 $name_len$ 很大的分支然后 ASAN 的告警出現(xiàn)了。第四階段pasta 把崩潰樣本收集到崩潰目錄文件名通常是crash-xxx或帶時間戳的形式。到這一步自動化部分已經(jīng)完成了使命候選路徑被驗證成一條真實的崩潰路徑接下來需要人工介入復(fù)核。4.3 用 ASAN 和 gdb 人工驗證崩潰結(jié)果拿到崩潰樣本后第一件事不是直接看它為什么崩而是先用 ASAN 確認(rèn)它是可復(fù)現(xiàn)的。我用帶地址消毒的編譯選項重新編譯目標(biāo)程序clang -fsanitizeaddress -g -o mini_demo_asan mini_demo.c ./mini_demo_asan crash-xxx正常輸出會有一段類似這樣的信息ERROR: AddressSanitizer: stack-buffer-overflow on address ...看到stack-buffer-overflow基本就可以確認(rèn)是棧上的緩沖區(qū)溢出。接下來用 gdb 跟進gdb ./mini_demo_asan run crash-xxx bt調(diào)用棧會顯示process_config里的memcpy或者strcmp幀結(jié)合反匯編就能定位越界復(fù)制發(fā)生的確切位置。這里有一個我在實際中經(jīng)常提醒自己的點崩潰發(fā)生在目標(biāo)代碼里和崩潰發(fā)生在框架生成的 harness 里意義是完全不同的。如果崩潰棧指向的是LLVMFuzzerTestOneInput或者某個容器/標(biāo)準(zhǔn)庫函數(shù)你要先懷疑是 harness 自身的未定義行為而不是目標(biāo)程序漏洞。這個人工驗證步驟不能省因為自動化工具報出來的崩潰里總有相當(dāng)比例是“假陽性”比如目標(biāo)程序在特定輸入下拋出異常、harness 驅(qū)動代碼自身的越界、甚至環(huán)境的內(nèi)存限制被誤判為崩潰。只有把棧和分析器輸出對齊才敢把這條結(jié)果寫進漏洞報告。4.4 這輪實驗里符號執(zhí)行解決的真正難點回到我在 4.1 埋的魔法校驗。假設(shè)我沒有提前準(zhǔn)備一個合法前綴的種子文件而完全依賴純 fuzz會發(fā)生什么AFL 確實有插樁和覆蓋率反饋它知道某個分支沒有被覆蓋也會嘗試變異數(shù)據(jù)去碰那個分支但0xCA 0xFE兩個字節(jié)都需要命中從全零狀態(tài)開始撞中的概率是 1/65536運氣好可能幾分鐘運氣差可能幾小時。如果校驗是四字節(jié)甚至八字節(jié) magic那就基本靠運氣了。在我的實驗流程里pasta 生成的路徑分析結(jié)果直接把這條分支約束交給了符號執(zhí)行組件。工具在data[0] ! 0xCA分支處記錄約束data[0]0xCA在data[1] ! 0xFE處追加約束data[1]0xFE求解器很快給出一個包含0xCA 0xFE前綴的具體輸入樣本。拿到這個樣本后fuzz 引擎把它當(dāng)成種子繼續(xù)變異很快又滿足了 $name_len$ 足夠大的條件最終觸發(fā)越界復(fù)制。所以這個實驗的意義非常明顯符號執(zhí)行不是替代 fuzz而是給 fuzz 鋪路。它解決“高門檻分支過不去”的問題fuzz 解決“過了門檻之后的大規(guī)模路徑發(fā)現(xiàn)”問題兩者缺一不可。5. 常見問題與排查技巧實錄5.1 問題速查表一張表先救急真實跑 pasta 的過程中報錯的形態(tài)其實很有限。我把遇到的典型問題整理成一張速查表你可以按圖索驥現(xiàn)象常見原因處置方法靜態(tài)掃描沒有找到 sink規(guī)則庫沒覆蓋目標(biāo)函數(shù)或者存在間接調(diào)用自定義規(guī)則補充函數(shù)名與調(diào)用鏈harness 生成失敗目標(biāo)代碼沒有可用的公開入口依賴頭文件缺失先構(gòu)建靜態(tài)庫調(diào)整簡單入口函數(shù)fuzz 覆蓋率長期很低目標(biāo)存在復(fù)雜格式校驗或魔法字節(jié)準(zhǔn)備種子文件、字典或引入符號執(zhí)行崩潰文件堆積如山未做調(diào)用棧去重多個樣本是同一缺陷按棧指紋和 PC 偏移去重符號執(zhí)行長時間不返回路徑爆炸約束求解器壓力過大限制路徑數(shù)量設(shè)置執(zhí)行超時目標(biāo)頻繁 OOM 被殺fuzz 引擎內(nèi)存限制未配置在容器里限制內(nèi)存排除 OOM 誤報崩潰在本地復(fù)現(xiàn)不出來harness 編譯參數(shù)與 fuzz 階段不一致使用同樣的編譯器和 sanitizer 選項這張表不能代替完整排查但它能在你剛上手時節(jié)省大量“對著日志發(fā)呆”的時間。5.2 靜態(tài)規(guī)則不命中的幾種原因最常見的原因就是目標(biāo)程序和規(guī)則庫中的正則不匹配。C 項目里很多人喜歡用宏和封裝函數(shù)比如memcpy被包成safe_copy靜態(tài)分析如果只看memcpy這個函數(shù)名看到的就是一條經(jīng)過封裝后名稱完全不同的調(diào)用鏈。解決辦法很簡單在規(guī)則庫里把safe_copy這類項目內(nèi)部封裝也聲明成 sink 族甚至直接參考目標(biāo)項目頭文件里的函數(shù)清單來擴展規(guī)則。間接調(diào)用是另一個頭疼的問題。函數(shù)指針、虛表調(diào)用、回調(diào)機制都會讓數(shù)據(jù)流分析難以追蹤。pasta 類框架在處理這類代碼時會比人工審計更吃力因為你需要額外配置“哪些指針是函數(shù)指針、它們可能指向哪些函數(shù)”這類信息。如果你發(fā)現(xiàn)某個候選路徑確實存在但靜態(tài)掃描沒抓到最直接的辦法是先用代碼搜索工具定位所有間接調(diào)用點再把可能的函數(shù)名補進規(guī)則。還有一個容易被忽略的原因構(gòu)建系統(tǒng)沒有正確生成編譯數(shù)據(jù)庫。pasta 分析需要知道你給它的目標(biāo)是用什么編譯參數(shù)構(gòu)建出來的如果compile_commands.json缺失或者不完整很多帶宏展開的代碼就無法進入分析范圍。我習(xí)慣在配置階段先跑一次項目的構(gòu)建確認(rèn)產(chǎn)物和數(shù)據(jù)庫都生成完整再啟動 pasta。5.3 harness 生成失敗和輸入格式問題harness 的本質(zhì)是一個“把任意字節(jié)流塞進目標(biāo)函數(shù)”的驅(qū)動。如果目標(biāo)程序是一個完整的可執(zhí)行文件主函數(shù)邏輯已經(jīng)占用了文件讀取和參數(shù)解析直接生成LLVMFuzzerTestOneInput就不一定有合適入口。這時候我通常的做法是先給目標(biāo)項目打補丁把核心解析邏輯抽成一個可復(fù)用的函數(shù)比如process_config然后讓 harness 只調(diào)用這個函數(shù)。依賴頭文件缺失也是高頻錯誤。很多項目只在特定模塊里暴露了接口harness 想引用相關(guān)頭文件結(jié)果編譯時發(fā)現(xiàn)引用的結(jié)構(gòu)體定義分散在多個私有目錄里。處理手段很粗暴但有效先編譯成靜態(tài)庫把需要暴露的接口單獨寫一個公共頭文件再讓 pasta 生成的 harness 去鏈接靜態(tài)庫。這樣程序集成員的可見性問題就繞開了。如果你發(fā)現(xiàn) pasta 生成的 harness 能編譯但一跑就崩先不要懷疑目標(biāo)程序先把 harness 放到一個最小環(huán)境里執(zhí)行一次確認(rèn)它是否在沒有目標(biāo)邏輯的情況下正常工作。這一步能很快區(qū)分“harness 自身 bug”和“目標(biāo)代碼漏洞”。5.4 符號執(zhí)行超時、路徑爆炸與內(nèi)存爆掉的處理符號執(zhí)行最經(jīng)典的老大難就是路徑爆炸。每遇到一個條件分支執(zhí)行路徑就會分裂真實程序動輒幾十萬條路徑如果全部探索內(nèi)存和時間的消耗都無法接受。我對付它有三個習(xí)慣第一給符號執(zhí)行設(shè)置單次執(zhí)行超時比如 30 秒超過就放棄當(dāng)前目標(biāo)第二限制最大路徑數(shù)量讓工具探索到一定數(shù)量后主動停止第三把探索范圍限制在單個函數(shù)或單個 source-sink 路徑上不要讓它從程序入口做全量勘探。還有一個容易被忽略的點約束求解器本身也不是無限能力。復(fù)雜的目標(biāo)可能大量使用浮點、非線性運算這類約束很多求解器無解。遇到這種情況我的處理方式是放棄對這種路徑做符號執(zhí)行改用動態(tài)插樁或者更豐富的字典來覆蓋。工具組合的價值就在于此這條路徑解不出來換一條思路總有一條能推進。如果目標(biāo)是大型網(wǎng)絡(luò)服務(wù)符號執(zhí)行基本沒法全程跑下去更適合把流量抓包轉(zhuǎn)換成語料再配合 fuzz 引擎做狀態(tài)化測試。pasta 的價值在輕量級解析類目標(biāo)上體現(xiàn)得最明顯遇到大型目標(biāo)時你要學(xué)會主動調(diào)整預(yù)期。5.5 崩潰結(jié)果如何去重和篩掉假陽性幾十個崩潰樣本并不代表幾十個漏洞它們往往指向同一個缺陷。去重最靠譜的依據(jù)不是文件內(nèi)容而是崩潰時的調(diào)用棧。用llvm-symbolizer或addr2line把 crash 地址轉(zhuǎn)成函數(shù)名加行號再用棧幀序列做指紋就能把同一缺陷的崩潰合并。篩假陽性時我按兩個維度判斷。第一個維度是崩潰位置是否在目標(biāo)函數(shù)內(nèi)部。如果是在外部工具庫、harness 自身代碼里先排除。第二個維度是可控性??幢罎l(fā)生后寄存器中的越界地址和源數(shù)據(jù)是否直接來自 fuzz 輸入如果完全不可控可能只是目標(biāo)代碼的潛在質(zhì)量缺陷不一定具備安全影響。不可控的崩潰也值得記錄但優(yōu)先級排在可控內(nèi)存破壞之后。還有一個實戰(zhàn)技巧把目標(biāo)程序的 sanitizer 輸出保存下來然后用腳本批量讀取崩潰目錄只保留輸出里包含ERROR: AddressSanitizer的樣本很多框架自身生成的臨時文件或由超時導(dǎo)致的“偽崩潰”會被自動濾掉效率比一個個手動跑高得多。6. 實操心得與后續(xù)擴展6.1 幾條讓 pasta 跑更快更穩(wěn)的經(jīng)驗第一個經(jīng)驗是先編譯成靜態(tài)庫。pasta 生成 harness 后如果每次都要重新全量編譯目標(biāo)項目時間損耗非常明顯。我通常先讓目標(biāo)項目出一個類似于.a的靜態(tài)庫然后 harness 只需要鏈接這個庫和少量驅(qū)動代碼迭代速度快很多。第二個經(jīng)驗是每個 fuzz 任務(wù)只聚焦一個 sink 族。如果你在一個 profile 里同時指定了 memcpy、system、sprintf 等所有 sinkfuzz 引擎的能量會被分散到多個方向上每個方向都跑不到深度。拆成多個 profile一次只跑一個 sink 族效果好得多。第三個經(jīng)驗是準(zhǔn)備一份字典。把目標(biāo)程序里常見的字段名、magic number、結(jié)構(gòu)標(biāo)記收集到一個字典文件里fuzz 引擎會優(yōu)先用這些詞去變異。我實測過一份好字典對覆蓋率的提升非常顯著尤其適合文本協(xié)議類目標(biāo)。第四個經(jīng)驗是設(shè)置超時上限。我一般給單輪 fuzz 配置 5 到 10 分鐘的超時不要讓引擎無限跑。把任務(wù)做成定時增量掃描比如每天晚上對新提交的代碼跑一輪比一次跑三天更能持續(xù)發(fā)現(xiàn)回歸問題。6.2 工具結(jié)果和人工審計怎么配合框架能幫你找到“可崩的路徑”但最終判斷“這是不是漏洞、影響面多大、怎么修”的活還是得人來干。我的工作流是讓 pasta 先跑出候選崩潰列表然后人工只關(guān)注前 10 名逐一確認(rèn)三個條件。第一外部輸入是否真正可控第二sink 調(diào)用點之前的校驗是否足夠弱比如沒有長度檢查或者只做了范圍判斷沒做內(nèi)容校驗第三崩潰是否穩(wěn)定復(fù)現(xiàn)且調(diào)用棧清晰。滿足這三個條件的候選基本可以進入漏洞報告流程。剩下的崩潰樣本即使不能確定可利用性我也會保留下來作為“質(zhì)量缺陷”備注因為這類不可控崩潰在特定版本或者特定配置下可能被誤用作不穩(wěn)定攻擊面。實際經(jīng)驗告訴我這對安全評估報告的完整度很有幫助。修復(fù)之后把崩潰樣本加進下一次 fuzz 的種子集用來做回歸驗證。這是很重要的一步否則修復(fù)可能只堵住一個入口換個格式繞過同樣的函數(shù)漏洞又復(fù)活了。6.3 后續(xù)可以怎么擴展規(guī)則與接入新引擎pasta 不是一組寫死的規(guī)則它最值得投入時間的地方是規(guī)則庫的積累。每個目標(biāo)項目都有自己特有的危險函數(shù)、封裝和調(diào)用鏈把它們做成自定義規(guī)則一次投入持續(xù)收益。長期跑下來這份規(guī)則庫會變成你對這個項目理解的可執(zhí)行表達(dá)比一長篇安全審計報告更容易傳承。引擎接入方面除了默認(rèn)的 fuzz 引擎我建議你把一些支持混合執(zhí)行的引擎放進來做交叉驗證。有些路徑用默認(rèn)引擎解不開換一個工具可能很快就突破了。符號執(zhí)行工具也是一樣多試幾種求解器不要在一種上死磕。如果你打算把這套流程放在日常安全評估里還可以考慮接入持續(xù)集成。每有新的提交自動對變更代碼跑一輪 pasta把新出現(xiàn)的崩潰樣本直接作為任務(wù)反饋給開發(fā)。這比人工定期全量掃描更能控制新漏洞的引入速度也是我目前最推薦的一種落地形態(tài)。最后說點我自己的使用感受。pasta 并不神奇它沒有把安全評估完全自動化它真正改變的是我分配精力的方式。以前拿到一個新目標(biāo)我會先讀代碼一遍遍檢查每個輸入點可能流到哪兒現(xiàn)在我把入口和 sink 清單扔給框架讓它先跑出真正的崩潰再用分析器確認(rèn)根因。有時候跑出來的結(jié)果確實會掃出一兩個我自己也不認(rèn)識的路徑這些路徑通常就是人工審計里最容易漏掉的部分。如果你也經(jīng)常面對“目標(biāo)太多、時間太少”我建議你從自帶的樣例開始先跑通一個最小閉環(huán)再把它用到真實目標(biāo)上。