自动化测试实验:基于变异测试的模糊器评估


简述:模糊测试是一种重要的软件测试技术,得到了学术界和工业界的广泛关注。近年来,关于模糊测试的研究不断涌现。实现模糊测试的程序称为模糊器(Fuzzer)。面对如此众多的 Fuzzer,如何准确、有效地评估 Fuzzer 的性能成了一项值得关注的难题。本文从变异杀死的角度对 Fuzzer 进行评估。
实验流程:编写脚本,利用变异测试工具重新运行模糊测试的产生的测试输入,从变异测试的角度(变异得分)评估 Fuzzer 的性能,工具:AFL(Fuzzer) + Mull(变异测试工具)
测试对象:所有 Real-world Projects
步骤:(1)利用 AFL 对实验对象进行模糊测试,产生测试输入;(2)利用Mull 复现 AFL 产生的测试输入,记录变异杀死情况;(3)编写脚本对运行结果进行分析,绘制统计图。

实验过程报告:

AFL过程解析

AFL对原程序进行模糊测试产生的用例分为两类:

1.一类会让原程序报错,这类测试用例存于crashes文件夹中。
2.另一类探明了原程序新的执行路径,这类测试用例存于queue文件夹中。

mull过程解析

按照定义好的变异算子,mull 先对源程序变异,得到变异体,利用变异体去衡量AFL产生的测试用例质量,分为三类情况:

1.AFL产生的测试用例会使变异体崩溃(变异体被杀死)
2.AFL产生的测试用例会使变异体输出和原程序不一样的结果(差分测试思路,同样导致变异体被杀死)
3.AFL产生的测试用例会使变异体输出和原程序一样的结果(变异体存活)
衡量指标

变异得分,即为所有被杀死的变异体除以变异体总数。变异得分越高则表明 AFL 效果越好。

注意:由于mull对每个变异体的执行过程高度封装,难以进入到子进程内部去修改运行逻辑,因此我们将变异被杀死的情况1和情况2分别进行实验。

主要工作:

在这里插入图片描述

硬件配置

服务器:华为云服务器 2核 CPU 4G 内存 50M 带宽 40G 云存储空间 Ubuntu 20.04 操作系统。

软件环境

操作系统:Ubuntu 20.04
C++项目构建:clang 12
脚本版本:python 3.8

测试源程序

在这里插入图片描述

Fuzzing 配置

fuzzer
afl-2.52b
补充库
bison 3.5.1
texinfo 6.7.0

实验过程解析

AFL

AFL的运行流程如下图:
在这里插入图片描述

AFL工作流程
①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);
②选择一些输入文件,作为初始测试集加入输入队列(queue);
③将队列中的文件按一定的策略进行“突变”;
④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;
⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。
我们在实验过程中大致遵循上述流程,步骤如下:
1.下载
从AFL官网下载AFL。
2. 安装
将压缩包解压完成之后,进入到其目录下打开终端。

make
sudo make install

如果在该目录下,出现了afl-fuzz的可执行文件,说明安装成功。
3. 模糊测试实验–以readelf为例
下载binutils,完成后解压进入其目录。
必须修改编译器环境为afl-gcc和afl-g++,在大部分的源码编译插桩中基本都需要这一步。

CC=~/.../afl-gcc CXX=~/.../afl-g++ ./configure  //替换为自己afl的目录
make
>mkdir fuzz_in
>mkdir fuzz_out
>~/../afl-fuzz -i fuzz_in -o fuzz_out binutils/readelf -a @@
AFL结果

输出文件夹fuzz_out内部结构如下:
在这里插入图片描述

  1. crashes:引起原程序崩溃的测试用例;
  2. queue:能探明原程序新的执行路径的测试用例;
  3. plot_data:按时间顺序存储了afl执行过程,用于绘图;
  4. hangs:导致目标超时的独特测试用例;
  5. fuzzer_stats:afl-fuzz的运行状态。
    选择说明:
    本实验后续基本使用 queue 中的用例作为变异测试的用例输入。这是有缺陷的,因为queue文件夹中存放的是所有具有独特执行路径的测试用例,一般并不会导致变异杀死,因此变异得分总体较低。但是crashes文件夹中的测试用例过少。权衡之后,我们更多衡量queue文件夹中的测试用例质量。
对AFL用例筛选:

由AFL得到对readelf分支全覆盖的用例,使用脚本筛选出可正确执行的部分用例并记录。

