pengsx01 发表于 2022-7-29 18:55

DBeaver Ultimate Edition License验证分析

本帖最后由 pengsx01 于 2022-8-2 17:48 编辑

前言看了论坛大佬写的两篇帖子,
DBeaver Ultimate edition JAVA程序逆向分析
DBeaver Ultimate edition 算法分析/本地服务器搭建

研究了一波DBeaver的源码,在这里分享一下我的过程与方法,补充一种绕过网络验证的方法。

测试环境操作系统:Windows10
Java版本:JDK11
软件版本:DBeaver Ultimate 22.1.0

License解密分析下载安装后打开,弹窗提示如下:


点击ImportLicense,随便填写一串字符串,界面会出现以下报错信息:


查看运行日志,日志路径:C:\Users\Niclas\AppData\Roaming\DBeaverData\workspace6\.metadata\dbeaver-debug.log能看到报错信息:


以上能看出导入License调用的方法在:com.dbeaver.lm.embedded.LicenseServiceEmbedded.importProductLicense
根据此处线索去安装目录下找对应的jar包,查找到的jar为安装目录下:DBeaverUltimate\plugins\com.dbeaver.lm.core_2.0.112.202206121739.jar
反编译jar包之后能找到对应的方法源码如下:


分析方法内代码,发现调用了方法:org.jkiss.lm.LMLicenseManager#importLicense(org.jkiss.lm.LMProduct,byte[])
查找到对应的jar包为:DBeaverUltimate\plugins\org.jkiss.lm_1.0.136.202206121739.jar
导入项目中,反编译之后能看到如下代码:


分析此处获取解密Key的方法,可以看到如下源码:


这里可以看出密钥来自于某一个jar包根目录下的keys文件夹中,文件名是以-public.key结尾。
在安装目录中查找后,发现该文件在:plugins\com.dbeaver.app.ultimate_22.1.0.202206121739.jar
如图:


此处也能推出String id =product.getId();中获取的id是:dbeaver-ue
回到方法org.jkiss.lm.LMLicenseManager#importLicense(org.jkiss.lm.LMProduct,byte[]),
继续分析 LMLicense license = new LMLicense(licenseData,decryptionKey);,
查找其解密方法,如图:


可以发现其加解密方式为:RSA/ECB/PKCS1Padding,
在jar包中找到的dbeaver-ue-public.key文件为RAS解密需要的公钥。

License校验分析根据上述解密过程分析,生成相应的License(License的生成方式见下面破解步骤)导入激活,出现如下弹窗:



查看日志文件dbeaver-debug.log,异常信息如下:


从日志可以看出验证的方法在:com.dbeaver.lm.validate.PublicLicenseValidator.validateLicense
根据路径查看该方法的代码,如下:


分析其逻辑,验证的步骤在:result =LMPublicAPI.checkLicenseStatus(monitor,clientId, licenseManager, license, product);,
查看其代码如下:

分析代码,发现StringlicenseStatusText = client.checkLicenseStatus(license, product);
返回的licenseStatusText如果为空,那么该方法将会直接返回LMLicenseStatus.VALID的校验状态(即有效的状态),
继续分析调用的checkLicenseStatus方法,代码如下:


这里能看出请求了网络接口,调用的请求方法为父类中的方法,在安装目录下找到PublicServiceClient的父类在com.dbeaver.remote.client_1.0.2.202206121739.jar中,
在类com.dbeaver.remote.client.AbstractRemoteClient中能看到HTTP请求的客户端构造代码如下:


返回类com.dbeaver.lm.validate.PublicServiceClient中,能看到其构造客户端的代码如下:


能分析出PUBLIC_SERVICE_URL为接口的请求地址,继续追溯该变量的来源,其逻辑如下:


回到获取校验结果StringlicenseStatusText = client.checkLicenseStatus(license, product);的代码,
当请求的路径无法访问时,那么得到的licenseStatusText就为空值了,就能直接得到有效的验证结果。
从上述代码中,能看出DEBUG_MODE为true或者是https://dbeaver.com/lmp/无法访问时,请求结果将会返回空值。

由此得到了两种通过网络校验的方法:
1.       在hosts文件中配置dbeaver.com的IP为127.0.0.1;
2.       在配置文件中设置lm.debug.mode=true

破解思路1.       生成一对RAS加解密使用的公钥和私钥;
2.       将com.dbeaver.app.ultimate_22.1.0.202206121739.jar中的公钥替换成自己生成的公钥;
3.       用自己的私钥生成一个License文件导入到DBEaver中;
4.       绕过License网络校验。

破解步骤1.       查看org.jkiss.lm_1.0.136.202206121739.jar内的代码,发现类org.jkiss.lm.LMMain中已经实现了生成密钥、生成License、解密License、导入License的测试代码,只需稍作修改就可以直接使用;
2.       导入项目需要依赖的jar包,需要依赖的jar包如下图:

3.       创建一个新的类,修改org.jkiss.lm.LMMain中的代码,得到如下代码:
import org.jkiss.lm.*;
import org.jkiss.utils.Base64;

import java.io.*;
import java.security.Key;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;


public class DBeaverLicenseActiveMain {
    private static final LMProduct TEST_PRODUCT;

