Squirrel:基于覆盖反馈的数据库管理软件模糊测试工具详解|工具分析
2023-3-24 19:36:46 Author: mp.weixin.qq.com(查看原文) 阅读量:24 收藏

一、数据库模糊测试简介

数据库管理系统(以下简称为数据库)作为信息技术领域的基石软件之一,在信息技术领域占据至关重要的地位。但是,数据库作为一种复杂软件,在开发过程中会不可避免地引入各种漏洞,这些漏洞有可能被攻击者利用,从而对数据库使用者造成损失。因此,需要一种方法在开发阶段检测数据库中可能存在的漏洞,以此减少可能带来的损失。

模糊测试技术因其高效、自动化等特性,被广泛应用于检测程序中存在的各种漏洞,并取得了卓越的成果。因此,有学者将模糊测试技术应用于数据库漏洞的检测,提出了各种有效的数据库模糊测试工具。

目前,数据库模糊测试工具主要针对数据库中存在的两种错误:逻辑错误和能触发崩溃的错误。前者是指数据库在查询时返回的结果与预期不符,后者则是能够引发数据库崩溃。

对于逻辑错误,数据库模糊测试工具最主要的问题是找到一个合适的oracle,即应当使用什么现象来判断数据库出现了逻辑错误。最早的RAGS使用差异测试的方法检测数据库中的逻辑错误,该方法将同一条SQL语句输入多个不同的数据库,然后比较返回结果,如果返回结果不一致,则某个数据库出现了逻辑错误。这种方法的优点是十分简单,缺点在于:由于不同数据库的语法不同,因此该方法只能使用这些数据库中共同支持的SQL语句,不涉及每个数据库的方言;如果所有数据库出现同样的错误,则无法检测。2020年,Manuel Rigger和Zhendong Su提出了SQLancer工具,该工具集成了TLP、PQS和NoREC 3种方法,用于检测数据库逻辑错误。其中,TLP方法将查询划分为多个分区,并将原输入的结果和划分后每个分区的结果的并集进行比较,以此发现数据库逻辑错误,作者表示划分方法不唯一,而SQLancer使用的方法是使用逻辑谓词进行划分,即选择某个值,在SELECT语句中的WHERE子句中添加该值为真、假和IS NULL,生成3个SELECT语句,在使用UNION将这些语句连起来。PQS方法则是在数据库中选择某一行,生成能够获得该行的查询,再在WHERE子句中随机生成一个为真的表达式,检查结果,如果结果中不存在该行,则数据库存在逻辑错误。NoREC方法是对查询进行逻辑上等价的变形,将能够被优化的查询变为不能被优化的形式,然后对比两种结果的差异,以此寻找逻辑错误。这些方法的优点在于可以测试数据库方言,缺点在于不能对返回结果不确定的查询进行测试。

对于能够引发崩溃的错误,由于数据库崩溃是发生该错误的现象,而且易于观测,因此oracle不是问题。其主要问题在于生成语法和语义正确的输入。根据生成输入方式的差异,检测这种错误的数据库模糊测试工具可以分为两种:基于模板生成的和基于变异的。基于模板生成的数据库模糊测试工具主要是SQLsmith,该工具使用模板随机生成SQL语句,属于黑盒模糊测试工具。该工具在初始化阶段会从数据库中读取已有的数据库的信息,使用这些信息生成SQL语句,主要生成SELECT语句。在生成SQL语句时,会根据特定顺序生成语句中每个子句,对于每个子句,则会根据特定的顺序生成子句中的各个部分,以此类推,直至生成查询中的基本单元(即数值常量、字符常量、数据库对象标识符等)。在生成时,会记录下必要的信息,之后的生成会使用这些信息,确保语义正确,具体的生成顺序和生成方法需要使用专家知识编写。该工具的优点在于生成的语句正确率较高,缺点在于:1、属于黑盒模糊测试工具,效率较低,难以探测深层代码;2、需要手动编写模板。基于变异的数据库模糊测试工具基于变异方法可以分为基于结构变异和基于序列变异,这两种变异方法能找到的bug完全不同,属于互补关系。其中基于结构变异的测试工具主要是Squirrel,该工具将SQL语句解析为语法树,对语法树结构进行变异,Squirrel的具体情况在第二节详细说明。而基于序列变异的测试工具主要是Griffin,该工具的变异方法是对SQL语句的组合和顺序进行变异,即每次选择多组输入,每组输入有若干SQL语句,从这些输入中随机选择一些SQL语句,生成新输入,实践证明该变异方法确实有效。Griffin的创新点在于提出了一种新的实例化方法,能够替换SQL语句中的数据库对象标识符,使输入具有正确的语义关系,该方法分析每条SQL语句产生和使用的数据库对象,并将信息记录下来,在新的输入中,用输入中SQL语句产生的数据库对象,替换输入中SQL语句使用但在该输入中没有生成的数据库对象,以此确保输入中数据库对象引用关系的正确性,进而实现语义正确。基于变异的测试工具的优点在于使用了覆盖率引导机制,效率更高,同时能够探索更深层的代码,缺点则在第五节说明,其中Squirrel的局限性很大程度上反映了基于变异的测试工具的局限性。

