从源码的角度学习 boofuzz 函数的使用
plight3 发表于 北京 技术文章 280浏览 · 2024-12-04 17:05

boofuzz是目前比较流行的网络黑盒模糊测试工具。网上介绍 boofuzz 如何使用的文章比较多,对每个 函数具体做了哪些事情分析的较少。本文从源码的角度对 boofuzz 的函数进行介绍,以便加深对boofuzz 的理解。

变异 or 生成

模糊测试测试用例的产生主要分为两种:基于变异和基于生成。

基于变异的模糊测试方法需要收集目标程序运行过程中的反馈信息,通常为代码覆盖率。通过代码覆盖率的增加来判断一个测试用例的好坏,基于“好的”测试用例进行变异。获取代码覆盖率需要对程序进行插桩,因此基于变异的模糊测试通常为白盒或灰盒(一些黑盒模糊测试方法根据目标程序的响应码来筛选测试用例)。

基于生成的模糊测试方法基本都是黑盒的,无法通过覆盖率来筛选出优秀的测试用例作为种子来进行变异。这类方法只能对测试用例的每个字段进行生成,新测试用例的产生与旧的测试用例没有关系。

本文介绍的 boofuzz 为基于生成的黑盒模糊测试方法,因此在后文中会用“测试用例生成”而不是“测试用例变异”来描述新测试用例的产生过程。

基本组成

概述

boofuzz 属于网络模糊测试工具,因此 boofuzz 生成的测试用例为数据包。本文认为在 boofuzz 中一个测试用例的基本组成元素为:Request、Block、Primitive,它们之间的关系如下图所示。一个Request 代表一个测试用例,即一个数据包;Primitive(原语) 代表数据包中的一个字段,为测试用例生成的基本单元;Block 用来表示多个字段的集合,目的是为了方便对多个字段进行统一操作(如对数据包的部分字段计算长度、计算校验和等)。

下面是一个http协议的数据包样例以及对应的boofuzz Request定义,其中:

  1. 整个数据包就是一个Request
  2. 数据包中每个可以分割的部分就是一个Primitive,如POST、空格、/index.html等。在对测试用例进行生成时实际上是对primitive进行生成。
  3. 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 都有的参数如下:

  1. name: 用于标识这个Primitive,不指定会自动分配(Request和Block也有此参数,用于标识它们自身)
  2. default_value: 默认值

BitField

关键参数:

  • width: bit 位的宽度,默认8
  • max_num: 最大值,默认None
  • full_range: 是否使用全量数据,默认False

生成策略:

  1. 如果使用全量数据,则生成从0到指定width能表示的最大值(指定max_num后,使用max_num)
  2. 如果不使用全量数据,则使用一些特殊的边界值

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. 对默认值进行重复

  1. 使用一些特殊值

  1. 将长度为1、2、4字节的特殊值依次替换原始值的每个位置

  1. 根据size和max_len限制长度

Delim

delimiter 是分隔符的意思

生成策略:

  1. 对默认值进行重复

  1. 使用特殊值

Float

关键参数:

  1. s_format: 格式化字符串,指定使用几位小数,默认”.1f”
  2. f_min: 最小值,默认0.0
  3. f_max: 最大值,默认100.0
  4. max_mutations: 最大生成次数,默认1000

生成策略:

  1. 使用默认值
  2. 从f_min到f_max 随机取值

FromFile

关键参数:

  1. filename: 文件通配符,如 *.txt
  2. max_len: 最大长度,默认0

生成策略:

  1. 使用从文件中读取的
  2. 如果给定max_len,则只使用长度小于等于max_len的值

Mirror

关键参数:

  1. primitive_name: 目标原语名称

生成策略:

  1. 和primitive_name对应的原语保持一致

RandomData

关键参数:

  1. min_length: 最小长度,默认0
  2. max_length: 最大长度,默认1
  3. max_mutations: 最大生成次数,默认25。会根据min_length,max_length,step 进行计算
  4. step: 每次生成的长度增加多少(min_length到max_length之间),默认None,为None时长度在min_length和max_length之间随机

生成策略:

  1. 根据长度,随机生成0-255之间的二进制字节

String

关键参数:

  1. size: 长度,为None时表示随机长度,默认None
  2. padding: 长度不够时用来填充的字节,默认\x00
  3. Encoding: 字符串编码方式,默认ascii
  4. max_size: 最大长度,当size为None时使用,默认None