    static {
      TEST_PRODUCT = new LMProduct(
                "dbeaver-ue",
                "DB",
                "DBeaver Ultimate",
                "DBeaver Ultimate Edition",
                "22.1",
                LMProductType.DESKTOP,
                new Date(),
                new String
      );
    }


    public static void main(String[] args) throws Exception {
      System.out.println("LM 2.0");
      // 生成RAS密钥对
      if (args.length > 0 && args.equals("gen-keys")) {
            System.out.println("Test key generation");
            generateKeyPair();
      // 生成加密License
      } else if (args.length > 0 && args.equals("encrypt-license")) {
            System.out.println("Encrypt license");
            encryptLicense();
      // 解密License
      } else if (args.length > 0 && args.equals("decrypt-license")) {
            System.out.println("Decrypt license");
            decryptLicense();
      // 导入License测试
      } else if (args.length > 0 && args.equals("import-license")) {
            System.out.println("Import license");
            importLicense();
      // 生成明文License
      } else {
            System.out.println("Test license generation");
            generateLicense();
      }
    }

    private static void encryptLicense() throws Exception {
      PrivateKey privateKey = readPrivateKey();
      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      String licenseID = LMUtils.generateLicenseId(TEST_PRODUCT);
      System.out.println("License ID: " + licenseID);
      System.out.println("Product ID (" + TEST_PRODUCT.getId() + "):");
      String productID = in.readLine();
      if (productID.isEmpty()) {
            productID = TEST_PRODUCT.getId();
      }

      System.out.println("Product version (" + TEST_PRODUCT.getVersion() + "):");
      String productVersion = in.readLine();
      if (productVersion.isEmpty()) {
            productVersion = TEST_PRODUCT.getVersion();
      }

      System.out.println("Owner ID (10000):");
      String ownerID = in.readLine();
      if (ownerID.isEmpty()) {
            ownerID = "10000";
      }

      System.out.println("Owner company (JKISS):");
      String ownerCompany = in.readLine();
      if (ownerCompany.isEmpty()) {
            ownerCompany = "JKISS";
      }

      System.out.println("Owner name (Serge Rider):");
      String ownerName = in.readLine();
      if (ownerName.isEmpty()) {
            ownerName = "Serge Rider";
      }

      System.out.println("Owner email (serge@jkiss.org):");
      String ownerEmail = in.readLine();
      if (ownerEmail.isEmpty()) {
            ownerEmail = "serge@jkiss.org";
      }

      LMLicense license = new LMLicense(
                licenseID,
                LMLicenseType.ULTIMATE,
                new Date(),
                new Date(),
                (Date)null,
                LMLicense.FLAG_UNLIMITED_SERVERS,
                productID,
                productVersion,
                ownerID,
                ownerCompany,
                ownerName,
                ownerEmail);
      byte[] licenseData = license.getData();
      byte[] licenseEncrypted = LMEncryption.encrypt(licenseData, privateKey);
      System.out.println("--- LICENSE ---");
      System.out.println(Base64.splitLines(Base64.encode(licenseEncrypted), 76));
    }

    private static void decryptLicense() throws Exception {
      PublicKey publicKey = readPublicKey();
      System.out.println("License:");
      byte[] encryptedLicense = LMUtils.readEncryptedString(System.in);
      LMLicense license = new LMLicense(encryptedLicense, publicKey);
      System.out.println(license);
    }

    private static void importLicense() throws Exception {
      final PrivateKey privateKey = readPrivateKey();
      final PublicKey publicKey = readPublicKey();
      System.out.println("License:");
      byte[] encryptedLicense = LMUtils.readEncryptedString(System.in);
      LMLicenseManager lm = new LMLicenseManager(new LMKeyProvider() {
            public Key getEncryptionKey(LMProduct product) {
                return privateKey;
            }

            public Key getDecryptionKey(LMProduct product) {
                return publicKey;
            }
      }, (LMLicenseValidator)null);
      lm.importLicense(TEST_PRODUCT, encryptedLicense);
    }

    private static void generateKeyPair() throws LMException {
      KeyPair keyPair = LMEncryption.generateKeyPair(2048);
      PublicKey publicKey = keyPair.getPublic();
      PrivateKey privateKey = keyPair.getPrivate();
      System.out.println("--- PUBLIC KEY ---");
      System.out.println(Base64.splitLines(Base64.encode(publicKey.getEncoded()), 76));
      System.out.println("--- PRIVATE KEY ---");
      System.out.println(Base64.splitLines(Base64.encode(privateKey.getEncoded()), 76));
    }

    private static void generateLicense() throws LMException {
      System.out.println("Gen keys");
      KeyPair keyPair = LMEncryption.generateKeyPair(2048);
      PublicKey publicKey = keyPair.getPublic();
      PrivateKey privateKey = keyPair.getPrivate();
      System.out.println("Gen Test license");
      String licenseID = LMUtils.generateLicenseId(TEST_PRODUCT);
      LMLicense license = new LMLicense(
                licenseID,
                LMLicenseType.ULTIMATE,
                new Date(),
                new Date(),
                (Date)null,
                LMLicense.FLAG_UNLIMITED_SERVERS,
                TEST_PRODUCT.getId(),
                TEST_PRODUCT.getVersion(),
                "10000",
                "JKISS",
                "Serge Rider",
                "serge@jkiss.org");
      byte[] subData = license.getData();
      byte[] encrypted = LMEncryption.encrypt(subData, privateKey);
      String encodedBase64 = Base64.splitLines(Base64.encode(encrypted), 76);
      byte[] encodedBinary = Base64.decode(encodedBase64);
      LMLicense licenseCopy = new LMLicense(encodedBinary, publicKey);
      System.out.println(licenseCopy);
      System.out.println("Gen subscription");
      LMSubscription subscription = new LMSubscription(
                licenseID,
                LMSubscriptionPeriod.MONTH,
                new Date(),
                new Date(),
                1,
                true);
      subData = LMEncryption.encrypt(subscription.getData(), privateKey);
      String subBase64 = Base64.splitLines(Base64.encode(subData), 76);
      byte[] subBinary = Base64.decode(subBase64);
      LMSubscription subCopy = new LMSubscription(subBinary, publicKey);
      System.out.println(subCopy);
    }

    private static PrivateKey readPrivateKey() throws LMException {
      File keyFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "private-key.txt");
      if (!keyFile.exists()) {
            throw new LMException("Cannot find private key file (" + keyFile.getAbsolutePath() + ")");
      } else {
            try {
                Throwable var1 = null;
                Object var2 = null;

                try {
                  InputStream keyStream = new FileInputStream(keyFile);

                  PrivateKey var10000;
                  try {
                        byte[] privateKeyData = LMUtils.readEncryptedString(keyStream);
                        var10000 = LMEncryption.generatePrivateKey(privateKeyData);
                  } finally {
                        if (keyStream != null) {
                            keyStream.close();
                        }

                  }

                  return var10000;
                } catch (Throwable var12) {
                  if (var1 == null) {
                        var1 = var12;
                  } else if (var1 != var12) {
                        var1.addSuppressed(var12);
                  }
                  throw var1;
                }
            } catch (Throwable var13) {
                throw new LMException(var13);
            }
      }
    }

    private static PublicKey readPublicKey() throws LMException {
      File keyFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "public-key.txt");
      if (!keyFile.exists()) {
            throw new LMException("Cannot find public key file (" + keyFile.getAbsolutePath() + ")");
      } else {
            try {
                Throwable var1 = null;
                try {
                  InputStream keyStream = new FileInputStream(keyFile);

                  PublicKey var10000;
                  try {
                        byte[] keyData = LMUtils.readEncryptedString(keyStream);
                        var10000 = LMEncryption.generatePublicKey(keyData);
                  } finally {
                        if (keyStream != null) {
                            keyStream.close();
                        }

                  }

                  return var10000;
                } catch (Throwable var12) {
                  if (var1 == null) {
                        var1 = var12;
                  } else if (var1 != var12) {
                        var1.addSuppressed(var12);
                  }

                  throw var1;
                }
            } catch (Throwable var13) {
                throw new LMException(var13);
            }
      }
    }
}

4.       上述代码传入参数gen-keys生成密钥对如下:
PlainText
--- PUBLIC KEY ---
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhXxuid2ALfSF68yUrH0H3e2eJ0S9MfJu
Vil1r03M/JXTu5FgjTyUf1Q5KJnXSLaxiibtHjAJUXRk10C9iks7FFNeOP8diDL2KcbPTSdy7iap
CYAaxZilIRIq0Ab/Zhi+HwPh9wuJk/6XEgoUIC1eiKj0aVMz25G5udXHtIx8OjavH2eoFdlOpDwm
6ZMj1ZSbkY50cnOb6eJv38oKF8fL4b3i3J0vv5lk90NYpVSGFwPs9S2ANsWT1CXXfl3wQOuBHOfL
KToJN9V5fqMgiAoPNT8BvO5NMMJ8d9MKQD4rtqq3S40E7iIZ/fN51h/kQ0ynCHXdsdze48jQ+quS
sifHTwIDAQAB
--- PRIVATE KEY ---
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCFfG6J3YAt9IXrzJSsfQfd7Z4n
RL0x8m5WKXWvTcz8ldO7kWCNPJR/VDkomddItrGKJu0eMAlRdGTXQL2KSzsUU144/x2IMvYpxs9N
J3LuJqkJgBrFmKUhEirQBv9mGL4fA+H3C4mT/pcSChQgLV6IqPRpUzPbkbm51ce0jHw6Nq8fZ6gV
2U6kPCbpkyPVlJuRjnRyc5vp4m/fygoXx8vhveLcnS+/mWT3Q1ilVIYXA+z1LYA2xZPUJdd+XfBA
64Ec58spOgk31Xl+oyCICg81PwG87k0wwnx30wpAPiu2qrdLjQTuIhn983nWH+RDTKcIdd2x3N7j
yND6q5KyJ8dPAgMBAAECggEATJu5JM5GfhlTspxaxxOKrEdu+MJugnfL8w8gR1ezSVMDjSZF70jR
QLIpi6+e6lBPXCYy95xB/Ml8Bj1VikTaxzOBY9ymKkB1HkzHNFRrlVoCsT0gID8WpgAzKeiaMxII
KuyjhpDMiG8YbHX0TvM6yduNSdVCccUUfh6+2lO2CAH1fRT+FJqEI8tUGbuB16YvM6t/mNjtbOo1
dSsRacc/7fV3vPP7a3kqc0PHpIDAyuKcLWn1HwEzBeAgp/TlX9J1bU8WcijKQBLcrxYmxAqDZOPD
imcV0XfKs6I2JUHEePUHiAoG59BhVGA/rJnkyQEpaD3mKkFImIIKm6poLvzboQKBgQD1iLWE4gKe
g/5TUAFO/aMhiMQG3vP410eBoWHZnvQt72VKX6hlgGwvZld1UF7hqljK1ICvvwFe2aGWDJAk5Zz5
0ipPj1UVkk5FsLoi/YT3Pj8tsNT0xJrilXDlpYAEsecbqvBs5QBGBKH+4QkFoCvCb6qWuDWQMNYJ
Ja/Su97nLQKBgQCLLQtqRutmw96XzkfV9yuqQ9nzk7Z7CfM00O4l0jP0tKTXomuSW1nRiYc3UfZJ
cJfPR91y40N4v7A8DaiReH5T5F6Mt6gnnSmRt/J6vE5tz3lODnoaUVOmjEpj7ytQdtr2xUYNOnEu
oRH2lPkhUdIBM35EfySpKBu7yWfc3ap16wKBgQDGZzKububI6kWvUp3ME34nUdl859njASph4GMu
M5iCKckCgSuU4WIKJzuSq2AQH9NiCrb1zHUyDM/abMppVjUzVZUk9uA87x1aiQTP02YHV4A7zoE2
TEwPvcwddU9t+8eQ/t8KTz2aVpIEYBknN5dEpXEGG1IE8sFxYMejlHX4/QKBgECo/NSzfkqQVapR
vC48V50TSP9RcUZYqRWwu/P2ZQ0boDpOy4uDxYcETj31ZmdYWC+FQ+1MiNxgspA0CE0NniN7xjG6
YfWFnvqEa7N6KTX7XnBVaYUwo5yNMUKcq5MGpVRg8trSfCMd0iqtq9E/IkJMmi1YpL+yUrA8MnT6
x2dhAoGBALSWWmS3fBhUi55YwnmxMGcZKRw3SR4qvgfMVsXx/wraLg6HdHYv5eugQ5JimqlAw6Bv
B6EIBnhrWT41s1uLkVrD3bYvOT5dKlgHmNUuosZdTkAZQAptiPq0tNnXJ5N++4NIf4vmTgbC4OhG
b5eH0TuNW8cBSErqYoCf/tVrjVTq
5.       创建文件public-key.txt,private-key.txt,dbeaver-ue-public.key,将私钥复制到private-key.txt中,将公钥复制到public-key.txt和dbeaver-ue-public.key中;
6.       将public-key.txt和private-key.txt文件放到用户目录下的.jkiss-lm文件夹中,用dbeaver-ue-public.key文件替换com.dbeaver.app.ultimate_22.1.0.202206121739.jar内的keys\dbeaver-ue-public.key文件;
7.       Java代码中传入参数encrypt-license,生成License字符串,如下:
PlainText
ReJalXttbssteHQ63re1sGcW2XbP4emJiJxLGaa/JEXXcRiuUzwfwAHh5gUWD+blYrIVKy4Ibx9k
xm6XcIlg4X1eWI4UrlTep+D5l2cKSqw63Hzi8hyau8H9OFfPe9PHig1Kla+u4fQCkn7AidqZPkV7
QVK2F++ZIUcPmc+qEkm3suOEtRgEKKBfsZpHTg+CUrUb37DlEz6qWnOI+5hy95B3z892TTJzkARc
HOhSdFM/1/q4WMUtfjcVcav7x7qiFDIfuWQsMNASVMxBJtbKsKv9tqc+2ExGV+9bZBHls7JehGOW
bG7YOpoZA49Ha6DnWlhoWAeFizL6zFLbH8+tZQ==
8.       修改配置文件dbeaver.ini,增加lm.debug.mode=true配置,如下:
PlainText
-startup
plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.2.400.v20211117-0650
-vmargs
-XX:+IgnoreUnrecognizedVMOptions
--add-modules=ALL-SYSTEM
-Dosgi.requiredJavaVersion=11
-Xms128m
-Xmx2048m
-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT
-Ddbeaver.distribution.type=exe
-Dlm.debug.mode=true

9.       打开DBeaver,导入License,如图:



后记仅供研究学习使用,请勿用于非法用途,请支持正版!

moresun 发表于 2024-3-28 18:22

TonyLiu 发表于 2024-3-28 16:26
老哥可以贴下代码吗?


import com.dbeaver.lm.api.LMEncryption;
import com.dbeaver.lm.api.LMException;
import com.dbeaver.lm.api.LMLicense;
import com.dbeaver.lm.api.LMLicenseType;
import com.dbeaver.lm.api.LMProduct;
import com.dbeaver.lm.api.LMProductType;
import com.dbeaver.lm.api.LMUtils;
import org.jkiss.utils.Base64;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
* 本代码依赖 2 个 jar 包
* <ul>
*   <li>${DBEAVER_ECLIPSE_HOME}/plugins/org.jkiss.utils_**.jar</li>
*   <li>${DBEAVER_ECLIPSE_HOME}/plugins/com.dbeaver.lm.**.jar</li>
* </ul>
*/
public class DBeaverLicenseCrack {
    private static String productID = "dbeaver-ue";
    private static String productVersion = "24.0.0";
    private static String ownerID = "10006";
    private static String ownerCompany = "DBeaver Corporation";
    private static String ownerName = "Ultimate User";
    private static String ownerEmail = "developer@dbeaver.com";
    /**
   * 根据当前版本修改实际jar包名称
   * 需要替换公钥的文件 (com.dbeaver.app.ultimate_**.jar)
   */
    private static String replaceJarFile = "com.dbeaver.app.ultimate_24.0.0.202403110838.jar";

    private static File privateKeyFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "private-key.txt");
    private static File publicKeyFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "public-key.txt");
    private static File licenseFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "license.txt");
    /**
   * 使用该文件,需要替换 jar 包中的 key 文件(请保留原始 jar 包), 并在 dbeaver.ini 添加参数-Dlm.debug.mode=true
   * <p>
   * 如果修改 jar 包中的 plugin.xml 的 product 节点添加 <property name="lmServerURL" value="http://localhost:7879"/> 需要一个授权服务器
   *   <ul>
   *         <li>Mac 版本的 jar 包路径是 ${DBEAVER_ECLIPSE_HOME}/plugins/com.dbeaver.app.ultimate_23.1.0.202306120813.jar</li>
   *         <li>Mac 版本的配置文件路径是 ${DBEAVER_ECLIPSE_HOME}/dbeaver.ini</li>
   *   </ul>
   * </p>
   */
    private static File jarPublicKey = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "dbeaver-ue-public.key");

    private static final String DBEAVER_ECLIPSE_HOME = "/Applications/DBeaverUltimate.app/Contents/Eclipse";

    public static void main(String[] args) throws Exception {
      String dbeaverPluginDir = DBEAVER_ECLIPSE_HOME + File.separator + "plugins";
      // 需要替换公钥的文件 (com.dbeaver.app.ultimate_**.jar)
      List<File> matchingFiles = findFilesByPattern(dbeaverPluginDir, replaceJarFile);
      if (matchingFiles.size() != 1) {
            throw new IOException("未找到需要替换的 jar 文件");
      }
      String dbeaverJar = matchingFiles.get(0).getAbsolutePath();
      generateKeyLicenseAndPatch(dbeaverJar, "keys/dbeaver-ue-public.key");
      String iniFile = DBEAVER_ECLIPSE_HOME + File.separator + "dbeaver.ini";
      updateEclipseIniFile(iniFile, "-Dlm.debug.mode=true");
    }

    /**
   * 在文件中追加一行配置文本(已经存在则跳过)
   *
   * @Param iniFile
   * @param configLine
   * @throws IOException
   */
    public static void updateEclipseIniFile(String iniFile, String configLine) throws IOException {
      boolean targetLineFound = false;

      // 读取 ini 文件
      File file = new File(iniFile);
      FileReader fr = new FileReader(file);
      BufferedReader br = new BufferedReader(fr);

      String line;
      StringBuilder content = new StringBuilder();
      while ((line = br.readLine()) != null) {
            content.append(line).append(System.lineSeparator());
            if (line.trim().equals(configLine)) {
                targetLineFound = true;
            }
      }
      br.close();

      // 如果未找到目标行,在内容的末尾追加文本
      if (!targetLineFound) {
            content.append(configLine).append(System.lineSeparator());

            // 将更新的内容写回 ini 文件
            FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(content.toString());
            bw.close();
      }
    }

    /**
   * 通过正则表达匹配,搜索匹配的文件
   *
   * @param directoryPath 目录
   * @param pattern       文件匹配正则
   * @return
   */
    public static List<File> findFilesByPattern(String directoryPath, String pattern) {
      List<File> matchingFiles = new ArrayList<>();
      File directory = new File(directoryPath);

      if (directory.exists() && directory.isDirectory()) {
            File[] files = directory.listFiles((dir, name) -> name.matches(pattern));

            if (files != null) {
                for (File file : files) {
                  if (file.isFile()) {
                        matchingFiles.add(file);
                  }
                }
            }
      } else {
            System.out.println("无效的目录,或者找不到目录路径!");
      }

      return matchingFiles;
    }

    /**
   * 生成公钥、私钥、授权码,并替换文件
   *
   * @param jarFilePath   需要替换公钥的 jar 文件
   * @param filePathInJar jar 包中的公钥文件路径
   * @throws LMException
   * @throws IOException
   */
    public static void generateKeyLicenseAndPatch(String jarFilePath, String filePathInJar) throws LMException, IOException {
      // 从 LMMain.generateKeyPair() 复制的代码
      KeyPair keyPair = LMEncryption.generateKeyPair(2048);
      PublicKey publicKey = keyPair.getPublic();
      PrivateKey privateKey = keyPair.getPrivate();
      System.out.println("\n--- PUBLIC KEY ---");
      String publicKeyContent = Base64.splitLines(Base64.encode(publicKey.getEncoded()), 76);
      System.out.println(publicKeyContent);
      writeFileToPath(publicKeyContent, publicKeyFile, "公钥");
      System.out.println("\n--- PRIVATE KEY ---");
      String privateKeyContent = Base64.splitLines(Base64.encode(privateKey.getEncoded()), 76);
      System.out.println(privateKeyContent);
      writeFileToPath(publicKeyContent, privateKeyFile, "私钥");
      writeFileToPath(publicKeyContent, jarPublicKey, "公钥");

      // 替换 jar 包中的 public key 文件
      replaceFileInJar(jarFilePath, filePathInJar, jarPublicKey.getAbsolutePath());

      // 从 LMMain.encryptLicense() 复制的代码
      LMProduct TEST_PRODUCT = new LMProduct("dbeaver-ue", "DB", "DBeaver Ultimate", "DBeaver Ultimate Edition", productVersion, LMProductType.DESKTOP, new Date(), new String);
      String licenseID = LMUtils.generateLicenseId(TEST_PRODUCT);
      LMLicense license = new LMLicense(licenseID, LMLicenseType.ULTIMATE, new Date(), new Date(), (Date) null, LMLicense.FLAG_UNLIMITED_SERVERS, productID, productVersion, ownerID, ownerCompany, ownerName, ownerEmail);
      byte[] licenseData = license.getData();
      byte[] licenseEncrypted = LMEncryption.encrypt(licenseData, privateKey);

      String licenseContent = Base64.splitLines(Base64.encode(licenseEncrypted), 76);
      System.out.println("\n--- LICENSE ---");
      System.out.println(licenseContent);
      System.out.println("--- 处理完成,请打开软件使用以上授权码(已复制到剪切板) ---");
      copyToClipboard(licenseContent);
      writeFileToPath(licenseContent, licenseFile, "授权码");
    }

    /**
   * 写入并覆盖文件内容
   *
   * @param content写入覆盖的内容
   * @param filePath 要写入的文件路径
   * @param tip      提示
   */
    public static void writeFileToPath(String content, File filePath, String tip) {
      try {
            Path path = Paths.get(filePath.getAbsolutePath());
            Files.write(path, content.getBytes());
            System.out.println(String.format("[%s内容]已成功写入文件:%s", tip, path));
      } catch (IOException e) {
            e.printStackTrace();
      }
    }

    /**
   * 替换 jar 包中的指定路径的文件为新文件
   *
   * @param jarFilePath
   * @param targetFile
   * @param newFile
   * @throws IOException
   */
    public static void replaceFileInJar(String jarFilePath, String targetFile, String newFile) throws IOException {
      File tempFile = File.createTempFile("temp-file", ".tmp");
      File tempJarFile = File.createTempFile("temp-jar", ".tmp");

      boolean replaceSuccess = false;
      try (JarFile jarFile = new JarFile(new File(jarFilePath));
             JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(tempJarFile))) {

            // Copy the existing entries to the temp jar, excluding the entry to be replaced
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();

                if (!entry.getName().equals(targetFile)) {
                  tempJarOutputStream.putNextEntry(new JarEntry(entry.getName()));
                  try (InputStream inputStream = jarFile.getInputStream(entry)) {
                        copyStream(inputStream, tempJarOutputStream);
                  }
                }
            }

            // 将新文件添加到临时jar
            tempJarOutputStream.putNextEntry(new JarEntry(targetFile));
            try (InputStream newFileStream = new FileInputStream(newFile)) {
                copyStream(newFileStream, tempJarOutputStream);
            }
            replaceSuccess = true;
      } finally {
            // 将原始 jar 文件替换为临时 jar 文件
            File originalJarFile = new File(jarFilePath);
            if (originalJarFile.delete() && tempJarFile.renameTo(originalJarFile) && replaceSuccess) {
                System.out.println(String.format("jar包中的文件 [%s] 替换成功!", targetFile));
            } else {
                System.out.println(String.format("jar包中的文件 [%s] 替换失败!", targetFile));
            }

            tempFile.delete();
            tempJarFile.delete();
      }
    }

    private static void copyStream(InputStream in, OutputStream out) throws IOException {
      byte[] buffer = new byte;
      int bytesRead;
      while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
      }
    }

    /**
   * 将指定字符串复制到剪切板
   *
   * @param text
   */
    public static void copyToClipboard(String text) {
      StringSelection stringSelection = new StringSelection(text);
      Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
      clipboard.setContents(stringSelection, null);
    }
}

moresun 发表于 2024-3-21 13:48

最新版本24.0.0需要调整一下引入的jar依赖 。原来的 org.jkiss.lm_**.jar 改为 com.dbeaver.lm.**.jar 然后修改一下import 路径即可,其他不变。已成功激活

pengxinkui 发表于 2023-7-29 23:59

成功了,感谢楼主分享,核心其实就是 LMMain.generateKeyPair() 方法生成密钥对,调用 LMMain.encryptLicense() 生成授权码。
以下是使用 main 方法直接生成并替换的代码(Mac 23.1.0):
项目需要 JDK 11 和 2 个 jar 包

package src;

import org.jkiss.lm.LMEncryption;
import org.jkiss.lm.LMException;
import org.jkiss.lm.LMLicense;
import org.jkiss.lm.LMLicenseType;
import org.jkiss.lm.LMProduct;
import org.jkiss.lm.LMProductType;
import org.jkiss.lm.LMUtils;
import org.jkiss.utils.Base64;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
* 本代码依赖 2 个 jar 包
* <ul>
*   <li>${DBEAVER_ECLIPSE_HOME}/plugins/org.jkiss.utils_**.jar</li>
*   <li>${DBEAVER_ECLIPSE_HOME}/plugins/org.jkiss.lm_**.jar</li>
* </ul>
*/
public class DBeaverLicenseCrack {
    private static String productID = "dbeaver-ue";
    private static String productVersion = "23.1.0";
    private static String ownerID = "10006";
    private static String ownerCompany = "DBeaver Corporation";
    private static String ownerName = "Ultimate User";
    private static String ownerEmail = "developer@dbeaver.com";

    private static File privateKeyFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "private-key.txt");
    private static File publicKeyFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "public-key.txt");
    private static File licenseFile = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "license.txt");
    /**
   * 使用该文件,需要替换 jar 包中的 key 文件(请保留原始 jar 包), 并在 dbeaver.ini 添加参数-Dlm.debug.mode=true
   * <p>
   * 如果修改 jar 包中的 plugin.xml 的 product 节点添加 <property name="lmServerURL" value="http://localhost:7879"/> 需要一个授权服务器
   *   <ul>
   *         <li>Mac 版本的 jar 包路径是 ${DBEAVER_ECLIPSE_HOME}/plugins/com.dbeaver.app.ultimate_23.1.0.202306120813.jar</li>
   *         <li>Mac 版本的配置文件路径是 ${DBEAVER_ECLIPSE_HOME}/dbeaver.ini</li>
   *   </ul>
   * </p>
   */
    private static File jarPublicKey = new File(new File(System.getProperty("user.home"), ".jkiss-lm"), "dbeaver-ue-public.key");

    private static final String DBEAVER_ECLIPSE_HOME = "/Applications/DBeaverUltimate.app/Contents/Eclipse";

    public static void main(String[] args) throws Exception {
      String dbeaverPluginDir = DBEAVER_ECLIPSE_HOME + File.separator + "plugins";
      // 需要替换公钥的文件 (com.dbeaver.app.ultimate_**.jar)
      String replaceJarFile = "com\\.dbeaver\\.app\\.ultimate_.+\\.jar";
      List<File> matchingFiles = findFilesByPattern(dbeaverPluginDir, replaceJarFile);
      if (matchingFiles.size() != 1) {
            throw new IOException("未找到需要替换的 jar 文件");
      }
      String dbeaverJar = matchingFiles.get(0).getAbsolutePath();
      generateKeyLicenseAndPatch(dbeaverJar, "keys/dbeaver-ue-public.key");
      String iniFile = DBEAVER_ECLIPSE_HOME + File.separator + "dbeaver.ini";
      updateEclipseIniFile(iniFile, "-Dlm.debug.mode=true");
    }

    /**
   * 在文件中追加一行配置文本(已经存在则跳过)
   *
   * @Param iniFile
   * @param configLine
   * @throws IOException
   */
    public static void updateEclipseIniFile(String iniFile, String configLine) throws IOException {
      boolean targetLineFound = false;

      // 读取 ini 文件
      File file = new File(iniFile);
      FileReader fr = new FileReader(file);
      BufferedReader br = new BufferedReader(fr);

      String line;
      StringBuilder content = new StringBuilder();
      while ((line = br.readLine()) != null) {
            content.append(line).append(System.lineSeparator());
            if (line.trim().equals(configLine)) {
                targetLineFound = true;
            }
      }
      br.close();

      // 如果未找到目标行,在内容的末尾追加文本
      if (!targetLineFound) {
            content.append(configLine).append(System.lineSeparator());

            // 将更新的内容写回 ini 文件
            FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(content.toString());
            bw.close();
      }
    }

    /**
   * 通过正则表达匹配,搜索匹配的文件
   *
   * @param directoryPath 目录
   * @param pattern       文件匹配正则
   * @return
   */
    public static List<File> findFilesByPattern(String directoryPath, String pattern) {
      List<File> matchingFiles = new ArrayList<>();
      File directory = new File(directoryPath);

      if (directory.exists() && directory.isDirectory()) {
            File[] files = directory.listFiles((dir, name) -> name.matches(pattern));

            if (files != null) {
                for (File file : files) {
                  if (file.isFile()) {
                        matchingFiles.add(file);
                  }
                }
            }
      } else {
            System.out.println("无效的目录,或者找不到目录路径!");
      }

      return matchingFiles;
    }

    /**
   * 生成公钥、私钥、授权码,并替换文件
   *
   * @param jarFilePath   需要替换公钥的 jar 文件
   * @param filePathInJar jar 包中的公钥文件路径
   * @throws LMException
   * @throws IOException
   */
    public static void generateKeyLicenseAndPatch(String jarFilePath, String filePathInJar) throws LMException, IOException {
      // 从 LMMain.generateKeyPair() 复制的代码
      KeyPair keyPair = LMEncryption.generateKeyPair(2048);
      PublicKey publicKey = keyPair.getPublic();
      PrivateKey privateKey = keyPair.getPrivate();
      System.out.println("\n--- PUBLIC KEY ---");
      String publicKeyContent = Base64.splitLines(Base64.encode(publicKey.getEncoded()), 76);
      System.out.println(publicKeyContent);
      writeFileToPath(publicKeyContent, publicKeyFile, "公钥");
      System.out.println("\n--- PRIVATE KEY ---");
      String privateKeyContent = Base64.splitLines(Base64.encode(privateKey.getEncoded()), 76);
      System.out.println(privateKeyContent);
      writeFileToPath(publicKeyContent, privateKeyFile, "私钥");
      writeFileToPath(publicKeyContent, jarPublicKey, "公钥");

      // 替换 jar 包中的 public key 文件
      replaceFileInJar(jarFilePath, filePathInJar, jarPublicKey.getAbsolutePath());

      // 从 LMMain.encryptLicense() 复制的代码
      LMProduct TEST_PRODUCT = new LMProduct("dbeaver-ue", "DB", "DBeaver Ultimate", "DBeaver Ultimate Edition", "23.1", LMProductType.DESKTOP, new Date(), new String);
      String licenseID = LMUtils.generateLicenseId(TEST_PRODUCT);
      LMLicense license = new LMLicense(licenseID, LMLicenseType.ULTIMATE, new Date(), new Date(), (Date) null, LMLicense.FLAG_UNLIMITED_SERVERS, productID, productVersion, ownerID, ownerCompany, ownerName, ownerEmail);
      byte[] licenseData = license.getData();
      byte[] licenseEncrypted = LMEncryption.encrypt(licenseData, privateKey);

      String licenseContent = Base64.splitLines(Base64.encode(licenseEncrypted), 76);
      System.out.println("\n--- LICENSE ---");
      System.out.println(licenseContent);
      System.out.println("--- 处理完成,请打开软件使用以上授权码(已复制到剪切板) ---");
      copyToClipboard(licenseContent);
      writeFileToPath(licenseContent, licenseFile, "授权码");
    }

    /**
   * 写入并覆盖文件内容
   *
   * @param content写入覆盖的内容
   * @param filePath 要写入的文件路径
   * @param tip      提示
   */
    public static void writeFileToPath(String content, File filePath, String tip) {
      try {
            Path path = Paths.get(filePath.getAbsolutePath());
            Files.write(path, content.getBytes());
            System.out.println(String.format("[%s内容]已成功写入文件:%s", tip, path));
      } catch (IOException e) {
            e.printStackTrace();
      }
    }

    /**
   * 替换 jar 包中的指定路径的文件为新文件
   *
   * @param jarFilePath
   * @param targetFile
   * @param newFile
   * @throws IOException
   */
    public static void replaceFileInJar(String jarFilePath, String targetFile, String newFile) throws IOException {
      File tempFile = File.createTempFile("temp-file", ".tmp");
      File tempJarFile = File.createTempFile("temp-jar", ".tmp");

      boolean replaceSuccess = false;
      try (JarFile jarFile = new JarFile(new File(jarFilePath));
             JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(tempJarFile))) {

            // Copy the existing entries to the temp jar, excluding the entry to be replaced
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();

                if (!entry.getName().equals(targetFile)) {
                  tempJarOutputStream.putNextEntry(new JarEntry(entry.getName()));
                  try (InputStream inputStream = jarFile.getInputStream(entry)) {
                        copyStream(inputStream, tempJarOutputStream);
                  }
                }
            }

            // 将新文件添加到临时jar
            tempJarOutputStream.putNextEntry(new JarEntry(targetFile));
            try (InputStream newFileStream = new FileInputStream(newFile)) {
                copyStream(newFileStream, tempJarOutputStream);
            }
            replaceSuccess = true;
      } finally {
            // 将原始 jar 文件替换为临时 jar 文件
            File originalJarFile = new File(jarFilePath);
            if (originalJarFile.delete() && tempJarFile.renameTo(originalJarFile) && replaceSuccess) {
                System.out.println(String.format("jar包中的文件 [%s] 替换成功!", targetFile));
            } else {
                System.out.println(String.format("jar包中的文件 [%s] 替换失败!", targetFile));
            }

            tempFile.delete();
            tempJarFile.delete();
      }
    }

    private static void copyStream(InputStream in, OutputStream out) throws IOException {
      byte[] buffer = new byte;
      int bytesRead;
      while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
      }
    }

    /**
   * 将指定字符串复制到剪切板
   *
   * @param text
   */
    public static void copyToClipboard(String text) {
      StringSelection stringSelection = new StringSelection(text);
      Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
      clipboard.setContents(stringSelection, null);
    }
}

WuAiKeLe 发表于 2022-8-2 08:05

很详细,每天都在用社区版.真没想过要研究那么深,顶楼主的研究精神.

daiqing 发表于 2022-8-2 08:27

Dlm.debug.mode=true 好,https验证一直没成功

yuwan1994 发表于 2022-8-2 10:51

这个思路不错,可惜不会java,要不然可以研究下

tianyulouzhu 发表于 2022-8-3 11:21

不会java ..... 按照教程成功了,免费评分冷却ing...

goastship 发表于 2022-8-3 21:49

看了楼主的讲解,明白了两种通过网络校验的方法的来由,非常感谢.

Airs_Lau 发表于 2022-8-4 10:04

感谢大佬的分析:lol

AwesomeOne 发表于 2022-8-5 10:39

感谢楼主分享!

lijingboforever 发表于 2022-8-5 11:14

学习了,那是不是 全家桶都可以按照这个思路进行反编译和破解了

pengsx01 发表于 2022-8-5 11:38

lijingboforever 发表于 2022-8-5 11:14
学习了,那是不是 全家桶都可以按照这个思路进行反编译和破解了

那个文件太多了,难度高了一大截{:301_1008:}
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: DBeaver Ultimate Edition License验证分析