作者:cq674350529
本文首发于信安之路,原文链接:https://mp.weixin.qq.com/s/5gwJpqj7ysue19OcoPI16A
通常,在对IoT设备的固件进行分析时,固件中与提供服务如HTTP
、Telnet
、RTSP
、UPnP
等相关的二进制程序是重点分析的对象。因为一旦在这些程序中发现漏洞,其很有可能会被远程利用,进而带来严重的安全隐患。
对固件二进制程序进行分析,常见的分析方法包括模糊测试、补丁比对、工具静态扫描和人工审计等。其中,模糊测试方法具备简单易用的特点,通常也比较有效,其在业界已被广泛使用。
下面,以某型号路由器为例,基于Boofuzz
框架,介绍对常见网络协议进行fuzz
的方法。
模糊测试采用黑盒测试的思想,通过构造大量的畸形数据作为应用程序的输入,来发现程序中可能存在的安全缺陷或漏洞。
模糊测试方法的分类有很多。根据测试用例生成方式的不同,可以分为基于变异的模糊测试和基于生成的模糊测试。根据对目标程序的理解程度,可分为黑盒模糊测试、灰盒模糊测试和白盒模糊测试。常见工具与方法的对应关系如下。
针对IoT设备,由于其资源受限和环境受限等特点,实际中常采用黑盒模糊测试的方式。在对网络协议进行测试时,可以将常见的网络协议分为两类:一类属于文本协议,如HTTP、FTP等,这类协议的特点是其数据包内容都是可见字符;另一类为二进制协议,其特点是数据包内容大部分是不可见字符,这类协议在工控设备如PLC中比较常见,通常属于私有协议。针对文本协议,笔者常采用Sulley框架进行测试;而针对二进制协议,则常采用kitty框架进行测试。
另外,在对IoT设备进行模糊测试时,需要考虑如何对设备进行监控,以判断是否出现异常。最简单的方式通过设备服务的可用性进行判断,如果设备提供的服务不可访问,表明设备可能崩溃了。但这种监控方式粒度比较粗,容易漏掉一些异常行为。另外,当设备出现异常后,还需要对环境进行恢复,以便继续进行测试。常见的方式就是重启设备。现在很多设备崩溃之后都会自动重启,如果测试目标设备没用提供这种机制,则需要采用其他方式解决。
由于Sulley框架目前已经停止更新维护,而Boofuzz框架是Sulley的继承者,除了修复一些bug之外,还增加了框架的可扩展性。下面对Boofuzz框架进行简单介绍。
由上图可知,该框架主要包含四个部分:
数据生成:根据协议格式利用原语来构造请求
会话管理/驱动:将请求以图的形式链接起来形成会话,同时管理待测目标、代理、请求,还提供一个web界面用于监视和控制
代理:与目标进行交互以实现日志记录、对网络流量进行监控等
通常,代理是运行在目标设备上。但是,对于IoT设备而言,大部分情况下都无法在目标设备上运行代理程序。
实用工具:独立的命令行工具,完成一些其他的功能
其中,数据生成和会话管理/驱动是比较重要的2个模块。对于数据生成模块,Boofuzz框架提供了很多原语来定义请求,如最基础的s_string()
、s_byte()
、s_static()
等。对于会话管理/驱动模块,其思想体现在下图中。
在上图中,节点ehlo
、helo
、mail from
、rcpt to
、data
表示5个请求,路径'ehlo'->'mail form'->'rcpt to'->'data'和'helo'->'mail from'->'rcpt to'->data'
体现了请求之间的先后顺序关系。callback_one()
和callback_two()
表示回调函数,当从节点echo移动到节点mail from时会触发该回调函数,利用这一机制,节点mail from可以获取节点ehlo中的一些信息。而pre_send()
和post_send()
则负责测试前的一些预处理工作和测试后的一些清理工作。
理解了这几个模块的功能后,使用该框架进行测试的主要步骤如下:
以某型号路由器为例,由于路由器上HTTP服务是最为常见的,故以http协议为例进行介绍。
首先,需要尽可能多地与设备进行交互,然后捕获相应的http请求数据包,如下。
以登录请求为例,对应的http请求报文示例如下。
POST /HNAP1/ HTTP/1.1
Connection: keep-alive
Content-Length: 400
HNAP_AUTH: E889FD5249E5D51C6C9424283DE3B5DB 1553349899
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36
Content-Type: text/xml; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
SOAPAction: "http://purenetworks.com/HNAP1/Login"
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login xmlns="http://purenetworks.com/HNAP1/"><Action>request</Action><Username>xxx</Username><LoginPassword>xxx</LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>
利用该框架中提供的原语对http请求进行定义,部分示例如下。
s_initialize('login') # 整个请求的名称
# 对应 POST /HNAP1/ HTTP/1.1
s_string('POST', name='method') # 对该字段进行fuzz
s_static(' ')
s_static('/HNAP1/', name='url') # 不对该字段进行fuzz
s_static(' ')
s_static('HTTP/1.1')
s_static('\r\n')
# 对应 Content-Length: 400
s_static('Content-Length')
s_static(':')
s_size('data', output_format='ascii', fuzzable=True) # size的值根据data部分的长度自动进行计算,同时对该字段进行fuzz
s_static('\r\n')
# 对应http请求数据
with s_block('data'):
s_string('<?xml version="1.0" encoding="utf-8"?>')
s_static('<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">')
s_static('<soap:Body>')
s_static('<Login xmlns="http://purenetworks.com/HNAP1/">')
s_static('<Action>')
s_string('login', max_len=1024) # 字段变异后的最大长度为1024
s_static('</Action>')
# 省略部分内容
s_static('</soap:Envelope>')
是否对某个字段进行fuzz需根据具体情况确定。对所有字段都fuzz,生成的畸形数据包会非常多,测试所耗费的时间比较长,但发现问题的可能性比较大;只对少部分字段进行fuzz,生成的畸形数据包会比较少,测试所耗费的时间更短,同时发现问题的可能性也比较小。
<?xml version="1.0" encoding="utf-8"?>
进行变异,是将其当作一个整体,还是拆分为更小的单元?至于具体怎么对某个字段进行变异,如针对字符串的变异,该框架内已包含一些规则。当然,也可以自己增加规则。
类似的,对网络数据包中的其他http接口请求进行同样的定义。
根据捕获的数据包定义完请求后,设置与会话相关的信息,包括目标设备的ip地址、端口等。
host = '192.168.2.1'
port = 80
# 其他参数可以按需设置,比如添加fuzz_loggers来保存测试用例和结果等
session = Session(session_filename='http_session', receive_data_after_fuzz=True, ignore_connection_reset=True, restart_sleep_time=10)
target = Target(
connection=SocketConnection(host, port, proto='tcp'),
netmon=Remote_NetworkMonitor(host, port, proto='tcp')) # 服务可用性监控
session.add_target(target)
然后将之前定义的请求按照一定的先后顺序链接起来,部分示例如下。
session.connect(s_get('login')) # 默认前置节点为root
session.connect(s_get('login'), s_get('setsysemailsettings'), callback=add_auth_callback)
session.connect(s_get('login'),s_get('setsyslogsettings'), callback=add_auth_callback)
session.connect(s_get('login'),s_get('setschedulesettings'), callback=add_auth_callback)
其中,由于setsysemailsettings
、setsyslogsettings
、setschedulesettings
等请求需要在登录之后才可以正常使用,所以需要在login请求之后发生。而setsysemailsettings
、setsyslogsettings
和setschedulesettings
这几个请求之间则没有明确的先后关系。add_auth_callback
为自定义的回调函数,主要用于从login请求中获取用于登录认证的信息如cookie,然后将其设置于setsysemailsettings
、setsyslogsettings
、setschedulesettings
等请求中。
这里通过设备HTTP服务的可用性来判断目标设备是否发生异常。如果HTTP服务无法访问,说明设备可能崩溃了。前面设置的Remote_NetworkMonitor()
就是用于对服务的可用性进行监测,其核心代码如下。
# 通过TCP全连接来判断目标端口是否在监听
if self.proto == "tcp" or self.proto == "ssl":
try:
self._sock.connect((self.host, self.port))
self.alive_flag = 1
except socket.error as e:
self.alive_flag = 0
Remote_NetworkMonitor()
为自行添加的代码,不属于Boofuzz
框架。前面也提到过,该监测方式的粒度比较粗,可能会存在漏报,可以采用或结合一些其他的方式进行改进。
至于对环境进行恢复,由于该设备崩溃后会自行重启,所以无须额外的操作,只需调用sleep()
等待设备重启后即可。
最后调用session.fuzz()
驱动整个过程,然后运行脚本即可。默认情况下,会在26000端口开启一个web服务,用于控制或查看测试的进度及相关信息等。在测试完成后,可以通过查看测试记录,看是否有测试用例造成目标设备出现异常,以进行进一步分析。
本文以IoT设备为例,对模糊测试框架Boofuzz
,以及利用该框架对网络协议进行fuzz
的基本流程进行了简要介绍。如果想要获得更好的效果,还需要对其中的细节进行进一步的优化与完善。
boofuzz: Network Protocol Fuzzing for Humans
Fuzzing Sucks! Introducing Sulley Fuzzing Framework
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1626/