二、Squirrel概要

Squirrel是由Rui Zhong等人在2020年提出的一个开源的基于变异的数据库模糊测试工具,其在SQLite、MySQL和MariaDB数据库中找到不少漏洞,是近些年最有名的开源的基于变异的数据库模糊测试工具,其最初是一款基于AFL的灰盒数据库模糊测试工具,不过最近作者进行了更新,基于AFL++实现了新版本的Squirrel。本文先基于最初的AFL版本简单介绍该工具的实现方法,再介绍该工具迁移至AFL++后发生的改变,最后介绍该工具AFL++版本的使用方法。

由于数据库的输入是高度结构化的SQL语句,因此与AFL这种传统的模糊测试工具相比,针对数据库的模糊测试工具有以下两个特点:

01

由模糊测试工具生成的SQL语句必须满足SQL语句的语法要求。由于数据库在处理SQL语句时,首先会解析SQL语句并生成对应的内部结构,如果SQL语句存在文法问题,则解析器会直接报错并返回,此时该SQL语句无法进入数据库内部。例如,Postgresql数据库使用bison进行语法解析,如果输入的SQL语句出现语法错误,则该输入只能探测bison部分的代码,这对于检测数据库内部的代码是没有帮助的,因此生成的SQL语句必须满足SQL语句的语法。

02

由模糊测试工具生成的SQL语句需要满足SQL语句的语义要求。数据库会根据SQL语句的语义来处理数据,如果在处理过程中,发现SQL语句的语义出现错误,则会导致数据库报错并提前返回。例如访问不存在的表时,数据库会提示该表不存在并终止后续操作。这会导致输入的SQL语句无法探测到更深层的代码,影响检测效果,所以生成的SQL语句需要满足SQL语句的语义要求。值得一提的是,从各种数据库过往发现的漏洞来看,有一小部分漏洞是由语义错误的SQL语句触发的,因此生成语义错误的SQL语句对于检测数据库内部的代码是有一定作用的,不过考虑到目前数据库模糊测试工具生成的SQL语句的语义正确率并不高,暂时还不需要在‘生成语义正确的SQL语句’和‘发现由语义错误的SQL语句引发的漏洞’之间进行权衡。

Squirrel采用语法保存变异、语义引导实例化以及代码覆盖率反馈等技术,使用SQL语句作为种子,由于不同数据库的SQL语句的语法存在差异,因此在使用Squirrel时,需要根据具体数据库编写语法解析器部分的代码,其本身支持SQLite、MySQL、Postgresql和MariaDB数据库。该工具由宾夕法尼亚州立大学的Rui Zhong等人开发,相关论文发表在2020年的CCS会议上。

项目地址   https://github.com/s3team/Squirrel

论文原文地址   https://dl.acm.org/doi/abs/10.1145/

3372297.3417260

2.1  基本结构

Squirrel的初始版本基于AFL开发,其基本结构如图1所示。Squirrel主要对AFL的变异方法部分进行修改,新的变异方法主要包含以下三个部分:解析器、变异器和实例化器。以下将以Postgresql数据库部分的实现为例,讲解这三个部分。Squirrel中,MySQL和MariaDB数据库的具体实现与Postgresql数据库基本相同,而SQLite数据库部分的具体实现在原理上相同,但是在具体代码上更复杂,与其他三个数据库有较大差别,表明作者在SQLite上花了更多心思,实际上,Squirrel在SQLite数据库中发现的bug要远多于其他三个数据库。

图 1 Squirrel基本结构

2.2  解析器

解析器的主要作用有两个:

01

将字符串形式的SQL语句解析为抽象语法树(AST)

02

将AST转换为对应的中间表示(IR)(对应图1中的IR Translation部分)

