1. 简述

用了很久的 ja-netfilter,一直发愁激活码从哪找,最近发现有大佬研究出了怎么生成激活码,看了一下各方源码,输出一下成果。

先把大佬的资料附上来。

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 的值:

  1. 按上述代码可以反推出 m 是 sign方法 返回值,就是密文,这个可以事先使用我们自己的私钥签名一个;
  2. exp,n 是 ide 内置的 public key(CA 机构的公钥) 的指数和模,这个在 power 插件打印出 modPow 的参数值,事先可以收集到;
  3. 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 官方生成激活码的流程如下:

  1. 生成一个用户证书 B
  2. 使用自有的 CA 证书私钥对用户证书 B 签名,签名值已经包含在用户证书 B 中
  3. 生成 license,一个 json 串,包含授权对象、到期时间,授权产品等
  4. 使用用户证书 B 的私钥对 license 签名,得到 license 签名值
  5. 将 license,license 签名值、用户证书 B按规则拼接得到一个字符串,这就是激活码

推测 JetBrains 验证激活码流程如下:

  1. 解析激活码,分别得到 license,license 签名值、用户证书 B
  2. 使用内置的 CA 证书对用户证书 B 验签,校验是否是一个合法的证书
  3. 使用用户证书 B,license 签名值对 license 验签,校验是否是一个未被篡改过的 license
  4. 如果都合法,则授权通过

可以看到,我们可以自己生成用户证书,但显然ide官方是不会给我们的证书进行签名的,好在我们有Power插件这把屠龙刀帮忙,我们可以自己对证书签名,然后通过 Power 插件绕过 JetBrains 对我们证书的验签,也就是验证流程中的第二步。

至此,我们配合 Power 插件伪造激活码的流程已经搞清楚,大致如下:

  1. 生成自己的用户证书,得到私钥和公钥
  2. 使用私钥对该证书自签名,得到签名过的证书及签名值,也就是 power插件中需要用到的 m 值
  3. 生成 license,可以参照再有 license 生成(这一部分是 base64 编码的,可以直接解码查看)
  4. 使用自己的用户证书的私钥对 license 签名,得到 license 签名值
  5. 将 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);
}