cURL审计:一个玩笑如何导致重大发现
2022年秋季,Trail of Bits对cURL进行了安全审计。cURL是一个广泛使用的命令行实用程序,用于在服务器之间传输数据并支持各种协议。该项目恰逢Trail of Bits的制造周,这意味着我们拥有比平时更多的人力资源,使我们能够采用非标准方法进行审计。
在讨论应用程序的威胁模型时,我们的一位团队成员开玩笑地问:“我们试过curl AAAAAAAAAA…了吗?“虽然这个评论是开玩笑的,但它激发了一个想法:我们应该对cURL的命令行界面(CLI)进行模糊测试。一旦我们这样做了,模糊测试器很快就发现了内存损坏错误,特别是释放后使用问题、双重释放问题和内存泄漏。由于这些错误存在于libcurl(一个cURL开发库)中,它们有可能影响许多使用libcurl的软件应用程序。
与cURL合作
cURL由OSS-Fuzz项目持续进行模糊测试,其测试工具在单独的curl-fuzzer GitHub存储库中开发。当我查阅curl-fuzzer存储库以了解cURL模糊测试的当前状态时,我注意到cURL的命令行界面(CLI)参数没有被模糊测试。考虑到这一点,我决定专注于测试cURL对参数的处理。我使用AFL++模糊测试器(AFL的一个分支)为cURL的CLI生成大量随机输入数据。我在链接时使用无冲突检测和AddressSanitizer编译cURL,然后分析可能指示错误的崩溃。
cURL通过命令行参数获取其选项。由于cURL遵循C89标准,程序的main()函数可以定义为无参数或有两个参数(argc和argv)。argc参数表示传递给程序的命令行参数数量(包括程序名称)。argv参数是指向从命令行传递给程序的参数的指针数组。
标准还规定,在托管环境中,main()函数接受第三个参数char *envp[];该参数指向一个以null结尾的char指针数组,每个指针指向包含程序环境信息的字符串。
这三个参数可以有任何名称,因为它们在其声明的函数中是局部的。
cURL在curl/src/tool_main.c文件中的main()函数将命令行参数传递给operate()函数,该函数解析它们并设置cURL的全局配置。然后cURL使用该全局配置执行操作。
模糊测试argv
当我开始尝试模糊测试cURL时,我寻找一种使用AFL模糊测试其参数解析的方法。我的搜索引导我找到了AFL创建者(Michal Zalewski)的一句话:
“AFL不支持argv模糊测试,因为老实说,它在实践中并不是非常有用。如果你真的想要,experimental/argv_fuzzing/中有一个示例展示了如何在一般情况下做到这一点。”
我查看了那个实验性的AFL功能及其在AFL++中的等效功能。argv模糊测试功能使得可以模糊测试从CLI传递给程序的参数,而不是通过标准输入。当你想在模糊测试中覆盖库的多个API时,这很有用,因为你可以模糊测试使用该库的工具的参数,而不是为每个API编写多个模糊测试。
AFL++ argvfuzz功能如何工作?
argvfuzz的argv-fuzz-inl.h头文件定义了两个宏,它们从模糊测试器获取输入并设置argv和argc:
AFL_INIT_ARGV()宏使用从命令行传递给程序的参数初始化argv数组。然后它从标准输入读取参数并将它们放入argv数组中。数组以两个NULL字符终止,任何空参数都编码为单独的0x02字符。
AFL_INIT_SET0(_p)宏类似于AFL_INIT_ARGV(),但还将argv数组的第一个元素设置为传递给它的值。如果你想保留程序名称在argv数组中,这个宏很有用。
这两个宏都依赖于afl_init_argv()函数,该函数负责从标准输入读取命令行(通过使用unistd.h头文件中的read()函数)并将其拆分为参数。然后该函数将生成的字符串数组存储在静态缓冲区中,并返回指向该缓冲区的指针。它还将argc参数指向的值设置为读取的参数数量。
要使用argv-fuzz功能,你需要在包含main()函数的文件中包含argv-fuzz-inl.h头文件,并在main()的开头添加对AFL_INIT_ARGV或AFL_INIT_SET0的调用。
准备字典
模糊测试字典文件指定模糊测试引擎在测试期间应关注的数据元素。模糊测试引擎调整其变异策略,以便处理字典中的令牌。在cURL模糊测试的情况下,模糊测试字典可以帮助afl-fuzz更有效地生成包含选项(以一个或两个破折号开头)的有效测试用例。
为了模糊测试cURL,我使用了afl-clang-lto编译器的自动字典功能,该功能在编译目标二进制文件期间自动生成字典。该字典在启动时传输到afl-fuzz,提高了其覆盖率。我还基于cURL手册页准备了一个自定义字典,并通过-x参数将其传递给afl-fuzz。我使用以下Bash命令准备字典:
|
|
为cURL连接设置服务
最初,我的重点仅仅是CLI模糊测试。但是,我必须考虑到模糊测试器生成的每个有效cURL命令都可能导致连接到远程服务。为了避免连接到这些服务但保持测试负责处理连接的代码的能力,我使用netcat工具作为远程服务的模拟。首先,我配置我的机器将传出流量重定向到netcat的监听端口。
我使用以下命令在后台运行netcat:
|
|
参数指示服务应在端口80上监听传入连接(-l 80),在当前连接关闭后继续监听其他连接(-k),并在建立连接后立即终止连接(-w 0)。
cURL预计将使用各种主机名、IP地址和端口连接到服务。我需要将它们转发到一个地方:先前创建的TCP端口80。
为了将所有传出的TCP数据包重定向到本地环回地址(127.0.0.1)的端口80,我使用了以下iptables规则:
|
|
该命令在iptables的网络地址转换表中添加一个新条目。-p选项指定协议(在本例中为TCP),-j选项指定规则的目标(在本例中为REDIRECT)。–to-port选项指定数据包将被重定向到的端口(在本例中为80)。
为了确保所有域名都将解析为IP地址127.0.0.1,我使用了以下iptables规则:
|
|
此规则在NAT表中添加一个新条目,指定协议(-p)为UDP,目标端口(–dport)为53(DNS的默认端口),目标(-j)为目标NAT。–to-destination选项指定数据包将被重定向到的地址(在本例中为127.0.0.1)。
上述设置确保每个cURL连接都指向地址127.0.0.1:80。
结果分析
模糊测试过程在具有Intel Xeon Platinum 8280 CPU @ 2.70GHz的32核机器上运行了一个月。在此期间发现了以下错误,其中大部分在模糊测试的前几个小时内发现:
CVE-2022-42915(使用HTTP代理与特定协议时的双重释放)
使用cURL与代理连接以及dict、gopher、LDAP或telnet协议会由于错误/清理处理中的缺陷触发双重释放漏洞。此问题在cURL 7.86.0中修复。
要重现该错误,请使用以下命令:
|
|
CVE-2022-43552(当HTTP代理拒绝隧道SMB/TELNET协议时的释放后使用)
cURL可以通过HTTP代理虚拟隧道支持的协议。如果HTTP代理阻止SMB或TELNET协议,cURL可能在其传输关闭代码中使用已释放的结构。此问题在cURL 7.87.0中修复。
要重现该错误,请使用以下命令:
|
|
TOB-CURL-10(使用并行选项和序列时的释放后使用)
通过使用cURL与并行选项(-Z)、不匹配的括号和创建51个主机的两个连续序列,可以触发释放后使用漏洞。cURL为错误缓冲区分配内存块,默认允许最多50个传输。在负责处理错误的函数中,当连接失败时,错误被复制到适当的错误缓冲区,然后释放内存。对于最后一个(51)序列,分配内存缓冲区,释放,然后将错误复制到先前释放的内存缓冲区。此问题在cURL 7.86.0中修复。
要重现该错误,请使用以下命令:
|
|
TOB-CURL-11(未使用的内存块未释放,导致内存泄漏)
cURL分配的内存块在不再需要时未释放,导致内存泄漏。此问题在cURL 7.87.0中修复。
要重现该错误,请使用以下命令:
|
|
Dockerfile
如果你想了解设置模糊测试工具的全过程并立即开始模糊测试cURL的CLI参数,我们为你准备了一个Dockerfile:
|
|
使用以下命令运行此文件:
|
|
不开玩笑
总之,我们的方法表明,模糊测试CLI可以成为识别软件漏洞的有效补充技术。尽管最初持怀疑态度,但我们的结果产生了有价值的见解。我们相信这提高了基于CLI的工具的安全性,即使OSS-Fuzz已经使用了多年。
在cURL清理过程中找到基于堆的内存损坏漏洞是可能的。但是,释放后使用漏洞可能无法被利用,除非以适当的方式使用释放的数据并且控制数据内容。双重释放漏洞需要进一步分配类似大小的内存并控制存储的数据。此外,由于漏洞在libcurl中,它可能以各种方式影响许多使用libcurl的不同软件应用程序,例如发送多个请求或在单个进程内设置和清理库资源。
还值得注意的是,尽管CLI利用的攻击面相对有限,但如果受影响的工具是SUID二进制文件,利用可能导致权限升级(参见CVE-2021-3156:sudo中的基于堆的缓冲区溢出)。
为了在未来提高类似工具的模糊测试效率,我们通过合并持久模糊测试模式扩展了AFL++中的argv_fuzz功能。在此处了解更多信息。
最后,我们的cURL审计报告是公开的。查看审计报告和威胁模型。