对SQL语句的解析是通过bison实现的。bison用于自动生成语法分析器程序,把LALR形式的上下文无关文法描述转换为可做语法分析的C或C++程序。关于bison的具体使用方法超出了本文的内容,在此不作赘述,有兴趣的可以在网上查阅相关资料了解,本文只简单讲解Squirrel中对bison的使用。

在Squirrel中,每个非终结符对应了一个类,该类定义如图所示。

每个非终结符类中,包含了指向该非终结符对应生成式中存在的所有非终结符的对象的指针和记录额外信息的变量。这些变量主要有4个:

01

data_type_:该变量为DATATYPE枚举类型,记录了当前非终结符对应的数据类型,仅用于identifier非终结符。在Squirrel中,该非终结符表示数据库对象标识符(即在SQL语句中用于标识表名、列名、索引名等数据库对象的字符串)。该变量记录了当前identifier代表的数据库对象类型(表名、列名或索引名等)。在实例化器中,会使用该变量。

02

data_flag_:该变量为DATAFLAG枚举类型,记录了当前identifier代表的数据库对象的上下文环境,仅用于identifier非终结符。以表名为例,使用CREATE语句创建表时,表名代表创建该表;而在SELECT语句的FROM子句中使用表时,表名代表使用已存在的该表;在DROP语句中删除表时,表名代表移除已存在的该表。对于这几种上下文环境,Squirrel分别使用kDefine、kUse和kUndefine表示。在实例化器中,会使用该变量。

03

scope_:该变量为int类型,用于记录数据类型的二级分类,仅用于identifier非终结符。例如,创建视图和表时,视图和表中列的数据类型都是kDataColumnName,上下文类型都是kDefine,而视图中列的scope_为11,表中列的scope_为2。在实例化器中,会使用该变量。

04

case_idx_:该变量为 unsigned int类型,用于记录规约生成式生成该非终结符时,使用的生成式编号。在将抽象语法树转换为IR时,会使用该值。

在bison解析SQL语句时,每次规约生成式都会生成对应的非终结符对象,并根据归约的生成式编号和该生成式中存在的终结符对新终结符类中的scope_变量和终结符类指针进行赋值,如果该生成式在规约时可以确定identifier非终结符的数据类型和上下文环境,则会对identifier非终结符对象的data_type_、data_flag_和scope_变量进行赋值。最终,在解析结束时,会得到一个以非终结符为节点的AST。

具体过程如下图所示,该图为bison中对CREATE TABLE语句的解析部分,其中if语句之前的部分是创建节点和设置case_idx_变量,以及连接子节点,这部分可以根据文法文件使用脚本自动生成。if语句后的部分是记录实例化中使用的data_type_、data_flag_和scope_变量,该部分需要手动编写。

bison文件决定了Squirrel支持的语法,使用Squirrel检测其他数据库或是对已支持数据库的文法进行扩充时,需要重新编写相应的bison文件,同时还要修改ast.h中非终结符类的定义和修改define.h中的ALLCLASS宏。除此之外,还要编写对应的词法分析器,这部分与编写常规词法分析器相同。

在得到AST后,Squirrel将AST转换为一种类似AST的IR结构。转换的目的主要有两个:1、得到的AST中只有非终结符节点,没有非终结符信息。SQL语句中的非终结符信息需要在转换过程中添加。2、在AST中,每个非终结符节点可能有若干个子节点,子节点数量的不确定性给后续的变异操作带来麻烦,因此需要对AST中的每个节点进行标准化处理,使得每个节点具有相同的结构,方便后续的变异操作。

IR节点的定义如下:

IR结构本质上还是一个树状结构,其中每一节点是一个IR对象,该对象中重要的变量如下所示:

01

type_:该变量的类型是IRTYPE枚举,枚举中的每个值代表一个非终结符。该变量表示IR对象的类型,在变异阶段,会根据这个类型对IR结构中节点的左右子树进行变异。

02

scope_、data_flag_和data_type_:这三个变量的含义与非终结符对象中的同名变量相同,在identifier非终结符对象转换为对应IR对象时,这三个变量会被复制到IR对象中。

03

int_val_、long_val_、float_val_、bool_val_和str_val_:这五个变量用于存储SQL语句中的数值常量、字符串常量和表名、列名等标识符。在Squirrel中,int_literal非终结符代表常量整数、float_literal非终结符代表常量浮点数、string_literal非终结符代表字符串常量、identifier非终结符代表数据库对象标识符,在实例化过程中,当处理这些类型的IR对象时,会将生成的值根据类型赋给这些对象中对应的变量,之后在将IR结构转换为SQL语句的过程中,这些变量中的值会被写入SQL语句,作为数值常量、字符串常量或表名、列名等标识符。

