1. 简述
用了很久的 ja-netfilter,一直发愁激活码从哪找,最近发现有大佬研究出了怎么生成激活码,看了一下各方源码,输出一下成果。
先把大佬的资料附上来。
- ja-netfilter:一切的源头,通过 java-agent 支持 jvm 插件,实际只是一个调用者。
- plugin-power:号称 RSA 的屠龙者,通过修改 RSA 验证过程的字节码,达到破解的目的。
- jetbra:用来生成 JetBrains 产品插件的激活码。
- ja-netfilter破解某IDE原理分析及生成激活码:讲了 power 插件的原理。
- RSA密码原理详解及算法实现:介绍了 RSA 加密的原理。
2. RSA 简介
这里简单介绍下 RSA 原理,后面会用到。
- 密文 C
- 明文 M
- 公钥 e
- 私钥 d
- n=p*q,p 和 q 是两个很大的质数
那么:
- C=M^e mod n
- M=C^d mod n
可以使用公钥加密、私钥解密,反之亦行,知道这些就行。
3. power 原理介绍
直入主题,power 干了什么?
JetBrains 产品都是使用 java 开发的,激活码验证也是调用的 jdk 方法,power 通过修改 BigInteger 的 BigInteger oddModPow(BigInteger y, BigInteger z)
方法的返回结果影响 RSA 验签结果!
先看看在 java 中实现 RSA 签名和验签的代码:
// 内容
byte[] content = null;
//私钥 用来签名
PrivateKey privateKey = null;
// 公钥 用来检验签名
PublicKey publicKey = null;
// 签名流程
signature.initSign(privateKey);
signature.update(content);
byte[] signatureBytes = signature.sign();
// 验签流程
signature.initVerify(publicKey);
signature.update(content);
boolean valid = signature.verify(signatureBytes);
signature 的 sign 和 verify 方法内部调用了 BigInteger.oddModPow
方法,下面是 jdk8 实现的 sign 方法代码:
// 所属类:sun.security.rsa.RSASignature
// 签名方法
protected byte[] engineSign() throws SignatureException {
byte[] digest = getDigestValue();
try {
byte[] encoded = encodeSignature(digestOID, digest);
byte[] padded = padding.pad(encoded);
//rsa核心方法
byte[] encrypted = RSACore.rsa(padded, privateKey, true);
return encrypted;
} catch (GeneralSecurityException e) {
throw new SignatureException("Could not sign data", e);
} catch (IOException e) {
throw new SignatureException("Could not encode data", e);
}
}
// 验签方法
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
if (sigBytes.length != RSACore.getByteLength(publicKey)) {
throw new SignatureException("Signature length not correct: got " +
sigBytes.length + " but was expecting " +
RSACore.getByteLength(publicKey));
}
byte[] digest = getDigestValue();
try {
// rsa核心方法
byte[] decrypted = RSACore.rsa(sigBytes, publicKey);
byte[] unpadded = padding.unpad(decrypted);
byte[] decodedDigest = decodeSignature(digestOID, unpadded);
return MessageDigest.isEqual(digest, decodedDigest);
} catch (javax.crypto.BadPaddingException e) {
// occurs if the app has used the wrong RSA public key
// or if sigBytes is invalid
// return false rather than propagating the exception for
// compatibility/ease of use
return false;
} catch (IOException e) {
throw new SignatureException("Signature encoding error", e);
}
}
可以看到 verify 方法就是 sign 方法的逆过程,两个都调用了 RSACore.rsa
方法,其代码如下:
// verfiy 方法调用的 rsa 方法
public static byte[] rsa(byte[] msg, RSAPublicKey key)
throws BadPaddingException {
return crypt(msg, key.getModulus(), key.getPublicExponent());
}
private static byte[] crypt(byte[] msg, BigInteger n, BigInteger exp)
throws BadPaddingException {
BigInteger m = parseMsg(msg, n);
// 终于看到 modPow 方法,此方法内部调用了 BigInteger.oddModPow
BigInteger c = m.modPow(exp, n);
return toByteArray(c, getByteLength(n));
}
Power 插件 hook oddModPow 方法目的就很明显了,修改其返回值,达到修改 RSACore.rsa(sigBytes, publicKey) 方法的返回值,从而影响 verfy 方法的结果,最终实现验签通过。
接下来问题的关键就是怎么确定 BigInteger c = m.modPow(exp, n)
中的 c, m,exp,n 的值:
- 按上述代码可以反推出 m 是 sign方法 返回值,就是密文,这个可以事先使用我们自己的私钥签名一个;
- exp,n 是 ide 内置的 public key(CA 机构的公钥) 的指数和模,这个在 power 插件打印出 modPow 的参数值,事先可以收集到;
- c 的值是关键,c 是我们要伪造的值,以便跟 sign 方法的 m 能匹配(因为 m 是我们自己的 private key签名得到的密文,而 exp,n 又是 ide 内置的 public key 并不是我们的 public key, 所以正常情况
m.modPow(exp, n)
算出的 c 并不正确,因为公私钥是不匹配的)
怎么让他们匹配呢,也就是说我们怎么去获取正确的 c,其实答案已经在 sign 方法中了。
我们可以看到在 sign 方法中,也调用了 byte[] encrypted =RSACore.rsa(padded, privateKey,true);
这其中的 encrypted 就是上面的 m 值,padded 就是上面 c 的值,为什么是这样,这其实就是 RSA 的加解密原理。知道这一点后就很容易反推出计算 padded 方法的值,抽取出 sign 方法中计算 padded 的代码如下:
// 计算padded值,values表示要签名的内容,keySize是公钥长度位数
private static byte[] calPadded(byte[] values, int keySize)
throws Exception {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(values);
byte[] bytes = messageDigest.digest();
RSAPadding padding = RSAPadding.getInstance(RSAPadding.PAD_BLOCKTYPE_1, (keySize + 7) >> 3, null);
DerOutputStream out = new DerOutputStream();
new AlgorithmId(AlgorithmId.SHA256_oid).encode(out);
out.putOctetString(bytes);
DerValue result = new DerValue(DerValue.tag_Sequence, out.toByteArray());
return padding.pad(result.toByteArray());
}
至此,c, m,exp,n 4个的值都知道后,就可以在 power 插件中 hook oddModPow 方法,当 m,exp,n 是我们指定的值时就直接返回 c。power 插件的配置文件 power.conf 其实就是配置的这4个值,其格式:m,exp,n->c
Power 插件的原理基本已经讲完,但离怎么生成出可用的激活码还有一些工作要弄清楚。
4. 插件激活码的生成
推测 JetBrain 官方生成激活码的流程如下:
- 生成一个用户证书 B
- 使用自有的 CA 证书私钥对用户证书 B 签名,签名值已经包含在用户证书 B 中
- 生成 license,一个 json 串,包含授权对象、到期时间,授权产品等
- 使用用户证书 B 的私钥对 license 签名,得到 license 签名值
- 将 license,license 签名值、用户证书 B按规则拼接得到一个字符串,这就是激活码
推测 JetBrains 验证激活码流程如下:
- 解析激活码,分别得到 license,license 签名值、用户证书 B
- 使用内置的 CA 证书对用户证书 B 验签,校验是否是一个合法的证书
- 使用用户证书 B,license 签名值对 license 验签,校验是否是一个未被篡改过的 license
- 如果都合法,则授权通过
可以看到,我们可以自己生成用户证书,但显然ide官方是不会给我们的证书进行签名的,好在我们有Power插件这把屠龙刀帮忙,我们可以自己对证书签名,然后通过 Power 插件绕过 JetBrains 对我们证书的验签,也就是验证流程中的第二步。
至此,我们配合 Power 插件伪造激活码的流程已经搞清楚,大致如下:
- 生成自己的用户证书,得到私钥和公钥
- 使用私钥对该证书自签名,得到签名过的证书及签名值,也就是 power插件中需要用到的 m 值
- 生成 license,可以参照再有 license 生成(这一部分是 base64 编码的,可以直接解码查看)
- 使用自己的用户证书的私钥对 license 签名,得到 license 签名值
- 将 license,license 签名值、用户证书B按规则拼接就得到激活码
大致源码如下
public Map<String, Object> generateLicense(License license) {
String licenseId = generateLicenseId();
license.setLicenseId(licenseId);
String licensePart = MAPPER.writeValueAsString(license);
byte[] licensePartBytes = licensePart.getBytes(StandardCharsets.UTF_8);
String licensePartBase64 = Base64.getEncoder().encodeToString(licensePartBytes);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(PRIVATE_KEY);
signature.update(licensePartBytes);
byte[] signatureBytes = signature.sign();
String sigResultsBase64 = Base64.getEncoder().encodeToString(signatureBytes);
String result = licenseId + "-" + licensePartBase64 + "-" + sigResultsBase64 + "-" + Base64.getEncoder().encodeToString(CRT.getEncoded());
return Collections.singletonMap("license", result);
}