前言
Oracle于今年4月19日披露出了一个java的加密算法漏洞,简单来说效果就是绕过ECDSA 签名算法,也就说是这个名白签了。该漏洞允许攻击者随意伪造相关SSL证书或SSL握手包,影响到Java 15、Java 16、Java 17、Java 18等版本,更新到最新版本就可以修复。
环境搭建
首先需要安排一个带有漏洞的java版本环境,这里就不得不吐槽一下Oracle官网,我来来回回找了好久才找到下载历史版本的地方,链接如下。https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
这里我下载的是17.0.2版本的jdk。
漏洞验证
我在漏洞发现者Neil Madden给的poc上小改一下,验证是否存在漏洞。代码如下:
package com.company;
import java.security.*;
public class Main {
public static void main(String[] args) {
String crypto_type[]=new String[]{"NONEwithECDSAinP1363Format","SHA1withECDSAinP1363Format","SHA224withECDSAinP1363Format","SHA256withECDSAinP1363Format","SHA384withECDSAinP1363Format","SHA512withECDSAinP1363Format","SHA3-224withECDSAinP1363Format","SHA3-256withECDSAinP1363Format","SHA3-384withECDSAinP1363Format","SHA3-512withECDSAinP1363Format"};
byte[] blankSignature = new byte[64];
for(int i=0;i<=9;i++) {
try {
Signature sig = Signature.getInstance(crypto_type[i]);
KeyPair keys = KeyPairGenerator.getInstance("EC").generateKeyPair();
sig.initVerify(keys.getPublic());
sig.update("Hello, World".getBytes());
Boolean s = sig.verify(blankSignature);
System.out.println(crypto_type[i]+"have vul ? :"+s);
} catch (Exception e) {
System.out.println(e);
}
}
}
}
使用ECDSA签名的均存在问题(底层使用的是同一个方法)。
漏洞分析
通过调试进入java库,找到具体验证签名的代码。
首先把我们的签名数据切割成了两块,装在数组r和s中。而后将数组中的数据颠倒一下。接着进行转换到IntegerModuloP类型的ri、si变量中。
int lengthE = Math.min(length, digest.length);
byte[] E = new byte[lengthE];
System.arraycopy(digest, 0, E, 0, lengthE);
ArrayUtil.reverse(E);
IntegerModuloP e = orderField.getElement(E);
随后对参数digest 进行拷贝颠倒放入IntegerModuloP类型 变量e中。
int lengthE = Math.min(length, digest.length);
byte[] E = new byte[lengthE];
System.arraycopy(digest, 0, E, 0, lengthE);
ArrayUtil.reverse(E);
IntegerModuloP e = orderField.getElement(E);
而后对si求倒数sInv,求u1、u2的值
u1 = e * sInv
u2 = ri * sInv
java中的实现如下
IntegerModuloP sInv = si.multiplicativeInverse();
ImmutableIntegerModuloP u1 = e.multiply(sInv);
ImmutableIntegerModuloP u2 = ri.multiply(sInv);
到这里问题显现出来了,很明显slnv的值为0最后求出的u1、u2的值也是0。而后根据传入的参数pp算出AffinePoint 类型的变量pub
AffinePoint pub = new AffinePoint(field.getElement(pp.getAffineX()),field.getElement(pp.getAffineY()));
根据u1、u2求出两个值temp1、temp2
byte[] temp1 = new byte[length];
b2a(u1, orderField, temp1);
byte[] temp2 = new byte[length];
b2a(u2, orderField, temp2);
temp1=0,temp2=0 而后算出MutablePoint类型的值p1、p2
MutablePoint p1 = ecOps.multiply(basePoint, temp1);
MutablePoint p2 = ecOps.multiply(pub, temp2);
根据函数名,猜测也是一个乘法的过程。最后使用p1、ri进行运算,判断结果值是否为0。
ecOps.setSum(p1, p2.asAffine());
IntegerModuloP result = p1.asAffine().getX();
result = result.additiveInverse().add(ri);//result=0,ri=0
b2a(result, orderField, temp1);
return ECOperations.allZero(temp1);
当temp1全为0时满足判断条件返回真,也就是签名校验成功。
想要进一步理解其中的理念需要简单理解一下ECDSA签名算法。
ECDSA签名算法
ECDSA工作在加密信息的哈希上面,而不是信息本身。哈希函数的选择由用户自己确定,显然,为了确保安全性,还是需要采用密码级安全的哈希函数。加密信息的哈希需要进行截取以确保哈希的字节长度与子群的阶n相等,被截取的哈希是一个正整数,通常用z来表示。
签名过程如下:
验证签名过程如下:
为了对数字签名进行验证,我需要公钥HA,截取后的哈希z,当然,也需要签名对(r,s),主要分以下三步:
如果r ≡ xp (mod n)则签名是有效的。
仔细观察签名验证过程可知,如果s为0则u1、u2为0,进而P为0,而如果r也为0,则等式两边均为0,满足相等条件。
在实际应用中,我们将签名均置为0即可绕过签名,使其在任何时候均能通过验证。
该漏洞在于没有验证签名的s、r值是否为0,签名过程不会产生r=0,s=0的签名,但在验证过程中签名的值是可被人为修改的,开发者应该考虑到对签名的验证。
结语
java15以上的高版本实际上应用的比较少,感觉用在实战的机会不多。本人的密码水平一般,有什么错误欢迎师傅们指出。
pcat给我说过,在签名算法这方面,可以关注更为健壮的Edwards-Curve Digital Signature Algorithm (EdDSA) ,该算法实现了一种 RFC8032 标准化方案。
参考文献
-
https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/ -
https://zhuanlan.zhihu.com/p/107599962
原文始发于微信公众号(BeFun安全实验室):CVE-2022-21449漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论