04

left_和right_:这两个变量的类型是指向IR对象的指针。为了使所有节点具有统一格式,Squirrel对AST进行了变形,使得每个节点至多有左右两个子节点。如果某个节点的子节点数量多于两个,那么在转换时,会创建unknown类型的IR节点来存放多出的子节点,如果unknown节点中的子节点依然多于两个,则继续创建unknown类型节点,直至所有节点满足至多有两个子节点的要求。子节点的左右顺序是由其生成式中非终结符的左右顺序决定的。

05

op_:该变量的类型是指向IROperator对象的指针,该类中有prefix_、middle、suffix_三个string类变量。该对象记录了SQL语句中的终结符信息,具体来说,prefix_记录生成式中left_变量代表的非终结符之前的终结符字符串,middle_记录生成式中left_变量代表的非终结符和right_变量代表的非终结符之间的终结符字符串,suffix_记录生成式中right_变量代表的非终结符之后的终结符字符串。

当由AST转换为IR结构时,以CreateTableStmt为例,具体情况如下图所示:

01

首先用switch语句根据节点中的case_idx_变量的值选择生成式对应的转换代码,即SWITCHSTART和CASESTART(0)、CASESTART(1)部分;

02

根据生成式中存在的非终结符,对节点中指向的非终结符对象分别进行转换,生成对应类型的IR对象,即SAFETRANSLATE部分;

03

若节点数大于两个,则根据节点数量生成对应数量的unknown类型的IR对象,将unknown类型对象的left_、right_指针指向合适的IR对象,并根据生成式给其op_对象赋值,即CASESTART(0)中的tmp5、tmp6和CASESTART(1)中的tmp6、tmp7、tmp8。

04

最后,生成该节点对应的IR对象,将该对象的left_、right_指针指向合适的IR对象,并根据生成式给其op_对象赋值。完成转换后,就得到了由IR对象组成的树状结构。

由于该IR结构与SQL语法密切相关,因此使用Squirrel检测其他数据库或是对已支持数据库的文法进行扩充时,需要重新编写对应文件,包括ast.h中的ALLTYPE宏和ast.cpp中对非终结符类translate、deep_delete和generate方法的定义。其中translate方法用于将非终结符对象转换为IR对象,deep_delete方法用于递归释放AST中当前节点以下的所有对象节点,generate方法根据生成式随机生成AST,不过在实际生成过程中因为缺乏引导机制,所以效果不是很好。

2.3  变异器

得到IR结构后,就可以基于IR结构进行语法保存变异。该变异方法的大致流程如下图所示:

01

遍历IR结构的所有节点,选择其中能够变异的节点。由于每个输入有多条SQL语句,因此为了使输入SQL语句的种类在变异时不发生变化,代表每条SQL语句的非终结符类型的IR节点以及在此之上的节点不参与变异。

02

对于选中的节点,分别进行替换、插入和删除三种变异:

其中,替换变异当该节点有子节点时,随机选择一个子节点,从ir_library_中随机选择一个与该子节点类型相同的节点,进行替换:

插入操作是当该节点的子节点少于两个时,随机从ir_library_中选择一个与该节点类型相同的节点,若选择的节点存在该节点缺少的子节点,则将缺少的子节点插入该节点。

删除是指当该节点存在子节点时,随机选择一个子节点进行删除:

ir_library_是一个map容器,其键是IRTYPE枚举,值是保存了指向对应类型的IR对象的指针的vector。ir_library_中指针指向的IR对象主要来源于fuzzer初始化阶段从外部文件导入的SQL语句和增加了代码覆盖率的输入。其用途是在变异时提供可供操作的IR对象。从外部导入的SQL语句直接到影响变异的质量,因此在进行测试时需要仔细选择导入的SQL语句。

03

对于每个变异后的节点,复制整个IR结构,并用变异后的节点替换IR结构中对应的节点,最终生成若干不同的IR结构。

04

对输入进行标准化处理,具体而言是对SQL语句中的具体值进行替换,即将数值常量替换为1,浮点常量替换为1.0,字符常量替换为'x',数据库对象标识符替换为x。然后计算该SQL语句的哈希值,防止变异生成具有重复结构的SQL语句。

该变异方法的基本依据是:SQL语句使用上下文无关文法,因此替换AST中的非终结符节点不会导致语法错误。因此基于类型对IR结构中的IR节点进行替换可以保存语法结构,不会产生错误语法。值得一提的是,Squirrel的语法保存变异不能保证生成语法正确的语句,原因在于:

01

插入操作只修改了IR对象的左右节点,没有修改IR对象op_成员中记录的终结符信息,这导致在需要同时修改终结符信息的场合会生成语法错误的SQL语句,例如在SQL语句中选择多个项目时,需要用逗号对项目进行分割,在其对应的AST部分中,插入子节点会使项目数量增加,但是由于没有添加对应的逗号,所以生成的SQL语句中,对于新添加的项目,没有逗号与其他项目进行分割,从而产生语法错误。

02

删除操作会盲目删除IR对象子节点,这很容易删除不该删除的节点,从而导致语法错误。此外,即使对象的子节点可以删除,由于没有修改IR对象op_成员中记录的终结符信息,因此也有可能导致错误。例如当从多个项目中删除一个项目时,分割该项目的逗号未被删除,因此在对应的SQL语句中会多一个逗号,从而导致语法错误。

03

unknown类型属于可以参与变异的类型,但是其相当于代表了很多种非终结符,因此对unknown类型节点进行变异后,得到的IR结构大概率是语法错误的。

Squirrel的变异部分与数据库无关,在测试不同数据库时基本不需要修改,只需要准备新的外部导入的SQL语句和在not_mutatable_types_中添加不需要变异的类型。但是考虑到Squirrel的变异方法较为简单,使用时可以进行一定程度的改进。进行变异的函数的入口是mutator.cpp文件中的mutate_all函数,可以从该函数开始,进行对变异方法的修改。

2.4  实例化器

实例化器的主要作用是对int_literal、float_literal、string_literal和identifier非终结符对应的IR节点赋值,其中identifier节点,实例化器需要确保其指代的数据库对象具有正确的依赖关系。具体而言,对于int_literal、float_literal和string_literal这三个代表常量的非终结符对应的IR节点,实例化器会随机选择一个常量值赋给IR节点中对应的变量;对于identifier非终结符对应的IR节点,实例化器会进行以下处理:

01

将整个IR结构根据语句进行拆分,拆分后得到每个SQL语句对应的IR子树。其中,子查询中的SELECT语句也会被从语句中拆分出来,作为单一语句处理,因此,Squirrel在处理依赖关系时会忽视子查询,即当子查询用于FROM子句充当表时,该表中的任何列都不会被外部的SELECT语句使用。

02

对于每个IR子树,扫描整个子树,记录所有data_type_不等于kDataWhatever(该枚举值等于0,即为data_type_未赋值时的默认值)的节点的指针,这些指针以scope_和data_type_为键,存储在scope_library_中,该值会在处理每个新语句前清空。

03

构建identifier对应的IR节点之间的依赖关系。在该步中,对于常量IR节点,进行随机赋值,对于identifier节点,构建identifier节点之间的关系,具体而言就是数据库对象之间的依赖关系。

Squirrel中有一个定义了不同数据类型节点之间依赖关系的表--relationmap_,当处理的节点的data_type_不等于kDataWhatever时,根据节点的data_type_在relationmap_中查询该节点类型是否与其他类型节点具有依赖关系,如果有依赖关系,则在scope_library_中随机选择一个对应类型的节点,在两者之间建立依赖关系。

如果当前节点的data_type_在relationmap_中有记录,则会根据data_type_、data_flag_和scope_构建依赖关系。其中,在Postgresql和MySQL部分,data_flag_ kMapToClosestOne没有被使用,因此该条件判断始终为假。在else部分,当当前节点的data_flag_ != kDefine或当前节点与类型与其他类型的依赖关系不为kRelationElement时,根据scope_在scope_library_中随机寻找在relationmap_描述中有依赖关系的类型的节点,构建依赖关系。

在Squirrel中,作者定义了两种关系:kRelationSubtype和kRelationElement。其中,kRelationSubtype表示两个identifier节点具有从属关系,例如列与表之间具有从属关系关系,具有从属关系的节点在scope_值方面相差一,可以通过scope_减一和对应节点的data_type_在scope_library_找到对应节点,如果两个节点的依赖关系是从属关系,则表示子节点的取值要依赖于父节点的取值,例如如果表名确定,则列名需要从对应表的列名中选取。而kRelationElement在Postgresql和MySQL部分实际有效的的地方仅有三处。其中第一、二处用于修改表名和列名,也就是ALTER TABLE table_name 中的第二个table_name和ALTER TABLE column_name中的第二个column_name,这两个非终结符节点的data_flag = kReplace。