python3 select.py ./readelf

运行select.py脚本,尝试运行所有模糊输入,找出使其不崩溃的模糊输入。

import sys
import subprocess
import os
# select.py
test_executable = sys.argv[1]


path = "./fuzz_out/queue" #文件夹目录
files= os.listdir(path) #得到文件夹下的所有文件名称
s = []
for file in files:
    if not file.endswith(".elf"):
        os.rename(path+"/"+file,path+"/"+file+".elf")
record = open("./fuzz_out/correct","w+")
recordE = open("./fuzz_out/error","w+")
for file in files: #遍历文件夹
    if not os.path.isdir(file) and file!=".state": #判断是否是文件夹,不是文件夹才打开
        try:
            subprocess.run([test_executable, "-a",path+"/"+file], check=True)
            record.write(file+'\n')
        except:
            recordE.write(file+'\n')
            
record.close()

变异测试

变异测试是一种基于故障的软件测试技术。它通过计算变异分数来评估测试套件的质量。它通过创建原始程序、和其稍微修改的版本(称为变异体),并针对每个版本运行测试套件来实现评估测试套件。如果测试套件检测到变更,则认为变异体已被杀死,否则该变异体得到存活。如果至少有一项测试使得运行失败,那么变异体就会被杀死。
mull作为变异测试工具,先对源程序变异,得到变异体。利用AFL的输出作为测试套件,衡量其质量。变异得分,即为所有变异体在这个测试用例组合的通过率越高,则AFL产生的测试套件的质量越好。
​变异测试运行示例
以下流程均以readelf为例。
1.首先需要 配置&编译&插桩,命令如下:

export CC=clang-12 
export CXX=clang++-12 //更改编译器环境
./configure  CFLAGS="-O0 -fexperimental-new-pass-manager -fpass-plugin=/usr/lib/mull-ir-frontend-12 -g -grecord-command-line -fprofile-instr-generate -fcoverage-mapping"

make

2.通过编写脚本 test.py 使得测试能够自动化进行,命令如下:

mull-runner-12 ./readelf -ide-reporter-show-killed   -test-program=python3 -- test.py ./readelf

test.py

import sys
import subprocess
import os

test_executable = sys.argv[1]


path = "./fuzz_out/queue" #文件夹目录
file = open("./fuzz_out/correct","r")
records = file.read().splitlines()
records = records[200:210]
for record in records:
    subprocess.run([test_executable, "-a",path+"/"+record], check=True)
file.close()

3.运行结果截图:
在这里插入图片描述

差分测试

思路:将AFL产生的未导致原程序崩溃的测试用例在原程序中的运行结果存储下来(利用select.py脚本),并且将同一测试用例在变异体中的运行结果存储下来(利用test.py脚本)。比较同一用例在原程序和变异体上运行的结果,若不同,则该变异体被杀死,否则该变异体存存活(利用differential_comparing.py脚本)。

脚本如下:
select.py

import sys
import subprocess
import os

test_executable = sys.argv[1]


path = "./fuzz_out/queue" #文件夹目录
files= os.listdir(path) #得到文件夹下的所有文件名
s = []
for file in files:
    if not file.endswith(".elf"):
        os.rename(path+"/"+file,path+"/"+file+".elf")
record = open("./fuzz_out/correct","w+")
recordE = open("./fuzz_out/error","w+")
i = 0
for file in files: #遍历文件夹
    if not os.path.isdir(file) and file!=".state": #判断是否是文件夹,不是文件夹才打开
        file1 = open("./fuzz_out/correct_result/"+str(i),"w+")
        try:
            subprocess.run([test_executable, "-SD",path+"/"+file], check=True,stdout=file1)
            record.write(file+'\n')
        except:
            recordE.write(file+'\n')
        file1.close()
        size = os.path.getsize("./fuzz_out/correct_result/"+str(i))
        if size==0:
            os.remove("./fuzz_out/correct_result/"+str(i))
            i-=1
        i = i+1
record.close()


test.py

import sys
import subprocess
import os

test_executable = sys.argv[1]


path = "./fuzz_out/queue" #文件夹目录
files= os.listdir(path) #得到文件夹下的所有文件名
s = []
for file in files:
    if not file.endswith(".elf"):
        os.rename(path+"/"+file,path+"/"+file+".elf")
record = open("./fuzz_out/correct","w+")
recordE = open("./fuzz_out/error","w+")
i = 0
for file in files: #遍历文件夹
    if not os.path.isdir(file) and file!=".state": #判断是否是文件夹,不是文件夹才打开
        file1 = open("./fuzz_out/correct_result/"+str(i),"w+")
        try:
            subprocess.run([test_executable, "-SD",path+"/"+file], check=True,stdout=file1)
            record.write(file+'\n')
        except:
            recordE.write(file+'\n')
        file1.close()
        size = os.path.getsize("./fuzz_out/correct_result/"+str(i))
        if size==0:
            os.remove("./fuzz_out/correct_result/"+str(i))
            i-=1
        i = i+1
record.close()

differential_comparing.py

import sys
import subprocess
import os

path1 = "./fuzz_out/correct_result/"
file2 = open("./fuzz_out/final", "rb")
corrects = []
for i in range(20, 50):
    with open(path1 + str(i), "rb") as file1:
        try:
            corrects.append(file1.read().split(str.encode('\n' + "./fuzz_out/queue/"))[1])
        except:
            corrects.append("")
check = file2.read()
checks = check.split(str.encode('\n' + "./fuzz_out/queue/"))
length = len(checks) - 1
count = 0
for i in checks:
    if i in corrects:
        count += 1

print(count)
print(length)
print(count / length)
file1.close()
file2.close()
差分测试结果

readelf
输出结果相同的测试用例数量:8
所有测试用例数量:197408
输出结果不同的比例:99 . 995947479332144593937429080888%
optdump
输出结果相同的测试用例数量:350188
所有测试用例数量:756270
输出结果不同的比例:53 . 695373345498300871381913866741%
size
输出结果相同的测试用例数量:64,804
所有测试用例数量:122,384
输出结果不同的比例:47 . 04863380834096%
nm
输出结果相同的测试用例数量:12
所有测试用例数量:144510
输出结果不同的比例:99.991696076396097155906165663276%
cxxfilt
输出结果相同的测试用例数量:30
所有测试用例数量:135510
输出结果不同的比例:99.97786141245%

差分测试结果说明

如上文所述,由于mull对每个变异体的执行过程高度封装,因此难以进入到子进程内部去修改运行逻辑,我们难以得到测试用例在每个变异体上的表现情况,即无法获知对于特定变异体,AFL产生的测试用例运行的结果是否与原程序有区别。而考虑到,mull工具对每个变异体都会执行相同的测试用例,因此我们转变视角:以测试用例为基准,计量有多少测试用例的在不同程序上的输出有区别。以此虽不能得出变异得分,但也能反映出AFL产生的测试用例的质量。即差分测试结果中的输出结果不同的比例。

结果分析

模糊测试过程及变异测试结果的绘图结果

对变异测试结果中,各变异算子的解释可查看 mull 官网:mull官网
cxxfilter
模糊测试过程:
在这里插入图片描述
变异测试结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
nm
模糊测试过程:
在这里插入图片描述
变异测试结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
objdump
模糊测试结果:
在这里插入图片描述
变异测试结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
readelf
模糊测试过程:
在这里插入图片描述
变异测试结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
size
模糊测试过程:
在这里插入图片描述
变异测试结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
strip
模糊测试过程
在这里插入图片描述
变异测试结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
xpdf
模糊测试过程:
在这里插入图片描述

结论

1.通过上述项目afl模糊测试过程结构可知,afl工具的执行效率较高,每个程序都能在每秒有大于750次执行,此外,由于待测程序本身质量较高,因此模糊测试过程中,并未得出能让待测程序崩溃的测试用例。而当AFL模糊测试过程进行充足时间后,可以探测出大量的独特的原程序执行路径,生成的测试用例保存在queue文件中。
2.在本次实验,变异体被杀死条件是测试用例使得变异体崩溃。而AFL在进行模糊测试过程中,因为测试的对象基本都是广泛在真实世界使用的程序,因此其bug较少,导致存储导致原程序崩溃的测试用例的crashes文件几乎没有结果。因此将queue文件夹中存有的测试用例用于mull的输入,其导致变异体崩溃的概率较小是正常的,因此变异得分低并不能说明AFL的效果不佳。
3.在本次实验中,我们进行了差分测试,以测试用例为基准,计量有多少测试用例的在不同程序上的输出有区别。以此虽不能得出变异得分,但也能反映出AFL产生的测试用例的质量,通过差分测试,我们发现,AFL的效果较好。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值