cURL安全审计:从玩笑到重大漏洞发现的奇妙之旅

本文详细介绍了Trail of Bits团队如何通过模糊测试cURL命令行界面发现多个高危漏洞,包括双重释放、释放后使用和内存泄漏问题,并分享了完整的技术实现方案和Docker复现环境。

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命令准备字典:

1
$ man curl | grep -oP '^\s*(--|-)\K\S+' | sed 's/[,.]$//' | sed 's/^/"&/; s/$/&"/'  | sort -u > curl.dict

为cURL连接设置服务

最初,我的重点仅仅是CLI模糊测试。但是,我必须考虑到模糊测试器生成的每个有效cURL命令都可能导致连接到远程服务。为了避免连接到这些服务但保持测试负责处理连接的代码的能力,我使用netcat工具作为远程服务的模拟。首先,我配置我的机器将传出流量重定向到netcat的监听端口。

我使用以下命令在后台运行netcat:

1
$ netcat -l 80 -k -w 0 &

参数指示服务应在端口80上监听传入连接(-l 80),在当前连接关闭后继续监听其他连接(-k),并在建立连接后立即终止连接(-w 0)。

cURL预计将使用各种主机名、IP地址和端口连接到服务。我需要将它们转发到一个地方:先前创建的TCP端口80。

为了将所有传出的TCP数据包重定向到本地环回地址(127.0.0.1)的端口80,我使用了以下iptables规则:

1
$ iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-port 80

该命令在iptables的网络地址转换表中添加一个新条目。-p选项指定协议(在本例中为TCP),-j选项指定规则的目标(在本例中为REDIRECT)。–to-port选项指定数据包将被重定向到的端口(在本例中为80)。

为了确保所有域名都将解析为IP地址127.0.0.1,我使用了以下iptables规则:

1
$ iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1

此规则在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中修复。

要重现该错误,请使用以下命令:

1
$ curl -x 0:80 dict://0

CVE-2022-43552(当HTTP代理拒绝隧道SMB/TELNET协议时的释放后使用)

cURL可以通过HTTP代理虚拟隧道支持的协议。如果HTTP代理阻止SMB或TELNET协议,cURL可能在其传输关闭代码中使用已释放的结构。此问题在cURL 7.87.0中修复。

要重现该错误,请使用以下命令:

1
2
$ curl 0 -x0:80 telnet:/[j-u][j-u]//0 -m 01
$ curl 0 -x0:80 smb:/[j-u][j-u]//0 -m 01

TOB-CURL-10(使用并行选项和序列时的释放后使用)

通过使用cURL与并行选项(-Z)、不匹配的括号和创建51个主机的两个连续序列,可以触发释放后使用漏洞。cURL为错误缓冲区分配内存块,默认允许最多50个传输。在负责处理错误的函数中,当连接失败时,错误被复制到适当的错误缓冲区,然后释放内存。对于最后一个(51)序列,分配内存缓冲区,释放,然后将错误复制到先前释放的内存缓冲区。此问题在cURL 7.86.0中修复。

要重现该错误,请使用以下命令:

1
$ curl 0 -Z [q-u][u-~] }

TOB-CURL-11(未使用的内存块未释放,导致内存泄漏)

cURL分配的内存块在不再需要时未释放,导致内存泄漏。此问题在cURL 7.87.0中修复。

要重现该错误,请使用以下命令:

1
2
3
$ curl 0 -Z 0 -Tz 0
$ curl 00 --cu 00
$ curl --proto =0 --proto =0

Dockerfile

如果你想了解设置模糊测试工具的全过程并立即开始模糊测试cURL的CLI参数,我们为你准备了一个Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# syntax=docker/dockerfile:1
FROM aflplusplus/aflplusplus:4.05c

RUN apt-get update && apt-get install -y libssl-dev netcat iptables groff

