cURL审计揭秘:一个玩笑如何引发重大安全发现

本文详细介绍了Trail of Bits团队如何通过fuzzing技术发现cURL中多个高危漏洞,包括双重释放、释放后使用等内存安全问题,并提供了完整的技术实现方案和漏洞复现方法。

cURL审计:一个玩笑如何引发重大发现

背景

2022年秋季,Trail of Bits对cURL进行了安全审计。cURL是一个广泛使用的命令行工具,用于在服务器之间传输数据并支持多种协议。该项目恰逢Trail of Bits的maker周,这意味着我们拥有比平时更多的人力资源,使我们能够采用非标准方法进行审计。

在讨论应用程序的威胁模型时,我们的一位团队成员开玩笑地问:“我们试过curl AAAAAAAAAA…了吗?“虽然这个评论是开玩笑的,但它激发了一个想法:我们应该对cURL的命令行界面(CLI)进行模糊测试。一旦我们这样做了,模糊测试器很快就发现了内存损坏错误,特别是释放后使用问题、双重释放问题和内存泄漏。由于这些错误存在于libcurl(一个cURL开发库)中,它们有可能影响许多使用libcurl的软件应用程序。

发现的漏洞

  • CVE-2022-42915 - 在使用HTTP代理与特定协议时出现双重释放。在cURL 7.86.0中修复
  • CVE-2022-43552 - 当HTTP代理拒绝隧道SMB/TELNET协议时出现释放后使用。在cURL 7.87.0中修复
  • TOB-CURL-10 - 使用并行选项和序列时出现释放后使用。在cURL 7.86.0中修复
  • TOB-CURL-11 - 未使用的内存块未被释放,导致内存泄漏。在cURL 7.87.0中修复

与cURL合作

cURL由OSS-Fuzz项目持续进行模糊测试,其测试工具在单独的curl-fuzzer GitHub仓库中开发。当我查看curl-fuzzer仓库以了解cURL模糊测试的当前状态时,我注意到cURL的命令行界面(CLI)参数没有被模糊测试。考虑到这一点,我决定专注于测试cURL对参数的处理。

我使用AFL++模糊测试器(AFL的一个分支)为cURL的CLI生成大量随机输入数据。我使用AddressSanitizer在链接时使用无冲突插桩编译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_ARGVAFL_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 设计