Peach
Peach是一款基于变异的模糊测试工具,被广泛应用于各种协议的测试。
编译
Gitlab在2020年收购Peach Fuzzer的母公司后,基于Peach Fuzzer Professional v4开发出协议测试工具GitLab Protocol Fuzzer,使用方法变化不大,所以下文都是基于GitLab Protocol Fuzze。
由于官方不提供编译好的二进制文件,需要自己编译,推荐是在Ubuntu 16.04上编译, Ubuntu 18.04实测也可以。
本文提供了两种编译方式,本地编译以及CI编译,其中本地编译要踩的坑比较多。
本地编译
安装过程主要参考
https://gitlab.com/gitlab-org/security-products/protocol-fuzzer-ce/-/issues/2#note_676566894
安装编译依赖的包
apt-get install -y \
coreutils apt-utils wget curl openssl ca-certificates bash-completion \
joe vim nano \
unzip p7zip \
fping hping3 httping thc-ipv6 gdb \
tcpdump wireshark-common \
locales-all \
git build-essential joe vim strace tcpdump python python-pip \
ruby doxygen libxml2-utils less openjdk-8-jre xsltproc asciidoctor \
nodejs node-typescript wget \
apt-transport-https dirmngr gnupg ca-certificates apt-utils
编译过程中需要用到Paket这个包管理工具下载相关依赖,由于国内网络原因,即使挂代理,也很容易因为网络原因而下载失败,强烈建议在海外主机上编译
这里把代码里面自带的Paket去掉,下载最新版本Paket替代(截止至2021/12/10,最新版本6.2.1)
git clone https://gitlab.com/gitlab-org/security-products/protocol-fuzzer-ce
cd protocol-fuzzer-ce/paket/.paket
rm -f paket.exe
rm -f paket.bootstrapper.exe
rm -f paket.targets
wget https://github.com/fsprojects/Paket/releases/download/6.2.1/paket.bootstrapper.exe
wget https://github.com/fsprojects/Paket/releases/download/6.2.1/paket.targets
wget https://github.com/fsprojects/Paket/releases/download/6.2.1/paket.exe
wget https://github.com/fsprojects/Paket/releases/download/6.2.1/Paket.Restore.targets
cd ../../
下载最新版pintool放在3rdParty/pin/目录下,同时修改build配置
wget https://software.intel.com/sites/landingpage/pintool/downloads/pin-3.21-98484-ge7cd811fd-gcc-linux.tar.gz
mv pin-3.21-98484-ge7cd811fd-gcc-linux.tar.gz 3rdParty/pin/
cd 3rdParty/pin/ && tar -xf pin-3.21-98484-ge7cd811fd-gcc-linux.tar.gz && mv pin-3.21-98484-ge7cd811fd-gcc-linux pin-3.21-98484-gcc-linux
sed -i s/pin-3.19-98425-gcc-linux/pin-3.21-98484-gcc-linux/g build/config/linux.py
cd ../../
下载Mono,官方要求是4.x版本,实测是有问题的,这里我们直接从官方源下载最新版
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
echo "deb https://download.mono-project.com/repo/debian stable-stretch main" | tee /etc/apt/sources.list.d/mono-official-stable.list
apt-get update -y
apt-get install -y mono-devel
对部分代码打Patch,不Patch的话编译过程会报错
参见
https://gitlab.com/gitlab-org/security-products/protocol-fuzzer-ce/-/issues/1
https://gitlab.com/gitlab-org/security-products/protocol-fuzzer-ce/-/merge_requests/7
sed -i '/^int main.*/itemplate<bool b>\nstruct StaticAssert {};\ntemplate <>\nstruct StaticAssert<true>\n{\n static void myassert() {}\n};\n' core/BasicBlocks/bblocks.cpp
sed -i 's/STATIC_ASSERT(sizeof(size_t) == sizeof(ADDRINT))/StaticAssert<sizeof(size_t) == sizeof(ADDRINT)>::myassert()/g' core/BasicBlocks/bblocks.cpp
sed -i 's/var config = new LicenseConfig();/\/\/var config = new LicenseConfig();/g' pro/Core/Runtime/BaseProgram.cs
一切正常的话linux_x86,linux_x86_64都应该是Available状态
./waf configure
如果遇到问题,可以尝试
./waf configure -v
可能会遇到的问题有两类
- 报错如下的是因为Mono版本问题,按上面说的,添加官方源后下载最新版本
linux_x86 is not available: Command ['/usr/bin/mono', '/home/xxxx/protocol-fuzzer-ce/paket/.paket/paket.exe', 'restore'] returned 1
- 报错如下是因为Paket版本问题,按上面说的,下载最新版替换掉原来的
linux_x86 is not available: Command ['/usr/bin/mono', '/home/xxxx/protocol-fuzzer-ce/paket/.paket/paket.bootstrapper.exe'] returned 1
开始编译
./waf build
重点,编译完成后,不要直接执行install,必须要先把mono版本降级到4.x,否则后面会失败
apt purge -y mono* libmono* doxygen
rm /etc/apt/sources.list.d/mono-official-stable.list
apt update -y
apt install -y mono-complete
./waf install
一切顺利的话,output目录下会有编译好的二进制文件
CI 编译
CI配置文件如下,Fork官方repo之后,main分支创建CI/CD流水线即可
image: ubuntu:18.04
stages:
- build
builder:
stage:
build
script:
- export DEBIAN_FRONTEND=noninteractive
- apt-get update -qq
- apt-get install -y -qq gnupg2
- apt-get install -y -qq gcc
- apt-get install -y -qq g++
- apt-get install -y -qq g++-multilib
- apt-get install -y -qq python
- apt-get install -y -qq ruby
- apt-get install -y -qq curl
- apt-get install -y -qq nodejs
- apt-get install -y -qq node-typescript
- apt-get install -y -qq default-jdk
- apt-get install -y -qq doxygen
- apt-get install -y -qq libxml2-utils
- apt-get install -y -qq xsltproc
- apt-get install -y -qq wget
- apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
- apt install -y apt-transport-https ca-certificates
- echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list
- apt update -qq -y
- apt-get install -y mono-devel
- cd paket/.paket
- rm -f paket.exe
- rm -f paket.targets
- rm -f paket.bootstrapper.exe
- wget https://github.com/fsprojects/Paket/releases/download/6.2.1/paket.bootstrapper.exe
- wget https://github.com/fsprojects/Paket/releases/download/6.2.1/paket.targets
- wget https://github.com/fsprojects/Paket/releases/download/6.2.1/paket.exe
- wget https://github.com/fsprojects/Paket/releases/download/6.2.1/Paket.Restore.targets
- cd ../../
- wget https://software.intel.com/sites/landingpage/pintool/downloads/pin-3.21-98484-ge7cd811fd-gcc-linux.tar.gz
- mv pin-3.21-98484-ge7cd811fd-gcc-linux.tar.gz 3rdParty/pin/
- cd 3rdParty/pin/ && tar -xf pin-3.21-98484-ge7cd811fd-gcc-linux.tar.gz && mv pin-3.21-98484-ge7cd811fd-gcc-linux pin-3.21-98484-gcc-linux
- cd ../../
- sed -i s/pin-3.19-98425-gcc-linux/pin-3.21-98484-gcc-linux/g build/config/linux.py
- sed -i '/^int main.*/itemplate<bool b>\nstruct StaticAssert {};\ntemplate <>\nstruct StaticAssert<true>\n{\n static void myassert() {}\n};\n' core/BasicBlocks/bblocks.cpp
- sed -i 's/STATIC_ASSERT(sizeof(size_t) == sizeof(ADDRINT))/StaticAssert<sizeof(size_t) == sizeof(ADDRINT)>::myassert()/g' core/BasicBlocks/bblocks.cpp
- sed -i 's/var config = new LicenseConfig();/\/\/var config = new LicenseConfig();/g' pro/Core/Runtime/BaseProgram.cs
- ./waf configure
- ./waf build
- apt purge -y mono* libmono* doxygen
- rm /etc/apt/sources.list.d/mono-official-stable.list
- apt update -y
- apt install -y mono-complete
- apt install -y mono-devel
- ./waf install
artifacts:
untracked: false
expire_in: 30 days
paths:
- output/
基础
使用Peach进行模糊测试可以分为几个步骤
- 创建数据模型
- 创建状态模型
- 添加Publisher
- 添加Monitor
Pit文件
Peach pit文件是一个XML格式的配置文件,它定义了模糊测试所需要的信息,文件里面的内容包括数据模型、状态模型等等。
以下是Peach提供的一个示例
<!-- 版本信息等,一般情况下不做改动 -->
<?xml version="1.0" encoding="utf-8"?>
<Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd">
<!-- 定义数据模型 -->
<DataModel name="TheDataModel">
<String value="Hello World!" />
</DataModel>
<!-- 定义状态模型 initialState表示初始状态 -->
<StateModel name="State" initialState="State1" >
<!-- 定义状态 -->
<State name="State1" >
<!-- 具体动作,output表示外发数据 -->
<Action type="output" >
<DataModel ref="TheDataModel"/>
</Action>
</State>
</StateModel>
<!-- 配置要使用的状态模型、数据模型等等 -->
<Test name="Default">
<!-- 指定要使用的状态模型 -->
<StateModel ref="State"/>
<!-- 指定Publisher,用于将生成的数据传输出去,Console表示打印到命令行 -->
<Publisher class="Console" />
</Test>
</Peach>
<!-- end -->
一般pit文件保存在pits目录下,运行
Peach.exe pits/example.xml
DataModel
DataModel描述了数据包中数据的类型、格式、大小等等信息。同时,DataModel还可以被其他DataModel通过ref所引用,类似于编程语言中的继承。
Peach支持非常多类型的数据,下面介绍几种常用的数据类型
Number
Peach支持从1到64比特任意长度的数字,同时可以定制字节序、符号等信息。
常用的属性:
size:比特长度(1-64)
value:默认值
valueType:类型(string/hex)
endian:字节序
signed:有无符号
mutable:是否允许变异,默认为true
<Number name="test" size="32" endian="big" signed="false"/>
String
字符串
常用属性:
length:长度(字节)
type:编码类型,默认utf8
value:默认值
nullTerminated:末尾是否添加NULL截断
<String name="test" value="test"/>
Blob
Blob通常用于表示二进制数据
常用属性:
length:长度(字节)
valueType:格式(hex/string)
<Blob name="test" valueType="hex" value ="1F 22 03"/>
Flags与Flag
Flags是Flag元素的容器,由于旧版本Peach不支持任意比特长度的Number元素,Flag被用来表示只占用一个或几个比特的标志位
<Flags name="flags" size="16">
<Flag name="flag1" position="0" size="1"/>
<Flag name="flag2" position="1" size="5"/>
</Flags>
Block
Block是一个容器元素,与DataModel类型,但区别是Block是DataModel的子元素,同样的,Block也支持被DataModel引用
<Block name="hello">
<Number value="1"/>
<String value="test"/>
</Block>
DNS
RFC1035定义了DNS报文格式
DNS Header
我们可以先定义出大致的结构,并给与合适的初始值
<DataModel name="Header">
<Number name="ID" size="16"/>
<Number name="QR" size="1" />
<Number name="Opcode" size="4" />
<Number name="AA" size="1" />
<Number name="TC" size="1" />
<Number name="RD" size="1" value="1" />
<Number name="RA" size="1" />
<Number name="Z" size="3" />
<Number name="RCODE" size="4" />
<Number name="QDCOUNT" size="16"/>
<Number name="ANCOUNT" size="16"/>
<Number name="NSCOUNT" size="16"/>
<Number name="ARCOUNT" size="16"/>
</DataModel>
Question
先来看看查询报文
查询报文有三个部分,QTYPE、QCLASS使用Number表示即可,但QNAME部分采用了DNS标准名称表示法,详细可参考RFC1035文档。
简单来说,每个域名(QNAME)是由一系列label构成,每个label第一个字节高两位被用来表示label的类型,一共有两种类型:00表示普通label,11表示压缩label
普通label
第一个字节高两位为00时,表示为普通label,剩下6比特被用来表示label的大小
以 www.baidu.com 为例,可以分为三个label,分别是www、baidu、com,用普通label表示为\x03www\x05baidu\x03com\x00
压缩label
第一个字节高两位为11时,表示为压缩label,剩下6比特与后面的字节共14比特构成一个指针,指针的值为对应字符串的偏移。
由于一个报文内可以同时有多个域名,而这些域名有一些部分是公共的,比如com这类后缀,通过指针可以节省空间。
下面这张图为例子,第二个域名为 www.example.com, 由于example.com在前面出现过,偏移为0xC,所以可以直接用指针
所以QNAME一共有三种情况:
1.全部由普通label表示,最后以NULL结尾
2.只有一个压缩label
3.前面由普通label表示,最后一个压缩label
普通label中,size的取值取决于后面的长度,这里用到Relation这个属性,取后面的长度赋值给size
<Block name="Label1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="www" nullTerminated="false"/>
</Block>
压缩label就比较简单
<Block>
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
第一种情况全部由普通label表示,最后以NULL结尾
<Block>
<Block occurs="1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="www" nullTerminated="false"/>
</Block>
<Block occurs="1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="google" nullTerminated="false"/>
</Block>
<Block occurs="1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="com" nullTerminated="false"/>
</Block>
<Blob name="NULL" length="1" valueType="hex" mutable="false" value="00"/>
</Block>
第二种情况 只有一个压缩label
<Block>
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
第三种情况前面由普通label表示,最后一个压缩label
<Block>
<Block occurs="1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="www" nullTerminated="false"/>
</Block>
<Block occurs="1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="google" nullTerminated="false"/>
</Block>
<Block occurs="1">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" value="com" nullTerminated="false"/>
</Block>
<Block>
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
</Block>
将三种情况通过Choice组合在一起,Fuzz过程中,Peach会随机在Choice中挑选一个来执行,类似于C语言中的switch
<DataModel name="Label">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" nullTerminated="false"/>
</DataModel>
<DataModel name="Labels">
<Block ref="Label" occurs="1">
<String name="id" value="www" nullTerminated="false"/>
</Block>
<Block ref="Label" occurs="1">
<String name="id" value="google" nullTerminated="false"/>
</Block>
<Block ref="Label" occurs="1">
<String name="id" value="com" nullTerminated="false"/>
</Block>
</DataModel>
<DataModel name="QNAME">
<Choice>
<Block ref="Labels">
<Blob name="NULL" length="1" valueType="hex" mutable="false" value="00"/>
</Block>
<Block ref="Labels">
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
<Block>
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
</Choice>
</DataModel>
Header中的QDCOUNT取决于Question的数量,同样用Relation处理
<DataModel name="DNS" ref="Header">
<Number name="QDCOUNT" size="16" mutable="true">
<Relation type="count" of="Question"/>
</Number>
<Block name="Question" ref="QNAME" maxOccurs="10">
<Number name="QTYPE" size="16" valueType="hex" mutable="false" value="00 01"/>
<Number name="QCLASS" size="16" valueType="hex" mutable="false" value="00 01"/>
</Block>
</DataModel>
到此数据建模部分基本完结,有些地方处理的方式比较粗糙,希望多多指教
StateModel
DNS协议通信比较简单,分别定义一个output跟input的action,这里我们不关心返回的结果,直接用Blob去匹配就行
<StateModel name="State" initialState="State1" >
<!-- 定义状态 -->
<State name="State1" >
<!-- 具体动作,output表示外发数据 -->
<Action type="output" >
<DataModel ref="DNS"/>
</Action>
<Action type="input" >
<DataModel>
<Blob />
</DataModel>
</Action>
</State>
</StateModel>
Publisher
Publisher是Peach Fuzzer的IO接口,所有的数据都是经过Publisher来处理,Peach支持相当多种类的Publisher,如Udp、Tcp、RawSocket、CAN、File等等。DNS协议只要涉及到Tcp与Udp这两个,具体支持的参数可查阅官方文档 https://gitlab.com/gitlab-org/security-products/protocol-fuzzer-ce/-/tree/main/docs/src.pro/Common/Monitors
<Publisher class="TcpClient">
<Param name="Host" value="8.8.8.8"/>
<Param name="Port" value="53" />
</Publisher>
<Publisher class="Udp">
<Param name="Host" value="8.8.8.8"/>
<Param name="Port" value="53" />
</Publisher>
Monitor
Monitor是Peach用来观察Fuzz对象状态的模块,Peach支持许多种类的Monitor,常用的有Ping Monitor、Socket Monitor、TcpPort Monitor、Run Command等等
TCP比较好处理,在每轮循环结束的时候,检测目标端口,当为Closed的时候,生成Fault
<Monitor class="TcpPort">
<Param name="Host" value="8.8.8.8" />
<Param name="Port" value="53" />
<Param name="Action" value="Fault" />
<Param name="State" value="Closed" />
<Param name="When" value="OnIterationEnd" />
</Monitor>
由于不好检测Udp端口是否开放,因此可以采用Run Command来检测,每次循环结束,执行自定义脚本,确定dns服务器是否正常
monitor.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-"
import dns.resolver
myResolver = dns.resolver.Resolver()
myResolver.nameservers = ['8.8.8.8']
try:
myAnswers = myResolver.query("www.baidu.com", "A",lifetime=2)
except:
exit(-1)
当自定义脚本返回值非0时,就认为产生错误
<Monitor class="RunCommand">
<Param name="Command" value="python3"/>
<Param name="Arguments" value="./pits/monitor.py" />
<Param name="FaultOnNonZeroExit" value="true" />
<Param name="When" value="OnIterationEnd" />
</Monitor>
优化
Peach支持许多种类的变异器(Mutator),但并不是所有的变异器都是我们想要使用的,可通过exclude去除掉不需要的变异器,相反地,也可以通过include指定变异器
<Mutators mode="exclude">
<Mutator class="DataElementDuplicate"/>
<Mutator class="DataElementRemove"/>
<Mutator class="DataElementSwapNearNodes"/>
<Mutator class="DataElementSwapNear"/>
</Mutators>
测试
将dns.xml保存至pits目录下,开始执行Peach.exe pits/dns.xml
dns.xml
<?xml version="1.0" encoding="utf-8"?>
<Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd">
<!-- 默认配置 -->
<Defaults>
<Number endian="big" />
</Defaults>
<!-- 定义数据模型 -->
<DataModel name="Header">
<Number name="ID" size="16" value="1234"/>
<Number name="QR" size="1" mutable="false"/>
<Number name="Opcode" size="4" mutable="false"/>
<Number name="AA" size="1" mutable="false"/>
<Number name="TC" size="1" mutable="false"/>
<Number name="RD" size="1" value="1" mutable="false"/>
<Number name="RA" size="1" mutable="false"/>
<Number name="Z" size="3" mutable="false"/>
<Number name="RCODE" size="4" mutable="false"/>
<Number name="QDCOUNT" size="16" mutable="false"/>
<Number name="ANCOUNT" size="16" mutable="false"/>
<Number name="NSCOUNT" size="16" mutable="false"/>
<Number name="ARCOUNT" size="16" mutable="false" />
</DataModel>
<DataModel name="Label">
<Number name="flag" size="2" value="0" mutable="false"/>
<Number name="size" size="6">
<Relation type="size" of="id"/>
</Number>
<String name="id" nullTerminated="false"/>
</DataModel>
<DataModel name="Labels">
<Block ref="Label" occurs="1">
<String name="id" value="www" nullTerminated="false"/>
</Block>
<Block ref="Label" occurs="1">
<String name="id" value="google" nullTerminated="false"/>
</Block>
<Block ref="Label" occurs="1">
<String name="id" value="com" nullTerminated="false"/>
</Block>
</DataModel>
<DataModel name="QNAME">
<Choice>
<Block ref="Labels">
<Blob name="NULL" length="1" valueType="hex" mutable="false" value="00"/>
</Block>
<Block ref="Labels">
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
<Block>
<Number name="flag" value="3" size="2" mutable="false"/>
<Number size="14"/>
</Block>
</Choice>
</DataModel>
<DataModel name="DNS" ref="Header">
<Number name="QDCOUNT" size="16" mutable="true">
<Relation type="count" of="Query"/>
</Number>
<Block name="Query" ref="QNAME" maxOccurs="10">
<Number name="QTYPE" size="16" valueType="hex" mutable="false" value="00 01"/>
<Number name="QCLASS" size="16" valueType="hex" mutable="false" value="00 01"/>
</Block>
</DataModel>
<!-- 定义状态模型 initialState表示初始状态 -->
<StateModel name="State" initialState="State1" >
<!-- 定义状态 -->
<State name="State1" >
<!-- 具体动作,output表示外发数据 -->
<Action type="output" >
<DataModel ref="DNS"/>
</Action>
</State>
</StateModel>
<!-- Monitor -->
<Agent name="TheAgent">
<Monitor class="RunCommand">
<Param name="Command" value="python3"/>
<Param name="Arguments" value="./pits/monitor.py" />
<Param name="FaultOnNonZeroExit" value="true" />
<Param name="When" value="OnIterationEnd" />
</Monitor>
</Agent>
<!-- 设置maxOutputSize以防止产生过大数据包 -->
<Test name="Default" maxOutputSize="100000" >
<!-- 变异策略 -->
<Strategy class="Random">
<Param name="MaxFieldsToMutate" value="6" />
<Param name="SwitchCount" value="200" />
</Strategy>
<Mutators mode="exclude">
<Mutator class="DataElementDuplicate"/>
<Mutator class="DataElementRemove"/>
<Mutator class="DataElementSwapNearNodes"/>
<Mutator class="DataElementSwapNear"/>
</Mutators>
<!-- 指定要使用的状态模型 -->
<StateModel ref="State"/>
<!-- 指定Publisher -->
<Publisher class="Udp">
<Param name="Host" value="8.8.8.8"/>
<Param name="Port" value="53" />
</Publisher>
<Agent ref="TheAgent"/>
</Test>
</Peach>
Wireshark抓包观察到畸形报文
完结
如果有不正确的地方,请各位大佬多多指点