# Clone a curl repository
RUN git clone https://github.com/curl/curl.git && cd curl && git checkout 2ca0530a4d4bd1e1ccb9c876e954d8dc9a87da4a

# Apply a patch to use afl++ argv fuzzing feature
COPY <<-EOT /AFLplusplus/curl/curl_argv_fuzz.patch
		diff --git a/src/tool_main.c b/src/tool_main.c
		--- a/src/tool_main.c
		+++ b/src/tool_main.c
		@@ -54,6 +54,7 @@
		 #include "tool_vms.h"
		 #include "tool_main.h"
		 #include "tool_libinfo.h"
		+#include "../../AFLplusplus/utils/argv_fuzzing/argv-fuzz-inl.h"

		 /*
		  * This is low-level hard-hacking memory leak tracking and similar. Using
		@@ -246,6 +247,8 @@ int main(int argc, char *argv[])
		   struct GlobalConfig global;
		   memset(&global, 0, sizeof(global));

		+  AFL_INIT_ARGV();
		+
		 #ifdef WIN32
		   /* Undocumented diagnostic option to list the full paths of all loaded
		      modules. This is purposely pre-init. */
EOT

# Apply a patch to use afl++ argv fuzzing feature
RUN cd curl && git apply curl_argv_fuzz.patch

# Compile a curl using collision-free instrumentation at link time and ASAN
RUN cd curl && \
	autoreconf -i && \
	CC="afl-clang-lto" CFLAGS="-fsanitize=address -g" ./configure --with-openssl --disable-shared && \
	make -j $(nproc) && \
	make install

# Download a dictionary
RUN wget 
https://gist.githubusercontent.com/ahpaleus/f94eca6b29ca8824cf6e5a160379612b/raw/3de91b2dfc5ddd8b4b2357b0eb7fbcdc257384c4/curl.dict

COPY <<-EOT script.sh
	#!/bin/bash
	# Running a netcat listener on port tcp port 80 in the background
	netcat -l 80 -k -w 0 &

	# Prepare iptables entries
	iptables-legacy -t nat -A OUTPUT -p tcp -j REDIRECT --to-port 80
	iptables-legacy -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1

	# Prepare fuzzing directories
	mkdir fuzz &&
		  cd fuzz &&
		  mkdir in out &&
		  echo -ne 'curl\x00http://127.0.0.1:80' > in/example_command.txt &&
		  # Run afl++ fuzzer
		  afl-fuzz -x /AFLplusplus/curl.dict -i in/ -o out/ -- curl
EOT

RUN chmod +x ./script.sh
ENTRYPOINT ["./script.sh"]

使用以下命令运行此文件:

1
2
$ docker buildx build -t curl_fuzz .
$ docker run --rm -it --cap-add=NET_ADMIN curl_fuzz

不开玩笑

总之,我们的方法表明,模糊测试CLI可以成为识别软件漏洞的有效补充技术。尽管最初持怀疑态度,但我们的结果产生了有价值的见解。我们相信这提高了基于CLI的工具的安全性,即使OSS-Fuzz已经使用了多年。

在cURL清理过程中找到基于堆的内存损坏漏洞是可能的。但是,释放后使用漏洞可能无法被利用,除非以适当的方式使用释放的数据并且控制数据内容。双重释放漏洞需要进一步分配类似大小的内存并控制存储的数据。此外,由于漏洞在libcurl中,它可能以各种方式影响许多使用libcurl的不同软件应用程序,例如发送多个请求或在单个进程内设置和清理库资源。

还值得注意的是,尽管CLI利用的攻击面相对有限,但如果受影响的工具是SUID二进制文件,利用可能导致权限升级(参见CVE-2021-3156:sudo中的基于堆的缓冲区溢出)。

为了在未来提高类似工具的模糊测试效率,我们通过合并持久模糊测试模式扩展了AFL++中的argv_fuzz功能。在此处了解更多信息。

最后,我们的cURL审计报告是公开的。查看审计报告和威胁模型。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计