生成策略

  1. 使用特殊值,测试命令注入

  1. 将默认值分别重复2、10、100次,如果长度超出最大长度则不使用

  1. 将一些特殊值扩展到边界长度(将_long_string_lengths和_long_string_deltas里的值两两相加)。或使用较大的长度

  1. 特殊长度,内容全为“D”的字符串中随机插入\x00。从_long_string_lengths 中相邻两个元素之间随机取8个位置进行插入。

  1. 根据max_len和size进行长度限制,超过max_len进行截断,小于size进行padding

Group & Simple

这两个Primitive,生成测试用例的生成策略比较类似,都是从一个list里取值。Group多了一个encode操作,而Simple直接返回str。总结,bytes用Group,字符串用Simple

Group

关键参数:

  1. values: 一个list,生成测试用例的时候从里面进行顺序选择
  2. encoding: 编码方式,默认ascii

生成策略

  1. 从list中取值

Simple

关键参数:

  1. fuzz_values: 一个list,生成测试用例的时候从里面进行顺序选择

生成策略:

  1. 同Group原语

Static

固定的值,在fuzz过程中不进行生成,用于定义静态字符串

绑定Block

Checksum

关键参数:

  1. block_name: 绑定Block的name
  2. algorithm: 计算checksum使用的算法(crc32, crc32c, adler32, md5, sha1, ipv4, udp),默认crc32。也可以传入一个自定义的函数,需要返回bytes
  3. length: 只有algorithm为自定义函数时需要指定
  4. endian: 大小端
  5. ipv4_src_block_name: algorithm为udp时需要使用,ipv4源地址所对应的Block name
  6. ipv4_dst_block_name: algorithm为udp时需要使用,ipv4目的地址所对应的Block name

生成策略:

此函数用于计算指定block_name Block的checksum

Repeat

关键参数:

  1. block_name: 绑定Block的name
  2. min_reps: 对绑定Block进行重复操作的最小次数
  3. max_reps: 对绑定Block进行重复操作的最大次数
  4. step: 重复操作次数在min_reps和max_reps之间变化的步长,默认1

生成策略:

此函数用于对指定block_name Block进行重复操作

Size

关键参数:

  1. block_name: 绑定Block的name
  2. offset: 从绑定Block的offset处开始计算长度,默认0
  3. Length: 用多少字节描述长度,默认4
  4. endian: 大小端
  5. output_format: 用bytes还是ascii表示,默认bytes
  6. 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

关键参数:

  1. name: qualified_name
  2. 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参数的例子:

  1. 定义一个Request包含一个Simple,一个Group和一个Block,Block下包含一个BitField和一个Simple。查看测试用例生成的结果:每次测试用例生成只改变一个Primitive。Group生成的是bytes,Simple生成的是str。

  1. 给s_block一个group参数用于绑定其他Primitive。下图中,Block绑定了name为”group_field1”的Group。 可以看到,在一次测试用例的生成过程中同时改变了两个Primitive,它们的生成策略两两进行组合。

s_align

关键参数:

  1. modulus: Block的长度需要为多少字节的倍数
  2. pattern: 用于填充的字节

根据参数可以看出s_align的作用为定义一个Block以modulus字节对齐。

内部同样实现了 __enter____exit__

Primitive操作

s_binary

传入连续的16进制字符串,删除其中的分割符如空格、\t、\n 等,将其转换成字节并定义为Static原语。如”0xa1”变成b”\xa1”

调用对应的Primitive

下面的函数,直接使用对应的 Primitive(部分函数参数名 default_value 变为 value):

  1. s_bit_field
    1. 等价 s_bit, s_bits, s_bit_field
  2. s_byte
    1. 等价 s_char
  3. s_word
    1. 等价 s_short
    2. s_intelhalfword: 小端的s_word
  4. s_dword
    1. 等价 s_int, s_long
    2. s_intelword: 小端的s_dword
    3. s_bigword: 大端的s_dword
  5. s_qword
    1. 等价 s_double
  6. s_bytes
  7. s_delim
  8. s_float
  9. s_from_file
  10. s_mirror
  11. s_random
  12. s_string
    1. s_cstring: s_string后定义一个s_static(”\x00”)
  13. s_group
  14. s_simple
  15. s_static
    1. 等价 s_dunno, s_raw, s_unknown
  16. s_checksum
  17. s_repeat
    1. 等价 s_repeater
  18. s_size
    1. 等价 s_sizer

s_lego

此函数没有文档,暂不清楚如何使用,不建议使用

s_hexdump

传入字符串,返回字符串的16进制表示

0 条评论
某人
表情
可输入 255