第三种情况是在CREATE INDEX中,在该语句的语法中,作者使用第一个table_name作为索引名,该非终结符节点的data_flag = kAlias。结合后面的部分来看,这地方是有问题的,因为kRelationElement加data_flag = kAlias是用来标记别名的,而index name与alias没关系,不确定是我理解有问题还是作者写错了。

节点之间的依赖关系用graph储存。graph是一个map,其键值为指向节点的指针,值为由指向与其有关联的节点的指针构成的vector,论文中将该结构成为依赖关系图。

04

基于节点的data_flag_和节点之间依赖关系对每个节点进行赋值。

该步骤中使用两个重要变量:data_library_和data_library_2d_。其中data_library_记录了每种类型的节点已经使用的名称,例如data_library_[kDataTableName]记录了所有已经使用的表名,data_library_[kDataColumnName]记录了所有已经使用的列名;data_library_2d_是对使用名称之间依赖关系的记录,例如表x中存在的列的名称可以用data_library_2d_[kDataTableName]["x"][kDataColumnName]来获得。这两个变量只有在处理每个输入时才会清空,在处理每个语句时不会清空。

该函数第一个循环用于确定依赖关系中的起始节点。

确定后,先使用fill_one函数实例化起始节点,再使用fill_stmt_graph_one函数实例化后续节点。

fill_one函数使用一系列条件语句确定实例化起始节点的方法。

其中,如果起始节点的data_flag_ = kDefine,则生成新标识符,将该标识符赋值给起始节点并加入data_library_,之后在relationmap_查询该data_type_对应的关系,如果该data_type_被用于kRelationSubtype关系,则将该标识符加入data_library_2d_。

如果起始节点的data_flag_ = kAlias,则根据该节点的data_type_在data_library_中随机选择一个标识符,作为该标识符的别名。之后生成新标识符,将该标识符赋值给起始节点并加入data_library_。如果选择的标识符在data_library_2d_存在,则复制其在data_library_2d_中的值,将对应键换为新标识符。

第三种情况是针对删除语句,如果该类型的节点在data_library_中存在,且起始节点的data_flag_ = kUndefine,则进行删除操作。具体而言就是在data_library_中随机选择一个标识符,赋值给起始节点,并使用remove_one_from_datalibrary函数和remove_one_pair_from_datalibrary_2d函数将与该标识符关联的信息从data_library_和data_library_2d_中删除。

第四五种情况是使用g_data_library_和g_data_library_2d_实例化起始节点,这两个东西和data_library_、data_library_2d_相似,区别在于后者在实例化过程中动态生成,记录本次实例化中产生的临时信息,随着实例化结束而清空,而前者记录了在程序初始化阶段导入的外部信息,就是在测试开始时,数据库中预设的各种数据库对象的信息。Squirrel没有强制要求导入外部信息,以上三个条件全部失败也很罕见,因此这两个部分基本不会使用。

处理完起始节点后,使用fill_stmt_graph_one函数实例化后续节点。该函数首先使用fill_one_pair处理与起始节点直接相连的节点,再递归调用fill_stmt_graph_one处理与相连节点直接相连的节点,直至最后一个节点。

对于fill_one_pair函数,其根据依赖关系中父节点和子节点的类型在relationmap_中查找关系,并根据关系选择不同的处理方法。

当r_type = kRelationElement时,如之前所述,仅有三种情况。当is_replace为真时,用于处理标识符修改的情况,将新标识符赋值给子节点,并将data_library_和data_library_2d_中与父节点标识符有关的内容改为与子节点标识符有关。

当is_alias为真时,用于处理别名的情况,将新标识符赋值给子节点,并将data_library_和data_library_2d_中与父节点标识符有关的内容复制,添加到由子节点标识符索引的map中。

当r_type = kRelationSubtype时,且父节点存在于data_library_2d_中时,根据子节点的data_flag_决定处理方法。

当子节点的data_flag_ = kDefine时,将新标识符赋值给子节点,并在父节点的data_library_和data_library_2d_中插入该标识符。此类情况的典型例子是对表中的列进行修改,增加新列时的情况。

当子节点的data_flag_ = kUndefine时,从data_library_2d_中父节点包含的子节点标识符中选择一个,赋值个子节点,之后在data_library_和data_library_2d_中删除与该标识符有关的信息。此类情况的典型例子是对表中的列进行修改,对列进行删除时的情况。

