下面将阐述WasmFuzzer,一款针对WebAssembly虚拟机的模糊测试工具,是如何被设计与实现的。
下面是WasmFuzzer的架构图,如图1所示。
如图所示,WasmFuzzer主要包括三个部分,展示层主要包括命令行接口和测试报告的生成,中间层主要有种子生成、测试用例的变异,变异策略和结果的判定。测试执行引擎包括Forkserver、读写文件和覆盖的获取。
WasmFuzzer是一款用于测试WebAssembly虚拟机的模糊测试工具。WasmFuzzer的工作流程如图2所示。图中的队列指的是当前等待被使用的WebAssembly模块列表。在WasmFuzzer运行过程当中,会以先进先出的顺序使用队列当中的WebAssembly模块。WasmFuzzer在运行开始时会读取初始WebAssembly代码文件,将它们解析后加入到队列当中。这些初始WebAssembly代码文件还会在此时被输入到WebAssembly虚拟机当中执行测试,以得到初始的路径信息。
WasmFuzzer最核心的工作是图2中所示的从“从队列中取出队首WASM模块”到“结束模糊测试”之间的循环:将当前WebAssembly模块通过WasmFuzzer内置的各种变异操作进行变异后输入给WebAssembly虚拟机进行执行;检测该虚拟机结束时的状态以判断它是否崩溃,同时根据运行时信息判断是否找到了新的路径;如果找到了新的路径或是出现了新的崩溃,那么则会将记录这个情况并将该WebAssembly模块加入到队列中;如果不继续在相同的WebAssembly模块上进行变异,则会从队列中取出另一个WebAssembly模块进行变异;直到队列为空后,再将所有曾经保存到队列中的WebAssembly模块重新加入到队列中以进行更多的模糊测试。
指令是WebAssembly代码当中的基础单位。若要完整地测试WebAssembly虚拟机,则需要尽可能地覆盖所有指令。WasmFuzzer提供生成指令的功能,包含两部分工作:创建指令对象和随机生成参数。
指令对象是WasmFuzzer中存储WebAssembly模块中指令的形式,需要按照一定的步骤进行创建。创建指令对象的步骤主要包括:先读取这个指令所需要的参数,之后使用参数创建这个指令的全新对象并获得它的指针,最后再将得到的指令对象指针返回。指令所需要的参数由外部生成,一般会需要是一个合法的参数。创建指令对象时直接使用独占指针创建这个指令的对象,用于保证该指令对象能够在不再使用的时候能够自动释放。在指令生成后,即可返回它的指针。
由于WebAssembly指令数量较多,在生成时如果每个指令都写一份各自的生成逻辑,代码的冗余会很高,在修复代码错误时也会有更高的代价。但是如果完全将代码分类后按照类型进行处理,则会因为需要大量的分支判断而导致性能的下降。因此,在此处将指令进行简单分类后再进行实现指令的生成,差别极小的将被合并处理,而存在着一定差别的则各自实现。这里的分类与第二章中WebAssembly的设计中的分类有着一定的区别。
单独使用的一条指令,包括unreachable、select、br、br_if等控制指令、drop和select这种参数指令、get和set这种变量指令、所有表格指令和所有内存指令。这些指令的特点是与上下指令无关,也与类型无关。它们涉及到的参数各自不同。在这种情况下,需要对这些指令进行各自实现。
以end指令结束的控制指令,包括block、if、loop这3条控制指令。这些指令的特点类似,区别仅仅在于if和loop有它们特定的参数。出于执行效率的考虑,这里还是各自实现,但是它们的实现形式几乎完全一致,仅仅在处理参数时有所不同。这里不统一实现的考虑在于处理参数的办法是不同的,如果要统一处理的话,在处理参数时还会需要进行一次分支判断。
常量指令,包括i32.const、i64.const、f32.const和f64.const。这些指令的行为完全一致,但是参数的长度有所不同。与指令块指令一样,这里对这4个指令也是分开实现,它们的实现形式几乎完全一致。
其他运算指令,包括各种数据类型的abs等单操作数指令,add、sub等双操作数指令,eqz等测试指令,eq等比较指令,reinterpret等转换指令。这些指令没有参数,行为完全一致,所以可以在各自分类中直接统一实现生成,无需考虑分支跳转的影响。
在创建指令对象的过程中需要将参数加入到指令内,所以应该先生成指令的参数再创建指令对象。由于模糊测试是自动化进行测试,生成参数也需要自动化地随机生成。为了与指令对象保持一致,生成参数的分类方法与创建指令对象时相同。如果在创建指令对象时是各自实现的,那么在生成参数时同样也是各自实现的;如果在创建指令对象时是在分类内统一实现生成的,那么在生成参数时同样也是在相同的分类内统一实现生成。根据指令的特点,生成参数时主要有着两种方法:从模块中选择参数和从数据域中生成参数。
从模块中选择参数是在指令的功能依赖模块内部情况时使用的,从模块中选择一个合法的值并返回生成参数成功,如果不存在合法的值则返回生成参数失败。例如global.set指令的作用是设置某个全局变量为栈顶值,它的参数仅有1个全局变量编号。所以在生成该指令的参数时,需要从模块内全局数组中获得所有全局变量,并从中选择其一作为指令的参数(或是直接返回生成参数失败)。
而从数据域中生成参数是在指令需要读取预先设定好的值时使用,从一个数据范围内随机选择值返回。例如i32.const指令的作用是往栈上增加一个32位整数常量,它的参数仅有1个常量值。所以在生成该指令的参数时,需要从32位整数域(从0到4294967295)中随机选择一个整数返回。
实际上WebAssembly指令中还存在着一些不需要生成参数的情况,例如nop指令的作用是不进行任何操作。在指令不需要参数的情况下,生成参数时直接返回成功即可。
模块是WebAssembly的基本单位。当已有一个模块时,就可以在它上面进行变异,获得一个新的模块,用于模糊测试。变异模块时首先需要有着一定量的变异操作,能够对WebAssembly模块进行各种各样的修改,以尝试生成各种各样的WebAssembly代码文件,进而测试到WebAssembly虚拟机的更多代码。在WasmFuzzer提供了若干个变异操作之后,如何高效率地使用这些变异操作成为了一个问题。由于模糊测试需要生成大量测试用例用于测试,如果变异操作使用不当会生成很多无用的测试用例,进而导致测试的效率降低。因此,需要按照一定的规则合理地使用这些变异操作。这也就是为什么需要设计并实现WebAssembly模块的变异策略。变异WebAssembly模块的操作和策略将在第四章详细论述。
在软件运行过程中,可能会发出一些信号,让外部介入,调用预设的信号处理软件WasmFuzzer进行处理。WasmFuzzer可以记录下当前的运行状态,进而改进测试质量与记录崩溃位置。这些信号虽然会引发软件崩溃从而报告软件错误,但是这些崩溃不一定是由软件的漏洞导致的。在记录下当前数据之后,还需要人工判断具体的情形。
测试结果主要分为3部分:在队列中的WebAssembly字节码、发生独特崩溃的WebAssembly字节码和独特挂起的WebAssembly字节码。这里的“独特”指的是这些字节码之间所运行的代码路径存在区别。若干个独特崩溃(挂起)可能会由同一个软件问题所触发。这些WebAssembly字节码通过WasmFuzzer提供的保存到文件接口保存到文件当中,进而为之后分析数据做准备。
在队列中的WebAssembly字节码是指WasmFuzzer运行过程中加入到队列当中的那些WebAssembly模块所对应的字节码。这些数据在被WasmFuzzer首次加入到队列中时就会保存到文件当中,可用于分析WasmFuzzer运行时所涉及到的代码覆盖情况。
发生独特崩溃的WebAssembly字节码指在WasmFuzzer运行过程中生成的WebAssembly模块导出到被测程序后,使得被测程序独特崩溃的那些字节码。这些数据在WasmFuzzer记录到独特崩溃之时就会被保存到文件当中,它们主要被用于分析测试结果。
在WasmFuzzer启动时,会需要读取一些初始的WebAssembly代码文件。这些文件被存储在一个文件夹中,WasmFuzzer启动时遍历整个文件夹当中的所有文件,并通过WasmFuzzer提供的文件读取功能将这些文件读取至内存,在WasmFuzzer内初始化这些WebAssembly模块。这些WebAssembly代码文件还将被输入到被测软件中进行测试,以获得最初始的路径信息。当需要将WebAssembly代码输入到被测软件时,以及需要生成测试结果时,WasmFuzzer会将内部存储的WebAssembly模块输出到文件当中。
读取WebAssembly代码文件时,通过一个被命名为WasmFuzzerFile的C风格的结构体进行实现,如表1所示。该结构可以直接序列化而不用任何编码解码工作(由于使用到了指针,具体占用的空间需要根据当前计算机环境决定)。
typedefstructWasmFuzzerFile{void*dataPtr;int32_tdataLen;MutationTypemutationType;uint64_tmutationPos;uint64_tmutationData;}WasmFuzzerFile;表1WasmFuzzerFile结构体定义WasmFuzzerFile结构总共有5个域:void型指针dataPtr,用于指向该结构所存储的代码数据,它实际上可能是WebAssembly模块类指针,也有可能是用于存放纯二进制数据的char型指针;32位有符号整数dataLen,用于对dataPtr进行分类,如果dataLen的值是-1则表明dataPtr是Module类指针,其他的值则说明dataPtr是char型指针,值表示文件的长度;枚举对象mutationType,可取未决定NOT_DECIDED、不变异NONE和覆写字节OVERWRITE_BYTE这3个值,用于定义输出到文件时是否需要进行额外的变异操作;64位无符号整数mutationPos,用于记录覆写字节的位置;64位无符号整数mutationData,用于存储覆写字节的值。
结构中mutationType的取值当中如果要变异的话,变异选项是只有一个覆写字节,后面的mutationData的数据类型是占用8个字节的64位整数。看起来虽然很矛盾,但是这是为了之后扩展的考虑而设计的。如果未来需要,mutationPos和mutationData这两个域都是64位整数,可以使用强制类型转换变成最长64位的指针,在当前的计算机架构上已经够用。在当前仅能覆写字节的情况下,mutationData只需要使用到这8个字节当中的1个字节即可。
读取WebAssembly代码文件时,该函数有2个参数:文件名和指向指向WasmFuzzerFile结构的指针的指针(即数据类型为WasmFuzzerFile**,实际在实现时使用的统一是void**,便于进行强制类型转换)。该指针在传入时应为空值,函数内部会分配堆上一个WasmFuzzerFile*型数据给该指针,并使用该数据读取WebAssembly代码文件。dataPtr和dataLen在读取WebAssembly代码文件时被赋值,且会在需要将模块写入文件时被使用。由于读取的文件可能不是WebAssembly代码,所以保留了dataLen为-1的情况用于存储纯二进制代码,WasmFuzzer提供的所有针对模块的变异操作均不能在这种情况下使用。而mutationType被默认设置为未决定,mutationPos和mutationData此时的值均无意义。读取WebAssembly代码文件的伪代码如表2所示。为了表述上的清晰,输入仅有一个文件名,输出为WasmFuzzerFile结构体变量指针。
//输入:文件名filename//输出:WasmFuzzerFile结构*fileWasmFuzzerFile*file=newWasmFuzzerFile;file->mutationType=NOT_DECIDED;二进制数据*data=读取文件(filename);模块*module=读取模块(data);if(module==NULL){//不是WebAssembly模块file->dataLen=data->长度;file->dataPtr=data;}else{//是WebAssembly模块file->dataLen=-1;file->dataPtr=module;deletedata;}returnfile;表2读取WebAssembly代码文件伪代码写入WebAssembly代码文件时,该函数同样是与读取时一致的2个参数:文件名和指向指向WasmFuzzerFile结构的指针的指针。在结构体内,mutationType、mutationPos和mutationData用于输出文件时使用。WebAssembly代码在输出到文件之后,检查mutationType的值。当mutationType是未决定时会先随机地变成不变异或覆写字节,同时mutationPos和mutationData会随机变成2个64位整数。之后如果mutationType是覆写字节则会重新打开输出的文件,并在mutationPos对文件长度取余数的位置写mutationData中的1个字节进行覆盖。写入WebAssembly代码文件的伪代码如表3所示。为了表述上的清晰,输入为文件名和WasmFuzzerFile结构变量指针,没有输出。
在这里展示两个初始WebAssembly代码文件,分别为add.wasm和nothing.wasm。这两个文件将在实验评估中使用。
第一个初始WebAssembly代码文件add.wasm包含一个导出的函数add,接受2个32位整数参数,并返回1个32位整数参数。这个函数的功能是将这两个参数相加。WebAssembly文本格式的add.wasm代码如表4所示。
//输入: 文件名filename、WasmFuzzerFile结构*file//输出: 无if(file->dataLen!=-1){//如果不是WebAssembly模块return;}写入模块(filename,file->dataPtr);if(file->mutationType==NOT_DECIDED){file->mutationType=随机选择(NONE,OVERWRITE_BYTE);file->mutationPos=随机整数();file->mutationData=随机整数();}if(file->mutationType==OVERWRITE_BYTE){覆写字节(filename,file->mutationPos,file->mutationData);}return;表3写入WebAssembly代码文件伪代码(module(func$add(param$lhsi32)(param$rhsi32)(resulti32)get_local$lhsget_local$rhsi32.add)(export"add"(func$add)))表4add.wasm(WebAssembly文本格式)第二个初始WebAssembly代码文件nothing.wasm包含一个导出的函数add,没有任何参数,返回1个32位整数参数。这个函数共3条WebAssembly指令:获取32位整数常量1、获取32位整数常量2、32位整数相加。这样的函数,正常运行的返回值是3。WebAssembly文本格式的nothing.wasm代码如表5所示。
变异WebAssembly模块是WasmFuzzer当中重要的组成部分。在第三章中已经论述过变异WebAssembly模块的原因,在本章当中将详细论述变异WebAssembly模块的操作与策略。
变异操作是对WebAssembly模块进行变异的方法。这些操作对模块的各个部分进行了一定的修改,让经WasmFuzzer修改而得的模块能覆盖到WebAssembly虚拟机的各种功能。
变异操作分为2个类型:针对函数的操作和针对函数以外的操作。目前WasmFuzzer已支持的变异操作如表6所示。
针对函数的操作主要是在指令序列上进行修改。指令序列是WebAssembly代码的核心部分,包含着实现函数功能的所有指令。在指令序列上进行修改时,可以生成一条新指令并插入到指令序列中,以及删除、移动已经存在的指令。这里指令序列的实现方式是侵入式链表,所以在插入、删除以及移动指令的时候开销不大,而且省去了插入新指令时的复制操作。生成新指令时,先随机生成一个指令,再根据这个指令的特点生成它的操作数,最后找到一个合适的插入点将该指令插入。删除指令是随机定位一条指令,将其从指令序列中删除。移动指令则是先随机定位一条指令用于移动,再随机定位另一条指令用于标记插入位置,将第一条指令从指令序列中删除后再插入到第二条指令的位置。除了在指令序列上进行操作之外,针对函数的操作还包括创建一个新的空函数、删除一个已有的函数和交换两个函数的位置。增加一个新函数需要先随机选取一个函数类型,之后创建一个新函数的独占指针并插入至模块中。删除一个函数则是随机定位一个函数,将其从模块中删去。交换两个函数的位置是随机定位两个不同的函数,直接交换它们的指针即可。
针对函数以外的操作是在模块其他部分进行的,这些操作用于完善WebAssembly文件的各种情形。目前支持的操作包括增加全局变量、增加导出、增加类型和设定开始函数等。这些操作能够反馈回针对函数的操作,使得可用的指令种类更多。例如global.set指令需要有一个合法的全局变量作为操作数,这个全局变量就可以先在针对函数以外的操作中创建。这些操作相对实现时较为简单,根据这些操作实际需要的限制条件进行实现即可。例如在增加一个全局量时,需要确定它是变量还是常量,还需要确定它的数据类型。这些都确定了之后,即可新增这个全局量到模块中。
在对一个合法的WebAssembly模块执行了变异操作之后,是有可能会让WebAssembly代码变得不再合法的。这里的设计思路为不保证变异后的WebAssembly代码合法,只是让变异后的WebAssembly代码保持其应有的结构。上文中所谓“合法”的WebAssembly代码指的是能够通过WebAssembly文档所论述的验证(Validation)[1]的WebAssembly代码,它有着非常严格的验证流程。例如,如果一个WebAssembly模块拥有着函数,这些函数的类型必须是类型数组中的函数类型,否则就是不合法的。为了尽可能地生成合法的WebAssembly代码,有一些变异操作看似很好实现但是并没有加入到WasmFuzzer当中。例如删除一个类型(eraseType)变异操作,在代码实现上非常简单,但是使用时很容易导致代码变得不合法。如果当前仅有1个函数,也只有1个类型,执行删除一个类型变异操作后就不存在任何类型了,那个未删除的函数也无法正常执行,直到增加一个兼容该函数的类型之后才能执行。在这种情况下可能导致WasmFuzzer生成的大量WebAssembly代码不合法且无法被WebAssembly虚拟机执行,影响WasmFuzzer进行模糊测试的效率。
不过也并不是所有会导致WebAssembly不合法的变异操作都不会使用,删除一个全局变量(eraseGlobal)变异操作就是一个例子。如果一开始的WebAssembly代码文件当中没有对全局变量进行任何操作,依次执行增加一个全局变量、插入一条global.set或global.get指令、删除一个全局变量这3个变异操作是会导致WebAssembly代码不合法,但是这种情况出现的概率相比删除一个类型导致WebAssembly代码不合法的概率要低很多,因此也就被保留下来。
增加一个空函数变异操作伪代码如表9所示。该变异操作需要保证类型数组当中有函数类型,否则将不予增加。之后随机选择一个函数类型,并生成一个该函数类型的函数对象,增加到函数数组的最后。
//输入: WebAssembly模块module//输出: 增加一个空函数后的WebAssembly模块module类型数组funcTypeVector=module.类型数组.筛选(种类=函数);if(funcTypeVector.大小()==0){returnmodule;}类型funcType=funcTypeVector.随机元素();函数*func=new函数(funcType);module.函数数组.增加元素(func);returnmodule;表8增加一个空函数的变异操作伪代码删除一个全局变量变异操作伪代码如表10所示。该变异操作需要先判断全局数组当中是否有全局变量,如果没有则直接返回。之后则随机选择其中一个全局变量,将该对象删除后再从全局数组中移除。由于全局数组在内存中占有连续的存储空间,此时删除中间的元素可能导致全局数组元素的移动。
//输入: WebAssembly模块module//输出: 删除一个全局变量后的WebAssembly模块moduleif(module.全局数组.大小()==0){returnmodule;}intposition=module.全局数组.随机位置();deletemodule.全局数组[position];module.全局数组.删除位置(position);returnmodule;表9删除一个全局变量的变异操作伪代码交换两个导出条目的位置变异操作伪代码如表11所示。该变异操作需要先保证导出数组中至少有着2条导出条目,否则将直接返回。之后随机选择其中2条导出条目,调用交换函数将它们交换即可。这里并不考虑交换的两个元素是同一个元素的情况,即如果交换了同一个元素,则执行了交换两个导出条目的位置变异操作后对WebAssembly模块没有改变。
除此之外,顺序遍历还可能导致出现生成的WebAssembly代码与之前已有的代码完全一致的情况的概率较大,进而执行了无用功。例如以下这种情况。当前WebAssembly代码中只有一个函数A,通过增加一个空函数B之后当前WebAssembly代码就包含了A和B两个函数。之后删除一个函数,由于删除哪个函数是随机的,如果删去了B函数则恢复了最初的情况,这次生成的代码完全没有执行的必要。删去A函数和删去B函数的概率都是50%,都是很高的概率。然而删去A函数就一定好吗?也不一定。删去A函数后WebAssembly代码当中仅包含了一个空函数B,之后如果将这个模块作为最初的模块进行变异的话,会先增加一个空函数C,然后从空函数B和空函数C当中选择一个删除。很显然这次删除空函数B还是空函数C都会和之前的WebAssembly代码完全一致,同样也没有导出并执行的必要。
从这里可以得到结论,虽然顺序遍历的执行速度快,但是可能会导致测试不足够充分或测试重复的问题,这也是接下来的变异策略所想要解决的问题。
在每次从队列中取出一个WebAssembly模块时,进行一个循环,每次执行循环体时从所有的变异操作当中随机选择一个变异操作执行,这就是随机变异策略。这些变异操作会累积影响当前WebAssembly模块,直到循环结束。假设队列头部的WebAssembly模块有着包含1条指令的1个函数,且循环体执行3次的话,如果随机到了插入一条指令、插入一条指令、删除一个函数这3个变异操作,则这个循环所生成的WebAssembly代码文件分别有:包含2条指令的1个函数、包含3条指令的1个函数和没有函数。使用伪代码说明的随机变异策略的流程如表12所示。
//输入: 从队列中取出的WebAssembly模块wasm、当前状态status//输出: 执行了随机变异策略后的状态statusfor(inti=0;i 在论述自适应变异之前,随机变异的实现模式也有需要讨论的地方。由于WasmFuzzer实际上是等概率进行随机选择,代码实现时可用switch语句,通过switch所得到的随机数来进入不同的case调用所对应的变异操作,之后break出switch语句即可。如果要设计一个不同概率的调用模式,switch语句实现起来将会比较繁琐。此处设计一个随机变异表,可用于不同概率下的随机变异。随机变异表是一个函数指针数组,里面每个元素都是一个对应不同变异操作的函数指针。假设希望插入一条指令的概率是50%,剩下的变异操作执行概率均相同。则在这16个变异操作的情况下,就可以将随机变异表的长度定为30,前15个(或者是穿插15个、后15个等)元素被定为插入一条指令,剩下的变异操作平均占据剩下的数组位置即可。之后每次随机变异时,从0~29中随机生成一个整数,再从随机变异表当中调用对应的变异操作即可。 在每次从队列中取出一个WebAssembly模块时,进行一个循环,每次执行循环体时从自适应变异表中随机选择的一个变异操作执行,这就是自适应变异策略。自适应变异的设计基于随机变异的思想,在其基础上尝试找到一种更加“智能”的方法来选择变异操作。 自适应变异表是从随机变异表扩展而来,它是一个长度为256的函数指针数组。这个数组的前16个位置是只读区域,不允许做任何修改。从第16个位置开始直到第255个位置都是可读写区域,允许进行修改。自适应变异表在WasmFuzzer启动时需要执行初始化,表内所有的位置均初始化为各种变异操作,它们的出现是等概率的。之后在循环体内从0~255中生成一个随机数,并把自适应变异表当中对应位置的变异操作取出执行。如果这次变异操作执行后,WasmFuzzer认为:发现了新的路径,则将这次变异操作覆盖到表内一些位置;发现了新的崩溃,则将这次变异操作覆盖到表内更多位置。这个过程被称作改变自适应变异表。自适应变异表进行改变的伪代码如表14所示,在记录了需要覆盖表内的多少位置后,随机从表内选取这个数量的位置,如果它们不在只读区域内就可以使用当前使得变异操作覆盖到这些位置当中。 //输入: 自适应变异表pool、当前使用的变异操作func、当前状态status//输出: 改变后的自适应变异表poolintcount=0;//当前要改变自适应变异表的条数if(status.找到了更多路径){count+=INC_COUNT_BY_NEW_PATH;//这个常数目前设置为2}if(status.找到了更多独特崩溃){count+=INC_COUNT_BY_NEW_UNIQUE_CRASH;//这个常数目前设置为16}for(inti=0;i //输入: 从队列中取出的WebAssembly模块wasm、当前状态status、自适应变异表pool//输出: 执行了自适应变异策略后的状态status、改变后的自适应变异表poolfor(inti=0;i 本章主要介绍了WasmFuzzer当中变异WebAssembly模块的操作与策略。WasmFuzzer提供了针对函数的变异操作和针对函数以外的变异操作,让WebAssembly代码能在模块上进行变异。为了合理地使用这些变异操作,WasmFuzzer当中集成了顺序遍历策略、随机变异策略和自适应变异策略这3种变异策略。 本章通过实验,对WasmFuzzer进行评估,步骤主要包括对实验进行设计和记录分析实验结果这两部分。 WasmFuzzer实现时提供了3种变异策略。由于自适应变异策略是随机变异策略的改进版本,实验仅对顺序遍历和自适应变异这2种变异策略进行分析。除此之外,实验中还需要将WasmFuzzer与AFL进行比较,分析结构化生成测试用例和二进制直接生成测试用例的优缺点。 实验程序选择开源的WebAssembly虚拟机Wasmer,Wasmer可以运行在命令行界面,也可以嵌入到C/C++、Rust、Python等其他语言中使用。Wasmer提供了三种关键特性:允许Wasm代码运行在其他语言中;可以让Wasm在各种Wasmer支持的操作系统上运行;可以充当桥梁,让Wasm文件可以和本机操作系统进行交互。 为了使WasmFuzzer能够捕获到Wasmer报出的panic,需要在Wasmer的主函数中加入捕获到panic就中止主线程的代码。 在进行模糊测试之前,要获取路径和覆盖的信息,可以对被测程序进行插桩。插桩指向程序中在一些位置中插入特定的软件代码,使得可以在被测程序外部获得被测程序的一些信息。 Wasmer是Rust编写的WebAssembly虚拟机。之前针对的C和C++编写的程序可以利用afl-gcc和afl-llvm实现插桩,Rust的插桩需要使用到AFL++的afl-llvm-rt来进行插桩,如表17所示。 在对虚拟机的模糊测试结束后,针对WasmFuzzer生成的能够引发崩溃的测试用例,还需要人工对它们进行分析,判断崩溃的成因。 本节先对WasmFuzzer进行功能验证的结果进行记录并判断,之后对WebAssembly虚拟机进行模糊测试实验,并记录实验数据。实验数据从出发的Wasmer的崩溃这个角度进行分析。 总体结果(overallresults) 已完成循环数量(cyclesdone) 路径数量(corpuscount) 独特崩溃数量(savedcrashes) 独特挂起数量(savedhangs) 循环进度(cycleprogress) 当前的测试用例(nowprocessing) 超时的路径数量(pathstimedout) 表覆盖(mapcoverage) 表密度(mapdensity) 计数覆盖(countcoverage) 阶段进度(stageprogress) 当前变异操作(nowtrying) 当前执行进度(stageexecs) 已执行次数(totalexecs) 执行速度(execspeed) 深度发现(findingsindepth) 有趣的路径数量(favoreditems) 找到新边的测试用例数量(newedgeson) 崩溃个数(totalcrashes) 超时个数(totaltmouts) 模糊策略区域(fuzzingstragegyyields) 插入/删除/移动指令(ins/era/movinstr) 增加/删除/交换函数(add/era/movfunc) 增加/删除/交换全局变量条目(add/era/movglobal) 增加/删除/交换导出条目(add/era/movexpor) 增加类型条目(addtype) 增加内存条目(addmemory) 设置/删除开始函数(set/erastart) 路径情况(itemgeometry) 变异层数(levels) 队列中未执行过的WebAssembly模块数量(pending) 队列中有趣的未执行过的WebAssembly模块数量(pendfav) 本地找到的路径数量(ownfinds) 其他并行执行的WasmFuzzer所导入的路径数量(imported) 当前程序运行稳定程度(stability) 之后按Ctrl+C组合键退出WasmFuzzer,可以在outDir文件夹中找到crashes文件夹。该文件夹内除了README.txt文件用于介绍信息之外,都是会导致被测程序崩溃的WebAssembly代码文件,其数量为独特崩溃个数。任意选择其中一个WebAssembly代码文件,任意选择其中一个WebAssembly代码文件,使用Wasmer执行这个文件,则可以看到Wasmer崩溃,如图4所示。 此时查看软件崩溃时的调用栈,如图5所示 对于-i参数接口的测试,指定了三个不同的输入文件夹input1、input2、input3,其中不同的input中所包含的种子文件不同。 对于-o参数接口测试,指定了三个不同的输出文件夹output1、output2、output3。 -m参数是指定目标程序的内存限制,对于-m参数,指定两个不同的值做测试。 -E参数是指定fuzz程序的执行次数,对于-E参数的测试,指定三个不同的值做测试。 -n参数是指定不插桩模式,对于-n参数,指定两个值做测试。 -n -T参数是指定fuzz程序的标题,对于-T参数,指定两个值做测试。 -T -b参数是指定绑定的CPU,对于-b参数,指定两个不同的值做测试。 -b -e指定种子文件的文件扩展名,对于-e参数,指定两个不同的值做测试。 -e 对于没有出口的递归调用,有的wasm文件会报出runtimeerror,有的wasm文件报出panic。预期结果应该是runtimeerror。 000000.wasm会出现panic。 003086.wasm会出现runtimeerror。 Wasmer2.1.1 Wasmer2.2.1 对应于表格20中的1和2。 对于wasmer2.2.1 wasmer2.2api执行000000.wasm wasmer2_2exec000000.wasmwasmer2.2api执行003086.wasm wasmer2_2exec003086.wasm对于wasmer2.1.1 wasmer2.1api执行000000.wasm wasmer2_1exec000000.wasmwasmer2.1api执行003806.wasm 当使用wasmer2.2api和wasmer2.1api执行003086.wasm时,会出现stackoverflow的运行时异常。如图7所示。 但000000.wasm和003086.wasm都是相似的没有出口的递归调用,说明wasmer2.1的api和wasmer2.2的api对处理调用栈溢出时会出现不一样的结果,在处理stackoverflow这种问题时会出现不一致。 对于同一个wasm文件,Wasmer不同版本在执行时,一些版本会出现超时现象,但是期望的结果确实出现运行时异常栈溢出。 003884.wasm Wasmer2.3.0 对应于表格20中的1、2、3。 使用wasmer2.1.1的api执行003884.wasm wasmer2_1exec003884.wasm使用wasmer2.2.1的api执行003884.wasm(正确的行为) wasmer2_2exec003884.wasm使用wasmer2.3.0的api执行003884.wasm(正确的行为) wasmer2_3exec003884.wasm使用Wasmer2.2.1CLI、Wasmer2.3.0CLI、Wasmer2.1.1CLI执行003884.wasm时 但是当使用Wasmer2.2.1CLI,Wasmer2.3.1CLI,Wasmer2.1.1CLI都会出现超时的现象。如图10所示。 说明Wasmer在遇到stackoverflow这种异常时,一些版本没有捕获到这种异常,而是一直不断地运行,会出现超时的情况。 当使用wasmer2.1.1的api多次执行同一个wasm文件时,有时候会出现运行时异常退出,有时候会出现超时。 003557.wasm wasmer2.1.1 对应表格20中的6 使用wasmer2.1.1的api多次执行003557.wasm。 第二种情况超时。如图13所示。 Wasmer2.1.1的api多次执行003557.wasm文件时,有时候会出现运行时异常退出,有时候会出现超时。说明Wasmer2.1.1的api对同一个wasm文件没有统一的结果。栈溢出有时候会被Wasmer2.1.1的api捕获到,有时候不会被Wasmer2.1.1的api捕获到。 WebAssembly作为一种新型语言,已经在许多场景下有着它独特的作用。而WebAssembly虚拟机作为执行WebAssembly代码的基础软件,它是否安全成了一个重要的问题。如果WebAssembly虚拟机存在着问题,将这款虚拟机集成的软件就有可能被攻破,以至于遭受损失。而模糊测试作为一种被广泛使用的软件测试技术,已经在很多领域上找到了之前没能找到的软件。将模糊测试技术应用在WebAssembly虚拟机上也就成了一个理所应当的选择。 这就是为什么本文设计并实现了一款针对WebAssembly虚拟机的模糊测试工具WasmFuzzer。该模糊测试工具能够结构化地变异并生成WebAssembly代码,并自动地将生成的代码导入到WebAssembly虚拟机当中进行测试,之后导出触发了崩溃的WebAssembly代码文件。WasmFuzzer提供了3种变异策略:顺序遍历、随机变异和自适应变异。这些变异策略有着各自的优点与缺点。在WasmFuzzer实现完成后,本文对它进行了实验评估,WasmFuzzer经过了实验评估后,认为它确实可以用于WebAssembly虚拟机的模糊测试。WasmFuzzer使用的一些技术对于WebAssembly虚拟机的测试是有着一定帮助的。这些技术也有着被迁移到测试适用于其他语言的虚拟机上的可能性。 不过,WasmFuzzer也存在着一些需要改进的地方。例如WasmFuzzer所采用的自适应变异策略是一种正反馈策略,可能导致某种变异操作被过度使用,进而影响到WasmFuzzer整体的测试结果。在未来可以对自适应变异策略进行一定的改进。虽然WasmFuzzer主要针对WebAssembly虚拟机进行模糊测试,但实际上只要是读取WebAssembly代码的软件均可以用WasmFuzzer进行测试。目前WasmFuzzer没有在这些软件上进行实验,未来也可以针对这些软件分析并改进WasmFuzzer。