6

OpenSSL-CVE-2015-1793漏洞分析 | WooYun知识库

 6 years ago
source link:
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

OpenSSL-CVE-2015-1793漏洞分析

0x00 前言


OpenSSL官方在7月9日发布了编号为 CVE-2015-1793 的交叉证书验证绕过漏洞,其中主要影响了OpenSSL的1.0.1和1.0.2分支。1.0.0和0.9.8分支不受影响。

360安全研究员au2o3t对该漏洞进行了原理上的分析,确认是一个绕过交叉链类型证书验证的高危漏洞,可以让攻击者构造证书来绕过交叉验证,用来形成诸如“中间人”等形式的攻击。

0x01 漏洞基本原理


直接看最简单的利用方法(利用方法包括但不限于此):

攻击者从一公共可信的 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,返回验证成功。

0x02 具体漏洞分析


漏洞代码位于文件: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

代码分析如下

#!c++
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> 处不用删除,而逻辑也是合理并前后一致的。

0x03 漏洞验证


笔者修改了部分代码并做了个Poc 。 修改代码:

#!c++
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

认证失败

0x04 安全建议


建议使用受影响版本(OpenSSL 1.0.2b/1.0.2cOpenSSL 1.0.1n/1.0.1o)的 产品或代码升级OpenSSL到最新版本


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK