导语:最近 ,我花费了一些时间去了解并研究了libFuzzer以及其技术。目前针对其我已经提出了一些相关补充,我预计这将大大提升我找到案例的时间。
最近 ,我花费了一些时间去了解并研究了libFuzzer以及其技术。目前针对其我已经提出了一些相关补充,我预计这将大大提升我找到案例的时间。
首先我对MichałZalewski和libFuzzer背后的研究人员以及各种对Sanitizers 所做的工作表示赞赏。模拟器可以附加到任意软件从而能够提升发现影响数百万设备的世界级漏洞的方便性,这一点在我看来应该与技术基础一样值得称赞。毫无疑问这是一项站在巨人肩膀上的工作。
你可以在这里找到我的fuzzer:https://github.com/guidovranken/libfuzzer-gv
请记住,这些功能都是非常具有实验性质的。当然我非常鼓励libFuzzer和其他fuzzer的开发人员将这些功能合并到他们的工作中,如果他们喜欢这些的话。
代码覆盖只是改变fuzzer的一种方式
代码覆盖率是像libFuzzer这样的fuzzing程序用来增加找到导致错误的代码路径的可能性的主要指标。但代码执行的确切过程由许多因素决定。这些因素并不仅仅由代码覆盖率指标来解释。所以我实现了一些额外的程序状态信号器,可以帮助快速发现错误的代码。没有这些的话就只有在很长时间的fuzzing之后才会发现某些错误。
在输入中给出足够的0xAA,由于堆栈溢出(递归太深),程序将崩溃。使用-stack_depth_guided = 1 -use_value_profile = 1,我的系统通常需要大约0.5 – 5秒钟才能崩溃。
使用-value_profile = 1(和ASAN_OPTIONS = coverage = 1:coverage_counters = 1),大约需要5-10分钟。我认为这是纯粹的机会。我已经完成了一个小时了,而它仍然在忙碌的运行。
(是的,这个具体的实现只适用于x86-64,如果它不适合你,请注释或修改它以适应你的架构。)
如果您需要一个超过某一堆栈深度的fuzzer输入作为文件,则可以在运行fuzzer之前,使用ulimit -s来降低堆栈大小。它会崩溃并且libFuzzer会将fuzzer输入写入磁盘。
我认为由于过度递归而导致的崩溃是一种不足的漏洞。对于服务器应用程序而言,不可信任的客户端可以在服务器上执行堆栈溢出这一点很重要。这一类的漏洞相对较少,但是我确实设法在一个知名度较高的软件(Apache httpd CVE-2015-0228)中找到一个远程的,未经身份验证的crasher。
有很多解析与上下文无关语法的应用程序,比如编程语言(一个表达式可以包含一个表达式,该表达式再包含一个表达式..)、序列化格式(JSON:数组可以包含一个数组,该数组再包含一个数组..)
从理论上来讲这是容易的。
PS:您可以使用我的工具来查找二进制文件中的调用图循环。
改变fuzzing的强度
此功能可量化在单次运行中击中检测位置的数量。访问非唯一位置的总和。因此,如果1次迭代的某个循环导致覆盖回调被调用5次,则5次迭代的相同循环导致总和值为5 * 5 = 25。
这样就能很好的去找慢输入了。
改变fuzzing的分配
在输入中给出足够的0xAA,程序将执行-1字节的分配。AddressSanitizer完全不能容忍这一点,所以这里会导致它崩溃。
使用-alloc_guided = 1 -value_profile = 1,通常我的系统需要10-25秒,直到它崩溃为止(这是我们想要的)。
只要-value_profile = 1(和ASAN_OPTIONS = coverage = 1:coverage_counters = 1),它仍然会运行一个多小时。但它几乎很少可以继续下去,并且它无法弄清楚逻辑。
我比较期望这个功能可以有助于找到某些阈值限制的问题。例如,一个应用程序如果涉及的东西少于8192个元素,那么它会运行正常。而一旦超出该阈值,它就会采用不同的错误逻辑(可能是使用realloc())。该功能可以引导fuzzers指向该核心。
除了发现崩溃之外,该功能还能够深入了解应用程序的最高内存使用情况,并且根据堆使用情况自动找到最坏情况的输入(因为fuzzing由malloc()引导)。如果您发现一个输入可以导致服务器应用程序预留50MB内存,而正常请求的平均内存使用量为100KB,那么这个在传统意义上可能不是漏洞的情况就会变成一个漏洞(尽管这可能是一个非常便宜的DoS机会),但它可能会使你考虑重构一些代码。
改变fuzzing的惯例
libFuzzer希望LLVMFuzzerTestOneInput返回0,而如果返回其他内容,它就会停止下来。目前这一点还没有被用于其他任何事情。所以我认为这里我很好的通过使用-custom_guided = 1来利用了它。
现在你可以连接libFuzzer到任何东西了。我正在尝试连接到LLVMFuzzerTestOneInput中的一个删除服务器,哈希这个服务器返回的内容,并返回到目前为止生成的唯一哈希数。所以我实际上是fuzzing了一个远程的,未测量的应用程序。
引导fuzzing禁用覆盖
使用-no_coverage_guided = 1来引导fuzzing禁用覆盖。这一点对于如果您想纯粹依赖分配指导来说,会很有用。
技术的尝试与抛却
我试着用一个柱状图来展示mutator的功效。所以每当某个mutator(如EraseBytes,InsertBytes,…)负责增加代码覆盖率时,我会增加其柱状图的值。然后,当需要选择下一个迭代的mutator时,我总是喜欢选择最有效的mutator(但是也可以选择较低效率的mutator,只有较小的可能性)。
在对类解释时,我创建了一个lin-log查找表。对于5个变体,它看起来像这样:
每次迭代,我都会对柱状图进行排序并保存索引的顺序。所以柱状图如下所示:
然后,(反向)排序的索引序列为:
选择一个新的mutator:
所以目前mutator 3是非常受青睐的(3/3的机会),但是仍然有1/5的机会选择mutator 4。
不幸的是,这个努力是徒劳的,这似乎只是减缓了fuzzing而已。显然,fuzzers需要进行不断的开发多样性才能达到新的覆盖范围。也或者是我一直在忽视某些东西,所以在你可以自由发表评论来和我进行探讨。
我认为,嵌入堆栈深度指导和代码强度指导的方法是在一次运行中保留应用程序命中的代码位置数组,散列数组,并使用唯一散列数作为指导。不幸的是,这个数字对于几乎每一个输入都是递增的,并且很快内存就会耗尽。也许一个不那么细致的覆盖工具可以进行此工作。
fuzzing的秘诀
Sanitizers 和fuzzers是不同的技术。您可以在没有Sanitizers 的情况下进行fuzzing(并且无需fuzzingSanitiz):将语料库生成加速一个数量级 – >然后用Sanitizers 测试语料库。
开发人员:您可以使用fuzzing来验证应用程序逻辑。当你认为绝对不会失败的assert() 失败时,在通常打印调试消息的地方放入一个abort(),好了现在开始fuzzing。
有时优化和编译器版本很重要。gcc + ASAN使用-O0检测以下程序中的问题,但不能使用-O1及更高版本:int main(){char * b; int l = strlen(b);}。clang没有找到任何优化标志。相反(与-O3崩溃,不与-O1崩溃)也可能发生。另外,依赖于特定的编译器版本和标志的安全性可能是向开放源代码软件提供后门代码的好方法,因此如果我是一个坏人的话,我想我可能会这么做。而维护者用自己的 clang -O2构建系统+回归测试+fuzzing钻机测试您的代码仍然可能不会检测到您的恶意代码隐藏在明显的地方,但尽管如此它仍然会被衍生进入一定百分比的二进制文件。