除此之外,如果父节点已经与该类型子节点建立了关系,则随机选择一个代表子节点的标识符,赋值给当前的子节点。此类情况的典型例子是在SELECT语句中从FROM子句中引用的表中引用列,此时该列名的data_flag_ = kUse,应当使用表中已有的标识符。

若父节点不存在于data_library_2d_,则去g_data_library_2d_中寻找,如果父节点已经与该类型子节点建立了关系,则随机选择一个代表子节点的标识符,赋值给当前的子节点。若都不存在,则返回false。

以如下输入为例,解释整个实例化过程:

CREATE TABLE x1 (x2 INT);

SELECT x3 FROM x4;

首先,将输入拆分成两条SQL语句,并依序处理。

对于第一条CREATE语句,扫描整个语句,获取相关非终结符节点的信息。其中,

节点x1:

data_flag_ = kDefine

data_type_ = kDataTableName

scope_ = 1

节点x2:

data_flag_ = kDefine

data_type_ = kDataColumnName

scope_ = 2。

于是有:

scope_library_[1][kDataTableName] = {x1}

scope_library_[2][kDataColumnName] = {x2}。

之后,建立依赖关系,首先处理x1节点,在relationmap_查询,发现无依赖关系,再处理x2节点,查询relationmap_,发现

relationmap_[kDataColumnName][kDataTableName] = kRelationSubtype

则根据scope_和对应类型查询能够建立依赖关系的节点

scope_library_[1][kDataTableName] = {x1}

发现只有x1,于是与x1建立依赖关系:

x1→x2

最后,由于x1无父节点,data_flag_ = kDefine,因此x1使用新名称table0,并更新两个变量:

data_library_[kDataTableName] = {"table0"}

data_library_2d_[kDataTableName]["table0"] = {}

由于x2有父节点x1,data_flag_等于kDefine,因此x2使用新名称column0,并更新两个变量:

data_library_[kDataColumnName] = {"column0"}

data_library_2d_[kDataTableName]["table0"][kDataColumnName] = {"column0"}

实例化后,SQL语句变为:

CREATE TABLE table0 (column0 INT);

对于第二条SELECT语句,扫描整个语句,获取相关非终结符节点的信息。其中,

节点x3:

data_flag_ = kUse

data_type_ = kDataColumnName

scope_ = 2

节点x4:

data_flag_ = kUse

data_type_ = kDataTableName

scope_ = 1

于是有:

scope_library_[1][kDataTableName] = {x4}

scope_library_[2][kDataColumnName] = {x3}。

之后,建立依赖关系,首先处理x3节点,查询relationmap_,发现:

relationmap_[kDataColumnName][kDataTableName] = kRelationSubtype

存在依赖关系,则根据scope_和对应类型查询能够建立依赖关系的节点

scope_library_[1][kDataTableName] = {x4}

发现只有x4,于是与x4建立依赖关系:

x4→x3

再处理x4节点,在relationmap_查询,发现无依赖关系.

最后,由于x4无父节点,data_flag_等于kUse,因此x4从data_library_中随机选择一个名称,由于此时

data_library_[kDataTableName] = {"table0"}

只有一个名称,x4选择table0。由于x3有父节点x4,data_flag_等于kUse,因此根据父节点名称在data_library_2d_中随机选择名称,由于

data_library_2d_[kDataTableName]["table0"][kDataColumnName] = {"column0"}

只有一个名称,x3选择column0。实例化后,SQL语句变为:

SELECT column0 FROM table0;

最终整个输入满足了数据库对象之间的依赖关系。

Squirrel在除SQLite外的3个数据库中的实现较为简单,只能处理简单的数据库对象之间的依赖关系。在SQLite中实现较为复杂,但是基本原理相同,只是对于节点的标记以及不同类型节点之间的依赖关系更为复杂。

Squirrel的实例化算法原则上与数据库无关,只要数据库中identifier非终结符之间的依赖关系不变,就不需要对实例化算法进行修改,但是考虑到实际使用中可能会定义更复杂的依赖关系,以及Squirrel的实例化算法较为简单,只能处理数据库对象之间的依赖关系,对于更复杂的语义约束条件,如类型约束,则无能为力,因此为了更好的效果,可以考虑对Squirrel的实例化算法进行修改。Squirrel实例化器的入口为mutator.cpp文件中的validate函数,可以从该函数开始,对实例化算法进行修改。

三、迁移至AFL++的变化

鉴于AFL++良好的设计,将Squirrel迁移至AFL++主要做了两点简单的改动:

01

