The Advantages of Block-Based Protocol Analysis for Security Testing
Dave Aitel
Immunity,Inc
摘要
本文介绍了一种黑盒测试方法,它能有效应对未知的或任意复杂的网络协议,用于与程序或系统的安全性相关的常见问题。通过引入基于块的方法来利用网络协议中的所有已知因素,并且限定所有未知因素的影响,可以由测试者智能地减少对程序的输入的潜在空间,以弥补其在目标系统应用和设计上知识的不足。
简介
传统的黑盒网络协议测试通常是这样的,首先根据待测试网络协议构建一个粗略客户端,然后由测试人员以自己认为能够引起软件故障的方式手动排列组合这些协议。由于下列原因,这种测试方式的效果不尽如人意。
- 如果网络协议同时由服务端和客户端的API定义,或者测试人员为客户端提供源码,那么这些API和源码很可能会影响测试人员的判断,这会反过来影响开发人员的判断,从而造成测试中的差异
- 即使对协议了如指掌,创建一个客户端也是一项巨大的项目,并且这个客户端也很难移植到其他的协议中去,即使是性质相似的协议
- 大多数情况下,测试人员对被测协议及其崩溃方式了解不足
另外,在灰盒测试中,在程序受到网络输入的压力时,对其进行检测和数据流的分析很难扩展到任意复杂的系统或者映射到其他架构。实际上,为程序设置超过一个最小测试框架会通过引入延迟或者改变程序内部存储状态改变测试结果,从而掩藏漏洞。
为了解决这些缺点,笔者开发并测试了一个模拟网络协议客户端的框架,并根据该框架自动执行黑盒测试。该框架由C API和类C脚本语言实现,使用它可以利用任何现有的协议和类似协议进行黑盒测试。使用它可以手动或编程性地探索网络输入的潜在空间,这样测试人员可以更有效地发现诸如缓存溢出,整数溢出或者内存分配错误等软件漏洞。
需要扁平化协议栈来进行模糊处理
黑盒测试的历史上散布着大量的Perl脚本的例子,它们通过复制某种形式的网络流量,用较长的字符串替换短字符串以期使缓冲区溢出。然而,当要把它们扩展到更复杂的、包含多个层级应用协议栈时,每个层的长度变得相关联起来,这妨碍了对网络分组的任何改动。事实上,当前的网络协议确实依赖于其他的应用层协议,比如http。
任何协议都可以分解为长度字段和数据字段,但是,当测试人员开始构造测试脚本时,他们会发现在构建低层数据包之前,需要知道所有较高层协议的长度。如果不能做到这些,低层的协议将阻碍测试的进行。
例如,如果测试者想要向一个特定的web应用发送长字符串,通过用更长的字符串替换变量来简单地修改现有请求(POST)是不够的。测试者还必须更新位于HTTP头的内容长度字段。对于更复杂的协议,会有更多的长度字段需要同步更新,其中一些是基于封装协议的长度计算的。盲目模糊测试,即发送纯粹随机的无格式的数据进行测试很可能只会浪费时间。
然而,通过创建特定的程序函数来计算每个长度字段的任务是繁重的。况且,对每种协议创建函数或者面向对象式的描述不便于复用或者手动修改。因此需要一种独立于协议层次结构的框架,从而不必关心任何上层协议的组成和各部分的长度。对于手动修改来说,最好能把协议看作是简单的一串字符串,而不是层层嵌套的协议结构。在自动生成长度字段的时候把协议栈扁平化是作者的SPIKE框架的亮点。
创建SPIKE框架
基于块的协议建模,也就是SPIKE,所使用的数据结构是包含块大小信息和字节队列的简单结构列表。二进制数据和字节大小占位符都填充在队列上。当字节占位符填充好后,就会分配一个新的块结构,并赋予一个唯一的名字。例如下面这段简单的SPIKE脚本:
s_block_size_binary_bigendian_word("somepacketdata");
s_block_start("somepacketdata")
s_binary("01020304");
s_block_end("somepacketdata");
它先把4个空字节放到SPIKE的队列中,同时分配一个名为”somepacketdata”的块监听器,并把它的内部状态设置为大端序的字符串。然后这个脚本启动”somepacketdata”块,它会搜寻所有可用的监听块直到查找到该名字的块,并更新内部的”start”指针。然后4字节的数据(0x01020304)填充到这个块的队列上,最后该块结束。这时,所有监听器的长度就最终确定了,最初的4个空字节(一个大端序的字是4字节长)也填充了正确的值。
这个SPIKE脚本相当简单,但SPIKE具有的能够使诸如HTTP, NetBIOS等低层次协议独立于像XML-RPC, SMB等高层次协议的能力可见一斑。与通常的模糊测试平台不同,SPIKE脚本不需要为了确定字节大小而预先构建所有的上层协议。相反,它们在块结束的时候才自动计算长度。这避免了层与层内部相互依赖的关系。根据正在建模的网络协议,块与块之间能够以任何方式嵌套组合。
该技术不擅长拆分数据包,这些情况是特殊处理的。但即使在这样的情况下,比如典型的复杂RPC协议,SPIKE依然可以用来组建每个分片(比如Microsoft RPC中的PDU),并且在分片中封装任意数量的数据类型。
SPIKE的典型数据类型
因为大多数网络协议需要对相同类型的数据进行编组和解组,所以它们已经演变成大致相同的方式。比如字符串的格式,它通常由一个大端序字长、ASCII字符串、空字节、一些填充数据以使数据结构以4字节对齐。每个协议设计者通常会使用他们自己的代码去编译这些数据类型,但是使用SPIKE只需使用一种通用的函数调用,如果数据类型复杂的话,帽可以在内部使用基于块的代码生成需要的数据类型。
随着SPIKE逐渐成熟,编码例程的收集变得越来越完整。所有Microsoft的应用层协议在发送Unicode字符串的时候都使用相似的格式,所以一旦创建了有效的SMB数据包,创建的数据类型就可以在Microsoft的DCE-RPC应用层协议中工作正常。
当然,即使原始图元也是非常有用的,比如创建Intel字序或者大端序的半字长(不管是否是SPIKE主机的平台),比如理解不同格式的16进制数据(s_binary()能够正确解析诸如 “0x0001”, “0001”和 “00 01”,这使得测试人员只需从任何源码中复制粘贴即可)。
自动模糊测试
一旦协议用线性的状态来表述,它内部的某些部分就会标记为变量。通过简单地在函数名字后面加上 “_variable()”就可以做到这一点。比如:
s_string_variable("POST");
s_binary_bigendian_word_variable(0x01);
s_unistring_variable("\\PIPE\\");
上面的SPIKE脚本片断标记这些数据类型为变量。每一个SPIKE数据结构跟踪当前在使用的变量。在SPIKE上迭代的一个简单的循环允许它首先将POST改成一组长字符串,然后将0x01改变成一组已知会触发整数溢出的整数,然后将Unicode字符串转换成一组长字符串。每个“长”字符串取自已知会导致各种协议问题的全局字符串。当发现新的攻击类时,就把字符串添加到此全局列表中。
应该注意的是,size指令也可以用作整数变量。由于协议已被平坦化,存在通过正常变量和大小变量的简单线性级数。
可估量的结果
应该注意的是,SPIKE已经开发了一年多了,在这段时间里,已经发现了几十个新的漏洞,其中一些是DCE-RPC漏洞。之所以提起这些,是因为基于块的协议建模策略和模糊测试的功效是很难衡量的。在创建新的协议建模器的“易于使用”方面以及找到新的漏洞方面,它可能仅在主观上是可衡量的。可以采取哪些具体测量是在已知易受攻击的服务中发现远程漏洞的结果,但是漏洞的关键是未知的。然后,模糊框架的能力可以被测试为耗用的时间,而不是发现漏洞的能力。在许多情况下,会发现多个漏洞。
为了本文的目的,在SPIKE 2.8版中内置了一个名为“管道”的DCM-RPC,以尝试定位RPC Locator漏洞(在MS-03-01中描述)。SPIKE 2.7已经引入了一些DCERPC功能,从而将Netbios + SMB + DCE-RPC构建到SPIKE中得到了简化。将新协议构建到SPIKE中的过程通常包含有关可从通用协议解析器(如Ethereal)收集的协议的任何知识,在这种情况下,Netmon和Ethereal都对SMB和DCE-RPC有完全的描述。
然而,SPIKE的独特的块数据结构允许在一天内完成在Netbios堆栈上的SMB上构建相当完整的DCE-RPC,并在接下来的几天内进行调试。SPIKE的RPC模糊器现在包括(从2.8版本开始)SunRPC模糊器,DCE-RPC模糊器,以及一个名为管道模糊器的DCE-RPC。它们都以一种相似的模式工作:绑定到远程RPC服务并与之通信,将随机数的随机但有效的数字类型作为参数发送给函数调用。选择这些数据类型既通过远程端的任何解析程序,又尝试溢出函数处理程序。
例如,事后看来,Microsoft RPC Locator服务将正确解析以下SPIKE代码并溢出:
s_intelword(0x03); /* must be 0x03 */
s_intelword(rand()); /* can be anything */
//next a long string in the form of "/.../<string>/"
s_msunistring("/.../AAAAAAAAA......AAAA/");
for (i=0; i<13; i++)
s_intelword(0x00);
/* the final padding words can be anything, but zeros work best */
在Windows 2000或更高版本中运行DCE-RPC的大多数程序都会使用特定选项进行编译,该选项将拒绝任何解析错误的请求。这是首先通过解组测试,然后通过任何应用层测试,最后用长字符串到达易受攻击的代码。s_msunistring()自动处理Unicode字符串的四字节对齐。
SPIKE的DCE-RPC模糊器包括十五种数据类型,它们将作为参数随机附加。intel序的整数值1,2,3在随机变量切换语句中都有它们自己的插槽,和一个,两个和三个零的单词一样。s_msunistring(s_get_random_fuzzstring());也用于从整个全局fuzz字符串集中选择一个随机字符串,然后将其作为Microsoft Unicode字符串类型放入数据。如果这个字符串是正确的格式(“/…/AAAAAA”),所有其他变量是正确的,并且没有额外的变量,那么Locator服务将处理它并溢出。要想找到合适格式的数据触发溢出,需要阅读任何与被测服务相关的文档,并从该文档添加任何有趣的字符串到全局fuzz字符串列表。字符串“/…/”和“/.:/”通常存在于与Locator服务相关的文档和示例中。有关给定RPC服务的功能的其他信息,通常只需要查看函数的定义。在Locator服务的情况下,RpcNsStringBindingLookupBegin()函数带入了该漏洞需要解析的部分参数。
SPIKE全局fuzz串集中有580个不同的字符串。平均来说,每170次测试中有一次,字符串被实际处理,所以平均需要98600个测试(或1.5个小时),在事先不知道函数参数的情况下(除了“/…/AA …“或”/.:/ A …“可能会导致溢出),以查找Locator溢出。事实上,SPIKE在Locator服务中使用这种方法成功地发现了两个类似的漏洞,并且还没有报告其他的访问冲突错误。
通过手动编码RPC函数的有效参数,可以大大减少尝试通过解析测试的时间。可以使用标准的反向工程来查找有效的参数,检查应用程序抛出的异常(0x6F7是抛出在无效编组的RPC数据包上)还是使用Muddle,这将处理RPC服务的可执行文件以返回一个IDL文件。但在某些情况下,Muddle对RPC服务不起作用,盲目的方法比冗长的逆向工程流程更为必要或更有利。
发现的其他漏洞
在为SPIKE 2.8命名管道模糊器构建DCE-RPC的过程中,作者在Windows 2000中遇到了几个bug。第一个错误是偶尔发生,当承受压力时,DCE-RPC堆栈会将无效接口或接口版本的调用转发到监听服务。这样服务就会因NULL引用崩溃。当服务是必需的系统服务(如lsass.exe)时,系统将重新启动。这也被报告为在许多情况下作为Windows 2000的本地根漏洞。当系统服务(如lsass.exe)崩溃时,本地攻击者可以创建的孤立命名管道,进而可以由特权进程连接到ImpersonateNamedPipeClient()导致root损坏。尽管这还没有人研究,但使用这种技术来绕过验证或以其他方式利用系统是可能的。
此外,当Windows 2000内核接收到Netbios连续数据包时,会发现严重的内核内存泄漏。这会导致针对目标机器的远程拒绝服务。虽然机器本身并没有完全用尽非分页池,但是它确实停止了对SMB请求的服务,这些请求有致命的禁用COM服务的作用。
总结
虽然静态分析或白箱分析是近期安全研究的主要内容,但黑盒测试仍然产生了大部分可利用的现实世界漏洞。对于给定的示例安全审计,SPIKE的基于块的协议建模和模糊策略已被证明可以在合理的时间内以及合理先验知识储备的情况下有效地发现可利用的漏洞。软件供应商和独立安全咨询公司明智的做法是扩展SPIKE或为已有安全隐患的暴露网络协议编写定制的SPIKE脚本。对于时间短的投资,黑盒测试,特别是SPIKE,一直表现出自己的高回报率。
参考资料
英文原文位于项目documentation目录内,以上翻译为个人兴趣,如有不妥,请下方留言不吝赐教。