boofuzz-网络协议模糊测试器

boofuzz

jtpereyda/boofuzz: A fork and successor of the Sulley Fuzzing Framework (github.com)

boofuzz是Sulley框架的继承者,修复了Sulley中存在的bug,并增强了可扩展性

boofuzz的特点

  • 安装简单 pip install boofuzz

  • 易于使用,基于Python脚本编写协议,不需要为协议编写复杂的配置文件

  • 扩展性强,利用callback机制能够方便地实现各种操作

  • 详细的测试记录

boofuzz的缺点

  • 性能较差(相同的benchmark效率约为peach的40%)

  • 缺乏易用的教程文档

安装使用

boofuzz为纯Python项目,使用pip即可直接安装

1
2
3
4
$ python -m venv venv
$ source venv/bin/activate
(venv) $ pip install -U pip setuptools
(venv) $ pip install boofuzz

以项目自带的样例http_simple为例

fuzz目标及会话的创建

1
2
3
session = Session(
target=Target(connection=TCPSocketConnection("127.0.0.1", 12345)),
)

http协议请求的定义,boofuzz存在两种协议定义的方式,静态定义和新型定义

静态定义

1
2
3
4
5
6
7
8
9
10
11
12
13
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("\r\n", "Request-CRLF")

新型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
req = Request("HTTP-Request", children=(
Block("Request-Line", children=(
Group(name="Method", values=["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"]),
Delim(name="space-1", default_value=" "),
String(name="URI", default_value="/index.html"),
Delim(name="space-2", default_value=" "),
String(name="HTTP-Version", default_value="HTTP/1.1"),
Static(name="CRLF", default_value="\r\n"),
)),
Block("Host-Line", children=(
String(name="Host-Key", default_value="Host:"),
Delim(name="space", default_value=" "),
String(name="Host-Value", default_value="example.com"),
Static(name="CRLF", default_value="\r\n"),
)),
Static(name="CRLF", default_value="\r\n"),
))

新型定义方式更加地灵活,并且可以通过类的基础、重写实现更多的自定义功能,因此后续只介绍新型协议定义

将定义的请求添加到会话中

1
session.connect(req)

使用python拉起一个http服务器

1
(venv) $ python -m http.server 12345

开始模糊测试

1
session.fuzz()

运行脚本,boofuzz自动监听26000端口,使用浏览器连接即可查看fuzz日志

GsddQeJy

boofuzz的完整运行日志保存在boofuzz-results目录下,使用boo工具打开日志文件即可在web中查看详细的日志记录

1
2
3
4
(venv) ➜  boofuzz ls boofuzz-results 
run-2024-08-19T09-47-49.db
(venv) ➜ boofuzz boo open boofuzz-results/run-2024-08-19T09-47-49.db
Serving web page at http://localhost:26000. Hit Ctrl+C to quit.

boofuzz使用

协议定义

以新型的定义方式为主进行介绍

Request 可以看作是要发送的消息,而 Blocks 则可视为消息内的块, Primitives 则是构成 Block/Request 的元素,这些元素可以是字节、字符串、数字、checksums。

Request

Request为顶级容器,定义一次完整的请求,可包含任何block结构或者primitive结构

1
2
3
4
5
6
7
8
message = Request(name="message", children=(
Word(name="word", default_value=1),
Block(name="block", children=(
String(name="string", default_value="str"),
Delim(name="delim", default_value=":", fuzzable=False),
)),
Static(name="CRLF", default_value=b"\n\r"),
))

Request对象包含names stack属性

names为Request对象包含的所有对象及子对象的字典,以名称为索引可以得到每一个对象

1
2
>>> message.names
{'message': <Request message>, 'message.word': <Word word 1>, 'message.block': <Block block None>, 'message.block.string': <String string 'str'>, 'message.block.delim': <Delim delim ':'>, 'message.CRLF': <Static CRLF b'\n\r'>}

stack为Request对象的栈结构,按入栈顺序储存包含的对象,不包括子对象

1
2
>>> message.stack
[<Word word 1>, <Block block None>, <Static CRLF b'\n\r'>]

Block

  • Block

    Block是基本的构造块,可包含 primitives(原语)、sizers、checksums 以及其它 blocks。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    block = Block(name="block1", children=(
    String(name="string", default_value="str"),
    Delim(name="delim", default_value=":", fuzzable=False),
    Block(name="block2", children=(
    String(name="string", default_value="num"),
    Delim(name="delim", default_value=":", fuzzable=False),
    DWord(name="dword", default_value=123, endian='>'),
    )),
    ))

    与Request相似,Block也存在stack属性,但不存在names属性

    1
    2
    >>> block.stack
    [<String string 'str'>, <Delim delim ':'>, <Block block2 None>]
  • Checksum

    Checksum顾名思义,即校验和,计算目标对象的校验和

    1
    2
    3
    4
    5
    6
    7
    8
    message2 = Request(name="message", children=(
    Checksum(name="checksum", block_name="block", algorithm="crc32", length=4, endian='<', fuzzable=False),
    Block(name="block", children=(
    String(name="string", default_value="num"),
    Delim(name="delim", default_value=":", fuzzable=False),
    DWord(name="dword", default_value=123, endian='>'),
    )),
    ))

    block_name参数为需要校验的对象的name,校验的对象可以为Request Block或Primitives

    algorithm参数为校验和算法,默认为crc32算法,可设置为crc32, crc32c, adler32, md5, sha1, ipv4, udp

    length校验和长度,默认4字节

    endian大小端,默认为<小端

    fuzzable必须手动设置为False,否则会对校验和进行变异

  • Repeat

    Repeat重复渲染其指向的对象,重复的次数可随机变异,也可以由特定的整数对象指定

    Repeat对象需要定义在其需要重复的对象后面(实际上不需要)

    测试中发现Repeat的行为与预期表现不符,无法通过设定variable参数实现指定的随机次数

    例如存在如下结构体定义:

    1
    2
    3
    4
    struct msg_struct {
    uint16_t item_num;
    int32_t block[item_num] = {value, value, value, ...};
    };

    按照API文档,定义对应的Request如下:

    1
    2
    3
    4
    5
    6
    7
    message = Request(name="msg_struct", children=(
    Word(name="item_num", default_value=1, max_num=20, signed=False),
    Block(name="block", children=(
    DWord(name="value", default_value=0)
    )),
    Repeat(name="repeat", block_name="block", variable="msg_struct.item_num"),
    ))

    但是在渲染时出现如下报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    Traceback (most recent call last):
    File "/workspace/boofuzz/test-boofuzz.py", line 156, in <module>
    session.fuzz()
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/sessions/session.py", line 957, in fuzz
    self._main_fuzz_loop(self._generate_mutations_indefinitely(max_depth=max_depth))
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/sessions/session.py", line 1085, in _main_fuzz_loop
    self._fuzz_current_case(mutation_context)
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/sessions/session.py", line 1452, in _fuzz_current_case
    self.transmit_fuzz(
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/sessions/session.py", line 859, in transmit_fuzz
    data = self.fuzz_node.render(mutation_context)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/blocks/request.py", line 128, in render
    return self.get_child_data(mutation_context=mutation_context)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/fuzzable_block.py", line 71, in get_child_data
    rendered += item.render(mutation_context=mutation_context)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/fuzzable.py", line 153, in render
    return self.encode(value=self.get_value(mutation_context=mutation_context), mutation_context=mutation_context)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/fuzzable.py", line 178, in get_value
    value = self.original_value(test_case_context=mutation_context.protocol_session)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspace/venv/lib/python3.11/site-packages/boofuzz/fuzzable.py", line 120, in original_value
    return test_case_context.session_variables[self._default_value.name]
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
    KeyError: 'msg_struct.item_num'

    调试发现session_variables始终为空,无法通过该方式使用Repeat类型实现预期效果

    通过对原类型的继承和重写,可以使用自定义的方式实现需要的重复效果,如下代码示例为对WordRepeat的简单修改,以实现预期的重复效果

    WordRepeater继承自Word,增加了repeater属性,用于记录变异后实际的重复次数

    FixRepeat继承自Repeat,修改了original_value方法,通过repeater属性决定重复的次数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class WordRepeater(Word):
    def __init__(self, name=None, default_value=0, *args, **kwargs):
    self.repeater = default_value
    super(WordRepeater, self).__init__(name, default_value, *args, **kwargs)

    def encode(self, value, mutation_context):
    self.repeater = value
    return super(WordRepeater, self).encode(value, mutation_context)

    class FixRepeat(Repeat):
    def original_value(self, test_case_context=None):
    if isinstance(self._default_value, ProtocolSessionReference):
    if test_case_context is None:
    return self._default_value.default_value
    else:
    value = test_case_context.current_message.names[self._default_value.name].repeater
    if (value < 1):
    value = 1
    return value - 1
    else:
    return self._default_value

    修改后的消息定义如下:

    1
    2
    3
    4
    5
    6
    7
    message = Request(name="msg_struct", children=(
    WordRepeater(name="item_num", default_value=1, max_num=20, signed=False),
    Block(name="block", children=(
    DWord(name="value", default_value=0)
    )),
    FixRepeat(name="repeat", block_name="block", variable="msg_struct.item_num"),
    ))

    测试效果满足预期,但仍存在问题,重复次数无法为0,至少包含1个元素

    1
    2
    3
    Test Step: Fuzzing Node 'msg_struct'
    Info: Sending 30 bytes...
    Transmitted 30 bytes: 07 00 01 00 00 08 01 00 00 08 01 00 00 08 01 00 00 08 01 00 00 08 01 00 00 08 01 00 00 08
  • Size

    Size计算目标对象的大小,使用方法与Checksum相同,block_name指向需要计算大小的对象的name

    fuzzable需要设置为False

    Size包含特殊参数math,用于指定长度计算算法,可以使用函数或lambda表达式

    如下示例,size用于计算block长度,指定长度算法使计算得到的长度为block长度加CRLF的长度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    size_math = lambda x: (x + 2)

    message = Request(name="message", children=(
    Size(name="size", block_name="block", signed=False, math=size_math, fuzzable=False),
    Block(name="block", children=(
    DWord(name="value", default_value=0),
    Delim(name="delim", default_value=";", fuzzable=False),
    String(name="str"),
    )),
    Static(name="CRLF", default_value=b"\n\r"),
    ))
  • Aligned

    Aligned类似于Block,可以包含子对象,与Block不同的是,会对子对象编码后进行填充对其

    1
    2
    3
    4
    5
    6
    7
    message = Request(name="message", children=(
    Aligned(name="align", modulus=4, pattern=b'\x00', children=(
    String(name="string", default_value="num", max_len=128),
    Delim(name="delim", default_value=":", fuzzable=False),
    DWord(name="dword", default_value=123, endian='>'),
    )),
    ))

    modulus参数为对其的单位,默认为1,即不对其

    pattern参数是用于填充对其的数据,默认为一个空字符

    children参数是其包含的子对象

Primitives

Primitive原语类型是boofuzz的基本数据类型,包含整数、字符串等多种类型

  • Static

    静态类型,在模糊测试变异过程中不会发生变异,数据由default_value指定,可以为str类型或bytes类型

    1
    Static(name="CRLF", default_value="\r\n")
  • Simple

    Simple从提供的bytes列表进行变异,利用fuzz_values列表中的值对默认值进行替换

    不提供fuzz_values参数时,等价与Static类型

    1
    Simple(name="simple", default_value=b"11", fuzz_values=[b"11", b"22", b"33"])
  • Delim

    分隔符类型,包含空格、回车、换行等多种字符,变异过程对分隔符进行替换、重复或缺少

    1
    Delim(name="delim", default_value=";")
  • Group

    Group类型与Simple类型类似,利用提供的数据对默认数据进行替换,但是Group类型提供的数据为str类型,可指定编码方式

    1
    Group(name="Method", values=["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"])
  • RandomData

    随机类型,变异的值随机,与提供的default_value无关,变异数据的长度由min_lengthmax_length参数提供,变异的次数由max_mutations参数提供

    1
    RandomData(name="rand", min_length=4, max_length=10, max_mutations=100)
  • String

    字符串类型,从boofuzz定义好的字符串库进行模糊测试,包含替换、重复等变异方式

    1
    2
    String(name="str1", default_value="hello", size=16, padding=b"_")
    String(name="str2", default_value="world", max_len=16, encoding="utf-8")

    size 固定生成数据的长度,不提供该参数表示动态长度

    padding 用于填充数据长度到固定长度,与size参数结合使用

    max_len 生成数据的最大长度

    encoding 字符串编码方式

  • FromFile

    从文件加载变异语料库,将文件按行分隔,每行内容作为变异的内容进行替换

    1
    FromFile(name="file", default_value=b"test", filename="input.txt", max_len=64)

    filename 需要加载的目标文件

    max_len 每行内容的最大长度,大于该长度的内容不参与变异

  • Mirror

    镜像类型,输出与其指向的对象相同的值,primitive_name指向需要镜像的对象的名称

    1
    2
    DWord(name="value", default_value=0)
    Mirror(name="mirror", primitive_name="value")
  • BitField

    比特类型,boofuzz中最基础的整数类型,后序的Byte Word DWord QWord类型与BitField类型类似,只是固定了比特宽度参数,不单独进行介绍

    1
    2
    BitField(name="bit", default_value=0, width=16, max_num=1000, endian='>',
    signed=False, full_range=True, fuzz_values=[2000, 3000, 10000])

    default_value 默认值,默认为0

    width 比特宽度,默认为8,1字节

    max_num 变异的最大值

    endian 大小端> <

    output_format 输出格式,默认为binary,可替换为asciiascii格式每字节的最高bit位为0

    signed 有符号数,默认为False,即默认无符号

    full_range 变异范围,为True时将在变异所有max_num范围内的数据

    fuzz_values 额外提供的变异值列表

  • Bytes

    字节流类型,与String类型类似,区别是Bytes类型为字节流,不需要提供编码方式

    1
    2
    Bytes(name="bytes1", default_value=b"123", size=128, padding=b"\xff")
    Bytes(name="bytes2", default_value=b"123", max_len=128)

自定义类型

boofuzz使用纯Python实现,利用类继承机制可以很轻松地实现自定义类型,例如在Repeat部分实现的FixRepeatWordRepeater类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class WordRepeater(Word):
def __init__(self, name=None, default_value=0, *args, **kwargs):
self.repeater = default_value
super(WordRepeater, self).__init__(name, default_value, *args, **kwargs)

def encode(self, value, mutation_context):
self.repeater = value
return super(WordRepeater, self).encode(value, mutation_context)

class FixRepeat(Repeat):
def original_value(self, test_case_context=None):
if isinstance(self._default_value, ProtocolSessionReference):
if test_case_context is None:
return self._default_value.default_value
else:
value = test_case_context.current_message.names[self._default_value.name].repeater
if (value < 1):
value = 1
return value - 1
else:
return self._default_value

通过继承原类型,可以为基础类型如FuzzableFuzzableBlock,也可以为Word Size等类型,重写mutations方法实现自定义的变异,重写encode方法实现自定义编码,重写render方法实现自定义输出渲染方式

Protocol Definition — boofuzz 0.4.2 documentation

连接 目标 会话

Connections

与被测目标的连接对象,基类为ITargetConnection,包含open/close send/recv方法,boofuzz包含如下的连接方式

除了上面的几种连接方式外,也可以通过实现open/close send/recv方法实现与任意目标的连接交互

Target

被测试的目标对象

1
2
3
4
5
6
7
8
9
target=Target(
connection=TCPSocketConnection(
host="127.0.0.1",
port=12345
),
monitors=[monitor],
monitor_alive=[monitor_alive],
repeater=repeater
)

connection 测试目标的连接对象

monitors 监视器对象列表

monitor_alive 监视器活跃时调用的函数列表,以活跃的监视器为参数

repeater 发送时使用的重复器,用于重复多次发送同一条数据

Session

会话为协议交互的构造提供了一个容器,常用参数如下

sleep_time 测试用例之间等待的秒数

pre_send_callbacks 每个测试请求前调用的注册函数列表

post_test_case_callbacks 每个测试用例之后调用的注册函数列表

post_start_target_callbacks 目标启动或重新启动后进程监视器调用的注册函数列表

web_port 监听的Web端口,默认为26000,设置为None禁用Web界面、

web_address 监听的Web地址,默认为localhost

receive_data_after_fuzz 传输完fuzzed消息后尝试接收响应

target 会话的目标

常用方法

  • connect

    1
    connect(src, dst=None, callback=None)

    在两个请求节点键创建一个连接并注册一个回调函数用于处理源请求与目的请求之间的传输过程

     Session 类维持着一个顶级节点(根节点),所有的 requests 初始时都必须连接到该节点

    1
    2
    session = Session()
    session.connect(session.root, req)

    如果仅指定src节点,则src节点将默认与根节点连接

    1
    session.connect(req)

    回调函数在src节点请求完成后,dst请求发送前执行(在仅指定src节点时,可视为root为src节点,实际添加的节点为dst节点,因此回调函数在添加的请求前执行)

    回调函数示例如下,session.last_send session.last_recv分为别会话前一次发送接收的数据,回调函数存在返回值时,其返回值将作为下一个请求发送的数据

    1
    2
    3
    4
    def req_callback(target:Target, fuzz_data_logger, session:Session, test_case_context, *args, **kwargs):
    print(session.last_send)
    print(session.last_recv)
    return b'1111111111111111'
  • fuzz

    对整个协议树开始模糊测试

  • render_graph_graphviz

    将会话的请求节点及节点间关系渲染为图形

    1
    2
    with open('somefile.png', 'wb') as file:
    file.write(session.render_graph_graphviz().create_png())

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    session.connect(req1)
    session.connect(req4)
    session.connect(req1, req2)
    session.connect(req1, req3)
    session.connect(req4, req2)
    session.connect(req2, req5)
    session.connect(req5, req3)
    session.connect(req1, req6)
    session.connect(req6, req7)
    session.co

    w29YdknI

监视器

Monitors 就是监视目标特定行为的组件,一般称之为监视器。监视器可以是被动的,也可以是主动的。 被动的意思是仅对目标进行观察并提供一些数据,而主动的意思则是表明监视器的行为更加的积极,比如直接和目标进行交互等。 更进一步地,有些监视器甚至具有启动、停止、重启目标的能力。

监测目标的崩溃或异常行为可能是一件复杂的事情,主要取决于在目标系统上有哪些可用的工具。 比如对嵌入式设备来说,在其上通常没有现成的能够监测崩溃/异常的工具。

Boofuzz 主要提供了三种监视器实现类:

  • ProcessMonitor:从 Windows 和 Unix 进程中收集调试信息的监视器类。该类也可以重启目标进程以及监测段错误。

  • NetworkMonitor:通过 PCAP 被动地捕获网络流量并将其写入测试用例日志中的监视器类。

  • CallbackMonitor:用于实现回调函数的监视器类,可以传递给 Session 类。

ProcessMonitor

ProcessMonitor进程监视器,boofuzz测试端作为RPC客户端,所有的命令转发到RPC服务器执行,被测目标需要运行RPC服务器,接收RPC命令执行相应的操作

RPC服务器部分可以通过继承pedrpc.ServerBaseMonitor实现,此外还需要添加set_start_commands set_stop_commands start_target stop_target 等方法,使用较为复杂

NetworkMonitor

NetworkMonitor与ProcessMonitor几乎相同,只是删减了部分方法,仍需要自行实现RPC服务器

CallbackMonitor

CallbackMonitor回调监视器用于在fuzz的特定阶段执行指定任务,有如下4个阶段回调

  • restart_callbacks –> target_restart

  • pre_send_callbacks –> pre_send

  • post_test_case_callbacks –> post_send

  • post_start_target_callbacks –> post_start_target

通过设置特定的回调,能够方便地在测试的过程中实现原本复杂的功能,如测试环境准备、测试环境清理、接口认证、中断连接等

如下示例使用了pred_callbackpost_callback功能,在测试开始前和结束后与被测目标进行通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def on_pre_callback(target:Target, fuzz_data_logger, session, *args, **kwargs):
target.send(b'hello world')
target.recv()

def on_post_callback(target:Target, fuzz_data_logger, session, *args, **kwargs):
target.send(b'byebye')
target.recv()

monitor = CallbackMonitor(
on_pre_send=[on_pre_callback],
on_post_send=[on_post_callback]
)

session = Session(
target=Target(
connection=TCPSocketConnection(
host="127.0.0.1",
port=12345
),
monitors=[monitor]
)
)

参考连接

jtpereyda/boofuzz: A fork and successor of the Sulley Fuzzing Framework (github.com)

boofuzz: Network Protocol Fuzzing for Humans — boofuzz 0.4.2 documentation

boofuzz 源码笔记(一) - IOTsec-Zone


boofuzz-网络协议模糊测试器
https://blog.noxke.icu/2024/07/25/studay/boofuzz-网络协议模糊测试器/
作者
noxke
发布于
2024年7月25日
许可协议