boofuzz是目前比较流行的网络黑盒模糊测试工具。网上介绍 boofuzz 如何使用的文章比较多,对每个 函数具体做了哪些事情分析的较少。本文从源码的角度对 boofuzz 的函数进行介绍,以便加深对boofuzz 的理解。
变异 or 生成
模糊测试测试用例的产生主要分为两种:基于变异和基于生成。
基于变异的模糊测试方法需要收集目标程序运行过程中的反馈信息,通常为代码覆盖率。通过代码覆盖率的增加来判断一个测试用例的好坏,基于“好的”测试用例进行变异。获取代码覆盖率需要对程序进行插桩,因此基于变异的模糊测试通常为白盒或灰盒(一些黑盒模糊测试方法根据目标程序的响应码来筛选测试用例)。
基于生成的模糊测试方法基本都是黑盒的,无法通过覆盖率来筛选出优秀的测试用例作为种子来进行变异。这类方法只能对测试用例的每个字段进行生成,新测试用例的产生与旧的测试用例没有关系。
本文介绍的 boofuzz 为基于生成的黑盒模糊测试方法,因此在后文中会用“测试用例生成”而不是“测试用例变异”来描述新测试用例的产生过程。
基本组成
概述
boofuzz 属于网络模糊测试工具,因此 boofuzz 生成的测试用例为数据包。本文认为在 boofuzz 中一个测试用例的基本组成元素为:Request、Block、Primitive,它们之间的关系如下图所示。一个Request 代表一个测试用例,即一个数据包;Primitive(原语) 代表数据包中的一个字段,为测试用例生成的基本单元;Block 用来表示多个字段的集合,目的是为了方便对多个字段进行统一操作(如对数据包的部分字段计算长度、计算校验和等)。
下面是一个http协议的数据包样例以及对应的boofuzz Request定义,其中:
- 整个数据包就是一个Request
- 数据包中每个可以分割的部分就是一个Primitive,如POST、空格、/index.html等。在对测试用例进行生成时实际上是对primitive进行生成。
- Block用于对Primitive的集合进行操作,在这个例子中使用Block来分割http请求头,和http请求体,并通过在s_size中指定Block的name来得到Content-Length的长度
POST /index.html HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 16\r\n
\r\n
Body content ...
# boofuzz/examples/http_with_body.py
s_initialize(name="Request")
with s_block("Request-Line"):
s_group("Method", ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"])
s_delim(" ", name="space-1")
s_string("/index.html", name="Request-URI")
s_delim(" ", name="space-2")
s_string("HTTP/1.1", name="HTTP-Version")
s_static("\r\n", name="Request-Line-CRLF")
s_string("Host:", name="Host-Line")
s_delim(" ", name="space-3")
s_string("example.com", name="Host-Line-Value")
s_static("\r\n", name="Host-Line-CRLF")
s_static("Content-Length:", name="Content-Length-Header")
s_delim(" ", name="space-4")
s_size("Body-Content", output_format="ascii", name="Content-Length-Value")
s_static("\r\n", "Content-Length-CRLF")
s_static("\r\n", "Request-CRLF")
with s_block("Body-Content"):
s_string("Body content ...", name="Body-Content-Value")
数据结构
下面是 boofuzz/blocks/request.py
中Request的初始化代码:
- Request 使用 self.stack 一个list来存储所有的 Block 和 Primitive。
- 如果 Primitive 定义在 Block 下面,则只将这个 Block 存入 self.stack 中。(一个 Block 下面可以也包含其它 Block)
- self.block_stack 用来存储当前“打开”的Block,这样做的目的是为了方便生成context_path(当前Block或Primitive在整个Request中的位置):
- 在Request1下面定义Block1,Block1的context_path 为Request。
- 在Request1下面定义Block1,在Block1下面定义Block2,Block2的context_path 为Request.Block1。
- 根据context_path生成qualified_name,qualified_name为context_path拼接上Block或Primitive的name
- Request中通过self.names字典存储所有的Block和Primitive,key为qualified_name。Primitive定义在Block下也可以通过self.names[qualified_name] 访问到
Primitive 的生成策略
本节介绍,每个Primitive的生成策略。对于每个 Primitive 都有的参数如下:
- name: 用于标识这个Primitive,不指定会自动分配(Request和Block也有此参数,用于标识它们自身)
- default_value: 默认值
BitField
关键参数:
- width: bit 位的宽度,默认8
- max_num: 最大值,默认None
- full_range: 是否使用全量数据,默认False
生成策略:
- 如果使用全量数据,则生成从0到指定width能表示的最大值(指定max_num后,使用max_num)
- 如果不使用全量数据,则使用一些特殊的边界值
Byte, Word, Dword, Qword
分别表示为width为8, 16, 32, 64 的 bit_field
Bytes
关键参数:
- size: 字节数,为None表示任意长度,默认None
- padding: 当字节数达不到size时用于填充,默认 \x00
- max_len: 最大长度,当size为None时用于限制最大长度,默认None
生成策略:
- 使用一些特殊值,测试溢出
- 对默认值进行重复
- 使用一些特殊值
- 将长度为1、2、4字节的特殊值依次替换原始值的每个位置
- 根据size和max_len限制长度
Delim
delimiter 是分隔符的意思
生成策略:
- 对默认值进行重复
- 使用特殊值
Float
关键参数:
- s_format: 格式化字符串,指定使用几位小数,默认”.1f”
- f_min: 最小值,默认0.0
- f_max: 最大值,默认100.0
- max_mutations: 最大生成次数,默认1000
生成策略:
- 使用默认值
- 从f_min到f_max 随机取值
FromFile
关键参数:
- filename: 文件通配符,如
*.txt
- max_len: 最大长度,默认0
生成策略:
- 使用从文件中读取的
- 如果给定max_len,则只使用长度小于等于max_len的值
Mirror
关键参数:
- primitive_name: 目标原语名称
生成策略:
- 和primitive_name对应的原语保持一致
RandomData
关键参数:
- min_length: 最小长度,默认0
- max_length: 最大长度,默认1
- max_mutations: 最大生成次数,默认25。会根据min_length,max_length,step 进行计算
- step: 每次生成的长度增加多少(min_length到max_length之间),默认None,为None时长度在min_length和max_length之间随机
生成策略:
- 根据长度,随机生成0-255之间的二进制字节
String
关键参数:
- size: 长度,为None时表示随机长度,默认None
- padding: 长度不够时用来填充的字节,默认\x00
- Encoding: 字符串编码方式,默认ascii
- max_size: 最大长度,当size为None时使用,默认None
生成策略
- 使用特殊值,测试命令注入
- 将默认值分别重复2、10、100次,如果长度超出最大长度则不使用
- 将一些特殊值扩展到边界长度(将_long_string_lengths和_long_string_deltas里的值两两相加)。或使用较大的长度
- 特殊长度,内容全为“D”的字符串中随机插入\x00。从_long_string_lengths 中相邻两个元素之间随机取8个位置进行插入。
- 根据max_len和size进行长度限制,超过max_len进行截断,小于size进行padding
Group & Simple
这两个Primitive,生成测试用例的生成策略比较类似,都是从一个list里取值。Group多了一个encode操作,而Simple直接返回str。总结,bytes用Group,字符串用Simple
Group
关键参数:
- values: 一个list,生成测试用例的时候从里面进行顺序选择
- encoding: 编码方式,默认ascii
生成策略
- 从list中取值
Simple
关键参数:
- fuzz_values: 一个list,生成测试用例的时候从里面进行顺序选择
生成策略:
- 同Group原语
Static
固定的值,在fuzz过程中不进行生成,用于定义静态字符串
绑定Block
Checksum
关键参数:
- block_name: 绑定Block的name
- algorithm: 计算checksum使用的算法(crc32, crc32c, adler32, md5, sha1, ipv4, udp),默认crc32。也可以传入一个自定义的函数,需要返回bytes
- length: 只有algorithm为自定义函数时需要指定
- endian: 大小端
- ipv4_src_block_name: algorithm为udp时需要使用,ipv4源地址所对应的Block name
- ipv4_dst_block_name: algorithm为udp时需要使用,ipv4目的地址所对应的Block name
生成策略:
此函数用于计算指定block_name Block的checksum
Repeat
关键参数:
- block_name: 绑定Block的name
- min_reps: 对绑定Block进行重复操作的最小次数
- max_reps: 对绑定Block进行重复操作的最大次数
- step: 重复操作次数在min_reps和max_reps之间变化的步长,默认1
生成策略:
此函数用于对指定block_name Block进行重复操作
Size
关键参数:
- block_name: 绑定Block的name
- offset: 从绑定Block的offset处开始计算长度,默认0
- Length: 用多少字节描述长度,默认4
- endian: 大小端
- output_format: 用bytes还是ascii表示,默认bytes
- inclusive:计算长度包不包含自身(长度字段本身)
生成策略:
此函数用于对指定block_name Block计算
函数介绍
本节介绍,使用boofuzz定义Request常用的函数
Request 操作
s_initialize
用来定义一个Request。boofuzz 使用一个全局字典blocks.REQUEST,来存储所有Request,key为Request的name。同时使用一个全局变量blocks.CURRENT来存储当前使用的Request,方便将Block和Primitive定义在对应的Request下(其它的Block和Primitive函数通过blocks.CURRENT参数来查找它们属于哪个Request)。
s_switch
用来切换当前使用的Request,实际上就是修改blocks.CURRENT
s_get
根据name从全局字典blocks.REQUEST取出对应的Request,name为None则返回blocks.CURRENT对应的Request
s_update
关键参数:
- name: qualified_name
- value: 用于替换的值
基本组成-数据结构 那一小节介绍Request使用self.names字典存储所有Block和Primitive,s_update函数根据qualified_name将对应Primitive的default_value替换成value
Block操作
s_block_start & s_block_end
基本组成-数据结构 那一小节介绍Request使用self.block_stack存储“打开”的Block,以方便为Request中的Block和Primitive生成context_path和qualified_name。
s_block_start 函数用于将当前Block“打开”,即向当前Request的block_stack中存入当前Block
s_block_end 函数用于将当前Block“关闭”,即向当前Request的block_stack中删除当前Block
s_block
用于定义一个Block。内部实现了 __enter__
和 __exit__
,可以使用with s_block(name=”xxx”)这样的语法,而不用手动调用 s_block_start 和 s_block_end。
此函数有一个比较有用的参数 group,用于将block和其他Primitive进行绑定(通常为Group或者Simple)。boofuzz 在生成测试用例时,一次只能改变一个Primitive(boofuzz引入组合变异来让多个Primitive同时进行改变,但这种方式只能用于相邻的Primitive,此处不讨论这种情况),使用s_block的group参数可以让此Block和group对应的Primitive同时进行改变。
下面是一个使用group参数的例子:
- 定义一个Request包含一个Simple,一个Group和一个Block,Block下包含一个BitField和一个Simple。查看测试用例生成的结果:每次测试用例生成只改变一个Primitive。Group生成的是bytes,Simple生成的是str。
- 给s_block一个group参数用于绑定其他Primitive。下图中,Block绑定了name为”group_field1”的Group。 可以看到,在一次测试用例的生成过程中同时改变了两个Primitive,它们的生成策略两两进行组合。
s_align
关键参数:
- modulus: Block的长度需要为多少字节的倍数
- pattern: 用于填充的字节
根据参数可以看出s_align的作用为定义一个Block以modulus字节对齐。
内部同样实现了 __enter__
和 __exit__
Primitive操作
s_binary
传入连续的16进制字符串,删除其中的分割符如空格、\t、\n 等,将其转换成字节并定义为Static原语。如”0xa1”变成b”\xa1”
调用对应的Primitive
下面的函数,直接使用对应的 Primitive(部分函数参数名 default_value 变为 value):
- s_bit_field
- 等价 s_bit, s_bits, s_bit_field
- s_byte
- 等价 s_char
- s_word
- 等价 s_short
- s_intelhalfword: 小端的s_word
- s_dword
- 等价 s_int, s_long
- s_intelword: 小端的s_dword
- s_bigword: 大端的s_dword
- s_qword
- 等价 s_double
- s_bytes
- s_delim
- s_float
- s_from_file
- s_mirror
- s_random
- s_string
- s_cstring: s_string后定义一个s_static(”\x00”)
- s_group
- s_simple
- s_static
- 等价 s_dunno, s_raw, s_unknown
- s_checksum
- s_repeat
- 等价 s_repeater
- s_size
- 等价 s_sizer
s_lego
此函数没有文档,暂不清楚如何使用,不建议使用
s_hexdump
传入字符串,返回字符串的16进制表示
-
-
-
-