go語言模糊測試與oss-fuzz
本文介紹了如何使用OSS-fuzz對一些go項目進行模糊測試,oss-fuzz是谷歌提出的一款多引擎的模糊測試平臺,該平臺以docker為基礎,能夠實現多種語言的持續模糊測試。
Google希望通過“模糊測試(fuzz testing,fuzzing)”為程序提供隨機數據輸入,作為開源開發的標準部分,oss-fuzz能夠針對開源軟件進行持續的模糊測試,其測試開發團隊也提到“OSS-Fuzz的目的是利用更新的模糊測試技術與可拓展的分布式執行相結合,提高一般軟件基礎架構的安全性與穩定性。
OSS-Fuzz結合了多種模糊測試技術/漏洞捕捉技術(即原來的libfuzzer)與清洗技術(即原來的AddressSanitizer),并且通過ClusterFuzz為大規模可分布式執行提供了測試環境,當然fuzzer也可以選擇AFL,libfuzzer。
在oss-fuzz中,go語言由于其運行環境,如網絡問題的聯通性等問題,相比于C與C++編寫的項目,環境搭建較為復雜。本文以容器運行時項目containerd為例,演示如何使用oss-fuzz構建模糊測試句號并介紹了go語言模糊測試中的一個開源項目 go-fuzz-header。
OSS-fuzz
項目地址:https://github.com/google/oss-fuzz
先將oss-fuzz下載到本地,project文件夾下列出了集成到oss-fuzz中進行持續模糊測試的項目,目前有將近600個項目,如下圖所示:

其中每個項目下有3個主要的文件,分別為project.yaml,Dockerfile和build.sh。
project.yaml
該文件記錄了項目的基本信息,以containerd為例,該文件夾下的project.yaml如下:
homepage: "https://github.com/containerd/containerd"main_repo: "https://github.com/containerd/containerd"primary_contact: "security@containerd.io"auto_ccs : - "adam@adalogics.com"language: gofuzzing_engines: - libfuzzersanitizers: - address
- homepage: 項目地址
- main_repo:托管代碼的源代碼存儲庫的路徑
- laguange: 項目編寫的編程語言
- primary_contact, auto_css: 聯系人列表
- fuzzing_engines: 模糊測試所使用的引擎,如afl,libfuzzer
- sanitizer: 消毒劑,支持ASAN 和MSAN ,可以有效的提高模糊測試發現crash的概率
- architectures: 架構列表
- ...
Dockerfile
Dockerfile 為項目定義了docker 鏡像,build.sh也將在鏡像中運行,containerd的Dockerfile如下:
FROM gcr.io/oss-fuzz-base/base-builder-goRUN apt-get update && apt-get install -y btrfs-progs libc-dev pkg-config libseccomp-dev gcc wget libbtrfs-devRUN git clone --depth 1 https://github.com/containerd/containerdRUN git clone --depth 1 https://github.com/cncf/cncf-fuzzingCOPY build.sh $SRC/WORKDIR $SRC/containerd
- FROM: 規定了項目的基本鏡像
- RUN:構建鏡像時執行的命令,首先下載了一些必須的軟件和庫,之后下載了項目源碼
- COPY: 將build.sh復制到鏡像中
- WORKDIR :指定工作目錄
- ...
在Dockerfile中可以看到cncf-fuzzing的項目,該項目致力于將CNCF中的開源項目集成到OSS-fuzz中進行持續的模糊測試,如kubernetes、cri-o、runc等。
build.sh
構建腳本,用來編譯項目的,生成的二進制文件應放在$OUT中,以示例,一般就是編譯和復制語句,示例如下:
#!/bin/bash -eu ./buildconf.sh# configure scripts usually use correct environment variables../configure make cleanmake -j$(nproc) all $CXX $CXXFLAGS -std=c++11 -Ilib/ \ $SRC/parse_fuzzer.cc -o $OUT/parse_fuzzer \ $LIB_FUZZING_ENGINE .libs/libexpat.a cp $SRC/*.dict $SRC/*.options $OUT/
以下位置對應的環境變量
- $OUT ->/out:用來存儲構建好的文件
- $SRC -> /src: 放源文件的位置
- $WORK -> work: 存儲中間文件的位置
更多的變量可以參考官方文檔(https://google.github.io/oss-fuzz/)
環境準備
1.確保Docker安裝成功以及主機能夠訪問外網。
2.克隆 oss-fuzz項目代碼到本地。
3.設置Dockerd走代理以解決pull鏡像時無法訪問的問題。oss中用到的鏡像都需要從谷歌拉取,有兩種解決方法,給docker掛個代理然后直接pull ; 或者利用github+dockerhub的方法將所有的鏡像先拖到dockerhub上,然后將源碼中所有的gcr.io/oss-fuzz-base/xxxx改成對應dockerhub上的就行;這里使用直接給docker走代理的方式。
首先創建 /etc/systemd/system/docker.service.d/proxy.conf 配置文件,添加以下內容設置代理:
[Service]Environment="HTTP_PROXY=http://127.0.0.1:7890"Environment="HTTPS_PROXY=https://127.0.0.1:7890"Environment="NO_PROXY=127.0.0.1"
然后重新加載配置并重啟服務:
systemctl daemon-reloadsystemctl restart docker
檢查加載的配置是否生效:
systemctl show docker --property Environment
4.修改containerd文件下的Dockerfile如下,首先是加了ENV 配置環境變量設置容器內部的網絡代理,確定容器內部能夠在git clone 或者 go install 等命令時不會報錯,這里注意地址要填主機的ip,不能是127.0.0.1;其次修改containerd的分支為1.6版本,因為最新版本的containerd 的go mod 規定的是go語言的1.18版本, 目前的go環境基礎容器暫時不支持 go 1.18。
FROM gcr.io/oss-fuzz-base/base-builder-goRUN apt-get update && apt-get install -y btrfs-progs libc-dev pkg-config libseccomp-dev gcc wget libbtrfs-devENV HTTP_PROXY "http://192.168.xx.xx:7890"ENV HTTPS_PROXY "http://192.168.xx.xx:7890"RUN git clone https://github.com/containerd/containerdWORKDIR containerdRUN git checkout -b remotes/origin/release/1.6 remotes/origin/release/1.6WORKDIR $SRCRUN git clone --depth 1 https://github.com/cncf/cncf-fuzzingCOPY build.sh $SRC/WORKDIR $SRC/containerd
構建Harness進行模糊測試
1.構建fuzz的基本鏡像
cd /path/to/oss-fuzzpython infra/helper.py build_image containerd
第一次構建需要下載很多基礎鏡像,如果在pull 鏡像時出現gcr.io的網絡連接問題則需要檢查代理是否生效。
成功構建鏡像后,查看鏡像列表,應該如下圖所示,包括oss-fuzz提供的幾個基礎環境的鏡像,和紅框內的構建的containerd的鏡像。

2.構建fuzz目標
python infra/helper.py build_fuzzers container
構建完成后,在/path/to/oss-fuzz/build/out/containerd 文件夾會生成對應編譯好的harness二進制文件,如 fuzz_apply、fuzz_archive_export、fuzz_parse_auth 等,每一個harness對應的一個fuzz目標。

harness的構建源碼可以從containerd(https://github.com/containerd/containerd/tree/11de19af68c7d21c8fe01058026257ecd5d6ed13/contrib/fuzz)項目中找到,每一個fuzz函數都對應一個harness。下圖為containerd中的構建腳本,其中 compile_go_fuzzer 對應的編譯引擎為go-fuzz,在containerd中使用的是在go-fuzz基礎上改進的go-fuzz-header。compile_native_go_fuzzer 對應的是 go 1.18 中的原生模糊測試。

在CNCF的很多go語言項目的模糊測試中,都用到了go-fuzz-header,前面的文章中已經介紹了go-fuzz(https://bbs.pediy.com/thread-271810.htm)和go native fuzz(https://bbs.pediy.com/thread-271810.htm),相比于go-fuzz,go原生模糊測試引擎除了標準字節數組外,還可以為 Harness 提供如int,bool等多種類型 , 但一些項目可能需要更復雜的類型,如結構、映射和切片 ,go-fuzz-header就是為了解決對復雜類型的結構體進行模糊測試的挑戰而出現的。以 containerd 中的 FuzzParseAuth (https://bbs.pediy.com/thread-271810.htm)為例:
package fuzz import ( fuzz "github.com/AdaLogics/go-fuzz-headers" runtime "k8s.io/cri-api/pkg/apis/runtime/v1" "github.com/containerd/containerd/pkg/cri/server") func FuzzParseAuth(data []byte) int { f := fuzz.NewConsumer(data) auth := &runtime.AuthConfig{} err := f.GenerateStruct(auth) if err != nil { return 0 } host, err := f.GetString() if err != nil { return 0 } _, _, _ = server.ParseAuth(auth, host) return 1}
go-fuzz-header首先使用模糊引擎提供的隨機字節 data 創建一個新的Consumer , 之后f調用GenerateStruct方法根據模糊測試引擎提供的隨機數據來填充auth結構體進行測試。
3.開始fuzz,選擇一個或者多個fuzz 對象開始進行模糊測試,以 fuzz_image_store為例,其中 --corpus-dir 參數可以指定種子目錄,不加該參數默認以空語料庫進行fuzz。
python infra/helper.py run_fuzzer --corpus-dir=./build/out/containerd/corpus containerd fuzz_image_store
在oss-fuzz中,對于go語言模糊測試默認使用 go-fuzz 來編譯harness,之后使用libfuzzer作為引擎進行fuzz,fuzz開始后會在out文件夾下生成一個文件夾,用來存放相關輸出。

經過漫長的等待之后可能會發生崩潰。

崩潰信息如下:
runtime: unexpected return pc for runtime.gopark called from 0x0stack: frame={sp:0x10c000078f40, fp:0x10c000078f60} stack=[0x10c000078000,0x10c000079000)0x000010c000078e40: 0x0000000000000000 0x00000000000000000x000010c000078e50: 0x0000000000000000 0x00000000000000000x000010c000078e60: 0x7a75662f706d742f 0x3833303039332d7a0x000010c000078e70: 0x39332d7a7a75662f 0x34303835383330300x000010c000078e80: 0xdef0995b8d5812aa 0x758f15f0dcd675250x000010c000078e90: 0xee5d5b00aa1475d6 0x1d3fd1a2d44b05790x000010c000078ea0: 0x0000000000000000 0x00000000000000000x000010c000078eb0: 0x0000006901000000 0x00000000000000000x000010c000078ec0: 0x0000000000000000 0x00000000000000000x000010c000078ed0: 0x0000000000070000 0x00000000000000000x000010c000078ee0: 0xffffffffffffffff 0x00ffffffffffffff0x000010c000078ef0: 0x000010c0001ddb80 0x000010c0005a36000x000010c000078f00: 0x000010c0004651e0 0x000010c0004653400x000010c000078f10: 0x000010c0001dc420 0x000010c0003920e00x000010c000078f20: 0x000010c0001948d0 0x000010c0001906b00x000010c000078f30: 0x000010c000582f90 0x000010c000582fd00x000010c000078f40: <0x000010c0005837d0 0x000010c0001905900x000010c000078f50: 0x0000000000000000 !0x00000000000000000x000010c000078f60: >0x000093f73283d9b8 0x000010c0001282c00x000010c000078f70: 0x0000000000001418 0x00000000000000000x000010c000078f80: 0x0000000000000000 0x00000000000000000x000010c000078f90: 0x00000a8c46505853 0x00000000000002070x000010c000078fa0: 0x0000000000000a88 0x00000000000000000x000010c000078fb0: 0x0000000000000000 0x00000000000000000x000010c000078fc0: 0x0000000000000203 0x00000000000000000x000010c000078fd0: 0x0000000000000000 0x00000000000000000x000010c000078fe0: 0x0000000000000000 0x00000000000000000x000010c000078ff0: 0x0000000000000000 0x0000000000000000fatal error: unknown caller pc runtime stack:runtime.throw({0x1f3f3fb, 0x328e0e0}) runtime/panic.go:1198 +0x71runtime.gentraceback(0x7f220a620c90, 0x1, 0x0, 0x7f220a620b30, 0x0, 0x0, 0x7fffffff, 0x7f220a620c90, 0x0, 0x0) runtime/traceback.go:274 +0x1956runtime.scanstack(0x10c000001ba0, 0x10c000051698) runtime/mgcmark.go:748 +0x197runtime.markroot.func1() runtime/mgcmark.go:232 +0xb1runtime.markroot(0x10c000051698, 0x1f) runtime/mgcmark.go:205 +0x170runtime.gcDrain(0x10c000051698, 0x3) runtime/mgcmark.go:1013 +0x379runtime.gcBgMarkWorker.func2() runtime/mgc.go:1269 +0xa5runtime.systemstack() runtime/asm_amd64.s:383 +0x46 goroutine 6 [GC worker (idle)]:runtime.systemstack_switch() runtime/asm_amd64.s:350 fp=0x10c00006af60 sp=0x10c00006af58 pc=0x5c4a20runtime.gcBgMarkWorker() runtime/mgc.go:1256 +0x1b3 fp=0x10c00006afe0 sp=0x10c00006af60 pc=0x5790b3runtime.goexit() runtime/asm_amd64.s:1581 +0x1 fp=0x10c00006afe8 sp=0x10c00006afe0 pc=0x5c6cc1created by runtime.gcBgMarkStartWorkers runtime/mgc.go:1124 +0x25 goroutine 17 [runnable, locked to thread]:runtime.goexit() runtime/asm_amd64.s:1581 +0x1 goroutine 7 [chan receive]:k8s.io/klog/v2.(*loggingT).flushDaemon(0x0) k8s.io/klog/v2@v2.30.0/klog.go:1181 +0x8bcreated by k8s.io/klog/v2.init.0 k8s.io/klog/v2@v2.30.0/klog.go:420 +0x115AddressSanitizer:DEADLYSIGNAL===================================================================12==ERROR: AddressSanitizer: ABRT on unknown address 0x00000000000c (pc 0x0000005c85e1 bp 0x7f220a620678 sp 0x7f220a620660 T8)SCARINESS: 10 (signal) #0 0x5c85e1 in runtime.raise.abi0 runtime/sys_linux_amd64.s:165 #1 0x5aa097 in runtime.crash runtime/signal_unix.go:861 #2 0x593c70 in runtime.fatalthrow.func1 runtime/panic.go:1257 #3 0x593bef in runtime.fatalthrow runtime/panic.go:1250 #4 0x5939b0 in runtime.throw runtime/panic.go:1198 #5 0x5b9975 in runtime.gentraceback runtime/traceback.go:274 #6 0x57b856 in runtime.scanstack runtime/mgcmark.go:748 #7 0x57a790 in runtime.markroot.func1 runtime/mgcmark.go:232 #8 0x57a54f in runtime.markroot runtime/mgcmark.go:205 #9 0x57c3b8 in runtime.gcDrain runtime/mgcmark.go:1013 #10 0x579404 in runtime.gcBgMarkWorker.func2 runtime/mgc.go:1269 #11 0x5c4a85 in runtime.systemstack.abi0 runtime/asm_amd64.s:383 DEDUP_TOKEN: runtime.raise.abi0--runtime.crash--runtime.fatalthrow.func1AddressSanitizer can not provide additional info.SUMMARY: AddressSanitizer: ABRT runtime/sys_linux_amd64.s:165 in runtime.raise.abi0Thread T8 created by T3 here: #0 0x50d32c in __interceptor_pthread_create /src/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:207:3 #1 0x55d070 in _cgo_try_pthread_create /_/runtime/cgo/gcc_libinit.c:100:9 #2 0x599d86 in runtime.newm runtime/proc.go:2230 #3 0x59a46e in runtime.startm runtime/proc.go:2485 #4 0x59a999 in runtime.wakep runtime/proc.go:2584 #5 0x59c164 in runtime.resetspinning runtime/proc.go:3216 #6 0x59c71d in runtime.schedule runtime/proc.go:3374 #7 0x59cc4c in runtime.park_m runtime/proc.go:3516 #8 0x5c4a04 in runtime.mcall runtime/asm_amd64.s:307 DEDUP_TOKEN: __interceptor_pthread_create--_cgo_try_pthread_create--runtime.newmThread T3 created by T1 here: #0 0x50d32c in __interceptor_pthread_create /src/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:207:3 #1 0x55d070 in _cgo_try_pthread_create /_/runtime/cgo/gcc_libinit.c:100:9 #2 0x599d86 in runtime.newm runtime/proc.go:2230 #3 0x59a46e in runtime.startm runtime/proc.go:2485 #4 0x59a999 in runtime.wakep runtime/proc.go:2584 #5 0x59e897 in runtime.newproc.func1 runtime/proc.go:4261 #6 0x5c4a85 in runtime.systemstack.abi0 runtime/asm_amd64.s:383 DEDUP_TOKEN: __interceptor_pthread_create--_cgo_try_pthread_create--runtime.newmThread T1 created by T0 here: #0 0x50d32c in __interceptor_pthread_create /src/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:207:3 #1 0x55cfbf in _cgo_try_pthread_create /_/runtime/cgo/gcc_libinit.c:100:9 #2 0x55cfbf in x_cgo_sys_thread_create /_/runtime/cgo/gcc_libinit.c:27:12 #3 0x1f0cb0c in __libc_csu_init (/out/fuzz_image_store+0x1f0cb0c) DEDUP_TOKEN: __interceptor_pthread_create--_cgo_try_pthread_create--x_cgo_sys_thread_create==12==ABORTINGMS: 2 EraseBytes-ChangeBinInt-; base unit: feb33bf726c50d41c5dc2c8cea890cb18040c1f80x10,0xd,0xb,0x3b,0x2,0x0,0x0,0x0,0x0,0x0,0x84,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x8,0x0,0x0,0x0,0x0,0x0,0x3,0xfa,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,\020\015\013;\002\000\000\000\000\000\204\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\010\000\000\000\000\000\003\372\000\000\000\000\000\000\000\000\000\000\000\000artifact_prefix='./'; Test unit written to ./crash-66c182f8f6dac7209a14e631d117b0879331cbfeBase64: EA0LOwIAAAAAAIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAD+gAAAAAAAAAAAAAAAA==