一、浏览器模糊测试简介
在当前整个计算机网络环境中,web浏览器是应用最广泛的软件之一,针对浏览器发起的攻击层出不穷。近年来,浏览器安全事件频发,给人们带来严重的损失。而Fuzzing的自动化属性以及其表现出的强大性能使得其能高效应用于浏览器漏洞发现领域。目前针对于浏览器的fuzz技术主要分为两个类别:基于生成的和基于变异的模糊测试技术。
基于生成的模糊测试技术是最早应用于浏览器模糊测试的技术。它主要是通过手工构建或者是尝试使用一些自动化方法,像上下文无关语法、深度学习的方法构建语法库模板,以此来生成测试用例。该类方法虽然能够使得生成的测试用例具有较高的语法正确性,但一方面语法库的构建成本较高,另一方面该方法很难产生能过够触及深层代码漏洞的复杂输入。
基于变异的方法,在初始阶段主要是采用一种随机变异的方法来根据现有输入变异生成一些子代码片段,以此组合来生成新的测试用例。但是由于浏览器解析测试代码时对代码的语法和语义上下文极其敏感,因而逐渐衍生出语法感知和语义感知突变技术。目前这两类技术的研究重点主要在于对浏览器的JavaScript引擎的模糊测试,基础思想都是首先将JS代码转换为语法树AST,再在语法树上进行相关变异操作。
本文所介绍的工具DIE就是一种使用语法和语义突变技术对JavaScript引擎进行模糊测试的工具。同时,该工具的特点在于:在变异过程中,会保留原始种子输入的有利性质和条件。这些希望保留的优选属性称为aspect。同时对其他部分进行变异,以便可以发现类似的或新的错误。
二、工具基本原理概要
01 动机
DIE设计保留属性变异的前提在于,它认为当一个POC能造成漏洞时,与POC具有相同的结构输入就有很大可能造成新的漏洞,并且通过在原有POC基础上进行保留变异,生成的新的代码就可以在原有的基础上进入更深层次的代码片段,具有更高的质量。
下图所示为两个相似漏洞的实例。首先这两个代码共有的前提条件有:①循环调用opt()函数,以此来启用JIT;②中都用不同的参数再次调用opt()函数;③则是定义了opt()函数内部的访问顺序。对比这两个POC,可以看到如果(a)被作为输入语料库,我们需要保留①-③中的某些属性,再添加④就可以发现新的漏洞。
因此DIE工具提出一种方面保持变异——在生成新的测试用例时,随机地保留了原始种子输入的有利性质和条件。另一方面保持变异包括两种变异策略:即结构保持和类型保持。结构保持突变尊重结构方面,如循环或分支;而类型保持突变则保持每个句法元素的类型突变,可以有效降低测试用例在执行过程中的语法和语义错误。该工具是2020年发表在S&P期刊上。
论文源码https://github.com/sslab-gatech/DIE
论文链接https://ieeexplore.ieee.org/abstract/
document/9519403
02 基础架构
DIE工具是在AFL的基础上构建的。主要变化是在AFL的基础上引入预处理阶段,并将AFL传统变异更改为在语法树上进行相关属性的保留变异。如下图所示为DIE工具的架构设计,主要分为3个模块:预处理、种子变异和fuzz执行。
首先①DIE预处理所有原始种子文件,通过动态\静态分析构建各种子文件所对应的类型化AST。②在fuzz的主循环中选择一个语料库中的测试用例及其类型AST。③DIE通过修改\插入新节点变异类型AST,在该过程中保持其类型和结构信息。④将变异后的类型AST转换回JavaScript文件和执行fuzz。⑤DIE记录运行时覆盖反馈信息决定新文件将被保存。并且记录运行时产生的崩溃信息。
2.1 类型AST构建
类型AST是在传统AST的基础上,为每个AST节点拓展出其类型和相关绑定信息。
类型AST的基本构建过程如下:首先DIE会调用babel工具中的各种api接口将JS代码解析为传统的AST,然后DIE会在引用标识符(即变量名、函数名等)的语句前对种子文件进行插桩,插桩代码会跳转到一个类型解析函数,解析得到标识符的类型。如下图所示。
通过上述静态分析我们可以得到AST中标识符等叶节点的基本类型,然后在AST的基础上,DIE参考ECMA-262标准从下到上静态地推断出其他AST节点的类型。ECMA-262规定了在特定表达式或内置API中使用的参数的类型。此外,DIE同样记录自定义函数的参数和返回值的类型,以便在新构建的AST节点中进行合法调用。为了完整起见,DIE还为没有定义值类型的语句标注了相应的描述性类型,如if语句、函数声明等。
2.2 变异类型AST
对于给定的输入,DIE以方面保留的方式改变类型AST,在变异过程中,DIE特别避免删除整个if语句、循环语句和自定义函数定义,这在一定程度上保留了现有JS代码的结构,这是结构保留变异的基本思想。具体的变异过程如下图伪代码所示,具体变异策略主要分为以下两种:
01
变异一个类型子树:DIE随机选择一个没有结构作用的子树AST。然后,子AST被替换为由具有相同类型的构建器构建的新AST(伪代码第5-6行所示)。
02
插入新语句或者新的变量:DIE定位语句块(例如,if语句的主体、函数或程序主体部分),并在块内随机选择一个代码点进行插入。接下来,DIE使用在该点声明的现有变量生成一个新的表达式语句,或者是生成插入一个新的变量声明(伪代码10-15行)。
当选择突变方法时,DIE优选sub-AST突变和新语句插入。DIE仅在长时间没有发现新的代码路径时才向输入中引入新的变量。
三、工具运行
01 运行环境与依赖下载
环境:Ubuntu 18.04
相关依赖下载安装:npm;nodejs;
radis-server;clang;AFL等
具体操作流程详情可见源码README文档;
02 目标JS引擎插桩
(1)下载引擎(以ChakraCore引擎为例):
进入engines目录:cd ~/DIE/engines
运行命令:
./download-engine.sh ch 1.11.24
./build-ch.sh 1.11.24
(2)修改proxy.py文件:
修改代码:
line8: this_is_chakra = True
line9: this_is_v8 = False
注释代码:
line114: new_cmdline = rewrite(new_cmdline)
(3)利用AFL对引擎插桩:
./build-ch-cov.sh 1.11.24
03 DIE服务端配置
(1)语料库准备:
项目运行的初始预料来源于已设定的种子库中,里面存储了针对于常见的四个JS引擎(ChakraCore/jsc/v8/
firefox)常见的一些POC文件。运行命令如下所示:
cd ~/DIE/
git clone https://github.com/sslab-gatech/DIE-corpus.git
python3 ./fuzz/scripts/make_initial_corpus.py ./DIE-corpus ./corpus
(2)与darid-server建立连接隧道
./fuzz/scripts/redis.py
(3)使用种子库运行
./fuzz/scripts/populate.sh [target binary path] [path of DIE-corpus dir] [target js engine (ch/jsc/v8/ffx)]
示例:
./fuzz/scripts/populate.sh ./engines/chakracore-1.11.24/out/Debug/ch ./DIE-corpus/ ch
(4)查看新创建的会话corpus
tmux attach -t corpus
04 DIE客户端配置
(1)执行测试:
./fuzz/scripts/run.sh [target binary path] [path of DIE-corpus dir] [target js engine (ch/jsc/v8/ffx)]
示例:
./fuzz/scripts/run.sh ./engines/chakracore-1.11.24/out/Debug/ch ./DIE-corpus/ ch
(2)查看创建的新会话fuzzer
tmux attach -t fuzzer
四、代码分析
01 类型AST构建:
DIE在具体实现过程中主要借助于babel工具中的各种api将JS代码转换为AST,并在AST上实现遍历和修改操作。
(1)首先依赖@babel/parser将JS解析为AST;要实现类型化AST,我们只需在原始Node结构中引入一个新的字段,该字段存储推断的类型,如下图所示。
(2)依赖接口@babel/traverse遍历节点路径解析得到类型AST,在解析过程中得到结点的类型。类型是由@babel/types定义的,像二进制表达式、逻辑表达式、数组类型、函数声明、数字、字符串等类型,如果不在这些@babel/types类型中则暂时定义为undefined。
02 测试用例变异与插入:
该部分代码位于DIE/fuzz/TS/base/estestcase.ts,主要包括了对测试用例结构的相关声明和变异、插入策略的实现。
(1)预变异premutate()
整体思想为调用this.nodes.set(path.node, number)结构判断是否对节点进行变异。当number数值为0时意味着跳过该结点跳过变异,当为-1时意味着它的子节点也会跳过变异。同时会对一些结点的变异类型设置一些倾向,具体变异倾向的类型详情可见代码DIE/fuzz/TS/base/espreferrnces.ts。以下为针对于各类型的结点的预变异处理:
1)当类型为函数时,不会变异整体函数且对所有函数的参数列表不变异;
a.
当其为函数声明的时候,像function test(){},不去变异函数的名称id;
b.
当其为Object或class方法的时候,这时如果它的关键字是一个变量或者声明不对其进行变异;
2)当是变量声明时,不对变量名称进行变异;
3)当是全局的函数调用,像main()不进行变异;
4)当是标记语句(case 1:等)、break语句不进行变异;
5)当是catch(e)语句时不对里面的异常e进行变异;
6)当是类或者结构体这种时,不对键值key进行变异;
7)当是函数调用表达式时,像a.b(c),倾向于这个结点变异为complexPreference:数组或新表达式、变量、标记符、或者ObjectLike类型。
8)当是赋值表达式时,左边参数变异的倾向是数组表达式、新的表达式、标志符等;
9)当是赋值匹配时,不对其和左边表达式进行变异;
10)当是更新表达式,像a++,变异的倾向是:数组或新表达式、标记符、内建程序;
11)当是for ..in 或者for...of结构时,不对其变异;
12)当函数参数是rest参数时,变异时这个结构本身不变,它的名称可以进行变异。
(2)预插入preInsert()
如果结点类型 是程序主体或者是代码块语句{}时,则将该结点,及其路径的body字段内容成对进行存储;其中存储的数组定义为:bodies: Array<pair<nodepath, array>>;
(3)预访问preVisit()
按照路径对其进行遍历,并进行预变异和预插入的过程;
(4)变异mutate():
首先将节点存储至位图,然后随机设置变异次数,变异次数的范围在this.MUTATE_MIN范围内。然后在变异次数范围内,随机选取位图内的结点,调用函数MUTATOR.mutate()进行变异。然后调用函数applyChange()应用变异得到的改变,具体操作是用新结点替换旧结点或者是新的操作符替换旧的操作符。
1)变异函数MUTATOR.mutate():位于DIE/fuzz/Ts/
base/esmutator.ts
首先设置变异改变类型:0表示对结点进行变异;1表示对操作符进行变异,并设置新、旧节点两个参数。针对于不同类型的结点的变异策略如下:
a.
对于代码块结构、函数结构、if和loop结构、函数参数、对象属性不进行变异;
b.
对于表达式变异:
具体实现函数为mutateExpOp函数,其位于DIE/fuzz/TS/base/engine/exp.ts中,主要包括对二进制、逻辑、更新和一元运算这四类表达式的变异操作,具体操作思想为:修改替换表达式中的操作符。
①二进制表达式:
代码中定义了二进制表达式的3类操作符,如下图所示:
然后调用函数mutateOp()对二进制表达式进行变异操作:首先通过解析表达式得到该表达式结点的操作符、左结点和右节点;然后根据操作符类型进行以下变异修改:
如果操作符不等于“+”或者操作符等于“+”且其左右操作数均为数字类型,则新的变异的操作符为任意NumArithOps。
如果操作符是比较类型:如果左右操作数都是数字类型则其操作符变异为任意CompOps。如果左右操作数不是全为数字类型,则新操作符为任意EqualOps.
②逻辑表达式:如果操作符为“&&”,则更改为“||”,否则新操作符为“&&”;
③一元表达式:原操作符为“+”,新操作符变异为“-”;原操作符为“-”则变为“+”;
④更新表达式:如果原操作符为“++”,则更改为“--”,否则替换后的新操作符为“++”;
c.
对于语句statement:
基本思想为生成新的statement语句来代替旧的语句来完成变异;其中新statement的构建依赖于函数build(),位于DIE/fuzz/TS/base/engine/statement.ts。
build()函数基本思想如下所示:
①DIE中生成新的语句主要是指生成以下四类语句:生成Objbased语句、生成新的变量声明语句、生成构造器语句、生成原型对象prototype语句。然后这四种语句对应编码为0,1,2,3。并将这四个数按照一定的个数占比存储至weights中。然后从weights中随机选取值来生成对应编码类型的语句。如下图代码所示,可以看出生成Objbased语句占比更多。
②genObjBasedStmt():根据结点的类型,选择生成相对应类型的语句。如下图代码所示主要包括以下类型。各类型语句的构建函数在这里不做过多详细介绍,可在代码中索引进行查看。
数字类型语句,包括赋值表达式、更新表达式和元表达式;
字符串类型语句,包括赋值表达式、内置对象属性获取、内置对象方法调用;
数组构建;
类型数组构建,主要是指一些固定类型的数组,像Int8Array、float32Arrray等;
函数调用语句构建(借助于bazel实现);
内置语句构建,包括内置属性和内置函数;
③genNewVar():构建新的变量声明语句,该部分功能主要依赖于@babel/types中的变量构建函数实现;
④genConstructorStmt():构造内置函数调用语句,与genObjBasedStmt()中的内置函数构建类似;
⑤genProtoTypeStmt():暂时不生成该类语句。
(5)插入insert():
插入所需要的语句其生成同上述变异中statement的生成相同。插入的基本思想是不在reutrn语句后面插入;如果是结点是程序体program,则在program下的body字段前插入语句。如果是结点是块代码语句,在块代码段内部开头插入语句。实现过程如下图代码所示:
03 原生AFL修改部分:
DIE用自己的突变引擎取代了AFL的二进制输入突变器。在代码中具体表现为:在afl-fuzz.c中,将主循环中的函数fuzz_one()替换为自定义的fuzz_js()函数。
(1)fuzz_js()函数首先调用自定义的generate_js函数,该函数的作用是:生成经过变异或者插入操作的测试用例,生成的测试用例存储至fuzz_input_dir中。
generate_js函数:
该函数的主要功能是执行代码文件esfuzz.js。
esfuzz.ts代码路径为DIE/fuzz/TS/esfuzz.ts,主要功能就是对输入的JS代码文件执行变异和插入操作。
(2)然后fuzz_js函数调用自定义的fuzz_dir函数。fuzz_dir函数的基本流程是按顺序遍历fuzz_input_dir种子文件,再调用AFL原生函数common_fuzz_stuff()执行fuzz过程。
五、结语
浏览器模糊测试主要在于对其各组件的测试,目前重点在对JavaScript引擎的模糊测试。而针对于JavaScript引擎测试的难点又在于对测试用例的生成,相较于传统变异策略,其需要保持JS本身的语法和语义结构特点,又要尝试生成高质量复杂的输入,便于执行到引擎的深层代码,有助于发现漏洞。本文所介绍的工具DIE就是一方面采用类型保持变异降低了语法和语义错误,又采用结构保持变异,保持输入的种子POC中一些特殊的结构,使得生成的测试用例可以执行到更深的层次。