Sully:
监视网络,并系统的自动维护着相应记录.
监视目标的健康状况,并有能力使用多种方法使其恢复到一个好的状态.
Sully 能够发现,跟踪,以及对检测到的错误进行分类.
Sully 能够并行执行,增加了测试的速度.
Sully 能够自动的确定测试案例中那一个单一系列引发的错误.
Sully的使用:
数据表示: 是使用任何fuzzer的第一步.运行目标程序,将协议独立的请求和响应 ,并使用sully中的blocks表示.
会话 : 将开发的请求连接在一起形成会话,并附加上各种可用的监视代理(socket,debugger,etc..)然后开始fuzzing
事后分析:检查生成的数据和监控结果,重放单一测试案例.
目录结构:
archived_fuzzies: 这是个自由形态的目录,以fuzz目标名组织在一起,用来存储已存档的fuzzer和fuzz 会话产生的数据.
audits: 一个活动会话所记录的PCAP数据,崩溃的二进制文件,代码覆盖范围和分析的图标都应该放在此目录.
一旦fuzz会话完成,所有的数据都要移到"Archived_fuzzies"目录中.
docs: 文档和生成的Epydoc API references
requests: Sully requests 库,每个目标应该拥有它自己的能被用来存储多个请求的文件.
___REQUESTS__.html: 包含已存储请求的类别和个别类型列表说明,按字母顺序维护.
http.py : 各种各样的web 服务fuzzing 请求
trend.py : 包含一个完整的fuzz 演练相关请求,在稍后的文档中讨论.
sulley: fuzzer 框架.除非你想扩展这个框架,否则不要轻易的改动这些文件.
legos: 用户定义的复杂原语
ber.py ASN.1/ BER 原语
dcerpc.py 微软RPC NDR 原语
misc.py 诸如E-mail 地址和主机名等各种各样的未分类原语
xdr.py XDR types
pgraph: python 图形抽象库,建立sessions时使用.
utils: 各种辅助例程
dcerpc.py 微软RPC 帮助例程.比如绑定一个接口以及生成一个请求.
misc.py 各种未分类的例程,比如CRC-16 以及UUID 处理例程
scada.py SCADA特有的帮助例程,包含DNP3 块编码器
__init__.py : 用于创建请求时定义的各种各样的别名.
blocks.py : Blocks 和block 帮助在这里定义.
instrumentation.py 外置仪表特征,用于并不支持debugger的监视目标
pedrpc.py 用于定义sully 在不同的代理和主fuzzer之间通信的客户端和服务器类.
primitives.py 各种fuzzer原语,包括静态,随机,字符串和整数定义
sessions.py 创建并执行一个会话的功能函数.
sex.py sully的自定义异常处理类
unit_tests: sully 的单元测试装置
utils: 各种独立的实用工具
crashbin_explorer.py 命令行工具,用于扫描序列化存储于crash 二进制文件结果。
pcap_cleaner.py 命令行工具,用于清除没有错误关联的PACP 目录 network_monitor.py PedRpc驱动的网络监视代理
process_monitor.py PedRpc驱动,基于调试器的目标监视代理
unit_test.py sulley的单元测试
vmcontrol.py PedRpc驱动的VMWare 控制代理 。
作者:
Pedram Amini
http://pedram.openrce.org
Aaron Portnoy
http://dvlabs.tippingpoint.com/team/aportnoy
安装和需求:
network_monitor.py :
CORE Pcapy,
http://oss.coresecurity.com/projects/pcapy.html
CORE Impacket
http://oss.coresecurity.com/projects/impacket.html
process_monitor.py
PaiMei
http://code.google.com/p/paimei/
http://www.openrce.org/downloads/details/208/PaiMei
http://www.nologin.org/main.pl?action=codeList&
数据表示:
初始化并命名一个请求
sulley使用基于数据块表示的方法来生成独立的请求,然后将这些请求放在一起生成一次会话。
s_initialize("new request")
现在,你可以添加原语,blocks 或者嵌套的blocks 来生成请求。每一个原语都能被单独的rendered或突变。rendering 一个原语
将返回其关联的raw data数据格式内容。突变一个原语将变化它内部的内容。rendering 和mutating 的概念被大多数fuzzer开发者
抽象出来的,所以不用为它担心。然而,当fuzzable 的值被穷尽时,任何可突变的原语都接受一个默认的值。
静态数据 以及 随机数据:
让我们开始一个最简单的原语,s_static(),这个原语添加一个任意长度静态不突变的值到request中,为了便利,sully 使用了
各种不同的别名 s_dunno(),s_raw(),s_unknown()都是s_static()的别名。
# these are all equivalent:
s_static("pedram\x00was\x01here\x02")
s_raw("pedram\x00was\x01here\x02")
s_dunno("pedram\x00was\x01here\x02")
s_unknown("pedram\x00was\x01here\x02")
原语,block 等,都有一个可选的名字关键字参数。指定一个名字项可以直接使用request 变量request.names["name"] 存取,而
不需要遍历block结构来找到你期望的值。
s_binary 和s_static类似,但不相同,s_binary 接受多种形式表示的二进制数据,SPIKE 用户应该很熟悉它的使用,
# yeah, it can handle all these formats.
s_binary("0xde 0xad be ef \xca fe 00 01 02 0xba0xdd f0 0d", name="complex")
大多数sulley原语又fuzz 启发式驱动,因此它们突变的数目是有限的,但是s_random除外,它被利用来生成各种长度的随机数值,该原语必须携带两个
参数'min_length','max_length',用来说明在每一次迭代生成随机数值得最大,最小长度值,该原语,还接受如下关键字参数.
num_mutations: (integer, default=25) 在恢复到默认值前的突变次数
fuzzable: (boolean, default=True) 开启和关闭该原语的可突变性
name: (string, default=None) 通过名字,sulley可以对该原语直接存取.
number_mutations 关键字参数指定在被认为该原语耗尽时应该突变该原语多少次。如果要生成固定长度的随机数,指定min_length 和max_length
相等即可
整数
二进制和anscii协议中可能会包含各种大小的整数域,如http协议的内容长度域,和所有的fuzz框架一样,我们在这里也需要来产生这些数据:
1 byte: s_byte(), s_char()
2 bytes: s_word(), s_short()
4 bytes: s_dword(), s_long(), s_int()
8 bytes: s_qword(), s_double()
每一个整数类型至少要接受一个单一的参数,即默认整数值,此外,下面这些可选关键字参数可被指定.
endian: (character, default='<') bit 位序 指定 '<' 为小端,'>'为大端
format: (string, default="binary")输出格式, "binary" 或 "ascii", 控制者原语表示的整数格式, 例如,值100 ascii格式为 ‘100’ 而 "\x64" 是二进制格式
signed: (boolean, default=False) 使得大小为有符号数,还是无符号数,仅当format="ascii"时适用
full_range: (boolean, default=False) 使该原语突变所有的可能值
fuzzable: (boolean, default=True) 开启和关闭该原语的可突变性
name: (string, default=None) 可以通过在request中指定该名称来直接存储该原语的值
full_range 参数是上面所有参数中最值得关注的。考虑一下,当你fuzz一个DWORD类型的值时,一共有4294967295 种可能值,以每秒10个测试案例的速度
你需要13年才能fuzz完这个原语,通过定义一些smart数值集来减小原有穷举空间,包括在0边界处,+,- 10,以及最大值除2,4,6,8,10.16,32等得到
的集合,而穷尽减小的空间值(141 测试案例) 仅需要10几秒。
字符串和分隔符
字符串能在协议中的使用非常广泛,邮件地址,主机名,用户名,密码等等,你不用怀疑,字符串的应用贯穿在整个fuzzing的过程中,s_string 原语
负责这些域,这个原语只需要带一个默认参数值为必选参数,下面这些附加参数也可被指定。
size: (integer, default=-1) 字符串的静态大小,对于动态大小,指定该值为-1
padding: (character, default='\x00') 如果明确大小被指定,而产生的大小小于指定值, 使用这个值来填充直到满足条件为止。
encoding: (string, default="ascii") 字符串使用的编码,有效的选项包含无论在python中任何地方都能被接受的s.encode,对于微软unicode字符串,指定 utf_16_le
fuzzable: (boolean, default=True) 开启和关闭该原语的可突变性
name: (string, default=None) 可以通过在request中指定该名称来直接存储该原语的值
字符串通常被分割符划分为几个子域,例如,空格被用作http协议请求的分割符,"GET /index.html HTTP/1.0"前面的/ 和 . 同样作为该请求的分割符。
但你定义协议时,请确保使用s_delim()原语代替分割符,正如其它原语一样,必须指定一个默认值作为它的第一个参数,同样,和其它参数一致,
s_delim()同样接受可选的fuzzable 和name 参数,定界符的突变包括,重复,替代,排除。
作为一个完整的例子,fuzzing http 标签时使用如下系列原语:
# fuzzes the string: <BODY bgcolor="black">
s_delim("<")
s_string("BODY")
s_delim(" ")
s_string("bgcolor")
s_delim("=")
s_delim("\"")
s_string("black")
s_delim("\"")
s_delim(">")
Blocks:
必须精通的原语,让我们来看下,如何组织并嵌入一个块,新的块使用s_block_start()定义,使用s_bolck_end()关闭,每个块必须给定
一个名称,作为s_block_start()的第一个参数,这个例程同样可以指定如下可选参数:
group: (string, default=None) 和该块关联的组的名称
encoder: (function pointer, default=None) 函数指针,在reader 该block数据返回之前,提供一次处理该block数据的机会
dep: (string, default=None) 可选的参数,用来指定该block依赖的值
dep_value: (mixed, default=None) 如使该block的值 能被readered,该值必须在dep域中被包含
dep_values: (list of mixed types, default=[]) 为了该block的值能被readerd,dep可能包含的值列表.
dep_compare (string, default="==") 应用于依赖的比较方法,有效的可选操作为"==", "!=", ">", ">=", "<" and "<=".
Group ,encoding,以及依赖性是大多数fuzz框架没有的特性,我们将在下面论述它们。
组
group允许你连接一个块到指定的group原语,和一个组关联的block必须为每一个组中的值循环穷尽该block的所有空间,例如,在表示一个有效的opcode列表或一些有
相同参数的行为时,组原语是非常有用的,s_group定义一个组,并接受两个必须的参数,第一个参数指定组名称,第二个参数指定一个需要迭代的原始值列表。
例如,下面的请求完成web ,服务器的fuzz:
# import all of Sulley's functionality.
from sulley import *
# this request is for fuzzing: {GET,HEAD,POST,TRACE} /index.html HTTP/1.1
# define a new block named "HTTP BASIC".
s_initialize("HTTP BASIC")
# define a group primitive listing the various HTTP verbs we wish to fuzz.
s_group("verbs", values=["GET", "HEAD", "POST", "TRACE"])
# define a new block named "body" and associate with the above group.
if s_block_start("body", group="verbs"):
# break the remainder of the HTTP request into individual primitives.
s_delim(" ")
s_delim("/")
s_string("index.html")
s_delim(" ")
s_string("HTTP")
s_delim("/")
s_string("1")
s_delim(".")
s_string("1")
# end the request with the mandatory static sequence.
s_static("\r\n\r\n")
# close the open block, the name argument is optional here.
s_block_end("body")
该脚本由sulley组件的引入开始,接下来,初始化并一个请求,并为该请求命名为“HTTP BASIC”,该名称稍后被引用,来对request进行直接存取,
下一步,一个组被定义,名称为“verbs”,其可能的字符串值为"GET", "HEAD", "POST", "TRACE",一个名为body的block被定义,并使用group参数连接到
先前定义的group原语,注意,s_block_start总是返回True,这让你可以通过使用if语句来组织block的结构,让程序更具有可读性,注意,
s_block_end()的名称参数是可选的,一系列的基本原语和数据原语被限定在body 块中,当定义的请求被载入到一个sulley的会话中时,fuzzer将为
每一个定义在组中的值,生成并传输body块中的所有可能值。
Encoders
编码者是一个简单但非常有效的block修改器,在block将readered的内容返回并传输到物理线路之前,一个函数能被指定并附加到该block上来对该数据进行修改。
下面是一个xor加密器:
def trend_xor_encode (str):
key = 0xA8534344
ret = ""
# pad to 4 byte boundary.
pad = 4 - (len(str) % 4)
if pad == 4:
pad = 0
str += "\x00" * pad
while str:
dword = struct.unpack("<L", str[:4])[0]
str = str[4:]
dword ^= key
ret += struct.pack("<L", dword)
key = dword
return ret
encoder 直带一个单一参数,即,传入被编码的数据,返回编码后的数据,通过定义一个编码器并附加到一个可fuzzable的block上,允许fuzz的开发者
可以继续他的开发而不用担心这个小障碍。
依赖性
Dependencies 允许你在读取一个block的值时应用一个依赖条件 ,可在块的初始化时连接一个原语来完成依赖性,该原语将被使用dep关键字的block依赖,当sulley读取
依赖的块时,它将检查说连接的原始值,并以此指导自己的行为。只有一个依赖的值时,可以通过指定dep_value 关键字参数,如果有多个依赖的值,依赖的值列表能被
dep_values关键字参数指定。最后,实际的条件比较关系可以通过dep_compare关键字修改。考虑如下一种情景,依赖于一个整数的值,但不同的数据被期望。
s_short("opcode", full_range=True)
# opcode 10 expects an authentication sequence.
if s_block_start("auth", dep="opcode", dep_value=10):
s_string("USER")
s_delim(" ")
s_string("pedram")
s_static("\r\n")
s_string("PASS")
s_delim(" ")
s_delim("fuzzywuzzy")
s_block_end()
# opcodes 15 and 16 expect a single string hostname.
if s_block_start("hostname", dep="opcode", dep_values=[15, 16]):
s_string("pedram.openrce.org")
s_block_end()
# the rest of the opcodes take a string prefixed with two underscores.
if s_block_start("something", dep="opcode", dep_values=[10, 15, 16], dep_compare="!="):
s_static("__")
s_string("some string")
s_block_end()
块的依赖性可以通过不同的数量和方式连接在一起,而得到强有力的组合。
block helps:
在使用sulley生成数据时,一个重要的方面就是熟悉block helper的利用,包括,大小,校验和,和重复。
Sizers
SPIKE使用者将会非常熟悉s_sizer()或者s_size()块助手,辅助函数使用block的名称作为它的第一个参数来确定该block的大小,并可接受如下参数:
length: (integer, default=4) 大小域的长度
endian: (character, default='<') bit 位序 指定 '<' 为小端,'>'为大端
format: (string, default="binary") 输出格式, "binary" 或"ascii", 控制着整数原语读入的格式
inclusive: (boolean, default=False) sizer是否应该计算它自身的长度?
signed: (boolean, default=False) 使sizer成为有符号或无符号数, 仅当格式为ascii时适用
fuzzable: (boolean, default=False) 该原语可fuzzing 性的开关
name: (string, default=None) 可以通过在request中指定该名称来直接存储该原语的值
sizer是数据生成中重要的组件,允许我们表示诸如XDR符号,ASN.1等复杂的协议,但读取sizer的时候,sulley将动态计算关联块的长度,
默认情况下,sulley将不会fuzz Sizer域,在许多情况下,可能会有这种需求,如果遇到,enable fuzzable标志即可。
Checksums
和Sizers相似,s_checksum() 助手使用block的名称作为其第一个参数来确定该block的checksum,如下的可选参数可被指定
algorithm: (string or function pointer, default="crc32"). 应用于目标块的checksum的算法(crc32, adler32, md5, sha1)
endian: (character, default='<') bit 位序 指定 '<' 为小端,'>'为大端
length: (integer, default=0) 校验和的长度,指定为0则自动计算
name: (string, default=None) 可以通过在request中指定该名称来直接存储该原语的值
algorithm 参数能被指定为"crc32", "adler32", "md5" or "sha1"其中之一,或者,你可以指定一个函数指针,作为你自己定制的算法。
Repeaters
s_repeat()或者s_repeater()助手,被用于重复一个块为一个变量所代表的次数。这对于测试需要解析一个表为多个元素的溢出案例是非常有用的。
这个辅助程序需要携带三个必须参数,被重复的block的名称,最小重复次数和最大重复次数,此外,下面的可选参数可使用:
step: (integer, default=1) 最小,最大重复次数的步长
fuzzable: (boolean, default=False) 该原语可fuzzing 性的开关
name: (string, default=None) 可以通过在request中指定该名称来直接存储该原语的值
考虑下下面这个例子:
它连接了三个我们所介绍的辅助程序,我们fuzz的协议的一部分是一个包含字符串的表格。
每一个表格中的项由两个字节的字符串类型域,两个字节的长度域,一个字符串域,最后以一个需要接受整个字符串域的crc-32校验和结尾
我们并不知道对这个有效的类型域是什么类型,所有我们使用随机数据来对其进行fuzz,
使用suelly定义的这部分协议如下:
# table entry: [type][len][string][checksum]
if s_block_start("table entry"):
# we don't know what the valid types are, so we'll fill this in with random data.
s_random("\x00\x00", 2, 2)
# next, we insert a sizer of length 2 for the string field to follow.
s_size("string field", length=2)
# block helpers only apply to blocks, so encapsulate the string primitive in one.
if s_block_start("string field"):
# the default string will simply be a short sequence of C's.
s_string("C" * 10)
s_block_end()
# append the CRC-32 checksum of the string to the table entry.
s_checksum("string field")
s_block_end()
# repeat the table entry from 100 to 1,000 reps stepping 50 elements on each iteration.
s_repeat("table entry", min_reps=100, max_reps=1000, step=50)
这个sulley脚本不仅可以发现解析表项时的错误,而且可能发现在处理超长表时的错误。
Legos
sulley使用legos来代表用户定义的组件,诸如,e_mail地址,主机名,以及用于微软RPC,XDR,ASN.1等协议原语,在ASN.1中 /BER字符串被描述为一个系列[0x04][0x84][dword length][string
,在fuzzing 基于ASN.1的协议时,在每一个字符串前面都包括一个长度和类型的前缀,将变得相当难处理,作为代替,我们定义该lege来引用它。
s_lego("ber_string", "anonymous")
除了可选择的可选关键字参数外,每一个lego都有相似的格式,来指定一个独立的lego。作为一个例子,考虑到tag lego的定义,当fuzz xml-ish协议时
是非常有用的:
class tag (blocks.block):
def __init__ (self, name, request, value, options={}):
blocks.block.__init__(self, name, request, None, None, None, None)
self.value = value
self.options = options
if not self.value:
raise sex.error("MISSING LEGO.tag DEFAULT VALUE")
#
# [delim][string][delim]
self.push(primitives.delim("<"))
self.push(primitives.string(self.value))
self.push(primitives.delim(">"))
这个lego例子接受一个期望的tag作为字符串,并且将其压缩在适当的分割符内,如此做扩展了blocks类,通过self.push将合适的分割符和字符
串添加到block的栈中。
这里是另外一个为了生成ANS.1/DER 整数的简单lego,最小的共同点是代表的整数都是4字节整数,并有如下的形式,
[0x02][0x04][dword] 0x02 指定整数的类型,0x04指定整数的长度是4字节长,doword代表我们实际传入的值,如下是
在sulley中的定义:
class integer (blocks.block):
def __init__ (self, name, request, value, options={}):
blocks.block.__init__(self, name, request, None, None, None, None)
self.value = value
self.options = options
if not self.value:
raise sex.error("MISSING LEGO.ber_integer DEFAULT VALUE")
self.push(primitives.dword(self.value, endian=">")) def render (self):
# let the parent do the initial render.
blocks.block.render(self)
self.rendered = "\x02\x04" + self.rendered
return self.rendered
Sessions
一旦你定义了许多请求,就是时候将它们连接在一起形成一个会话了 。sulley 比其它好的fuzz框架最大的优势在于它对协议模糊测试的深度.
它将各个请求连接在一起形成图, from sulley import *
s_static("helo")
s_initialize("ehlo")
s_static("ehlo")
s_initialize("mail from")
s_static("mail from")
s_initialize("rcpt to")
s_static("rcpt to")
s_initialize("data")
s_static("data")
sess = sessions.session()
sess.connect(s_get("helo"))
sess.connect(s_get("ehlo"))
sess.connect(s_get("helo"), s_get("mail from"))
sess.connect(s_get("ehlo"), s_get("mail from"))
sess.connect(s_get("mail from"), s_get("rcpt to"))
sess.connect(s_get("rcpt to"), s_get("data"))
fh = open("session_test.udg", "w+")
fh.write(sess.render_graph_udraw())
fh.close()
在fuzz测试时,sulley沿着图形结构,从根节点开始fuzz测试每一个组件,在这个例子中,它从helo请求开始fuzzing,一旦完成,sulley将fuzz
mail from 请求,它在每一个测试案例前添加一个有效的helo请求,接下来,sulley将对rpct to 进行fuzzing,通过在每一个测试案例前添加一个
有效的helo和mail from 请求,这个过程一直到data请求完成后又从ehlo重新开始,这种将协议拆分为多个独立的请求,并fuzz了协议图的所有可能路径
的能力是非常强大的。
当实例化一个sesssion时,下面的可选参数可能被使用:
session_filename: (string, default=None) 用于存储系列化数据的文件名 指定一个文件名允许你停止以及恢复一个fuzzer的执行。
skip: (integer, default=0). 需要跳过的测试案例数
sleep_time: (float, default=1.0) 在发送测试案例时睡眠的时间值
log_level: (integer, default=2) 设置日子级别,级别越高,得到的日志信息就越多
proto: (string, default="tcp") 用于通信的协议
timeout: (float, default=5.0)设置等待 send /recv 返回之前的超时时间.
另外一个要介绍的sulley的高级特性是可以在协议图结构的每一边注册一个回调函数,这允许我们在两个节点间注册一个回调函数,来实现诸如
询问回答系统的功能,回调函数的原型如下:
def callback(node, edge, last_recv, sock)
node是将要被发送到node,edge是沿着当前路径到node的最后边,last_recv 包含从最后一次sock传输返回的数据,sock是一个可用的sock,
回调函数在如下的情景中是非常有用的,例如,下一个包的大小由上一个包指定,如果你需要动态填入目标IP地址,注册一个callback从sock.
getpeername()函数获取,边的回调函数可以在session.connect函数中又参数关键字 callback指定。
Target 和 Agents
下一步是定义目标,为它们添加代理并将其加入到会话中,下面这个例子初始化一个运行在vmware 中的目标,并为其连接了3个代理:
target = sessions.target("10.0.0.1", 5168)
target.netmon = pedrpc.client("10.0.0.1", 26001)
target.procmon = pedrpc.client("10.0.0.1", 26002)
target.vmcontrol = pedrpc.client("127.0.0.1", 26003)
target.procmon_options = \
{
"proc_name" : "SpntSvc.exe",
"stop_commands" : ['net stop "trend serverprotect"'],
"start_commands" : ['net start "trend serverprotect"'],
}
sess.add_target(target)
sess.fuzz()
实例化目标绑定在主机 10.0.0.1 ,端口 为 5168 .一个网络监控代理运行在目标系统上,监听的默认端口为26001,网络监控代理将记录所有的socket通信为独立的pcap包,
,使用测试案例号作为PCap文件标号。进程监控代理同样运行在目标系统上,监听默认端口26002.进程监控代理需要指定附加的进程名作为其参数,以及停止和打开该进程的
命令行语句。最后,VMware 代理运行在本地引擎,监听26003,目标被添加到session并开始fuzz测试,sulley有能力fuzzing多个目标,每一个目标都要有唯一的代理,
这可以将总测试空间划分在多个不同的目标上来为你节省时间。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)