LibreSSL之CVE-2023-35784缝隙分析
颁布功夫 2024-06-28LibreSSL是2014年心脏滴血缝隙发作后,,,,,,,OpenBSD fork OpenSSL 1.0.1g并进行守护的安全SSL库。。。。。
一、缝隙信息
A double free or use after free could occur after SSL_clear in OpenBSD 7.2 before errata 026 and 7.3 before errata 004, and in LibreSSL before 3.6.3 and 3.7.x before 3.7.3. NOTE: OpenSSL is not affected.
? 补丁
Index: lib/libssl/s3_lib.c
===================================================================
RCS file: /cvs/src/lib/libssl/s3_lib.c,v
diff -u -p -r1.238 s3_lib.c
--- lib/libssl/s3_lib.c 21 Aug 2022 19:39:44 -0000 1.238
+++ lib/libssl/s3_lib.c 15 May 2023 05:05:28 -0000
@@ -1573,6 +1573,7 @@ ssl3_free(SSL *s)
sk_X509_NAME_pop_free(s->s3->hs.tls12.ca_names, X509_NAME_free);
sk_X509_pop_free(s->internal->verified_chain, X509_free);
+ s->internal->verified_chain = NULL;
tls1_transcript_free(s);
tls1_transcript_hash_free(s)
上面的缝霞述和补丁是目前能在网络上找到的所有有意思的公开内容。。。。。
二、缝隙分析
这里分析3.6.3版本打过补丁后的ssl3_free函数(/// ...略... 为省略的部门无关代码,,,,,,,下文一样):
// 3.6.3 s3_lib.c
void
ssl3_free(SSL *s)
{
if (s == NULL)
return;
tls1_cleanup_key_block(s);
ssl3_release_read_buffer(s);
ssl3_release_write_buffer(s);
/// ...略...
sk_X509_NAME_pop_free(s->s3->hs.tls12.ca_names, X509_NAME_free);
sk_X509_pop_free(s->internal->verified_chain, X509_free);////// [1]
s->internal->verified_chain = NULL;////// [2]
tls1_transcript_free(s);
/// ...略...
freezero(s->s3,sizeof(*s->s3));
s->s3 = NULL;
}
相比3.6.2版本的ssl3_free函数,,,,,,,该函数多了[2]处赋值为NULL的语句。。。。。
2.1 几个结构体
这里涉及到几个重要的结构体(v3.6.2),,,,,,,首先是SSL结构体。。。。。
SSL结构体是LibreSSL(蕴含OpenSSL)进行安全套接字编程时最直接与法式员打交路的结构体,,,,,,,安全编程重要的操作都直接或间接与其有关,,,,,,,因而,,,,,,,其沉要性不言而喻。。。。。
2.1.1 SSL
typedef struct ssl_st SSL;
struct ssl_st {
/* protocol version
* (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION, DTLS1_VERSION)
*/
int version;
const SSL_METHOD *method;
/// ...略...
int server;/* are we the server side? - mostly used by SSL_clear*/
struct ssl3_state_st *s3; /* SSLv3 variables */ ////////// [1]
struct dtls1_state_st *d1; /* DTLSv1 variables */
X509_VERIFY_PARAM *param;
/// ...略...
SSL_CTX * initial_ctx; /* initial ctx, used to store sessions */
#define session_ctx initial_ctx
struct ssl_internal_st *internal; /////////// [2]
};
在该结构体中,,,,,,,沉点关注末尾的internal成员变量,,,,,,,由于导致缝隙产生的verified_chain位于该结构体变量内;;;;;;;;我们同时注意s3成员变量,,,,,,,注意的原因见下文。。。。。
2.1.2 ssl_internal_st
ssl_internal_st结构体的界说如下:
typedef struct ssl_internal_st {
struct tls13_ctx *tls13;
uint16_t min_tls_version;
uint16_t max_tls_version;
/*
* These may be zero to imply minimum or maximum version supported by
* the method.
*/
uint16_t min_proto_version;
uint16_t max_proto_version;
/// ...略...
int empty_record_count;
size_t num_tickets;
/* Unused, for OpenSSL compatibility */
STACK_OF(X509) *verified_chain; /////// [1]
} SSL_INTERNAL;
缝隙产生的成员变量verified_chain位于ssl_internal_st结构体的末尾,,,,,,,为一STACK_OF(X509)指针。。。。。
2.1.3 BTW
这里顺便提一下SSL(指广义上的SSL,,,,,,,蕴含后续的TLS)的证书系统,,,,,,,有利于我们理解缝隙产生的机理。。。。。
? SSL证书系统
(1)逐级签发
通常来说,,,,,,,服务器返回的证书蕴含了多个机构,,,,,,,由根证书宣告机构逐级向下签发。。。。。以百度的证书为例,,,,,,,根证书宣告机构GlobalSign宣告给机构GlobalSign BE,,,,,,,而GlobalSign BE再宣告给百度。。。。。
(2)证书蕴含宣告者、宣告给谁、公钥、校验等信息
服务器返回的证书中,,,,,,,蕴含了各个机构的多种信息,,,,,,,好比宣告者,,,,,,,宣告给谁,,,,,,,公钥信息,,,,,,,版本、有效期、以及署名等。。。。。
(3)链式蕴含
逐级签发的证书,,,,,,,确定了在数据序列上由下至上的链式蕴含结构。。。。。 操作系统至少蕴含根证书。。。。。 证书系统确保了链式可信,,,,,,,从而要求操作系统至少要蕴含根证书,,,,,,,好比GlobalSign的根证书。。。。。
(4)自署名
一级 重要供测试用 缺点:中央人攻击
这里也提一下自署名证书,,,,,,,通常由OpenSSL法式天生,,,,,,,由自己宣告给自己,,,,,,,也因而,,,,,,,无法通过操作系统的证手札任链,,,,,,,无法匹敌中央人攻击,,,,,,,通常仅用于测试。。。。。
下图为服务器返回的百度证书链:

? STACK_OF(X)宏
该缝隙产生于STACK_OF(X509)宏指向的堆,,,,,,,因而,,,,,,,有必要弄明显verified_chain的堆结构。。。。。
#typedef STACK_OF(type) struct stack_st_##type
typedef struct stack_st {
int num;
char **data;
int sorted;
int num_alloc;
int (*comp)(const void *, const void *);
} _STACK; /* Use STACK_OF(...) instead */
STACK_OF(X509) *verified_chain;
这里,,,,,,,我们重要关注num成员变量和指向数组的指针data(LibreSSL使用了Linux偏早期的编码风格,,,,,,,使用char **暗示)。。。。。
客户端获取到服务器端的证书后,,,,,,,verified_chain在堆中的结构如下图:

能够看到,,,,,,,num的值为3,,,,,,,暗示证书链为三级证书链(仍以百度的证书为例,,,,,,,三级别离为GlobalSign、GlobalSign BE和百度各自的证书),,,,,,,因而data数组指向3个x509_st证书结构体,,,,,,,上图示例中打印出了最底层的根证书的部门成员变量。。。。。
2.2 sk_X509_pop_free宏
我们看一下最终开释s->internal->verified_chain的sk_X509_pop_free宏。。。。。
#define sk_X509_pop_free(st, free_func) SKM_sk_pop_free(X509, (st), (free_func))
#define SKM_sk_pop_free(type, st, free_func) \
sk_pop_free(CHECKED_STACK_OF(type, st), CHECKED_SK_FREE_FUNC(type, free_func))
void
sk_pop_free(_STACK *st, void (*func)(void *))
{
int i;
if (st == NULL) /////// [*]
return;
for (i = 0; i < st->num; i++)
if (st->data[i] != NULL)
func(st->data[i]); /////// [1]
sk_free(st); /////// [2]
}
能够看到,,,,,,,该宏最终挪用sk_pop_free函数。。。。。在该函数内,,,,,,,先判断st是否蹬宗NULL([*]处),,,,,,,若蹬宗则直接返回,,,,,,,注明已经不再必要开释了;;;;;;;;若st不蹬宗NULL,,,,,,,则使用for循环共循环stack_st结构体里的num次,,,,,,,并每次使用函数的第二个参数(为一函数指针)逐个开释数组内的各个成员([1]处),,,,,,,对于sk_X509_pop_free(s->internal->verified_chain, X509_free)而言,,,,,,,func即为X509_free函数;;;;;;;;最后开释st。。。。。
简而言之,,,,,,,对于缝隙而言,,,,,,,sk_pop_free的第一个参数st(即s->internal->verified_chain)上次开释后没有赋值为NULL,,,,,,,第二次sk_pop_free时由于st不蹬宗NULL,,,,,,,从而导致再次被sk_free函数开释。。。。。
三、缝隙验证
凭据缝霞述和补丁信息,,,,,,,我们知路必要有机遇执行sk_X509_pop_free(s->internal->verified_chain, X509_free)两次。。。。。
这涉及到SSL结构体的沉用,,,,,,,我们先看一个最单一的使用SSL结构体的伪代码。。。。。
3.1 SSL_new和SSL_free的配对使用
首先为SSL结构体变量申请出堆空间,,,,,,,而后使用SSL_connect函数实现SSL衔接的握手,,,,,,,这时会得到服务器端响应的证书,,,,,,,最后使用SSL_free函数开释堆空间,,,,,,,伪代码如下:
SSL* ssl = SSL_new(sslCtx);
// ...
SSL_connect(ssl);
// ...
SSL_free(ssl);
3.2 SSL_clear函数
凭据缝霞述,,,,,,,查看OpenSSL官方对SSL_clear函数的诠释,,,,,,,看看能获取到哪些信息提醒。。。。。

也就是说,,,,,,,SSL_clear提供沉置SSL对象的职能,,,,,,,以为下次新衔接做筹备,,,,,,,这样预防内部资源的申请和初始化,,,,,,,有利于提高资源的利用效能。。。。。并且提到了SSL_shutdown函数的使用。。。。。下面为SSL_clear函数的代码:
int
SSL_clear(SSL *s)
{
if (s->method == NULL) {
SSLerror(s, SSL_R_NO_METHOD_SPECIFIED);
return (0);
}
/// ...略...
s->internal->first_packet = 0;
/*
* Check to see if we were changed into a different method, if
* so, revert back if we are not doing session-id reuse.
*/
if (!s->internal->in_handshake && (s->session == NULL) &&
(s->method != s->ctx->method)) { /////// [1]
s->method->ssl_free(s); /////// [2]
s->method = s->ctx->method;
if (!s->method->ssl_new(s)) /////// [3]
return (0);
} else
s->method->ssl_clear(s);
return (1);
}
在沉置SSL结构体对象时,,,,,,,若是我们创造前提满足[1]处,,,,,,,可能使法式代码运行到[3]处,,,,,,,从而有机遇使得sk_X509_pop_free(s->internal->verified_chain, X509_free)执行两次。。。。。注意到,,,,,,,在[2]处已经有一次对ssl_free指向的函数的挪用,,,,,,,以及[3]处的ssl_new指向的函数的一次挪用。。。。。
3.3 ssl3_new函数
先看看ssl3_new函数:
int
ssl3_new(SSL *s)
{
if ((s->s3 = calloc(1, sizeof(*s->s3))) == NULL)
return (0);
s->method->ssl_clear(s); /////// [1]
return (1);
}
注意[1]处。。。。。
3.4 ssl3_clear函数
还有,,,,,,,ssl3_clear函数的源代码:
void
ssl3_clear(SSL *s)
{
unsigned char *rp, *wp;
size_t rlen, wlen;
tls1_cleanup_key_block(s);
sk_X509_NAME_pop_free(s->s3->hs.tls12.ca_names, X509_NAME_free);
sk_X509_pop_free(s->internal->verified_chain, X509_free); ///////// [1]
s->internal->verified_chain = NULL;
freezero(s->s3->hs.sigalgs, s->s3->hs.sigalgs_len);
s->s3->hs.sigalgs = NULL;
s->s3->hs.sigalgs_len = 0;
/// ...略...
memset(s->s3, 0, sizeof(*s->s3));
s->s3->rbuf.buf = rp;
/// ...略...
s->s3->hs.state = SSL_ST_BEFORE|((s->server) ? SSL_ST_ACCEPT : SSL_ST_CONNECT);
}
观察[1]处的代码。。。。。
3.5 触发过程
若是我们能先后触发ssl3_free和ssl3_new,,,,,,,那么也就能触发该缝隙。。。。。
创建SSL必要的高低文环境;;;;;;;; 挪用SSL_connect函数,,,,,,,使得SSL衔接返回证书链; 而后使用SSL_shutdown函数关关该SSL衔接; 接下来挪用TLSv1_method函数,,,,,,,以返回一个新的SSL_METHOD指针(和第一次使用SSL_CTX_new函数创建SSL_CTX变量时传入的参数分歧); 挪用SSL_set_ssl_method函数,,,,,,,其第二个参数为上一步返回的新SSL_METHOD指针;;;;;;;; 正常开释有关资源。。。。。
使用TLSv1_method函数的原因
我们看一下TLSv1_method函数的有关代码:
// ssl_methods.c
const SSL_METHOD *
TLSv1_method(void){
return (&TLSv1_method_data);
}
// ...
static const SSL_METHOD TLSv1_method_data = {
.dtls = 0,
.server = 1,
.version = TLS1_VERSION,
.min_tls_version = TLS1_VERSION,
.max_tls_version = TLS1_VERSION,
.ssl_new = tls1_new,
.ssl_clear = tls1_clear,
.ssl_free = tls1_free, /////// [1]
.ssl_accept = ssl3_accept,
.ssl_connect = ssl3_connect,
.ssl_shutdown = ssl3_shutdown,
/// ...略...
.enc_flags = TLSV1_ENC_FLAGS,
};
可能看到,,,,,,,此时,,,,,,,ssl_free函数其实指向了tls1_free函数,,,,,,,而tls1_free函数内部挪用了ssl3_free。。。。。
// t1_lib.c
void
tls1_free(SSL *s)
{
if (s == NULL)
return;
free(s->internal->tlsext_session_ticket);
ssl3_free(s); /////// [1]
}
我们再来看一下SSL_set_ssl_method函数:
int
SSL_set_ssl_method(SSL *s, const SSL_METHOD *method)
{
int (*handshake_func)(SSL *) = NULL;
int ret = 1;
if (s->method == method)
return (ret);
if (s->internal->handshake_func == s->method->ssl_connect)
handshake_func = method->ssl_connect;
else if (s->internal->handshake_func == s->method->ssl_accept)
handshake_func = method->ssl_accept;
if (s->method->version == method->version) { //// [1]
s->method = method;
} else {
s->method->ssl_free(s); //// [2]
s->method = method; //// [3]
ret = s->method->ssl_new(s); //// [4]
}
s->internal->handshake_func = handshake_func;
return (ret);
}
我们使用TLSv1_method函数返回的method->version有意不蹬宗s->method->version,,,,,,,从而导致[2]的执行,,,,,,,而此时的s->method->ssl_free的值为tls1_free,,,,,,,且运行到[4]处时,,,,,,,s->method->ssl_new的值为tls1_new:

凭据tls1_free函数的代码,,,,,,,它会挪用ssl3_free一次,,,,,,,而ssl3_free内部会执行一次sk_X509_pop_free(s->internal->verified_chain, X509_free),,,,,,,导致s->internal->verified_chain被开释。。。。。
当法式执行到SSL_set_ssl_method内的[4]处时,,,,,,,会挪用tls1_new函数。。。。。
我们再来看一下tls1_new的代码:
int
tls1_new(SSL *s)
{
if (!ssl3_new(s)) ///// [1]
return (0);
s->method->ssl_clear(s);
return (1);
}
能够看到,,,,,,,无论若何,,,,,,,[1]处的ssl3_new函数城市执行,,,,,,,而ssl3_new内部会挪用s->method->ssl_clear,,,,,,,此时的s->method->ssl_clear指向tls1_clear函数,,,,,,,tls1_clear函数的实现如下:
void
tls1_clear(SSL *s)
{
ssl3_clear(s); /////// [1]
s->version = s->method->version;
}
把稳到tls1_clear内部会挪用ssl3_clear,,,,,,,而ssl3_clear内部肯定会执行sk_X509_pop_free(s->internal->verified_chain, X509_free)语句,,,,,,,从而导致s->internal->verified_chain被再次开释。。。。。在调试器中的崩溃如下图:

四、缝隙建复
4.1 对于3.6.3
对于3.6.3版,,,,,,,官方直接在ssl3_free函数里把s->internal->verified_chain赋值为NULL:
// ssl3_free(SSL *s)
sk_X509_pop_free(s->internal->verified_chain, X509_free); /////// [1]
s->internal->verified_chain = NULL; /////// [2]
4.2 最新版本3.9.2
在分析打过补丁后的较新版本时,,,,,,,会发现ssl3_free函数没有了3.6.3版时把verified_chain赋值为NULL的语句,,,,,,,好比最新版3.9.2的:
void
ssl3_free(SSL *s)
{
if (s == NULL)
return;
tls1_cleanup_key_block(s);
ssl3_release_read_buffer(s);
ssl3_release_write_buffer(s);
/// ...略...
sk_X509_pop_free(s->s3->hs.peer_certs, X509_free);
sk_X509_pop_free(s->s3->hs.peer_certs_no_leaf, X509_free);
sk_X509_pop_free(s->s3->hs.verified_chain, X509_free); /////// [1]
tls_key_share_free(s->s3->hs.key_share);
/// ...略...
freezero(s->s3->peer_quic_transport_params,
s->s3->peer_quic_transport_params_len);
freezero(s->s3, sizeof(*s->s3)); /////// [2]
s->s3 = NULL;
}
同时,,,,,,,注意[2]处的freezero函数。。。。。
原因在于结构体界说的变动,,,,,,,从sk_X509_pop_free宏的第一参数能够看出一些端倪:
sk_X509_pop_free(s->s3->hs.verified_chain, X509_free);
该版本几个有关结构体的界说如下:
// 3.9.2
struct ssl_st
{
/* protocol version
* (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION, DTLS1_VERSION)
*/
int version;
const SSL_METHOD *method;
/// ...略...
int server; /* are we the server side? - mostly used by SSL_clear*/
struct ssl3_state_st *s3; /* SSLv3 variables */ /////// [1]
struct dtls1_state_st *d1; /* DTLSv1 variables */
/// ...略...
};
typedef struct ssl3_state_st
{
long flags;
unsigned char server_random[SSL3_RANDOM_SIZE];
/// ...略...
int in_read_app_data;
SSL_HANDSHAKE hs; /////// [2]
/// ...略...
} SSL3_STATE;
以及ssl_handshake_st:
typedef struct ssl_handshake_st
{
/*
* Minimum and maximum versions supported for this handshake. These are
* initialised at the start of a handshake based on the method in use
* and the current protocol version configuration.
*/
uint16_t our_min_tls_version;
/// ...略...
/* Certificate chain resulting from X.509 verification. */
STACK_OF(X509) *verified_chain; /////// [1]
SSL_HANDSHAKE_TLS12 tls12;
SSL_HANDSHAKE_TLS13 tls13;
}SSL_HANDSHAKE;
能够看到在[2]处的hs是一个SSL_HANDSHAKE类型的结构体成员变量。。。。。ssl3_free函数里的freezero函数
会把s->s3的堆空间的内容全数设置为零,,,,,,,查看freezero函数及其内部的explicit_bzero的代码即可得知。。。。。
? freezero函数和explicit_bzero函数
// freezero.c
void
freezero(void *ptr, size_t sz)
{
/* This is legal. */
if (ptr == NULL)
return;
explicit_bzero(ptr, sz);
free(ptr);
}
// explicit_bzero.c
void
explicit_bzero(void *buf, size_t len)
{
memset(buf, 0, len); /////// [1]
__explicit_bzero_hook(buf, len);
}
这样,,,,,,,[1]处的memset函数会导致s->s3->hs.verified_chain被赋值为NULL,,,,,,,从而在sk_pop_free函数[1]处时即返回,,,,,,,进而预防了缝隙的产生。。。。。
[1]https://ftp.openbsd.org/pub/OpenBSD/patches/7.2/common/026_ssl.patch.sig
[2]https://www.cvedetails.com/cve/CVE-2023-35784/


京公网安备11010802024551号