OpenSSL 1.1.1 系列中的 SM2 解密缓冲区溢出漏洞 CVE-2021-3711 介绍

    OpenSSL 维护者在2021年8月24日发布了安全提示,披露了代号为 CVE-2021-3711 和 CVE-2021-3712 的两个漏洞。其中CVE-2021-3711 的问题来自 SM2 解密缓冲区溢出,该漏洞存在于 OpenSSL 1.1.1 系列中 1.1.1k 及之前的版本,在8月12日由 John Ouyang 报告,现在已经由 Matt Caswell 修复,OpenSSL 维护者建议使用 OpenSSL 1.1.1 系列的用户尽快升级到 1.1.1l (注意l是字母L的小写形式)版本。
    为了解密 SM2 算法加密的消息,用户需要调用 API 函数 EVP_PKEY_decrypt( )。该函数的实现位于 crypto\evp\pmeth_fn.c 文件中,函数定义如下:
int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx,

                     unsigned char *out, size_t *outlen,

                     const unsigned char *in, size_t inlen)
    通常在解密时,用户将调用 EVP_PKEY_decrypt( ) 函数两次。第一次调用时,将 NULL 赋值给参数 out,此时函数并不做实际的解密操作,而是在返回时把能够容纳明文的缓冲区的长度数值赋值给参数 outlen(注意此时算出的缓冲区长度可能比实际明文长度更长一些)。用户可以按照获得的缓冲区长度动态开辟一个缓冲区,接下来用户第二次调用 EVP_PKEY_decrypt( ) 函数,此时要将新开辟缓冲区的起始地址赋值给参数 out,这时函数将执行解密操作,当函数返回时将把解密后得到的明文存放到指针 out 指向的缓冲区中,将解密得到的明文的实际长度数值赋值给指针 outlen 指向的、类型为 sizt_t 的变量。
    根据对漏洞 CVE-2021-3711 的描述,在 OpenSSL 1.1.1 系列版本中,1.1.1k 及之前的版本在计算 SM2 解密时,当用户调用 EVP_PKEY_decrypt( ) 函数获取存放明文缓冲区的长度时,得到的缓冲区长度可能出现问题,算出的缓冲区长度可能偏小。攻击者可以提交一个精心设计的 SM2 密文,使得用户使用偏小的缓冲区来存放解密后获得的明文,最多可能导致缓冲区溢出 62 个字节。
    下面分析一下这个漏洞的技术细节:根据 GB/T 32918.4—2016《信息安全技术 SM2椭圆曲线公钥密码算法 第4部分:公钥加密算法》,SM2 密文由三部分组成,如下图:

OpenSSL 1.1.1 系列中的 SM2 解密缓冲区溢出漏洞 CVE-2021-3711 介绍_第1张图片    

    根据 GB/T 35276—2017《信息安全技术 SM2密码算法使用规范》,对 SM2 密文要进行 ASN.1 编码,编码方式如下:
SM2Cipher ::= SEQUENCE {
    XCoordinate    INTEGER,                  -- x分量
    YCoordinate    INTEGER,                  -- y分量
    HASH           OCTET STRING SIZE(32),    -- 杂凑值
    CipherText     OCTET STRING              -- 密文
}

    其中 HASH 是一个 SM3 杂凑值,长度固定为 32 字节,CipherText 是与明文对应的密文。

     OpenSSL 维护者对 CVE-2021-3711 漏洞的修复,主要改动之处在 crypto\sm2\sm2_crypt.c 文件中的 sm2_plaintext_size( ) 函数中,这个函数被用于计算存放明文的缓冲区长度。看一下修改前后的源代码对比:

OpenSSL 1.1.1 系列中的 SM2 解密缓冲区溢出漏洞 CVE-2021-3711 介绍_第2张图片

    sm2_plaintext_size( ) 函数的参数名称、个数都被修改了,但参数 *pt_size 始终被用于表示解密后获得的明文长度。在修改之前的函数实现中,参数 msg_len 表示 ASN.1 编码形式的 SM2 密文长度,SM2 明文长度的计算方法是:
*pt_size = msg_len - overhead;
    注意 overhead 的计算方法是:
overhead = 10 + 2 * field_size + (size_t)md_size;
    这里的设计思路是:
(1) 变量 overhead 表示 ASN.1 编码开销(包括类型标志位、负载长度部分所占的长度,这里使用了最小值 10 字节)、x 分量和 y 分量所占的长度(用 2 * field_size 表示)、SM3 杂凑值所占的长度(用 md_size 表示)三部分之和;
(2) 用整个 ASN.1 编码编码形式的 SM2 密文长度减去 overhead,就得到了估算的明文长度。
    为什么 ASN.1 编码开销的最小值是 10 字节呢?回忆一下 ASN.1 编码的一般构成形式:

 OpenSSL 1.1.1 系列中的 SM2 解密缓冲区溢出漏洞 CVE-2021-3711 介绍_第3张图片

    类型标志位占用 1 个字节,负载长度可能采用短编码、长编码两种形式(具体什么是短编码、长编码,请查阅 ASN.1 编码文档),因此负载长度至少占 1 个字节,有可能更长。负载可能与待编码数据长度相同,也有可能加上填充字节(例如对于 INTEGER 类型有可能要添加 1 字节填充,具体规定请请查阅 ASN.1 编码文档)。
对于 SM2 密文的 ASN.1 编码,从下图可以看出 ASN.1 编码开销的最小值是 10个 字节。

OpenSSL 1.1.1 系列中的 SM2 解密缓冲区溢出漏洞 CVE-2021-3711 介绍_第4张图片


    再回到对 SM2 明文长度的估算,其公式是:
*pt_size = msg_len - overhead
         = msg_len - (10 + 2 * field_size + (size_t)md_size)
    当 overhead 的值偏大时,可能出现明文缓冲区长度 *pt_size 的值偏小。
    注意 ASN.1 编码开销取最小值 10 字节时,overhead 的值有可能偏小,此时不会导致 *pt_size 的值偏小。md_size 变量的值是常量 32,那么导致 *pt_size 的值偏小的唯一来源就是 x 分量和 y 分量所占的长度被估计的偏大了!这里使用的估计值是 2 * field_size,由于 SM2 算法在椭圆曲线上的一个点的坐标通常由两个 32 字节的分量(x,y)表示,那么两个分量占用的长度一般为 2×32 = 64。但是考虑一种非常极端的情况,即 x 和 y 分量的长度都是 1 个字节,尽管这种情况几乎不可能出现,但是一旦出现,就会导致两个分量的长度被多算了 62 个字节,从而使得计算出的明文缓冲区长度被少算了 62 个字节,于是导致 62 字节的缓冲区溢出。

    在1.1.1l版的修复代码中,SM2 明文长度的计算过程是:
(1) 对 ASN.1 编码形式的 SM2 密文进行解码,
sm2_ctext = d2i_SM2_Ciphertext(NULL, &ct, ct_size)
(2) 检查解码是否成功,
if (sm2_ctext == NULL) {

        SM2err(SM2_F_SM2_PLAINTEXT_SIZE, SM2_R_INVALID_ENCODING);

        return 0;

    }
(3) 将解码时获得的 SM2 明文长度赋值给参数 *pt_size,
*pt_size = sm2_ctext->C2->length;
(4) 释放调用解码函数 d2i_SM2_Ciphertext( ) 时在该函数内部动态开辟的缓冲区
SM2_Ciphertext_free(sm2_ctext);

    在修复之后,SM2 明文的长度是通过对 ASN.1 编码形式的 SM2 密文进行解码,获取到的解码后与明文对应的那一部分密文的长度,此长度等于明文长度,不再采用老版本中的方式,这就解决了缓冲区长度的估算值可能偏小的问题,但是解码的运算量比较大,效率下降了。另外可采取的一种修复漏洞方式是令
overhead = 10 + 2 * 1 + (size_t)md_size;
    此时 overhead 的取值是一种极端情况下的最小值,根据
*pt_size = msg_len - overhead
    这时估算出的存放明文缓冲区的长度 *pt_size 就会偏大,就能够保证一定可以容纳明文,不会导致缓冲区溢出,同时避免了 ASN.1 解码操作,提升了效率。

你可能感兴趣的:(OpenSSL,C语言,ASN.1编码,openssl)