Go-知識測試-模糊測試
- 1. 定義
- 2. 例子
- 3. 數據結構
- 4. tesing.F.Add
- 5. 模糊測試的執行
- 6. testing.InternalFuzzTarget
- 7. testing.runFuzzing
- 8. testing.fRunner
- 9. FuzzXyz
- 10. RunFuzzWorker
- 11. CoordinateFuzzing
- 12. 總結
建議先看:https://blog.csdn.net/a18792721831/article/details/140062769
Go-知識測試-工作機制
1. 定義
模糊測試(Fuzzing)是一種通過構造隨機數據對代碼進行測試的測試方法,相比于單元測試,
它能提供更為全面的測試覆蓋,從而找出代碼中的潛在漏洞。
從1.18開始,Go開始正式支持模糊測試。
模糊測試要保證測試文件以_test.go
結尾。
測試方法必須以FuzzXxx
開頭。
模糊測試方法必須以*testing.F
作為參數。
2. 例子
假設有個函數,根據輸入內容,將輸入進行翻轉然后在和輸入拼接,從而返回一個回文串(不一定是嚴格意義下的回文串)
函數如下:
func PalindromeStr(in string) string {b := []byte(in)for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {b[i], b[j] = b[j], b[i]}return in + string(b)
}
接著使用單元測試
func TestPalindromeStr(t *testing.T) {testCase := []struct{ in, out string }{{"abc", "abccba"},{"abcdef", "abcdeffedcba"},{"abcdefg", "abcdefggfedcba"},{" ", " "},}for _, c := range testCase {o := PalindromeStr(c.in)if o != c.out {t.Error("Not equal", c.out, "got:", o)}}
}
使用go test -v
執行示例測試,-v 表示控制臺輸出結果
接著使用模糊測試
func FuzzPalindromeStr(f *testing.F) {testCase := []string{"abc", "def", " ", "a", "aaa", "aaaaaaaaaaaaaaaaaaaa"}for _, c := range testCase {f.Add(c) // 輸入測試種子}f.Fuzz(func(t *testing.T, a string) {b := PalindromeStr(a)// 返回結果進行判斷,回文串的規則就是第一個字符和最后一個字符相同,依次類推for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {if b[i] != b[j] {t.Error("Not palindrome")}}})
}
使用go test -fuzz=Fuzz -fuzztime=100s
啟動模糊測試,-fuzz表示執行模糊測試,-fuzztime表示持續時間
發現執行了100s,也沒法問題。
那么我們就自動構造一個錯誤,如果是utf-8字符,返回回文串,否則返回輸入內容,模擬異常邏輯:
func PalindromeStr(in string) string {b := []byte(in)if !utf8.Valid(b) {return in}for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {b[i], b[j] = b[j], b[i]}return in + string(b)
}
因為單元測試中沒有中文字符,所以單元測試通過,但是模糊測試呢:
報錯了,同時testdata目錄中有相應的輸入和輸出
非utf8字符串,觸發了錯誤邏輯
3. 數據結構
由于模糊測試可以覆蓋人類經常忽略的邊界用例,因此模糊測試對于發現安全漏洞特別有價值。
模糊測試的結構如下:
看起來很像單元測試的擴展。
一個模糊測試可以分為兩部分,一是通過 f.Add 添加隨機種子,二是通過 f.Fuzz 函數開始隨機測試。標記測試結果的方法與之前的單元測試是通用的。
模糊測試的testing.F
結構:
go在1.18中,在testing的包中增加了fuzz.go
文件,支持模糊測試:
type F struct {common // 通用測試結構,更多見 https://blog.csdn.net/a18792721831/article/details/140062769fuzzContext *fuzzContext // 與 testContext 類似,用于控制執行testContext *testContextinFuzzFn bool // 標記 fuzz 是否在運行中corpus []corpusEntry // 種子,語料庫result fuzzResult // 模糊測試結果fuzzCalled bool // 是否啟動
}
通用測試結構 common 提供了諸如標記測試結果的能力,而增加的 corpus 則用于保存通過 f.Add 添加的種子和測試過程中生成的隨機輸入。
每次執行 f.Add 都會生成一個 corpusEntry 對象,然后加入 corpus 語料庫中。
corpusEntry 結構用于保存待測函數的所有輸入:
type corpusEntry = struct {Parent stringPath stringData []byteValues []any // Values 要與待測函數索旭耀的參數完全一致Generation intIsSeed bool
}
f.Add 每次添加的種子個數需要與待測函數所需要的參數完全一致,因為測試執行時,每次取出一組種子作為函數的入參。
4. tesing.F.Add
func (f *F) Add(args ...any) {// 將輸入的參數加入到數組中var values []anyfor i := range args {// 模糊測試只能支持基本數據類型,對于復雜類型是不支持的if t := reflect.TypeOf(args[i]); !supportedTypes[t] {panic(fmt.Sprintf("testing: unsupported type to Add %v", t))}values = append(values, args[i])}f.corpus = append(f.corpus, corpusEntry{Values: values, IsSeed: true, Path: fmt.Sprintf("seed#%d", len(f.corpus))})
}
支持模糊測試的參數類型:
var supportedTypes = map[reflect.Type]bool{reflect.TypeOf(([]byte)("")): true,reflect.TypeOf((string)("")): true,reflect.TypeOf((bool)(false)): true,reflect.TypeOf((byte)(0)): true,reflect.TypeOf((rune)(0)): true,reflect.TypeOf((float32)(0)): true,reflect.TypeOf((float64)(0)): true,reflect.TypeOf((int)(0)): true,reflect.TypeOf((int8)(0)): true,reflect.TypeOf((int16)(0)): true,reflect.TypeOf((int32)(0)): true,reflect.TypeOf((int64)(0)): true,reflect.TypeOf((uint)(0)): true,reflect.TypeOf((uint8)(0)): true,reflect.TypeOf((uint16)(0)): true,reflect.TypeOf((uint32)(0)): true,reflect.TypeOf((uint64)(0)): true,
}
除了這些之外的類型都不支持。
5. 模糊測試的執行
在src/tesing/fuzz.go
的initFuzzFlags
中定義了模糊測試的參數:
func initFuzzFlags() {matchFuzz = flag.String("test.fuzz", "", "run the fuzz test matching `regexp`")flag.Var(&fuzzDuration, "test.fuzztime", "time to spend fuzzing; default is to run indefinitely")flag.Var(&minimizeDuration, "test.fuzzminimizetime", "time to spend minimizing a value after finding a failing input")fuzzCacheDir = flag.String("test.fuzzcachedir", "", "directory where interesting fuzzing inputs are stored (for use only by cmd/go)")isFuzzWorker = flag.Bool("test.fuzzworker", false, "coordinate with the parent process to fuzz random values (for use only by cmd/go)")
}
首先使用-fuzz=reg
觸發模糊測試,-fuzztime=30s
指定模糊測試持續的時間,如果不指定,則一直運行。
-fuzzminimizetime
最小失敗時間,默認一分鐘,-fuzzcachedir
緩存目錄,默認是命令執行目錄,fuzzworker
工作目錄,默認是命令執行目錄。
6. testing.InternalFuzzTarget
在testing.M
中,對于單元測試,示例測試和性能測試,都有一個內部類型用于存儲編譯生成的執行參數。模糊測試也有:
在1.18中三種內部類型增加成4種了。
在編譯的時候,load操作也增加了 Fuzz
開頭的模糊測試函數
在渲染測試的main入口中,也增加了模糊測試的模板
在testing.M.Run中,增加了模糊測試的支持
InternalFuzzTarget的結構:
type InternalFuzzTarget struct {Name stringFn func(f *F)
}
很簡單,和單元測試等的結構非常類似,name和對應的func,func 的參數是 *testing.F
7. testing.runFuzzing
在runFuzzing中首先對全部的模糊測試進行匹配,找到本次期望執行的模糊測試case
接著構造testing.F
對象,調用testing.fRunner
執行case
8. testing.fRunner
在testing.fRunner中啟動執行,類似于單元測試的 testing.tRunner。
性能測試是 testing.runN
示例測試是 testing.runExample
單元測試是 testing.tRunner
第一個defer函數主要處理這幾個事情:失敗后資源清理,保證測試報告完成,失敗退出,等待子測試完成,成功輸出報告。
第二個defer函數是等待所有的子測試完成后,發送信號,表示子測試結束。
9. FuzzXyz
接著回到模糊測試中:
func FuzzPalindromeStr(f *testing.F) {testCase := []string{"abc", "def", " ", "a", "aaa", "你好"}for _, c := range testCase {f.Add(c) // 輸入測試種子}f.Fuzz(func(t *testing.T, a string) {b := PalindromeStr(a)// 返回結果進行判斷,回文串的規則就是第一個字符和最后一個字符相同,依次類推for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {if b[i] != b[j] {t.Error("Not palindrome")}}})
}
模糊測試可以認為是兩部分,第一部分是輸入測試種子,第二部分是判決模糊的參數執行是否成功。
在testing.F.Add中,將參數種子放到了testing.F.corpus里面,并且要求輸入的種子參數的數量,每次Add的時候,必須和被測試方法的入參一致。
在第二部分的Fuzz中,首先對入參進行了校驗:
入參是一個func類型的參數,并且第一個參數是*testing.T
的參數,后面是可變參數。
第一個*testing.T
主要是復用了單元測試的測試管理能力,比如報告輸出,成功失敗的標記等等。
后面的可變參數則是被模糊測試的函數入參列表。
接著對可變參數列表進行類型判斷,只有基本類型才能模糊測試,如果入參中存在復雜類型,那么是無法模糊測試的。
接下來就是模糊的核心邏輯了,如何根據輸入的參數種子,派生更多的入參用例:
模糊測試的goroutine分為三種
這三種的含義先存疑。
在CheckCorpus中,對入參種子和可變參數進行校驗,確保種子數組中每一組都符合要求。
在ReadCorpus中,則是隨機取出本次執行的參數,如果是指定fuzz的目錄和id,那么會使用指定的目錄下的指定參數去執行
ReadCorpus調用了internal的fuzz實現
在ReadCorpus中調用了readCorpusData
不過上面都是執行特定的模糊case 。
在前面已知模糊測試的goroutine中有三種:
const (seedCorpusOnly fuzzMode = iotafuzzCoordinatorfuzzWorker
)
第一種 seedCorpusOnly 是 testing.runFuzzTests 中創建的,由testing.M調用
第二種 fuzzCoordinator 是 默認的,如果執行的go命令中沒有指定 test.fuzzworker , 默認是 false
在 testing.runFuzzing 中創建的
第三種 fuzzWorker 是由 命令行參數指定的。
根據這里的邏輯,基本上可以看出,seedCorpusOnly 是讀取指定模糊case, fuzzCoordinator是生成模糊case,fuzzWorker是執行case。
接著創建一個 testing.T 的對象,然后調用 testing.tRunner 執行case 。
testing.tRunner執行case的參數是 corpusEntry 類型的輸入。
也就是說,對于模糊測試,會先直接調用測試種子執行,然后會根據執行情況,在進行隨機參數。
如果是 fuzzCoordinator 類型的,那么執行 CoordinateFuzzing
如果是 fuzzWorker ,執行 RunFuzzWorker
如果是 seedCorpusOnly ,執行 run func,相當于直接用測試種子運行
在 fuzzWorker中,對于輸出做了重定向。
10. RunFuzzWorker
RunFuzzWorker方法是在internal中實現的
如果一個case經過了10還沒有被執行,就認為是餓死了
在serve方法中調用了workerServer.Fuzz 方法
并且是持續性調用的
在workerServer.fuzz中進行模糊測試
在workerServer.fuzz中,第一次調用直接使用種子
接著就是持續性測試了
在 mutator.mutate 中根據種子進行隨機
會對一次隨機的多個參數,隨機選擇一個參數,然后對這個參數進行隨機
比如對于整型,會隨機加或者減一個數
對于字符串,對字節碼隨機加減
也就是說,如果你的種子里面有中文,才會隨機中文。
11. CoordinateFuzzing
CoordinateFuzzing方法在internal中實現的
首先會對并發數,緩存目錄,日志等進行初始化
根據并發數,創建多個 worker 執行模糊測試
在coordinate中也是持續測試
如果沒有啟動,那么就調用啟動初始化等操作,如果收到了退出信號,那么就退出
如果隨機輸入已經生成,那么就使用隨機輸入調用
接著是for-select進行持續性測試,除非模糊測試失敗,或者執行模糊測試的時候,有設置超時時間,或者主動退出等
在workerClient.fuzz中執行
也是調用 mutator.mutate 進行隨機
同時,在調用FuzzXyz后,會記錄case的執行情況,用于分析執行的覆蓋率等等
12. 總結
Go 1.18 的 Fuzz 測試使用了一種稱為 “coverage-guided fuzzing” 的技術來生成隨機輸入。這種技術的基本思想是通過監視被測試代碼的覆蓋率來引導輸入的生成。
具體來說,Fuzz 測試首先使用你提供的種子值(seed values)來運行測試。然后,它會監視這些測試運行過程中哪些代碼被執行了,以及輸入值如何影響代碼的執行路徑。
接著,Fuzz 測試會嘗試修改種子值或者組合種子值,生成新的輸入,以嘗試覆蓋更多的代碼路徑。例如,如果你的種子值是字符串,Fuzz 測試可能會改變字符串的長度,添加、刪除或修改字符,等等。
如果新的輸入導致了更多的代碼被執行,或者觸發了新的代碼路徑,那么這個輸入就會被保存下來,用作后續測試的種子值。這樣,Fuzz 測試就可以逐漸 “學習” 如何生成能夠觸發更多代碼路徑的輸入。
這種方法可以有效地發現一些難以預見的邊界情況,特別是那些可能導致程序崩潰或者行為異常的情況。
需要注意的是,雖然 Fuzz 測試可以自動生成大量的輸入,但是它并不能保證完全覆蓋所有可能的輸入。因此,你仍然需要編寫單元測試和集成測試,以確保你的代碼在預期的輸入下能夠正確工作。
Coverage-Guided Fuzzing 相關論文和鏈接
Coverage-guided fuzzing 是一種基于代碼覆蓋率的模糊測試技術,通過生成輸入數據并監控代碼覆蓋率來發現潛在的錯誤和漏洞。這種方法的核心思想是通過最大化代碼覆蓋率來提高測試的有效性。
- “American Fuzzy Lop (AFL)”
AFL 是一種流行的 coverage-guided fuzzing 工具,由 Micha? Zalewski 開發。雖然 AFL 本身不是一篇論文,但它的設計和實現對該領域有著重要影響。
- 鏈接: AFL GitHub Repository
- “Fuzzing: Brute Force Vulnerability Discovery”
這篇論文由 Michael Sutton, Adam Greene, 和 Pedram Amini 撰寫,詳細介紹了模糊測試的基本概念和技術,包括 coverage-guided fuzzing。
- 鏈接: Fuzzing: Brute Force Vulnerability Discovery
- “Coverage-based Greybox Fuzzing as Markov Chain”
這篇論文由 Marcel B?hme, Van-Thuan Pham, 和 Abhik Roychoudhury 撰寫,提出了一種基于覆蓋率的灰盒模糊測試方法,并將其建模為馬爾可夫鏈。
- 鏈接: Coverage-based Greybox Fuzzing as Markov Chain
- “AFLFast: A Framework for Extremely Fast Fuzzing”
這篇論文由 Marcel B?hme, Van-Thuan Pham, Manh-Dung Nguyen, 和 Abhik Roychoudhury 撰寫,介紹了 AFLFast,這是一種改進的 AFL 版本,通過優化輸入生成策略來提高模糊測試的效率。
- 鏈接: AFLFast: A Framework for Extremely Fast Fuzzing
- “LibFuzzer: A Library for Coverage-Guided Fuzz Testing”
LibFuzzer 是 LLVM 項目的一部分,提供了一個用于覆蓋率引導模糊測試的庫。雖然沒有正式的論文,但其設計和實現文檔非常詳細。
- 鏈接: LibFuzzer Documentation
- “Fuzzing with Code Fragments”
這篇論文由 Patrice Godefroid, Hila Peleg, 和 Rishabh Singh 撰寫,提出了一種基于代碼片段的模糊測試方法,通過組合代碼片段來生成新的測試輸入。
- 鏈接: Fuzzing with Code Fragments
- “Evaluating Fuzz Testing”
這篇論文由 Marcel B?hme, Van-Thuan Pham, Manh-Dung Nguyen, 和 Abhik Roychoudhury 撰寫,評估了不同模糊測試工具和技術的有效性,包括 coverage-guided fuzzing。
- 鏈接: Evaluating Fuzz Testing
- “Fuzzing: Art, Science, and Engineering”
這篇論文由 Patrice Godefroid 撰寫,全面介紹了模糊測試的藝術、科學和工程,包括 coverage-guided fuzzing 的技術細節和應用。
- 鏈接: Fuzzing: Art, Science, and Engineering
這些論文和資源提供了關于 coverage-guided fuzzing 的深入理解和最新研究成果。通過閱讀這些文獻,你可以更好地理解這種技術的原理、實現和應用。