引言
Hi!
欢迎查看「WINGFUZZ SaaS版」在线帮助文档,本文档将包括「快速开始」教程,实战测试DEMO,进阶使用指导,以及系统相关介绍等内容。
通过本文档,希望您可以顺利掌握 WINGFUZZ SaaS 的使用流程,体验到智能模糊测试这一强大技术的魅力,找到更多bug :)
同时,也请善用文档自带的搜索功能,以助解决使用过程中遇到的问题。
也别忘记加入我们的用户社区,向我们直接反馈问题:
接下来,就请开始您的Fuzzing之旅吧!
什么是智能模糊测试?
模糊测试(Fuzzing、Fuzz Testing) 是一种自动化的软件安全与质量测试技术。
传统模糊测试将自动生成海量带有随机性的测试用例,输入至动态运行中的待测软件,触发软件异常,发现软件缺陷。作为一种动态测试技术,模糊测试可以提供触发异常的实际输入,并可以保证极低的误报率。
智能模糊测试 则通过灰盒插桩、覆盖率引导、Sanitizer等新技术的引入,极大提升了总体测试效果,并可覆盖更多缺陷类型,同时提供代码行级别的缺陷定位,协助用户进行软件修复。
可以认为,模糊测试主要包含3个环节模块。首先,引擎自动生成测试用例;其次,将用例输入至运行中的待测对象;最后,触发异常情况并通过检测器发现缺陷。智能模糊测试则在3个模块的基础上,引入了反馈学习机制,通过监控待测对象运行情况,收集信息反馈至生成模块,引导生成更有针对性的精准用例,最终覆盖更多程序分支,发现更多BUG。
智能模糊测试可针对多种语言,有效发现多类软件缺陷,如内存问题、逻辑问题、未定义行为、未知崩溃等,这些缺陷类型通常是影响软件质量与安全性的高危问题。
当前,智能模糊测试技术已在全球业界广泛应用,Google、Microsoft、Linux基金会、CNCF基金会等组织均使用这一技术针对关键基础软件开展质量与安全测试。智能模糊测试也已在众多关键软件项目中发现大量真实缺陷,如OpenSSL、Chromium、Linux、Office、MariaDB等等。
我的第一次模糊测试
在本章中,我们会引导您从零开始,完成安装、编译、测试几个关键步骤,体验 WINGFUZZ 的基本使用流程。
为了开始体验,您需要先注册WINGFUZZ平台的账号,申请试用。【点击此处进入注册页面】
申请后,我们会有客服人员和您沟通确认,开通使用权限。
您可以进入【联系我们】章节查看我们的联系方式。
安装 WINGFUZZ SDK
1. 确认系统依赖
在安装之前,您应该确认系统满足以下的依赖:
- X86_64 架构的 Linux 系统
- glibc >= 2.17 (可以使用
ldd --version
命令查看) - 基本命令行工具,包括:
curl
,tar
,xz
,sha1sum
等。(大部分Linux发行版已经自带这些功能,如果没有,请安装coreutils
工具包。)
是否支持其他架构?
除了常见的x86_64架构,我们还支持x86(32bit)、ARM、MIPS、龙芯等。 这些架构的支持是「WINGFUZZ专业版」产品的一部分,请咨询我们的客服了解详细信息。
2. 下载安装SDK
使用您的账号登录WINGFUZZ系统,您应该能在系统的右上角看到一个「SDK」图标,点击这个图标,就可以进入SDK下载页面。
首先是选择操作系统,目前WINGFUZZ SDK只支持Linux操作系统,您只能选择此项。
其次是选择安装方式。 选择「只针对当前用户安装」时,SDK会安装在您的用户目录下,不需要root权限,但只有您的用户能够使用。 选择「全局安装」时,SDK会安装在系统的/usr/目录下,因此需要root权限,安装后该机器上所有用户均可使用。 您可以根据实际情况选择安装方式。
选择之后,复制下方的安装脚本,在终端中粘贴并运行,就可以开始安装WINGFUZZ SDK了。
在安装过程中,会自动弹出一个网页,请求授权SDK登录,在弹出的网页中直接点击确认即可。如果该网页需要登录,您可以使用您的账号登录后,就可以看到授权页面。
如果您在非图形化环境中(远程ssh连接等),安装程序无法自动弹出浏览器,此时控制台中会显示一个网址,您需要在可用的浏览器中访问这个网址,就可以看到授权页面。访问该网址的浏览器和SDK并不需要是同一台机器。
稍等一会,安装程序会自动下载SDK并安装,当您看到如下的提示时,说明安装已经成功完成:
Install finished
WingFuzz is installed at /home/user/.wfuzz
You can use 'wfuzz' command after restart terminal
3. 激活环境变量
如果在上一步,您选择了「全局安装」,则不需要进行这一步,直接进入【4. 验证安装】即可。
如果您选择了「只针对当前用户安装」,由于PATH环境变量此时尚未更新,此时还不能使用 wfuzz
命令。
使用以下两种方式之一可以激活PATH环境变量的修改:
- 重启当前终端,对于图形化的系统,您可以关闭控制台再打开,对于远程连接,您可以断开重连,即可激活环境变量。
- 如果您不想断开当前会话,也可以手动运行
source ~/.bashrc
命令,即可激活环境变量。
4. 验证安装
在命令行中输入 wfuzz
,如果出现以下的消息,则说明安装已经成功。
Usage: wfuzz COMMAND [OPTIONS]
Add --help after any command to show help for the command.
Commands:
run SHELLCMD : Execute any shell command with wfuzz environments.
replay [MODULE] [HASH] : Replay a crash.
fuzz MODULE : Run fuzzing test for MODULE.
install [OPTIONS] : Install the packages required for wfuzz .
uninstall [OPTIONS] : Uninstall the packages required for wfuzz .
login : Login to wfuzz platform.
logout : Logout to wfuzz platform.
env : Show wfuzz managed environment variables.
config : Show wfuzz configuration information.
编写测试文件
安装完成后,您可以使用这个简单的例程来启动模糊测试:
将下面的内容存储为 first-fuzz.cpp
:
#include <stdio.h>
#include <string.h>
#include <string>
#include <wfuzz.h>
void echo(std::string content) {
char buf[10];
strncpy(buf, content.c_str(), 10);
printf("%s\n", buf);
}
WFUZZ_TEST_ENTRYPOINT(echo);
这段程序的错误在哪?
strncpy
函数虽然在写入的时候,不会超过目标的长度, 但它并不保证目标会以\0
结束,当源字符串长度大于目标长度时, 它会拷贝源字符串的前n个字符,并不会写入\0
。后续的
printf
函数的%s
接受一个 c-style-string, 它假设字符串以\0
结束,如果发生了上面的情况, 则打印字符串的长度会超出buf的范围,直到在后续的内存中找到\0
为止。 由于buf
在栈上,这里是一个典型的栈缓冲区溢出(stack-buffer-overflow)错误。
WFUZZ_TEST_ENTRYPOINT(echo)
是什么?这是 WINGFUZZ 的测试入口,由引擎生成的测试用例数据将从
echo
函数传入。关于测试入口、测试驱动的详细介绍,可参看【编写测试驱动】章节。
我用过开源的模糊测试工具 LibFuzzer / AFL,例程好像不太一样?
首先,WINGFUZZ 兼容 LibFuzzer 的测试入口,如果您手头有现成程序,可以直接使用。
那我们为什么要做这样的优化呢?
开源的模糊测试工具一般都是接受一个原始缓冲区,例如 LibFuzzer 的测试入口写法就只能是
LLVMFuzzerTestOneInput(char *buffer, size_t length)
, 但实际程序中函数调用的参数一般是结构化的数据。 对于开源的模糊测试工具来说,我们需要手写转换器,将入口传入的数据对接到实际待测函数可接受的类型上。 这个过程既麻烦,又容易出错,可能导致测出的问题只是转换过程中的问题,而非实际代码的问题。WINGFUZZ的测试入口支持结构化的输入,可以直接使用目标数据结构, 由WINGFUZZ测试框架自动生成结构化的数据,避免了上面的问题。 同时,数据结构的信息还会反馈给生成器,能够更有效的生成测试数据,增加测试覆盖率。
阅读章节【进阶使用-数据类型支持】可以进一步了解WINGFUZZ原生支持的数据类型和如何扩展类型支持。
编译待测程序
使用以下命令可以编译这个文件:
wfuzz-c++ -o first-fuzz first-fuzz.cpp
编译完成后,请在当前目录下查看 first-fuzz
文件,如果存在,说明已经成功编译了待测程序。
实际发生了什么?
wfuzz-cc
/wfuzz-c++
内部使用了clang
/clang++
编译器。 我们会为编译命令添加必要的参数,在链接时添加必要的运行时库。 使用wfuzz-cc
编译和使用clang
编译本质上是一样的。 因此,如果您的程序支持clang
,就可以无缝替换为wfuzz-cc
。 我们使用的clang版本为12.0.0。
开始我的第一次模糊测试
终于准备好了!
执行下面的命令,就可以开始测试了:
wfuzz fuzz first-fuzz
经过短暂的启动检查后,您可以看到如下的界面:
┌───────────────────────────────────────────────────────────────────────────────┐
│██╗ ██╗███████╗██╗ ██╗███████╗███████╗ by Wingtecher 1.6.10-fast │
│██║ ██║██╔════╝██║ ██║╚══███╔╝╚══███╔╝ │
│██║ █╗ ██║█████╗ ██║ ██║ ███╔╝ ███╔╝ Platform: │
│██║███╗██║██╔══╝ ██║ ██║ ███╔╝ ███╔╝ https://wingfuzz.com │
│╚███╔███╔╝██║ ╚██████╔╝███████╗███████╗ Login as: │
│ ╚══╝╚══╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝ USERNAME │
├──────────────────────────────────────────┬────────────────────────────────────┤
│ Regression │ Fuzz │
├──────────────────────────────────────────┴────────────────────────────────────┤
│ App: first-fuzz Module: echo │
├───────────────────────────────────────────────────────────────────────────────┤
│ ███████████ │
├─────────────────────────────┬────────────────────────┬────────────────────────┤
│Status: NULL │Test Case: 0 │ │
│Job: 1 │Path: 0 │Crash: 0 │
│CPU Time: 0s │Execution: 0 │ │
│Start: 08/10 20:43 │Exec/Sec: 0 │ │
├─────────────────────────────┴────────────────────────┴────────────────────────┤
│Fuzzer #1: Starting fuzzer instance afl++ │
│Fuzzer #1: Lifecycle starting │
│Fuzzer #1: Lifecycle running │
│ │
└───────────────────────────────────────────────────────────────────────────────┘
看到这个界面说明您的测试已经成功启动了。这个例程在模糊测试面前太简单了,您应该可以立即看到Crash的数字变成了1。 接下来您可以使用以下步骤,在平台上看到详细的错误报告。
- 在平台导航菜单中找到「应用测试」 -> 「项目管理」。
- 在右边的视图中找到
first-fuzz
这个应用。 - 进入详情后,选择模块
echo
。 - 点击进去后,在下方的缺陷列表就能看到一个缺陷,类型为内存问题,详情中有stack-buffer-overflow-read。
- 点击缺陷编号进入页面,可以看到缺陷详情,下方会有触发时的调用栈。
默认参数下模糊测试会一直运行下去,当您想停止测试时,输入 Ctrl-C
就可以停止。
为什么测试是在本地运行,而不是云端运行?
WINGFUZZ同时提供本地测试和远程测试两种模式。
本地测试的模式更适合测试驱动开发、调试的过程,因为可以更高效地获取系统反馈,开展debug工作。远程测试则适合驱动开发完成后,利用云端算力开展实际测试。因此,在本文档的前几个章节,我们都将基于本地测试模式,介绍WINGFUZZ的基础用法,便于用户及时发现问题。
而远程测试模式当然也已经开放,详情可参见【远程测试】章节。
如果您完成了这个例子,想要更进一步了解模糊测试在实际项目上如何使用,请继续查看【开始实战测试】章节。
关于 「应用」、「模块」 等概念,您可以参考 进阶使用 章节。
关于 「内存问题」、「未定义行为」 等错误类型,您可以参考 支持的问题类型 章节。
开始实战测试
在上一章中,我们使用了一个简单的例程,开始了模糊测试。您一定很关心,如何在一个实际项目上开始模糊测试,测出实际的问题。本章我们以一个非常知名的软件库OpenSSL为例,介绍如何进行它的模糊测试。
在此次测试中,我们将使用OpenSSL比较老的一个版本,演示如何在它上面找出【心脏出血漏洞】。心脏出血漏洞最初在2012年被引入OpenSSL代码库,但直到2014年才被发现。当时就被广泛认为是进入互联网时代以来,影响最大的一个安全漏洞。这个漏洞最初是由Google和Codenomicon两个公司各自独立发现的,发现过程中均使用了模糊测试技术。
编译OpenSSL
1. 下载上游代码
我们从Github上下载受此安全漏洞影响的源码。代码库为https://github.com/openssl/openssl。受此漏洞影响的版本为1.0.1f。您可以使用下面的命令来直接下载指定的代码分支:
git clone https://github.com/openssl/openssl --branch=OpenSSL_1_0_1f --depth=1
2. 编译OpenSSL
接下来,使用 wfuzz-cc
编译器编译 OpenSSL:
cd openssl
export CC=wfuzz-cc
./config --prefix=$PWD/install
make
make install_sw
#install_sw相比于install,不安装文档,只安装库文件和头文件
3. 检查编译文件
编译完成后,OpenSSL 的库将会安装在 openssl/install 目录下。请检查下列文件是否存在:
- openssl/install/lib/libcrypto.a
- openssl/install/lib/libssl.a
- openssl/install/include/openssl/
如果均存在,说明已经成功的编译安装了OpenSSL。
编译测试程序
这一步我们需要编写一个测试程序,从模糊测试器获取输入数据,调用OpenSSL库进行检测。这部分程序我们称之为测试驱动。
1. 创建测试驱动文件
创建一个文件fuzzer.cpp,使用以下的内容:
// Licensed under the Apache License, Version 2.0 (the "License");
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <wfuzz.h>
SSL_CTX *sctx;
void ssl_heartbleed_init() {
SSL_library_init();
SSL_load_error_strings();
ERR_load_BIO_strings();
OpenSSL_add_all_algorithms();
assert (sctx = SSL_CTX_new(TLSv1_method()));
/* These two file were created with this command:
openssl req -x509 -newkey rsa:512 -keyout runtime/server.key \
-out runtime/server.pem -days 9999 -nodes -subj /CN=a/
*/
assert(SSL_CTX_use_certificate_file(sctx, "runtime/server.pem",
SSL_FILETYPE_PEM));
assert(SSL_CTX_use_PrivateKey_file(sctx, "runtime/server.key",
SSL_FILETYPE_PEM));
}
void ssl_heartbleed(std::string content) {
SSL *server = SSL_new(sctx);
BIO *sinbio = BIO_new(BIO_s_mem());
BIO *soutbio = BIO_new(BIO_s_mem());
SSL_set_bio(server, sinbio, soutbio);
SSL_set_accept_state(server);
BIO_write(sinbio, content.data(), content.size());
SSL_do_handshake(server);
SSL_free(server);
}
WFUZZ_TEST_ENTRYPOINT_WITH_INIT(ssl_heartbleed, ssl_heartbleed_init);
其中 ssl_heartbleed_init
函数用于初始化测试环境。由于这些初始化本身只需要执行一次,如果把它重复执行,会拖慢测试的速度。因此我们使用一个独立的函数来定义它。这个函数的名称本身是不重要的,只需要在下面的注册部分引用即可。
ssl_heartbleed
函数是实际执行测试的部分,它有一个参数content,这说明在测试过程中,每次引擎需要随机生成一个std::string类型的content来调用这个函数。
最后WFUZZ_TEST_ENTRYPOINT_WITH_INIT是一个宏,用来注册测试入口和初始化入口。
2. 编译测试驱动
使用以下命令可以编译这个文件:
wfuzz-c++ -o openssl-fuzz fuzz.cpp -I./install/include \
./install/lib/libssl.a \
./install/lib/libcrypto.a
编译完成后,请在当前目录下查看 openssl-fuzz
文件,如果存在,说明已经成功编译了测试驱动。
3. 创建证书文件
您可能注意到,在初始化函数中,会读取两个文件 runtime/server.pem
和 runtime/server.key
。这两个文件是用于SSL连接的公钥和私钥。可以通过 openssl
命令来生成:
mkdir -p runtime
openssl req -x509 -newkey rsa:512 -keyout runtime/server.key \
-out runtime/server.pem -days 9999 -nodes -subj /CN=a/
执行完该命令后,请确认当前目录下的runtime子目录中创建了 server.pem
和 server.key
两个文件。
开始测试
执行下面的命令,就可以开始测试了:
wfuzz fuzz openssl-fuzz
经过短暂的启动检查后,您可以看到如下的界面:
┌───────────────────────────────────────────────────────────────────────────────┐
│██╗ ██╗███████╗██╗ ██╗███████╗███████╗ by Wingtecher 1.6.10-fast │
│██║ ██║██╔════╝██║ ██║╚══███╔╝╚══███╔╝ │
│██║ █╗ ██║█████╗ ██║ ██║ ███╔╝ ███╔╝ Platform: │
│██║███╗██║██╔══╝ ██║ ██║ ███╔╝ ███╔╝ https://wingfuzz.com │
│╚███╔███╔╝██║ ╚██████╔╝███████╗███████╗ Login as: │
│ ╚══╝╚══╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝ USERNAME │
├──────────────────────────────────────────┬────────────────────────────────────┤
│ Regression │ Fuzz │
├──────────────────────────────────────────┴────────────────────────────────────┤
│ App: openssl-fuzz Module: ssl_heartbleed │
├───────────────────────────────────────────────────────────────────────────────┤
│ ██████████████ │
├─────────────────────────────┬────────────────────────┬────────────────────────┤
│Status: Active │Test Case: 2 │ │
│Job: 1 │Path: 0 │Crash: 1 │
│CPU Time: 0s │Execution: 2 │ │
├─────────────────────────────┴────────────────────────┴────────────────────────┤
│Edges count: 2201 │
│Case eeea9c8e0b46f0122a4d871a6cc7d76d2a3079a8 is crashed during inspecting │
│New problem discovered: eeea9c8e0b46f0122a4d871a6cc7d76d2a3079a8, type: Undefin│
│New case discovered: f9916051e42aad872abf5490a795934339dc1578, Total count: 2 │
│Edges count: 2575 │
└───────────────────────────────────────────────────────────────────────────────┘
看到这个界面说明您的测试已经成功启动了。经过一小段时间之后,您应该可以看到Crash的数字变成了2。说明已经成功检出了2个问题。 接下来您可以使用以下步骤,在平台上看到详细的错误报告。
- 在平台导航菜单中找到「应用测试」 -> 「项目管理」。
- 在右边的视图中找到
openssl-fuzz
这个应用。 - 进入详情后,选择模块
ssl_heartbleed
。 - 点击进去后,在下方的缺陷列表就能看到发现问题,如果您的步骤完全按照教程进行的话,此处应该能看到两个问题,其一是一个未定义行为的null-pointer-use问题,其二是内存问题的heap-buffer-overflow-read问题。这里这个内存问题就是著名的Heartbleed漏洞。
- 点击缺陷编号进入页面,可以看到错误详情,下方会有两个调用堆栈,第一个是触发此问题的调用栈,第二个是这段内存申请时的调用栈。
如果您想了解如何在自己的项目上集成模糊测试,请继续阅读下一章 集成我的项目
关于 「应用」、「模块」 等概念,您可以参考 进阶使用 章节。
关于 「内存问题」、「未定义行为」 等错误类型,您可以参考 支持的问题类型 章节。
集成我的项目
到了这里,您应该对WINGFUZZ基本的使用已经比较了解了,接下来本章节将介绍在一般的项目中,如何集成模糊测试。
您可能需要对待测项目的编译流程以及代码结构有一定的了解,才能比较好的完成这个步骤。
替换编译器
1. 替换范围
wfuzz-cc编译器如同其他编译器一样,可以直接链接二进制的目标文件或者动态库文件。 这些二进制文件只要是ABI兼容的,就可以使用,并不需要将全部的编译单元都使用wfuzz-cc编译。 但未使用wfuzz-cc编译的代码,不会有插桩。 在实际测试时,不会收集这部分代码的运行时信息,因此,生成器对它们也不会有任何针对性的优化。
一般来说,我们自己需要测的代码,需要使用wfuzz-cc编译器来编译。 而第三方库,不是我们系统的一部分,则可以使用其他编译器,或者直接使用二进制发布的版本。 但这个规则并不是绝对的,可以根据需要调整。 如果第三方库很重要,是我们系统核心功能的提供者,而我们的代码只做了浅层包装, 那就应该针对它们也使用替换的编译器。 另一方面,如果我们系统很大,有些通用的基础功能,并不是测试关注的重点, 那也可以使用默认编译器编译这部分,可以降低编译期和运行期的开销,增加测试效率。
2. 常见构建系统的适配
如果要集成模糊测试,第一个步骤是先替换编译器,确认项目能够使用wfuzz-cc编译器编译出来。 这个步骤的难易程度取决于您的项目使用了哪个构建系统,以及构建配置的灵活性。
对于常见的构建系统,例如Automake、CMake、QMake等,一般都支持CC/CXX环境变量。 您可以通过环境变量直接指定编译器,例如:
export CC=wfuzz-cc CXX=wfuzz-c++
./configure
make
如果您使用了自定义的构建系统,就需要找到构建系统中实际定义编译器的地方,进行替换。
3. 编译程序
替换编译器后,由于我们需要针对目标程序进行插桩,编译时间和编译器内存占用都会有所增加。 一般来说,替换后的编译时间是替换前的1.5-2倍。
有些构建系统在替换编译器时,并不会重新编译,此时您需要清理构建临时文件后再重新编译, 保证所有文件都是最新的。
4. 验证二进制文件
有时候,构建系统可能并不展示实际的编译命令,而是显示一些简要的信息。 此时通过阅读这些简要信息,可能并不能判断程序实际用了什么编译器。
如果不确定编译器替换是否真的成功,可以使用以下的命令,验证最终的二进制文件确实是通过wfuzz-cc编译的:
objdump -t TARGET | grep __wfuzz
将这个命令的 TARGET
替换为编译出的二进制文件名称,可以是可执行程序,也可以是动态库。
这个命令会显示二进制文件的符号表,wfuzz-cc在编译时会自动加一些符号,这些符号均以 __wfuzz
为开始。
看到这些符号就可以确认编译器替换确实成功了。
选择测试入口
在实际进行驱动编写工作前,第一个重要的步骤是选择一个测试入口,设计测试循环。选择一个好的入口是做好模糊测试的基本要求。一个好的入口有以下的特点:
-
首先,从功能角度上,这个入口应该接收单一来源的输入,在处理这个输入的过程中,又会用到我们想测的大部分功能。这样才能有效地测到系统的各个方面。
例如,很多格式的序列化反序列化库,会有一个统一的parse,接受一个输入,将它解析成指定的格式。这个功能内部会调用各种子结构的解析,而对外提供一个统一的入口,这就是一个很好的例子。 反之,要么输入过于复杂,要么功能过于简单,都不适合作为测试的入口。
-
其次,这个入口的执行过程应该与输入直接相关,相同的输入总是能得到相同的结果,这样才使得外部输入和变异有意义。反之,如果执行过程是随机化的,或者有很复杂的内部状态,每次调用的结果跟输入只是部分有关,那么即使测出了问题,也容易出现无法复现的问题。
例如,一个排序算法,给定一个输入,总是能得到相同的结果,这就是一个较好的测试入口,反之,一个shuffle算法,每次把输入按照随机顺序输出,就不是一个合适的目标。
-
最后,对于正常情况下可构造的输入,程序总会有输出,不会崩溃,也不会提前退出。这样才能够区分错误输入导致的业务错误和BUG导致的系统问题。反之,如果遇到业务错误程序也崩溃,那么测试结果中就会出现大量的业务问题,掩盖掉真正的系统问题。
例如,一个函数如果使用抛异常的方法来代表业务错误,那么在测试入口中一定要catch掉业务错误的异常,否则就会变成未捕捉的异常,导致程序崩溃。
测试入口的选择跟具体的待测对象息息相关。对于复杂的应用、类库,还可能会存在多个测试入口。我们可以通过实际测试工作积累经验,掌握快速识别定位入口的方法。
编写测试驱动
基本用法
对于任何函数,都可以使用以下的宏,将它定义为一个测试入口:
WFUZZ_TEST_ENTRYPOINT(function)
其中function是待测的函数。可以支持不同的输入参数。
上述的宏在wfuzz.h中定义,测试驱动需引入这个文件,例如:
#include <wfuzz.h>
void test_something(char* data, size_t len) {
//TODO: 调用需要测试的方法
}
WFUZZ_TEST_ENTRYPOINT(test_something);
在这个例子中,test_something会被反复调用,每次会输入不同的数据。 在函数执行过程中,框架会对程序的内存访问、程序行为进行检测,如果出现问题,就会报告这个问题。
带有全局初始化的测试
在上一个例子中,test_something会被反复调用。 如果程序执行之前,需要先进行某些初始化操作,而且初始化操作由于性能或者逻辑限制,不能反复调用, 则可以单独定义一个初始化函数,此函数只执行一次。使用下面的宏来注册这个入口:
WFUZZ_TEST_ENTRYPOINT_WITH_INIT(function, init);
其中function和基本用法中的作用一致,但多了一个init函数,它是个void()类型的函数。 示例程序如下:
#include <wfuzz.h>
CTX ctx;
void test_something_init() {
init_ctx(&ctx);
}
void test_something(char* data, size_t len) {
parse(&ctx, data, len);
}
WFUZZ_TEST_ENTRYPOINT_WITH_INIT(
test_something,
test_something_init
);
在这个例子中,模糊测试器会在启动后,先执行一次init,然后再循环执行function。
测试函数类型
对于测试函数,返回值类型可以是任意的,函数返回值会被忽略。
在 C 和 C++ 环境下,测试函数支持的参数类型不同:
-
C 环境下,参数类型只能是
(char* data, size_t len)
,测试框架将会把输入的原始缓冲区传给测试函数。测试函数可以任意使用。 -
C++ 环境下,支持任意数量的参数。测试框架内置部分参数类型的适配,同时支持用户自己扩展框架,以使其支持任意类型的参数。
阅读章节【进阶使用-数据类型支持】可以进一步了解WINGFUZZ原生支持的数据类型和如何扩展类型支持。
开始测试
相比于示例中比较简单的用法,对于实际项目,可能需要对测试过程做更多的定制。
本章节会介绍测试中常用的参数。这些参数可以任意组合,以达到您的测试需求。
在命令行中输入 wfuzz fuzz --help
也可以看到更详细的参数列表。
多测试入口
如果您的程序比较复杂,从单一的测试入口,可能不能达到满意的覆盖率。 如果针对每个测试入口,编译一个二进制文件,可能会增加构建系统配置的复杂性。
WINGFUZZ支持在同一个二进制文件中,定义多个测试入口,并且连接到同一个可执行文件中。 后续在测试的时候,可以指定某一个测试入口执行。
您可以使用下方的例程来测试多入口功能,将下面的内容存为 multi-entrypoint.cpp
文件:
#include <wfuzz.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
WFUZZ_TEST_ENTRYPOINT(add);
WFUZZ_TEST_ENTRYPOINT(sub);
使用WINGFUZZ编译器编译上面的程序:
wfuzz-c++ -o multi-entrypoint multi-entrypoint.cpp
编译完成后,启动测试:
wfuzz fuzz multi-entrypoint
您会发现此时测试并未启动,而是显示了一个提示:
There are multiple candidate entries. Use -e/--entrypoint argument to choose one from them:
add
sub
这个错误提示的意思就是说该程序中有多个测试入口,需要用户指定一个入口才能开始测试。
wfuzz fuzz
命令支持用 -e
或 --entrypoint
指定测试入口。
如果想要测试其中的 add
函数,可以使用下面的命令:
wfuzz fuzz multi-entrypoint -e add
使用这个命令后,就可以开始测试了,此时平台上,进入应用multi-entrypoint后,看到正在测试的模块名称是add。
如果想要测试sub函数,将上面命令行参数改为 -e sub
即可
指定测试时间
由于模糊测试的运行是反复迭代的,因此可以一直运行下去。
默认的启动参数并未限制测试的时间,如果用户想要停止,只能使用 Ctrl-C
键中断当前的测试。
如果您想通过脚本自动运行模糊测试,一般情况您可能会希望测试跑一段时间后就停止。
我们提供了 -t
或 --timeout
参数,可以限制测试的运行时间。
该参数后面可以跟一个数字,单位是秒。
以【我的第一次模糊测试】章节中提供的运行命令为例, 如果想要加上时间限制,可以使用下面的命令:
wfuzz fuzz first-fuzz --timeout 300
这里限制了时间长度为300秒,运行到这个时间后就会结束测试。
并行化测试
使用默认参数启动测试的情况下,我们只会启动一个目标程序。 如果您的测试机器性能比较强,有比较多的核心可用于测试,则利用多核心测试可以明显的提高测试效率。
我们提供了 -j
或 --jobs
参数,可以设置测试的并行数量。该参数后面可以跟一个数字,数值就是并行的数量。
一般情况下,如果想要充分利用机器资源,可以使用(CPU数量-1)作为并行的数量,保留一个核心给WINGFUZZ SDK用于用例分析。
以【我的第一次模糊测试】章节中提供的运行命令为例, 如果想要加上并行测试,可以使用下面的命令:
wfuzz fuzz first-fuzz -j4
此时会同时启动4个模糊测试器和目标程序。 在网站上可以看到这4个模糊测试器的详细信息:
- 在网站上的“应用测试” -> “测试列表”中找到这个测试
- 点击列表最右边的“查看报告”链接。
- 在“测试详情” -> “节点分布数” 下方点击“查看详情”
- 在打开的页面中,可以看到模糊测试器列表,此处可以查看每个测试器的详细状态。
初始种子
模糊测试算法类似于遗传算法,会从一个简单的初始输入开始,进行一些随机的修改,生成新的输入。 最初用到的测试输入集合就是初始种子。 初始种子库对于测试效果的影响是比较明显的。
默认情况下,我们的模糊测试器会随机生成比较简单的初始输入。 但如果程序需要比较复杂的输入,此时默认初始种子的效果可能就不太好。 此时可以手动生成一些输入文件,用它们作为初始的种子。 例如,可以使用单元测试的输入样例,作为初始种子。
另一种情况是,我们的程序经过一段时间测试之后,会有一个代码覆盖率。 通过分析这个覆盖率可以检查测试的效果,是否覆盖到了我们关心的代码位置。 如果有我们关心的地方未覆盖到,说明这个分支的条件比较复杂,全自动的生成方式可能不容易进入这个分支。 此时,我们可以在初始种子中加入一个样例,这个样例可以覆盖到此分支。 后续的测试中,模糊测试器就会对这个样例进行修改变异,生成更多的样例,就能更好的测到这部分代码。
初始种子库是一个目录,这个目录中的每个文件会被系统视为一个种子。
以【我的第一次模糊测试】章节中提供的运行命令为例,
如果想要指定初始种子库,可以使用下面的命令,其中 DIR
是初始种子库的目录路径。
wfuzz fuzz first-fuzz -i DIR
项目/模块名称
项目模块名称是在网站上显示的名称。 在每次测试开始的时候,SDK会先从平台获取到之前跑过的输入样例,进行一次回归。 这个回归就是按照项目模块名称来查找的。
另外,我们在测出程序缺陷的时候,会进行去重,去重也是按照项目模块为单位进行的。 不属于同一个项目/模块的问题,即使内容完全一样,也不会被认为是重复的。
默认情况下,我们会使用可执行文件的名称作为项目名称, 使用测试函数的名称作为模块名称。 如果想要修改默认行为,可以使用以下的参数:
-p
/--project
参数可以设定项目名称。-m
/--module
参数可以设定模块名称。
以【我的第一次模糊测试】章节中提供的运行命令为例, 如果想要更换项目模块名称,可以使用下面的命令:
wfuzz fuzz first-fuzz -p first-fuzz-2 -m echo2
此时运行起来后,在平台上看到的项目名称就是 first-fuzz-2
,模块名称是 echo2
。
如果您之前启动过first-fuzz
,此时在平台上可以看到两个项目。
它们的数据是不同的,发现的缺陷也是分开的。
进阶使用
本章包含 WINGFUZZ 进阶使用、高级功能相关介绍指引。
远程测试
本章介绍如何使用 WINGFUZZ 的远程测试模式,在云端开展 Fuzzing,解放您的CPU。
建议您先通过前面章节的阅读,确保本地模式下可以已经可以完成编译、驱动编写等流程。
1. 使用命令行
使用命令行执行远程测试,在fuzz
命令中添加-R
参数以及-t
参数即可。-R
参数指定了远程模式,-t
参数指定测试时长,单位为秒。如启动一个10分钟的测试:
wfuzz fuzz -R -t 600 XXX
2. 使用平台“应用上传”功能
跟随系统页面提示指引,即可分步完成应用上传,启动远程测试。
- 选择对应语言、架构,上传二进制文件;
- 等待系统检查二进制是否符合要求,如编译是否正确、是否存在测试入口等;
- 定义项目、模块信息,选择目标测试入口,启动测试。
数据类型支持
对于测试函数,返回值类型可以是任意的,函数返回值会被忽略。
在C和C++环境下,测试函数支持的参数类型不同:
C环境下,参数类型只能是(char* data, size_t len),测试框架将会把输入的原始缓冲区传给测试函数。测试函数可以任意使用。
C++环境下,支持任意数量的参数。测试框架内置部分参数类型的适配,同时支持用户自己扩展框架,以使其支持任意类型的参数。
-
内置参数类型
以下类型可以直接作为测试函数的参数:
参数类型 说明 char* buf, size_t len 原始缓冲区数据输入,长度为len,没有\0结尾。C语言环境下,只支持这个类型,并且前面不能有别的参数。C++环境下,还可以在它前面加任意的别的类型,但这两个参数的位置只能是最后两个。 bool int8_t/uint8_t 同时也包括[signed/unsigned] char int16_t/uint16_t 同时也包括[signed/unsigned] short int32_t/uint32_t 同时也包括[signed/unsigned] int/long int64_t/uint64_t 同时也包括[signed/unsigned] long long float/double char*/std::string C-style或C++字符串 std::vector T是任意支持的类型(包括内置支持和自定义支持) std::tuple<T...> T是任意支持类型的列表(包括内置支持和自定义支持) std::map<K,V> K,V是任意支持的类型(包括内置支持和自定义支持) 以上类型,可以按值传参,也可以按引用传参。可以添加const/volatile修饰。使用按引用传参时,引用对象的生命周期会持续到测试函数执行结束。
-
自定义参数类型
当以上的内置参数类型不能满足需求时,可以使用自定义类型。使用时,只需要写一个转换函数。
转换函数的格式类似于测试函数,可以接收任意的参数,返回要转换的类型。再使用**WFUZZ_CUSTOM_CONVERTER(function)**宏注册一下转换函数。然后就可以像使用内置类型一样,使用这些类型了。示例程序如下:
#include <wfuzz.h> #include <stdio.h> #include <math.h> struct Point{ int x; int y; }; Point point_converter(int x, int y) { return { x, y }; } WFUZZ_CUSTOM_CONVERTER(point_converter); void test_point(Point p) { printf("distance: %f\n", sqrt(1.0*p.x*p.x + 1.0*p.y*p.y)); } WFUZZ_TEST_ENTRYPOINT(test_point);
转换函数可以嵌套使用,例如上面程序中定义了Point类型的转换函数,后续的自定义类型就可以基于Point函数定义:
struct Circle { Point center; int radius; }; Circle circle_converter(Point center, int radius) { return { center, radius }; } WFUZZ_CUSTOM_CONVERTER(circle_converter); void test_circle(Circle c) { printf( "Circle: center: (%d, %d), radius: %d\n", c.center.x, c.center.y, c.radius ); } WFUZZ_TEST_ENTRYPOINT(test_circle);
优化测试效率
您可以从多个角度来提升模糊测试的效率,包括使用并行测试、初始种子、优化测试驱动等。
1. 并行测试
使用wfuzz fuzz
命令的-j
参数,即可指定并行测试数量,WINGFUZZ 将使用多个核心进行测试。
在本地模式下,-j
参数没有具体限制,您可以按需使用计算资源。如使用4核并行命令:、
wfuzz fuzz -j4 XXX
在远程模式下,BETA版免费试用账户的并行数上限为8。超过这一个上限,测试将无法运行。如果您需要超过8核并行数量的远程测试,可以联系工作人员进行申请。
远程模式下,通过命令行使用8核并行测试命令如下:
wfuzz fuzz -j8 -R XXX
您也可以通过平台的 「应用上传」 功能来指定并行数量。
2. 提供初始种子
「种子(Seed)」 可以理解为初始测试用例,WINGFUZZ 引擎可以基于种子进一步构造测试数据。在诸如文件测试一类的场景下,提供质量良好的种子输入可以有效提高模糊测试效果。
可以通过wfuzz fuzz
命令的-i
参数来指定种子文件目录。-i
参数可以多次使用,指定多个目录。
wfuzz fuzz -i DIR1 -i DIR2 XXX
当前初始种子功能仅在本地模式可用,远程模式支持将于近期上线
种子从哪里来?
可以考虑使用已有的功能测试、单元测试等测试用例作为种子。 通常来讲,种子文件的覆盖面越广,对测试的提升效果就越好。
3. 驱动优化
Coming soon ...
可以发现的问题
WINGFUZZ引擎可以检测以下的几类问题:
内存问题
由于C/C++不是内存安全的语言,实际使用中会出现各种内存不安全使用的问题。本章会详细介绍每种问题,给出例程和说明。
如果想要使用下列例程,请使用 wfuzz-cc
/wfuzz-c++
编译器编译这些程序。通过 main 函数直接调用下列的函数即可。
注意编译时请加上 -O0
编译参数,因为部分例程本身没有副作用,会被优化器整体删除。
WINGFUZZ可检测的内存问题包含以下类型:
- 1. 栈缓冲区溢出 (stack-buffer-overflow)
- 2. 堆缓冲区溢出 (heap-buffer-overflow)
- 3. 全局缓冲区溢出 (global-buffer-overflow)
- 4. 双重释放 (double-free)
- 5. 释放后使用 (use-after-free)
- 6. 申请释放不匹配 (alloc-dealloc-mismatch)
- 7. 错误的释放 (bad-free)
- 8. 申请内存过大 (allocation-size-too-big)
- 9. 返回后使用 (use-after-return)
- 10. 作用域结束后使用 (use-after-scope)
- 11. 栈溢出 (stack-overflow)
- 12. 内存泄漏 (memory-leak)
- 13. 段错误 (segv)
1. 栈缓冲区溢出 (stack-buffer-overflow)
风险
极高
说明
当程序读写的内存超出了预定范围时,就有可能造成越界读写的问题。最常见的原因是对字符串的结束符未正确处理,除此之外还可能是因为对边界条件的检查出错引起的。
对于越界读取来说,有可能会读到本身用户无权读取的数据,造成数据泄露。 对于越界写入来说,会造成其他的有效数据被错误的覆盖,甚至覆盖到程序运行时的重要结构,比如栈帧。这些问题可能会造成如远程代码执行等极严重的安全漏洞。
WINGFUZZ平台会给出发生溢出时的代码调用栈。
例程
#include <stdio.h>
void stack_buffer_overflow() {
char a[3] = { 'a', 'b', 'c' };
printf("%s\n", a);
}
2. 堆缓冲区溢出 (heap-buffer-overflow)
风险
极高
说明
类似于栈缓冲区溢出问题,不过发生在动态申请的堆内存中。两者的风险也是类似的,都有非常高的安全风险。
WINGFUZZ平台会给出两个调用栈:其一是发生溢出时的代码调用栈,其二是溢出的内存块本身申请时的代码调用栈。
例程
#include <stdio.h>
#include <stdlib.h>
void heap_buffer_overflow() {
int *a = (int*)malloc(3 * sizeof(int));
printf("%d\n", a[3]);
free(a);
}
3. 全局缓冲区溢出 (global-buffer-overflow)
风险
极高
说明
类似于栈缓冲区溢出问题,不过发生在全局变量的内存地址中。两者的风险也是类似的,都有非常高的安全风险。
WINGFUZZ平台会给出发生溢出时的代码调用栈,以及溢出的全局变量的名称。
例程
int buf[3];
int main() {
return buf[3];
}
4. 双重释放 (double-free)
风险
极高
说明
如果针对同一个内存地址释放了两次,就会触发此问题。 一般来说,双重释放说明程序中对象的生命周期管理出现了问题。导致程序中使用了野指针。 这可能会造成如远程代码执行等极严重的安全漏洞。
WINGFUZZ平台会给出三个调用栈: 其一是第二次释放时的代码调用栈; 其二是对应的内存块第一次释放时的代码调用栈; 其三是对应的内存块申请的代码调用栈。
例程
#include <stdlib.h>
void double_free() {
int *a = (int*)malloc(sizeof(int));
free(a);
free(a);
}
5. 释放后使用 (use-after-free)
风险
极高
说明
如果一个内存块释放之后,还在程序中使用,就会触发此问题。 因为释放后的内存块有可能被分配给其他的程序使用, 此时就可能读取或改写其他程序的内存空间。这可能会造成如远程代码执行等极严重的安全漏洞。
WINGFUZZ平台会给出三个调用栈: 其一是使用释放后内存的代码调用栈; 其二是对应的内存块释放时的代码调用栈; 其三是对应的内存块申请时的代码调用栈。
例程
#include <stdlib.h>
void heap_use_after_free() {
char *x = (char*)malloc(10 * sizeof(char));
free(x);
int res = x[5];
}
6. 申请释放不匹配 (alloc-dealloc-mismatch)
风险
高
说明
如果内存申请和释放的方式不匹配,就会造成这种问题。错误的释放方式可能会造成其他该释放的系统资源未释放,造成资源泄漏。 更严重的情况下可能会造成程序本身崩溃。
WINGFUZZ平台会给出两个调用栈,其一是释放时的调用栈,其二是对应的内存申请时的调用栈。
例程
#include <string>
#include <stdlib.h>
void alloc_dealloc_mismatch() {
std::string *a = new std::string;
free(a);
}
7. 错误的释放 (bad-free)
风险
高
说明
类似于alloc-dealloc-mismatch,如果释放的指针本身不是通过new或malloc申请出来的,而是其他的指针,就会造成这种问题。 错误的释放方式可能会造成其他该释放的系统资源未释放,造成资源泄漏。 更严重的情况下可能会造成程序本身崩溃。
WINGFUZZ平台会给出释放时的调用栈,以及对应内存地址的动态信息。
例程
#include <stdlib.h>
void bad_free(){
char a[4];
char *b = &a[0];
free(b);
}
8. 申请内存过大 (allocation-size-too-big)
风险
高
说明
动态申请内存过大 会导致进程占用过多系统资源,造成系统卡顿或者崩溃。 如果系统将外界可控的输入数据作为申请内存的大小,就可能导致攻击者故意构造极大的数字作为参数。 另一种常见的问题是符号处理的问题,将负数作为参数传给了malloc,会被解读成一个巨大的无符号数。
WINGFUZZ平台会给出内存申请的调用栈,以及此次内存申请的参数。
例程
#include <stdio.h>
#include <stdlib.h>
void allocation_size_too_big() {
void *p = malloc(-1);
printf("malloc returned: %zu\n", (size_t)p);
}
9. 返回后使用 (use-after-return)
风险
高
说明
当一个函数返回后,该函数中所有的局部变量的声明周期也会结束,对应的析构函数也已完成调用。 此时这段内存的状态不确定的,如果通过指针/引用等方式访问到了这段内存,可能会造成程序状态错误,导致程序崩溃或者出现信息泄露等问题。
WINGFUZZ平台会给出使用时的调用栈,以及使用到的变量在存活时的栈信息。
例程
int *ptr;
void func(){
int a = 0;
ptr = &a;
}
int main(){
func();
return *ptr;
}
10. 作用域结束后使用 (use-after-scope)
风险
高
说明
当一个变量的作用域结束后,该变量的声明周期也会结束,对应的析构函数也已完成调用。 此时这段内存的状态不确定的,如果通过指针/引用等方式访问到了这段内存,可能会造成程序状态错误,导致程序崩溃或者出现信息泄露等问题。
WINGFUZZ平台会给出使用时的调用栈,以及使用到的变量在存活时的栈信息。
例程
void use_after_scope() {
int *p;
{
int a[1];
p = a;
}
return p[0];
}
11. 栈溢出 (stack-overflow)
风险
高
说明
在软件中,如果调用栈指针超出栈边界,则会发生栈溢出。调用栈可能由有限数量的地址空间组成,通常在程序开始时确定。调用堆栈的大小取决于许多因素,包括编程语言、机器架构、多线程和可用内存量。当程序尝试使用比调用栈上的可用空间更多的空间时(即,当它试图访问超出调用栈边界的内存时,这本质上是缓冲区溢出),被称为栈溢出,通常会导致程序崩溃。
栈溢出的最常见原因是过深或无限递归,其中函数调用自身的次数太多,以至于存储与每次调用相关的变量和信息所需的空间超出了栈的容量。
WINGFUZZ平台会给出溢出时的调用栈。
例程
void stack_overflow() {
char data[1024];
stack_overflow();
}
12. 内存泄漏 (memory-leak)
风险
中
说明
当申请的内存未被释放时,就会出现内存泄漏问题。 内存泄漏可能会造成程序使用的内存持续增加,直到占满全部可用内存,导致系统卡顿或程序崩溃。
内存泄漏的常见原因有两点,其一是申请出的内存指针未被释放就被抛弃。 其二是使用智能指针时,构造了环状引用,导致整个环无法被释放。
WINGFUZZ平台会给出已泄漏内存在申请时的调用栈。
例程
#include <memory>
struct circular_ref {
std::shared_ptr<circular_ref> ref;
};
int main() {
std::shared_ptr<circular_ref> a = std::make_shared<circular_ref>();
a->ref = a;
}
13. 段错误 (segv)
风险
高
说明
现代操作系统均使用段式内存管理,每个内存段有其读写权限,如果尝试读取/写入没有对应权限的页,就会触发段错误,导致程序崩溃。
最常见的段错误是对NULL指针,或其附近的地址进行了读写。
WINGFUZZ平台会给出发生段错误时程序的调用栈。
例程
int segv() {
volatile int *x = NULL;
return *x;
}
未定义行为问题
未定义行为(Undefined Behavior,UB)是指执行某种计算机代码所产生的结果,这种代码在当前程序状态下的行为在其所使用的语言标准中没有规定。常见于编译器对源代码存在某些假设,而执行时这些假设不成立的情况。
断言错误问题
断言(Assertion)是在程序开发过程中用于检测“不应该”发生的状况的手段,仅应在开发中使用。如果软件发布后断言仍能触发,则可能会造成异常状态。
未捕获异常问题
未捕捉异常(Uncaught Exception)是指程序抛出异常,但未被捕捉的情况。可能导致程序崩溃或敏感信息泄漏。
超时问题
指程序因各种原因导致超时无响应的情况。超时通常是由死锁等原因造成。
其他问题
WINGFUZZ如果发现以上范围以外,引起程序崩溃的状况,将归为其他问题,并提供尽量详情的缺陷上下文,供用户分析。
常见问题
1. 模糊测试一般需要测多长时间?
A: 模糊测试理论上可以一直持续运行,不断覆盖罕见输入。实际测试中,建议时长与目标复杂度相关。简单的功能可能只需要数分钟即可充分测试(覆盖率增长达到稳定值),复杂应用可能需要数小时。
2. 支持JAVA测试么?
A: WINGFUZZ 专业版当前已经提供JAVA测试支持,可以联系水木羽林工作人员获取详情。
3. 支持ARM、MIPS等环境测试么?
A: WINGFUZZ 专业版当前已经提供ARM、MIPS架构测试支持,可以联系水木羽林工作人员获取详情。
联系我们
感谢您使用WINGFUZZ SaaS版!
有任何问题都可通过以下渠道反馈,欢迎您随时与我们取得联系!
扫一扫,加入 WINGFUZZ SaaS 官方交流群:
扫一扫,关注水木羽林公众号: