如何将高级模糊测试技术应用于cURL
2022年底,Trail of Bits受开源技术改进基金(OSTIF)委托,对cURL文件传输命令行工具及其库libcurl进行安全评估。我们的工作范围包括代码审查、威胁建模,以及本文重点介绍的工程努力:分析和改进cURL的模糊测试代码。
我们将讨论此过程的多个方面,包括如何识别代码库中缺乏覆盖的重要区域,然后修改模糊测试代码以覆盖这些遗漏区域。例如,通过在模糊测试器初始化期间设置某些libcurl选项并引入新的种子文件,我们将HTTP严格传输安全(HSTS)处理代码的行覆盖率翻倍,并将Alt-Svc头的行覆盖率增加了五倍。我们还扩展了模糊测试的协议集,包括WebSocket,并启用许多新libcurl选项的模糊测试。最后,我们将解释cURL团队可以采用的一些更复杂的模糊测试技术,以进一步提高覆盖率,将模糊测试扩展到cURL命令行,并减少当前测试用例格式固有的低效性。
cURL如何进行模糊测试?
OSS-Fuzz是Google为开源项目提供的免费服务,作为cURL的持续模糊测试基础设施。它支持C/C++、Rust、Go、Python和Java代码库,并使用覆盖引导的libFuzzer、AFL++和Honggfuzz模糊测试引擎。OSS-Fuzz于2017年7月1日采纳cURL,相关代码位于GitHub上的curl-fuzzer仓库,这是我们这部分工作的重点。
该仓库包含模糊测试cURL和libcurl所需的代码(设置脚本、测试用例生成器、测试工具等)和语料库(初始测试用例集)。它设计用于模糊测试单个目标,即libcurl支持的协议,如HTTP(S)、WebSocket和FTP。curl-fuzzer下载最新版本的cURL及其依赖项,编译它们,并针对这些目标构建二进制文件。
每个目标接受一个特殊结构的输入文件,使用适当的libcurl调用处理它,然后退出。每个目标关联一个语料库目录,包含用于模糊测试协议的有趣种子文件。这些文件使用自定义类型-长度-值(TLV)格式结构,不仅编码原始协议数据,还编码协议的特定字段和元数据。例如,HTTP协议的模糊测试器包括协议版本、自定义头以及libcurl是否应跟随重定向的选项。
第一印象:HSTS和Alt-Svc
我们的任务是分析和改进模糊测试器对libcurl的覆盖率,该库提供curl的内部功能。我们首先想到的问题是:当前的覆盖率情况如何?为了回答这个问题,我们想查看OSS-Fuzz定期生成的最新覆盖率数据。在查看了公开可访问的oss-fuzz-coverage Google Cloud Storage存储桶的URL后,我们找到了cURL的覆盖率报告(将来参考,您可以通过OSS-Fuzz内省页面到达)。这是2022年9月28日的一份报告,在我们合作开始时。
阅读报告后,我们很快注意到几个源文件几乎没有任何覆盖率,包括一些实现安全功能或负责处理不可信数据的文件。例如,hsts.c提供解析和处理Strict-Transport-Security响应头的函数,在OSS-Fuzz上运行超过五年后,仅有4.46%的行覆盖率、18.75%的函数覆盖率和2.56%的区域覆盖率:
负责处理Alt-Svc响应头的文件altsvc.c同样覆盖率不足:
对模糊测试代码的调查揭示了这些数字如此低的原因。第一个问题是语料库目录缺少包含Strict-Transport-Security和Alt-Svc头的测试用例,这意味着模糊测试器无法快速跳转测试这些代码区域中的错误;它必须使用覆盖率反馈自行构建这些测试用例,这通常是一个较慢的过程。
第二个问题是模糊测试器从未设置CURLOPT_HSTS选项,该选项指示libcurl使用HSTS缓存文件。因此,在模糊测试器运行期间从未启用HSTS,hsts.c中的大多数代码路径从未被命中。
实现HSTS良好覆盖的最终障碍是其规范中的一个问题,该规范告诉用户代理在通过未加密的HTTP发送时忽略Strict-Transport-Security头。然而,这在模糊测试的上下文中产生了一个问题:从我们的模糊测试目标的角度来看,它从未建立实际的TLS连接,每个连接都是未加密的,Strict-Transport-Security总是被忽略。对于Alt-Svc,libcurl已经包含一个变通方法,当设置某个环境变量时,为调试构建放松HTTPS要求(尽管curl-fuzzer未设置此变量)。因此,解决此问题只需为HSTS向libcurl添加类似功能,并确保curl-fuzzer设置所有必要的环境变量。
我们为解决这些问题所做的更改如下:
- 我们向curl-fuzzer添加了Strict-Transport-Security和Alt-Svc的种子文件(ee7fad2)。
- 我们在curl-fuzzer中启用了CURLOPT_HSTS(0dc42e4)。
- 我们添加了一个检查,允许libcurl的调试构建在设置CURL_HSTS_HTTP环境变量时绕过HSTS的HTTPS限制,并在curl-fuzzer中设置CURL_HSTS_HTTP和CURL_ALTSVC_HTTP环境变量(6efb6b1和937597c)。
我们的更改合并到上游后的第二天,OSS-Fuzz报告了两个文件的覆盖率显著提升:
一年多后(2024年1月29日),我们的三个修复使hsts.c的行覆盖率翻倍,altsvc.c的行覆盖率几乎增加了五倍:
播种错误
进一步探索curl-fuzzer,我们看到了许多其他提高覆盖率的机会。我们发现的一个低 hanging fruit是语料库目录中的种子文件集。虽然libcurl支持众多协议(其中一些让我们惊讶!)和功能,但并非所有协议都在语料库中作为种子文件表示。这很重要:正如我们之前提到的,一套全面的初始测试用例,覆盖尽可能多的主要功能,是获得覆盖率的捷径,并显著减少发现错误前花费的模糊测试时间。
我们创建新种子文件的功能,希望促进新覆盖率,包括(ee7fad2):
- CURLOPT_LOGIN_OPTIONS:为IMAP、LDAP、POP3和SMTP设置协议特定的登录选项
- CURLOPT_XOAUTH2_BEARER:指定用于HTTP、IMAP、LDAP、POP3和SMTP服务器的OAuth 2.0 Bearer访问令牌
- CURLOPT_USERPWD:指定用于身份验证的用户名和密码
- CURLOPT_USERAGENT:指定User-Agent头的值
- CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256:设置SSH连接远程服务器的预期SHA256哈希
- CURLOPT_HTTPPOST:设置POST请求数据。curl-fuzzer之前仅使用CURLOPT_MIMEPOST选项实现此功能,而类似但已弃用的CURLOPT_HTTPPOST选项未被使用。我们还添加了对这种旧方法的支持。
某些其他CURLOPTs,如上一节中的CURLOPT_HSTS,在模糊测试器的初始化函数中全局设置更有意义。这些包括:
- CURLOPT_COOKIEFILE:指向从中读取cookie的文件名。它还启用cookie引擎的模糊测试,该引擎从响应中解析cookie并将其包含在未来的请求中。
- CURLOPT_COOKIEJAR:允许模糊测试负责将内存中cookie保存到文件的代码
- CURLOPT_CRLFILE:指定为TLS连接读取的证书吊销列表文件
未来方向
当我们开始更多了解curl-fuzzer的内部结构时,我们制定了几个战略建议,以提高模糊测试器的效能,但我们合作的时间线不允许我们自行实施。我们在最终报告中向cURL团队提出了这些建议,并在下面扩展了其中几个。
字典
字典是libFuzzer的一个功能,对于libcurl所说的基于文本的协议尤其有用。协议的字典是一个文件,枚举在协议上下文中有趣的字符串,如关键字、分隔符和转义字符。向libFuzzer提供字典可能会提高其搜索速度,并导致更快发现新错误。
curl-fuzzer已经为HTTP目标利用此功能,但目前未为libcurl支持的众多其他协议提供字典。我们建议cURL团队为这些协议创建字典,以提高模糊测试器的速度。这可能是一个LLM的好用例;ChatGPT可以生成一个起点字典,以响应以下提示(将
字典可用于引导模糊测试器。字典作为文件传递给模糊测试器。libFuzzer接受的最简单输入是ASCII文本文件,其中每行包含一个带引号的字符串。字符串可以包含转义字节序列,如"\xF7\xF8"。可选地,可以使用键值对,如hex_value="\xF7\xF8"用于文档目的。注释通过以#开头的行支持。为我写一个
argv模糊测试
在我们第一次与curl合作时,我们中的一个人开玩笑说:“我们试过curl AAAAAAAAAA…了吗?”这句话背后有很多智慧;它促使我们模糊测试curl的命令行界面(CLI),这产生了多个漏洞(参见我们的博客文章,cURL审计:一个笑话如何导致重要发现)。
此CLI模糊测试是使用AFL++的argv-fuzz-inl.h头文件执行的。该头定义了宏,允许目标程序从模糊测试器在标准输入上提供的数据构建包含命令行参数的argv数组。我们建议cURL团队使用AFL++的此功能持续模糊测试cURL的CLI(实现细节可以在上面链接的博客文章中找到)。
结构感知模糊测试
curl-fuzzer的一个弱点固有于其当前构建输入的方式,即使用自定义类型-长度-值(TLV)格式。TLV方案(或类似方案)对于模糊测试像libcurl这样的项目可能有用,它支持大量全局和协议特定的选项和参数,需要编码在测试用例中。
然而,这种二进制格式的脆弱性使模糊测试器低效。这是因为libFuzzer不知道输入应该遵守的结构。curl-fuzzer期望输入数据采用严格格式:一个2字节的记录类型字段(在我们合作时只有52个有效)、一个4字节的数据长度字段,最后是数据本身。因为libFuzzer不考虑此格式,它生成的大多数变异在TLV解包阶段无效,必须被丢弃。Google的模糊测试指南因此警告使用TLV输入。
结果,用于引导变异到有趣代码路径的覆盖率反馈性能比我们仅处理原始数据时差得多。事实上,libcurl可能包含使用当前朴素TLV策略永远不会发现的错误。
那么,cURL团队如何在保持TLV格式灵活性的同时解决此问题?进入结构感知模糊测试。
结构感知模糊测试的想法是通过编写自定义变异器来协助libFuzzer。在高层次上,自定义变异器的工作仅包括三个步骤:
- 尝试将来自libFuzzer的输入数据解包为TLV。
- 如果数据无法解析为有效TLV,而不是丢弃它,返回一个语法正确的虚拟TLV。这可以是任何东西,只要它可以成功解包。
- 如果数据构成有效TLV,通过调用LLVMFuzzerMutate函数变异在步骤1中解析出的字段。然后,序列化变异后的字段并返回结果TLV。
通过这种方法,不会浪费任何时间丢弃输入,因为每个输入都有效;变异器只创建正确结构的TLV。在解码数据级别(而不是编码方案级别)执行变异允许更好的覆盖率反馈,这导致更快更有效的模糊测试器。
curl-fuzzer上的一个开放问题提出了几个更改,包括结构感知模糊测试的实现,但自2019年以来没有任何进展。我们强烈建议cURL团队重新审视此主题,因为它有可能显著提高模糊测试器发现错误的能力。
我们的2023年后续工作
2023年底,我们有机会在OSTIF支持的另一次审计中重新访问cURL及其模糊测试代码。请继续关注我们后续工作的亮点在未来的博客文章中。
如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News