本文介绍了如何使用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==