更新历史

  • 2024-09-14:补充一些说明内容。

影响范围

Openssh Server 9.1

漏洞触发

漏洞利用很简单,在连接建立与版本协商的时候将 softwareid 设置成特定版本(如 PuTTY_Release_0.64)即可触发。

漏洞分析

这个漏洞的成因位于算法协商与密钥交换阶段:

do_ssh2_kex(ssh);
  -> kex_proposal_populate_entries
    -> compat_kex_proposal(ssh, cp);    # First Free
do_authentication2(ssh)
  -> ssh_dispatch_run_fatal
    -> ssh_dispatch_run
      -> input_userauth_request
        -> mm_getpwnamallow
          -> copy_set_server_options
            -> assemble_algorithms
              -> kex_assemble_names     # Double Free

首先查看上游修复位置,主要修复了这一段代码,根据 BugZilla 中的报告,这里应该是第一次被 free 的位置:

compat.c
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
    char *cp = NULL;
 
    if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
        return xstrdup(p);
    debug2_f("original KEX proposal: %s", p);
    if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
        if ((p = match_filter_denylist(p,
            "[email protected]")) == NULL)
            fatal("match_filter_denylist failed");
    if ((ssh->compat & SSH_OLD_DHGEX) != 0) {                 [1]
        cp = p;                                               [2]
        if ((p = match_filter_denylist(p,
            "diffie-hellman-group-exchange-sha256,"
            "diffie-hellman-group-exchange-sha1")) == NULL)
            fatal("match_filter_denylist failed");
        free(cp);                                             [3]
    }
    debug2_f("compat KEX proposal: %s", p);
    if (*p == '\0')
        fatal("No supported key exchange algorithms found");
    return p;
}

在 SSH_OLD_DHGEX 被设置时(也就是代码中的 [1] 处),会将字符串 p 赋值给 cp([2] 处)然后释放掉([3] 处)。此时传进来的字符串 p 是 option->kex_algorithms。此时这个指针已经变成了一个悬空指针。

崩溃现场位于 kex_assemble_names 函数。kex_assemble_names 函数在 assemble_algorithms 中被调用,传进来的参数也是 option->kex_algorithms 导致了 double free。

接下来看一下漏洞路径,重点在于 SSH_OLD_DHGEX 这个参数,这个参数是在 compat_banner 函数中定义的:

static struct {
    char    *pat;
    int    bugs;
} check[] = {
    ... ...
    { "PuTTY_Local:*,"    /* dev versions < Sep 2014 */
      ... ...
      "PuTTY_Release_0.64*",
                SSH_OLD_DHGEX },
    ... ...
};

除了 PuTTY 的旧版本外,理论上其它带有 SSH_OLD_DHGEX 标志的客户端也能触发漏洞。

补丁分析

补丁的做法很简单,在上面通过 ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX 保证传入的都是满足条件的值,然后分别对 SSH_BUG_CURVE25519PADSSH_OLD_DHGEX 进行处理:

diff --git a/compat.c b/compat.c
index 46dfe3a9c..478a9403e 100644
--- a/compat.c
+++ b/compat.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: compat.c,v 1.120 2022/07/01 03:35:45 dtucker Exp $ */
+/* $OpenBSD: compat.c,v 1.121 2023/02/02 12:10:05 djm Exp $ */
 /*
  * Copyright (c) 1999, 2000, 2001, 2002 Markus Friedl.  All rights reserved.
  *
@@ -190,26 +190,26 @@ compat_pkalg_proposal(struct ssh *ssh, char *pkalg_prop)
 char *
 compat_kex_proposal(struct ssh *ssh, char *p)
 {
-       char *cp = NULL;
+       char *cp = NULL, *cp2 = NULL;
 
        if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
                return xstrdup(p);
        debug2_f("original KEX proposal: %s", p);

首先处理 SSH_BUG_CURVE25519PAD 的情况,这里会选出满足条件的字符串放到 cp 中(而不是 p):

        if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
-               if ((p = match_filter_denylist(p,
+               if ((cp = match_filter_denylist(p,
                    "[email protected]")) == NULL)
                        fatal("match_filter_denylist failed");

接下来处理 SSH_OLD_DHGEX 的情况,因为上文的 cp 已经被赋值了,接下来会将 cp 传入 match_filter_denylist 并返回 cp2,在结束的时候释放 cp。最后返回 cp2,这个过程中只有 cp 在中途创建并被释放,p 并未被释放,避免了上文所说的第一次释放的问题。

        if ((ssh->compat & SSH_OLD_DHGEX) != 0) {
-               cp = p;
-               if ((p = match_filter_denylist(p,
+               if ((cp2 = match_filter_denylist(cp ? cp : p,
                    "diffie-hellman-group-exchange-sha256,"
                    "diffie-hellman-group-exchange-sha1")) == NULL)
                        fatal("match_filter_denylist failed");
                free(cp);
+               cp = cp2;
        }
-       debug2_f("compat KEX proposal: %s", p);
-       if (*p == '\0')
+       if (cp == NULL || *cp == '\0')
                fatal("No supported key exchange algorithms found");
-       return p;
+       debug2_f("compat KEX proposal: %s", cp);
+       return cp;
 }

参考资料