在AFL版本中,AFL原有的fuzz_one函数被修改,将AFL原有的变异方法删除,并替换为解析器、变异器和实例化器,最终的输入使用common_fuzz_stuff函数执行。在AFL++中,这三个模块被统一放入一个.so文件,并使用AFL++自定义变异方法的接口导入AFL++。

02

在AFL版本中,使用数据库自带的C/C++接口连接数据库,使用时不需要额外的driver。在AFL++中,Squirrel使用driver连接数据库,本质上没啥区别,只是将与数据库连接的部分从fuzzer中拆分出来,更好修改。

四、在AFL++中的使用

由于Squirrel在AFL++中只是作为一种变异方法实现,因此首先需要安装AFL++,关于AFL++的安装方法本文不多赘述,可以自行参考其他文章。

关于Squirrel的安装方法,如官方文档所示,基于ubuntu 22.04:

01

安装前置程序:

sudo apt install libmysqlclient-dev cmake ninja-build clang pkg-config clang-format libpq-dev libyaml-cpp-dev

02

下载Squirrel,进入目录后,使用以下命令:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DXXXXX=ON -Wno-dev

其中,XXXXX可以为SQLITE、MYSQL、POSTGRESQL或Mariadb,分别对于4中数据库。

03

使用以下命令编译:

cmake --build build -j

编译好的文件在build目录下,其中,libxxxxx_mutator.so为使用AFL++接口自定义的变异方法,使用时需要使用环境变量导入AFL++,db_driver为待测目标,用于连接数据库。

关于Squirrel的安装方法,如官方文档所示:

01

需要配置yaml文件,该文件记录了数据库连接时使用的各种参数,如用户名、密码、ip、端口等。

02

使用环境变量导入相关文件的路径:

export SQUIRREL_CONFIG=/path/to/config.yml

export AFL_CUSTOM_MUTATOR_ONLY=1

export AFL_CUSTOM_MUTATOR_LIBRARY =  REPO_DIR/build/libxxxx_mutator.so

export AFL_DISABLE_TRIM=1

其中,第一个是yaml文件的路径,后三个是向AFL++中导入自定义变异方法,该部分环境变量可以参考其他关于AFL++的文章。

03

使用AFL++对db_driver进行测试:

afl-fuzz -i input -o output -- ./build/db_driver

启动后,会打印此时使用的共享内存的ID,并暂停30s。在这30s内,需要在命令行中将该ID手动输入环境变量,并启动数据库相应服务器:

export __AFL_SHM_ID=xxxx

xxxx为共享内存ID。对于SQLite,直接对harness进行测试,不用额外的启动步骤。

关于对数据库插桩,在编译时,直接将afl-gcc/afl-g++或afl-clang-fast/afl-clang-fast++设置为编译器即可。

五、结语

Squirrel是一个使用覆盖率引导机制的基于变异的数据库模糊测试工具,与之前缺乏引导机制的基于模板生成的工具相比,其探测Bug的能力和增加覆盖率的能力有了显著提升。但是,Squirrel依然存在巨大的局限性:

01

目前的插桩方式并不适合数据库,由于数据库中有许多代码与输入无关,对这些代码进行插桩会干扰覆盖率信息的收集,使得一些未触发新边的种子被保留,污染种子池。同时也有研究者表示,代码覆盖率机制不太适用于数据库模糊测试,因为数据库核心组件(如优化器)在执行数十条语句后就会饱和(覆盖率>95%)。

02

Squirrel变异操作会存在产生语法错误语句。除此之外,变异完全随机,缺乏有效的引导方法。

03

Squirrel只能处理不同数据库对象之间的依赖关系,但是SQL语义正确不只需要满足依赖关系正确,还需要满足其他很多条件,因此Squirrel产生的语句正确率有限。

04

Squirrel缺乏种子选择机制和能量调度策略。其种子选择机制使用AFL原有的机制,即选择一组覆盖所有边的种子,鉴于种子与边并非完全对应的关系,这方法的效率不佳;而能量调度策略则是完全没有,输入越长变异次数越多,与输入的价值无关,因此效率较低。

05

对于不同的数据库需要编写对应的解析器,这增加了人工成本。

由此可见,数据库模糊测试领域还有很大的研究空间。


文章来源: https://mp.weixin.qq.com/s?__biz=MzU1NTEzODc3MQ==&mid=2247485067&idx=1&sn=8c0ff8fe3ca996c1a80d55d6b102081b&chksm=fbd9ad37ccae242105c5652c81b43fb25ed36ede89ed35b849c1b0b803a50f3ae575374c2c80&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh