udp测试工具(附贴心教程(二))

  • A+
所属分类:轻松一刻

模糊测试

模糊测试是一种暴力可靠性测试技术。它采用黑盒测试的思想,通过自动或半自动的生成大量畸形的随机数据来作为应用程序的输入,并监视程序异常,以发现应用程序中可能存在的漏洞或安全缺陷。

模糊测试是一个很重要的流程,有助于发现影响当今复杂应用程序的未知关键错误,有时候甚至能暴露出一些让人“脑洞大开”或者“七窍生烟”的奇葩问题。

本文分为上中下三篇文章,介绍一些现有的运用在不同方向上的优秀fuzzing开源项目,包括针对通信协议Fuzzers、和文件Fuzzers

通信协议Fuzzers

针对通信协议的开源的Fuzz工具——BooFuzz

1、什么是BooFuzz

BooFuzz是一个开源的网络协议模糊测试框架,它是由Python语言编写的,可以部署在Windows、Linux和Mac平台。BooFuzz继承自Sulley,它提供了对于网络协议进行模糊测试的规范和功能函数,您可以在此基础上编写针对特定目标的Python脚本,对目标进行量身定制的Fuzz,但这需要开发者对所测试协议的格式有一定了解。BooFuzz并不是直接对服务器程序或客户端程序进行Fuzz的,为了更好地模拟服务器或客户端在真实情境下的工作状态,其模糊测试的对象是会话。

BooFuzz可用于IoT设备通信协议的测试。想路由器、HTTP server和Telnet这些模块通常能够与外界进行交互,从而为攻击者提供了入口。如果这些模块或使用的协议存在漏洞,其很有可能会被直接利用来进行远程攻击,从而带来严重的安全隐患。BooFuzz根据测试人员编写的脚本生成大量变异报文,并发送给目标设备,实时监控目标设备,通过设备服务的可用性来判断设备崩溃情况。

与Sulley不同,BooFuzz还具有以下特点:更轻松的安装体验;支持多种协议,比较灵活;内置支持串行模糊测试、以太网和IP层、UDP广播;能够更好地记录测试数据;测试结果CSV导出等。

由于BooFuzz继承了Sulley框架,若想了解BooFuzz的框架,应当先了解Sulley的整体架构。Sulley框架如下(来源: Fuzzing Sucks! Introducing the sulley fuzzing framework. Pedram Amini & Aaron Portnoy. Black Hat US 2007):

由图基本可以看到Sulley主要包括四大组件,分别为Data Generation(数据生成)、Session(会话管理)、Agents(代理)和Utilities(独立单元工具)。其整体的架构和peach、sfuzz相似,是一种比较好的商业化架构。

目前BooFuzz主要支持两种底层的通信方式:socket和串口serial。这两种通信方式又可以延申出TCPSocketConnection、UDPSocketConnection、SSLSocketConnection、RawL2SocketConnection、RawL3SocketConnection、SocketConnection和SerialConnection几种通信方式。

BooFuzz可以从socket底层开始封装测试,比如可以测试tcp、udp、ftp和http等协议。这类fuzz工具还有一个比较典型的特点,就是必须存在对崩溃进行监控的监控器。BooFuzz本身提供了三种监控器:networkmonitor、processmonitor和callbackmonitor。BooFuzz支持的编译算法比较简单,仅仅支持对于string、num和字典数据的变异。

2、BooFuzz 脚本编写

2.1 创建session对象

所有的Fuzz都是有针对的对象的,也就是说,在开始Fuzz前,我们首先要确定对哪个程序进行Fuzz。BooFuzz是针对Session进行模糊测试的,因此我们首先要定义一个Session对象。当您创建Session时,您将传递一个Target对象,该对象本身接收一个Connection对象(可用选项包括SocketConnection和SerialConnection)。例如:

session = Session( target=Target( connection=SocketConnection("127.0.0.1", 8021, proto='tcp')))

2.2 定义消息

准备好会话对象后,需要在协议中定义消息,细节可参照静态协议定义:https://boofuzz.readthedocs.io/en/stable/user/static-protocol-definition.html#static-primitives

每一个消息都是以一个s_initialize函数开头的,模块的常用语法如下:

s_initialize('grammar') # 初始化块请求并命名 s_static("HELLOrn") # 始终发送此消息 s_static("PROCESS") # 在HELLOrn之后立即发送 s_delim("") # 使用s_delim()原语代替分隔符 s_string("AAAA") # 这是我们的fuzz字符串 s_static("rn") # 告诉服务器"done"

下面是fuzz FTP协议中的几个消息定义:

2.3 连接请求

如果需要fuzz多个请求,则需要将定义的请求按照一定的先后顺序连接起来。在前面的fuzz FTP协议中定义了user、pass、stor和retr这几个消息,在定义消息后,要将之前定义的请求按一定的先后顺序连接,比如:

session.connect(s_get("user"))

session.connect(s_get("user"), s_get("pass"))

session.connect(s_get("pass"), s_get("stor"))

session.connect(s_get("pass"), s_get("retr"))

其意味着pass请求要在user请求之后发生,stor和retr要在pass之后发生,而stor和retr这两个请求之间则没有明确的先后关系。上述代码等同于下图:

BooFuzz的快速入门指南参见官方文档:

https://boofuzz.readthedocs.io/en/stable/user/quickstart.html

3、BooFuzz实战

3.1BooFuzz安装

这里仅介绍在kali linux的安装,其他系统的安装方法请参考官方手册:

https://boofuzz.readthedocs.io/en/stable/user/install.html

首先下载源码: git clone https://github.com/jtpereyda/boofuzz.git 若出现报错: fatal: unable to access 'https://github.com/jtpereyda/boofuzz.git/': gnutls_handshake() failed: Error in the pull function. 则将命令中的https换成git即可:git clone git://github.com/jtpereyda/boofuzz.git

BooFuzz要求Python版本不低于3.5,且使用pip安装,因此要确保有最新版的pip和setuptools,可执行命令pip3 install -U pip setuptools来更新pip和setuptools版本,然后执行pip install boofuzz命令进行安装即可。

3.2实战思路

3.3开始Fuzz

Step1根据网络数据包构造请求

比如您要对路由器的登录接口进行fuzz测试,则先访问路由器的登录界面,使用BurpSuite设置代理,并进行抓包。注意,在开始之前要尽可能多地与设备进行交互。BooFuzz方法不仅局限于IoT设备,也可用于对常见的服务程序进行测试。

在此,我们将对bWapp的登录接口进行fuzz测试。先使用bWapp登录界面,并使用BurpSuite进行抓包。BurpSuite抓取到的报文如下:

根据数据包报文,利用BooFuzz框架对HTTP请求进行定义:

Step2设置会话信息

定义一个Session对象,其中参数包括TCP连接、每一次fuzz的时间间隔以及每一次fuzz日志的输出。格式如下:

在此给出一个最简单的会话设置,包括IP地址和端口:

完成后要将我们构造的请求包加入到我们的会话图(Session)中,作为唯一的一个节点:

session.connect(s_get("Login"))

Step3添加监视器

如前面所说,判断目标设备是否崩溃的方法之一,就是判断设备是否在线。结合Step2中引用的函数Remote_NetworkMonitor(),该函数并非官方API,也可以不添加,其核心代码如下:

注意:完成各部分设置后,需要调用session.fuzz()才能进行测试。

Step4编写完整测试脚本

进入boofuzz目录,创建测试脚本bfuzztest.py,并编写脚本,完成脚本内容如下(脚本代码附在文末):

Step5开始Fuzz

执行python3 [脚本文件名]来运行编写好的脚本

每次测试的日志数据将保存到当前目录下boofuzz-results目录中的SQLite数据库中,运行boo open,将在26000端口开启一个Web服务器,用于控制和查看测试进度。(在某些版本中运行fuzz脚本会自动打开26000端口)在浏览器访问localhost:26000即可。

对于测试出来的漏洞,我们需要根据测试结果来分析其产生的原因。在Kali中自带DB Browser for SQLite工具,用它打开boofuzz-result目录下的相关文件即可查看fuzz的日志。在其他系统中可以使用数据库查看软件来查看日志。

从数据库中可以看到,构造的数据包按照我们的代码对相关字段进行了变异,从而达到了fuzz的效果。

4、相关博客

https://blog.csdn.net/song_lee/article/details/104334096

https://jackfromeast.site/2021-03/boofuzz-1.html

https://cloud.tencent.com/developer/article/1418469

https://www.cnblogs.com/yuzhouliu/p/15180591.html

附:bfuzztest.py脚本

#!/usr/bin/env python # Designed for use with boofuzz v0.0.9 # coding=utf-8 from boofuzz import * def main(): session = Session( target=Target( connection=SocketConnection("192.168.153.143", 80, proto='tcp') ), ) s_initialize(name="Login") with s_block("Login-Line"): # LINE 1 s_static("POST", name="Method") s_delim(" ", name='space-1') s_string("/login.php", name='Login-URI') # variation s_delim(" ", name='space-2') s_static('HTTP/1.1', name='HTTP-Version') s_static("rn") # LINE 2 s_static("Host", name="Host") s_static(": ") s_static("192.168.153.143", name="ip") s_static("rn") # LINE 3 对应 Content-Length: 52 s_static('Content-Length') s_static(': ') s_size('data', output_format='ascii', fuzzable=True) # size的值根据data部分的长度自动进行计算,同时对该字段进行fuzz s_static('rn') # ... s_static('rn') # 对应http请求数据 with s_block('data'): s_static(' login=bee&password=') s_string('1231', max_len=1024) # 需要变异,且最大长度为1024 s_static('&security_level=0&form=submit') session.connect(s_get("Login")) session.fuzz() if __name__ == "__main__": main()

文件Fuzzers

模糊测试应当用于测试每个需要接受某种形式输入的接口,起码要测试每个从潜在恶意来源获取输入的接口。除了前面提到的通信协议,文件也同样可能存在安全风险,尤其是我们熟知的二进制文件。

对于程序来说,少数字节错位都有可能使得整个应用程序毁于一旦。这些使程序崩溃的畸形文件通过损坏程序的字节,而使其破坏自己的栈、堆。在旧式、无内存的操作系统中,畸形文件通常能将整个计算机宕掉。此外,还有JS文件和HTML等Web相关的文件,这些文件都有可能引发安全问题。

文件类型模糊测试的通常做法是:先准备一份插入程序中的正确的文件,然后用随机数据替换该文件的某些部分,最后用程序打开经模糊后的文件,观察程序是否出现异常或出现了什么异常。

下面将介绍两款工具:DrillerDomato

针对文件的开源的Fuzz工具——Driller

1、什么是Driller

Driller依赖于模糊测试工具AFL和二进制分析工具angr,Driller是在AFL的基础上开发的crash模糊测试工具。Driller在AFL的基础上加入了动态符号执行引擎,当模糊测试发生stuck时,使用动态符号执行去突破这些限制,生成满足fuzz需求的新输入,使得fuzz能够继续执行。具体实现是,通过监测AFL的执行,我们可以决定什么时候开始符号执行以探索新路径。如果AFL执行了x轮后,bitmap上显示没有发现新的状态转换(也即新的代码块转移),说明AFL卡住了,这时候调用angr进行符号执行。每个具体输入对应于PathGroup中的单个路径, 在PathGroup的每一步中,检查每个分支以确保最新的跳转指令引入先前AFL未知的路径。当发现这样的跳转时,SMT求解器被查询以创建一个输入来驱动执行到新的跳转。这个输入反馈给AFL,AFL在未来的模糊步骤中进行变异。这个反馈循环使我们能够将昂贵的符号执行时间与廉价的模糊时间进行平衡,并且减轻了模糊对程序操作的低语义洞察力。

Driller结合了AFL的高效、低消耗、快速的优点和动态符号执行探索能力强的优点,又避免了AFL较难突破特殊的边界和动态符号执行路径爆炸的问题。不过基于符号执行增强的模糊测试技术仍然会受限于符号执行中的约束求解问题,符号执行的引入可能会弱化模糊测试本身的可扩展性。

2、Driller的安装

此安装过程使用的是ubuntu16.04。

2.1、环境准备

(1)安装一些必需的软件包

apt-get install build-essential gcc-multilib libtool automake autoconf bison debootstrap debian-archive-keyring libtool-bin python3-dev libffi-dev virtualenvwrapper git wget flex python libglib2.0-dev qemu-system libacl1-dev python3 python3-pip libssl-dev

(2)安装在编译qemu时所需的环境

apt-get build-dep qemu

2.2、安装AFL

(1)新建一个文件夹来保存所有的过程文件

mkdir driller && cd driller

(2)从GitHub上可以下载AFL源码

git clone git://github.com/google/AFL.git

(3)进入源码文件夹,执行编译命令

cd AFL make -j4

(4)编译qemu

cd qemu_mode ./build_qemu_support.sh

2.3、安装Driller

在安装与使用Driller时,最好在一个单独的Python虚拟环境中进行,这里使用的是Aconda环境。

Part1:安装Anaconda

(1)下载Anaconda的安装脚本

wget https://repo.anaconda.com/archive/Anaconda3-2020.11-Linux-x86_64.sh

(2)赋予执行权限

chmod +x Anaconda3-2020.11-Linux-x86_64.sh

(3)运行脚本

./Anaconda3-2020.11-Linux-x86_64.sh

(4)修改shell的环境配置

vim ~/.bashrc 在其中添加实际安装路径 export PATH="/root/anaconda3/bin:$PATH"

(5)刷新重新运行

source ~/.bashrc

Part2:建立Driller虚拟环境

(1)使用Aconda建立一个名字叫做driller的虚拟环境

conda create -n driiler python=3.8

(2)进入虚拟环境中

conda activate driiler

Part3:安装Driller

(1)安装angr

pip install angr

(2)安装cle

pip install cle

(3)安装tracer

pip install git+git://github.com/angr/tracer

(4)安装driller

pip install git+git://github.com/shellphish/driller

可以导入driller即为安装成功。

3、Driller的使用

这种安装方法是采用一种Driller和AFL并行运行的过程,将Driller中求解输入的部分和AFL fuzz的部分分别放在两个terminal运行。

(1)创建Fuzz的程序源码为buggy.c,其内容如下:

#include #include int main(int argc, char *argv[]) { char buffer[6] = {0}; int i; int *null = 0; read(0, buffer, 6); if (buffer[0] == '7' && buffer[1] == '/' && buffer[2] == '4' && buffer[3] == '2' && buffer[4] == 'a' && buffer[5] == '8') { i = *null; } puts("No problem"); }

(2)编译buggy.c

由于使用AFL QEMU模式,因此不需要对源码进行插桩

gcc -o buggy buggy.c -g

(3)打开一个terminal, 用AFL进行FUZZ

mkdir -p workdir/input echo 'init' > workdir/input/seed1 # 提供初始化种子输入 echo core | sudo tee /proc/sys/kernel/core_pattern AFL/afl-fuzz -M fuzzer-master -i workdir/input/ -o workdir/output/ -Q ./buggy

接下来就是开始AFL的Fuzz过程(窗口较小时不会显示界面,调整窗口大小即可):

当输入为7/42a8就会发生crash。

process timing:显示的是fuzzer的运行时间,最近一次发现新路径的时间,最近一次崩溃的时间和最近一次挂起的时间。

overall results:显示的是运行的总周期数,总路径数,崩溃次数和挂起次数。

cycle progress:显示的是正在处理的测试样例的编号和由于超时放弃的的输入数量。

map coverage:第一行显示的是已经命中了多少分支元组与位图可以容纳的数量的比例。左边的是当前输入,右边的是整个输入语料库的值。第二行显示的是二进制文件中元组命中计数的变化。

stage progress:显示的是正在测试的策略,进度,总执行次数和执行速度。一般正常的执行速度应该在500以上。

findings in depth:显示的是基于应用到代码的最小化算法获得的fuzzer最喜欢的路径数和真正获取更好的边缘覆盖率的测试用例数,还有崩溃和超时的数量,注意这里的tmout和hang有所不同,tmout数量包括了所有超过超时的测试用例,即使它们没有超过超时足够的幅度而分类为hangs(挂起)

