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

Trail of Bits 团队通过对 cURL 命令行接口(CLI)进行模糊测试,发现了多个内存损坏漏洞,包括 use-after-free、double-free 和内存泄漏。

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

在讨论该应用程序的威胁模型时,我们的一位团队成员开玩笑地问:“我们有没有尝试过 curl AAAAAAAAAA…”?虽然这条评论只是玩笑之语,但它激发了一个想法:我们应该对 cURL 的命令行界面 (CLI) 进行模糊测试。一旦我们这样做了,fuzzer 很快就发现了内存损坏的错误,特别是 use-after-free 问题、double-free 问题和内存泄漏。由于这些错误存在于 cURL 开发库 libcurl 中,因此它们可能会影响许多使用 libcurl 的软件应用程序。这篇博文描述了我们如何发现以下漏洞:

使用 cURL

cURL 由 OSS-Fuzz 项目持续进行模糊测试,其 harness 在单独的 curl-fuzzer GitHub 存储库中开发。当我查阅 curl-fuzzer 存储库以查看 cURL 模糊测试的当前状态时,我注意到 cURL 的命令行界面 (CLI) 参数没有被模糊测试。考虑到这一点,我决定专注于测试 cURL 对参数的处理。我使用 AFL++ fuzzer(AFL 的一个分支)为 cURL 的 CLI 生成大量随机输入数据。我使用链接时使用无冲突检测编译了 cURL AddressSanitizer,然后分析了可能表明存在错误的崩溃。

cURL 通过命令行参数获取其选项。由于 cURL 遵循 C89 标准,因此程序的 main() 函数可以定义为没有参数或带有两个参数(argcargv)。 argc 参数表示传递给程序的命令行参数的数量(包括程序的名称)。 argv 参数是指向从命令行传递给程序的参数的指针数组。

该标准还规定,在托管环境中,main() 函数接受第三个参数,即 char *envp[];此参数指向以 null 结尾的 char 指针数组,每个指针都指向一个包含有关程序环境信息的字符串。

这三个参数可以有任何名称,因为它们是声明它们的函数的局部变量。

curl/src/tool_main.c 文件中 cURL 的 main() 函数将命令行参数传递给 operate() 函数,该函数解析它们并设置 cURL 的全局配置。 然后,cURL 使用该全局配置来执行操作。

图 1.1:cURL 的 main() 函数(curl/src/tool_main.c#236–288

模糊测试 argv

当我开始尝试模糊测试 cURL 的过程时,我寻找一种使用 AFL 来模糊测试其参数解析的方法。我的搜索引导我找到了 AFL 的创建者 Michal Zalewski 的一句引言

“AFL 不支持 argv 模糊测试,因为 TBH,它在实践中并没有那么有用。 experimental/argv_fuzzing/ 中有一个示例,展示了如果你确实需要,如何在一般情况下执行此操作。”

我查看了那个实验性 AFL 功能及其在 AFL++ 中的等效功能argv 模糊测试功能可以模糊测试从 CLI 传递给程序的参数,而不是通过标准输入。当你想要在模糊测试中覆盖库的多个 API 时,这会很有用,因为你可以模糊测试使用该库的工具的参数,而不是为每个 API 编写多个模糊测试。

AFL++ argvfuzz 功能如何工作?

argvfuzzargv-fuzz-inl.h 头文件定义了两个宏,它们从 fuzzer 获取输入并设置 argvargc

  • 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/src/tool_main.c

准备字典

模糊测试字典文件指定了模糊测试引擎在测试期间应关注的数据元素。 模糊测试引擎会调整其突变策略,以便它将处理字典中的 token。 在 cURL 模糊测试的情况下,模糊测试字典可以帮助 afl-fuzz 更有效地生成包含选项(以一个或两个破折号开头)的有效测试用例。

为了模糊测试 cURL,我使用了 afl-clang-lto 编译器的 autodictionary 功能,该功能在编译目标二进制文件期间自动生成字典。 此字典在启动时传输到 afl-fuzz,从而提高其覆盖率。 我还根据 cURL manpage 准备了一个自定义字典,并通过 -x 参数将其传递给 afl-fuzz。 我使用了以下 Bash 命令来准备字典:

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

设置 cURL 连接的服务

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

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

$ netcat -l 80 -k -w 0 &

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

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

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

$ 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 规则:

$ 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 代理时发生 Double free)

由于错误/清理处理中的缺陷,将 cURL 与代理连接以及 dict、gopher、LDAP 或 telnet 协议一起使用会触发 double-free 漏洞。 此问题已在 cURL 7.86.0 中修复。

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

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

CVE-2022-43552(当 HTTP 代理拒绝隧道 SMB/TELNET 协议时发生 Use after free)

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

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

$ 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(在使用 parallel 选项和序列时发生 Use after free)

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

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

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

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

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

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

$ curl 0 -Z 0 -Tz 0
$ curl 00 --cu 00
$ curl --proto =0 --proto =0

Dockerfile

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

## 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"]

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

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

玩笑归玩笑

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

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

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

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

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

  • 原文链接: blog.trailofbits.com/202...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Trail of Bits
Trail of Bits
https://www.trailofbits.com/