openssl在8月24日发布了openssl 1.1.1l的稳定版,其中修复了一个高危漏洞:CVE-2021-3711。该漏洞会影响openssl 1.1.1l 之前的所有包含SM2商密算法版本,其中也包括基于openssl改造过的版本:北京大学的gmssl和阿里巴巴的babassl。
漏洞产生的原因,是解密 SM2公钥加密后的数据时,有可能分配了一个过小的内存,导致解密后的明文长度,大于该内存长度,造成内存越界,从而导致整个应用程序崩溃。而这个漏洞,对基于openssl搭建的国密网关服务、WEB服务,有一定概率导致服务崩溃,从而产生严重影响。目前,市面上比较通用的国密密码套件是使用SM2_SM4_SM3(0xE013),也就是说,该密码套件是采用SM2加解密的方式进行密钥协商,因此,这就很有可能触发该漏洞,导致程序崩溃。
漏洞分析
解密 SM2公钥加密后的数据时,应用程序会调用函数EVP_PKEY_decrypt(),该函数定义如下:
int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx,
unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);
通常应用程序会调用两次这个函数:第一次,在进入时,“out”参数传 NULL,在函数返回时,“outlen”参数会返回”out”所需的缓冲区大小。然后应用程序分配足够的缓冲区,并再次调用 EVP_PKEY_decrypt(),但这次”out”传递的是非NULL。整个流程如下图所示:
static int pkcs7_decrypt_rinfo(unsigned char **pek, int *peklen,
PKCS7_RECIP_INFO *ri, EVP_PKEY *pkey,
size_t fixlen)
{
EVP_PKEY_CTX *pctx = NULL;
unsigned char *ek = NULL;
size_t eklen;
int ret = -1;
pctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!pctx)
return -1;
if (EVP_PKEY_decrypt_init(pctx) <= 0)
goto err;
if (EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DECRYPT,
EVP_PKEY_CTRL_PKCS7_DECRYPT, 0, ri) <= 0) {
PKCS7err(PKCS7_F_PKCS7_DECRYPT_RINFO, PKCS7_R_CTRL_ERROR);
goto err;
}
if (EVP_PKEY_decrypt(pctx, NULL, &eklen,
ri->enc_key->data, ri->enc_key->length) <= 0)
goto err;
ek = OPENSSL_malloc(eklen);
if (ek == NULL) {
PKCS7err(PKCS7_F_PKCS7_DECRYPT_RINFO, ERR_R_MALLOC_FAILURE);
goto err;
}
if (EVP_PKEY_decrypt(pctx, ek, &eklen,
ri->enc_key->data, ri->enc_key->length) <= 0
|| eklen == 0
|| (fixlen != 0 && eklen != fixlen)) {
ret = 0;
PKCS7err(PKCS7_F_PKCS7_DECRYPT_RINFO, ERR_R_EVP_LIB);
goto err;
}
ret = 1;
OPENSSL_clear_free(*pek, *peklen);
*pek = ek;
*peklen = eklen;
err:
EVP_PKEY_CTX_free(pctx);
if (!ret)
OPENSSL_free(ek);
return ret;
}
我们再来看使用SM2算法时,EVP_PKEY_decrypt内部实现:通过函数指针的方式,会调用到pkey_sm2_decrypt函数,从下图可以看到,当”out”为NULL时,会调用sm2_plaintext_size函数,其中“outlen”参数会返回”out”所需的缓冲区大小。
static int pkey_sm2_decrypt(EVP_PKEY_CTX *ctx,
unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen)
{
EC_KEY *ec = ctx->pkey->pkey.ec;
SM2_PKEY_CTX *dctx = ctx->data;
const EVP_MD *md = (dctx->md == NULL) ? EVP_sm3() : dctx->md;
if (out == NULL) {
if (!sm2_plaintext_size(ec, md, inlen, outlen))
return -1;
else
return 1;
}
return sm2_decrypt(ec, md, in, inlen, out, outlen);
}
问题就出在sm2_plaintext_size函数里。首先我们先需要了解一下SM2公钥加密后的ASN.1数据结构(ASN.1抽象语法标记,是一种数据格式),下图引用于《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》
通常情况SM2算法中xy分量的长度是32,但是也有可能小于32,问题就来了,sm2_plaintext_size函数的作用是获取CipherText密文的长度,计算方式简单粗暴:
overhead = 10 + 2 * field_size + (size_t)md_size;
overhead:整个加密后的数据中,不含密文后的长度
10: ASN.1格式中,所有标记的长度
2 * field_size:xy分量的长度,field_size是SM2密钥中的一个值(这个值是固定的32),而x/y分量的实际长度是有可能小于32的!
md_size:杂凑值的长度
*pt_size = msg_len - overhead;
pt_size:计算出的密文长度
msg_len:整个加密后的数据长度
所以计算出的pt_size有可能偏小,结合上面提到的EVP_PKEY_decrypt()调用方式,就可能分配一个偏小的缓冲区,从而造成内存越界,程序崩溃。
int sm2_plaintext_size(const EC_KEY *key, const EVP_MD *digest, size_t msg_len,
size_t *pt_size)
{
const size_t field_size = ec_field_size(EC_KEY_get0_group(key));
const int md_size = EVP_MD_size(digest);
size_t overhead;
if (md_size < 0) {
SM2err(SM2_F_SM2_PLAINTEXT_SIZE, SM2_R_INVALID_DIGEST);
return 0;
}
if (field_size == 0) {
SM2err(SM2_F_SM2_PLAINTEXT_SIZE, SM2_R_INVALID_FIELD);
return 0;
}
overhead = 10 + 2 * field_size + (size_t)md_size;
if (msg_len <= overhead) {
SM2err(SM2_F_SM2_PLAINTEXT_SIZE, SM2_R_INVALID_ENCODING);
return 0;
}
*pt_size = msg_len - overhead;
return 1;
}
这个漏洞有可能被恶意攻击者利用,攻击者在国密SSL握手时,生成一个x/y分量长度小于32的加密数据,就可以导致服务端程序崩溃。
漏洞修复
目前openssl已经出了1.1.1l了,已修复这个高危漏洞。
https://github.com/openssl/openssl/commit/36cf45ef3ba71e44a8be06ee81cb31aa02cb0010?branch=36cf45ef3ba71e44a8be06ee81cb31aa02cb0010&diff=unified
https://github.com/openssl/openssl/commit/ad1ca777f9702f355a2f74dc5eed713476825f23?branch=ad1ca777f9702f355a2f74dc5eed713476825f23&diff=split
漏洞溯源
这个漏洞非常的隐蔽,并且发生的概率并不高,所以很难被发现,但是一旦被利用,将会造成严重影响。
从openssl的提交记录中可以看到,在2018年openssl支持SM2算法时,这个漏洞就一直存在了(下图),同样的,基于openssl改造的gmssl、babassl都存在这个漏洞。
希望大家尽快更新openssl的版本,或修改相关代码以修复漏洞。
CVE-2021-3711漏洞链接:
https://www.openssl.org/news/vulnerabilities.html#CVE-2021-3711
能为国密应用的发展贡献自己的一份力量,这个还是挺高兴的!