fuzzing strategy yields:用于追踪各种fuzzing策略所获得的路径和尝试执行的次数的比例,用于验证各种方法的有效性。

path geometry:第一个数据指的是fuzzing过程中达到的路径深度,pending指的是还有多少输入数据没有经过任何测试。pend fav时指fuzzer在这个队列中真正想到达的条目。接下来的是这个fuzzing部分找到的新路径数量和在进行并行化fuzzing时从其他fuzzer实例导入的路径数量 。最后一个数据可以衡量观测到痕迹的一致性,如果程序对相同的输入始终表现相同,则为100%。

(4)创建一个run_driller.py脚本来运行driller,其内容如下:

#!/usr/bin/env python import errno import os import os.path import sys import time from driller import Driller def save_input(content, dest_dir, count): """Saves a new input to a file where AFL can find it. File will be named id:XXXXXX,driller (where XXXXXX is the current value of count) and placed in dest_dir. """ name = 'id:%06d,driller' % count with open(os.path.join(dest_dir, name), 'wb') as destfile: destfile.write(content) def main(): if len(sys.argv) != 3: print('Usage: %s' % sys.argv[0]) sys.exit(1) _, binary, fuzzer_dir = sys.argv # Figure out directories and inputs with open(os.path.join(fuzzer_dir, 'fuzz_bitmap'), 'rb') as bitmap_file: fuzzer_bitmap = bitmap_file.read() source_dir = os.path.join(fuzzer_dir, 'queue') dest_dir = os.path.join(fuzzer_dir, '..', 'driller', 'queue') # Make sure destination exists try: os.makedirs(dest_dir) except os.error as e: if e.errno != errno.EEXIST: raise seen = set() # Keeps track of source files already drilled count = len(os.listdir(dest_dir)) # Helps us name outputs correctly # Repeat forever in case AFL finds something new while True: # Go through all of the files AFL has generated, but only once each for source_name in os.listdir(source_dir): if source_name in seen or not source_name.startswith('id:'): continue seen.add(source_name) with open(os.path.join(source_dir, source_name), 'rb') as seedfile: seed = seedfile.read() print('Drilling input: %s' % seed) for _, new_input in Driller(binary, seed, fuzzer_bitmap).drill_generator(): save_input(new_input, dest_dir, count) count += 1 # Try a larger input too because Driller won't do it for you seed = seed + b'0000' print('Drilling input: %s' % seed) for _, new_input in Driller(binary, seed, fuzzer_bitmap).drill_generator(): save_input(new_input, dest_dir, count) count += 1 time.sleep(10) if __name__ == '__main__': main()

(5)打开另一个terminal运行driller部分

source ~/.bashrc conda activate driiler python run_driller.py ./buggy workdir/output/fuzzer-master

启动成功后显示如下内容。

测试一段时间后我们可以打开输出文件夹里的fuzzer-master,就可以看到有crashes,hangs,queue三个文件夹存放着生成的对应的样例,以及fuzz_bitmap, fuzzer_stats, plot_data三个文件。

crash文件夹包含导致测试程序接收致命信号的测试用例,进入crashes可以看到触发得到的crash,得到的crash可以用xxd查看。

hang文件夹包含导致测试程序超时的测试用例,本次为0。

queue文件夹包含每个独特执行路径的测试用例,以及用户给出的起始文件。

查看里面的文件,afl逐渐从初始输入的“init”接近“7/42a8”,这将使我们的程序崩溃。

3、相关文献及博客:

[1] Stephens N , Grosen J , Salls C , et al. Driller: Augmenting Fuzzing Through Selective Symbolic Execution[C]// Network and Distributed System Security Symposium. 2016.

[2] 博客Driller工具分析:https://blog.csdn.net/Chen_zju/article/details/80791281

静候下文,仍有更多工具汇总

  • 我的微信公众号
  • 扫一扫关注
  • weinxin
  • 我的新浪微博号
  • 扫一扫关注
  • weinxin
小辉博客

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: