DBeaver Ultimate edition 算法分析/本地服务器搭建
DBeaver Ultimate edition 算法分析
官方下载地址: https://dbeaver.com/download/ultimate/
当前分析版本号: dbeaver-ue-22.1.0
前记
前几天出了 该软件的 爆破分析; 机票 :
DBeaver Ultimate edition JAVA程序逆向分析
这次来尝试下不一样的...([b]尽量不硬编码源程序) ;
环境
JAVA/javassist/Python
流程:
1.解析license算法, 生成注册器, 生成 publicKey/privateKey/license/subscription
2.替换程序自带的 publicKey
3.配置 host 映射, 搭建本地验证服务器
4.利用第一步生成的 license 注册
WIN10/Mac m1 测试通过
分析
license
首先通过在程序界面 import 一个license ,
查看关键字, 定位到目标 jar包 (也可以查看程序 log 日志, 位置: /Users/artemis/Library/DBeaverData/workspace6/.metadata)
可以看到, 程序先将我们输入到 icense去除空格以及删除‘-’,‘#’这些特殊标记符号之后 base64 解密成字节数组
这里格式化字符串写的挺好,
就是你的license 可以有换行, 可以有 ----PUBLIC-KEY--- 这些文件头, 也可以有 # this is comment 这些注释, 格式化之后会只留下关键的 license
程序操作publicKey/license/subscription 都有这个逻辑; 当然; 这个并不重要..
然后在licenseManager.importLicense 将 字节数组 解析成 注册凭证,
核心的算法都在 importLicense 里面,我们继续看
可以看到, 获取了一个publicKey 用来解密, 既然公钥解密, 那么加密就是私钥了 (这里有用,我们后面会生成密钥,用来注册license)
刚开始以为 publicKey 是走接口获取到的, 毕竟里面有个 URL 什么的;
> 后来发现其实是 jar包里的资源文件;
我们模糊搜索下‘-public.key’, 定位到了这个 jar包: com.dbeaver.app.ultimate_22.1.0.202206121904.jar (这里有用, 后面我们会替换该 publicKey )
推算出 productId = ‘dbeaver-ue’ , 这个应该就是跟着发版来走的; 而且是固定值
众所周知,publicKey 与 privateKey 是 成双成对的, 而且 我们拿不到私钥, 后面只能想办法用我们自己生成的 公钥与私钥了
接着往下看, 拿到publicKey 后,开始解密, ras 解密, 没什么好说的, 关键是解密后的 license 格式:
即使解密成功, 但是 license 格式不对, 也是不行的 ;
STANDARD((byte)0, 218, "Initial basic license format"),
EXTENDED((byte)1, 238, "Extended format with owner email and corporate license info");
if (buffer.capacity() != this.licenseFormat.getEncryptedLength()) {
throw new LMException("Bad " + this.licenseFormat + " license length (" + buffer.capacity() + ")");
可以看到 license 第一位字节为 凭证格式,是个枚举类型, 不同的格式 license 长度是不同的,
后面我们选 STANDARD 这种license的话 , 第一位就是0,并且 license总字节长度必须是 218
后 16为 是 license_id字符串, 在往后是 licenseType,licenseStartTime,licenseEndTime..... 这里不多解析了, 生成的时候按这个规则来就行
有一点注意到事, 必须满足位数要求, 比如 license_id 是16位, 你可以只写10位, 剩下的6为必须用0 或者空填充
可以看到, license 的读取是用的 JAVA 的Buffer 来操作的; 我们生成的话 也可以用 Buffer 来操作;
好; 接下来开始手写 license 的生成,具体代码如下;
// 填充字符串
for (int i = 0; i < 64; i++) {
FILL += " ";
}
// 生成公钥/私钥
KeyPair keyPair = LMEncryption.generateKeyPair(2048);
publicKey = keyPair.getPublic();
privateKey = keyPair.getPrivate();
String pubKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String priKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded());
// license 原始
ByteBuffer buffer = ByteBuffer.allocate(218);
buffer.put((byte) 0);
// licenseId
fillString("this is lice_id", 16, buffer);
// licenseType ULTIMATE('U', "Ultimate license"),
buffer.put((byte) 85); // 'U' = 85
// licenseIssueTime
buffer.putLong(SIMPLE_DATE_FORMAT.parse("2020-01-01").getTime());
// licenseStartTime
buffer.putLong(SIMPLE_DATE_FORMAT.parse("2020-01-01").getTime());
// licenseEndTime
buffer.putLong(SIMPLE_DATE_FORMAT.parse("2099-01-01").getTime());
// flags
buffer.putLong(250L);
// proeudcId
fillString("dbeaver-ue", 16, buffer);
// productVersion
fillString("prod_version", 8, buffer);
// ownerId
fillString("this is owner_id", 16, buffer);
// ownerCompany
fillString("this is ownerCompany", 64, buffer);
// ownerName
fillString("this is ownerName", 64, buffer);
// license 私钥加密
encodeToString = Base64.getEncoder().encodeToString(LMEncryption.encrypt(buffer.array(), privateKey));
// 将 publicKey/privateKey/license 保存到文件
System.out.println("--- PUBLIC KEY ---");
System.out.println(org.jkiss.utils.Base64.splitLines(org.jkiss.utils.Base64.encode(publicKey.getEncoded()), 76));
try (FileWriter fileWriter = new FileWriter(outPath + "dbeaver-ue-public.key")) {
fileWriter.write(org.jkiss.utils.Base64.splitLines(org.jkiss.utils.Base64.encode(publicKey.getEncoded()), 76));
}
try (FileWriter fileWriter = new FileWriter(outPath + "dbeaver-ue-private.key")) {
fileWriter.write(org.jkiss.utils.Base64.splitLines(org.jkiss.utils.Base64.encode(privateKey.getEncoded()), 76));
}
try (FileWriter fileWriter = new FileWriter(outPath + "license.key")) {
fileWriter.write(org.jkiss.utils.Base64.splitLines(org.jkiss.utils.Base64.encode(LMEncryption.encrypt(buffer.array(), privateKey)), 76));
}
// 替换原程序中的公钥
JarFileReplaceUtil.replaceFile(srcPath + "/com.dbeaver.app.ultimate_22.1.0.202206121904.jar", outPath + "dbeaver-ue-public.key", "keys/dbeaver-ue-public.key");
生成之后,可以看到多了这三个文件
然后复制生成的 license.key ,重新启动程序 (替换了 jar包里的 公钥资源文件,所以需要重启) ,
> 注册一下试试;
哎,居然报错了; 跟进去再看看,
发现个坏消息 , 原来除了用 publicKey 解密, 还有一层校验,
程序会会带着 本地解密后的 license信息 请求 服务器,来获取 subscription 订阅信息;
服务器的 数据 我们是没法子整的 , 但是我们也可以 本地搭建一个服务器; 让程序来请求我们自己的本地服务器来验证;
subscription
仔细瞅瞅,看到 接口响应成功的 response 也是有些规则的,示例如下:
response = "VALID:SUCCESS" + "\r\n" + "-- SUBSCRIPTION" + "\r\n" + subscription
VALID 与 SUCCESS 为状态, 并且必须用: 切割, 程序例有教研 ,':' 的ascii 为 58;
如果响应文本没有包含':'的话, 会直接报错;
int divPos = licenseStatusString.indexOf(58);
if (divPos == -1) {
var19 = new LicenseCheckResult(GeneralUtils.makeErrorStatus("Bad check status: " + licenseStatusString), LMLicenseStatus.INVALID);
return var19;
}
除了状态, resposne 后面的文本才是 subscription 主体, 其中包含了服务器返回的订阅信息;
如果这里为空 也是会注册失败的;
不过好消息是, subscription 的算法 与 license 如出一辙;只不过 subscription 的 格式有些不一样;
try {
this.format = LMSubscriptionFormat.valueOf(buffer.get());
} catch (Exception var5) {
log.warning("Unsupported subscription format: " + buffer.get(0));
this.format = LMSubscriptionFormat.STANDARD;
}
if (buffer.capacity() != this.format.getEncryptedLength()) {
throw new LMException("Bad " + this.format + " subscription length (" + buffer.capacity() + ")");
} else {
this.licenseId = LMUtils.getStringFromBuffer(buffer, 16);
this.period = LMSubscriptionPeriod.getById((char)buffer.get());
this.periodDays = buffer.getInt();
this.lastRenewDate = LMUtils.getDateFromBuffer(buffer);
this.expirationDate = LMUtils.getDateFromBuffer(buffer);
this.activationDate = LMUtils.getDateFromBuffer(buffer);
this.deactivationDate = LMUtils.getDateFromBuffer(buffer);
this.totalRenewCount = buffer.getInt();
this.active = buffer.get() != 0;
}
public enum LMSubscriptionFormat implements LMSerializeFormat {
STANDARD((byte)0, 59, "Initial subscription format");
}
可以看到, subscription 第一位 LMSubscriptionFormat 固定为 0,
并且 subscription 解密后的字节长度 也固定为 59,
这里要注意 subscription 的 license_id 与 前面 license 的 license_id 必须一致;
在往后面的几个字段也看不懂是啥, 只看到有个过期时间 expirationDate, 别的瞎填写吧;
看懂规则后, 我们按前面生成 license 的地方 顺便把 subscription 也生成一份出来, 代码如下:
// STANDARD((byte)0, 59, "Initial subscription format");
ByteBuffer subscriptionBuffer = ByteBuffer.allocate(59);
subscriptionBuffer.put((byte) 0);
fillString("this is lice_id", 16, subscriptionBuffer);
subscriptionBuffer.put((byte) 89);
subscriptionBuffer.putInt(1);
subscriptionBuffer.putLong(SIMPLE_DATE_FORMAT.parse("2099-01-01").getTime());
subscriptionBuffer.putLong(SIMPLE_DATE_FORMAT.parse("2099-01-01").getTime());
subscriptionBuffer.putLong(SIMPLE_DATE_FORMAT.parse("2099-01-01").getTime());
subscriptionBuffer.putLong(SIMPLE_DATE_FORMAT.parse("2099-01-01").getTime());
subscriptionBuffer.putInt(1);
subscriptionBuffer.put((byte) 1);
subscriptionToString = Base64.getEncoder().encodeToString(LMEncryption.encrypt(subscriptionBuffer.array(), privateKey));
try (FileWriter fileWriter = new FileWriter(outPath + "subscription.key")) {
fileWriter.write(subscriptionToString);
}
System.out.println("subscriptionToString : \r\n" + subscriptionToString);
现在文件都有了, 还差一部, 就是让搭建一个本地服务器用来验证 subscription ;
这里我就用 python 搭了; (简单方便快捷)
然后还需要配置 host 将 ‘dbeaver.com’ 转发到 127.0.0.1 (这个地址是 开charles 之后 将 DBeaver 配置代{过}{滤}理后抓到的)
这里踩了一些坑, 因为 原域名是开启了 SSL 的, 所以我们也必须开始; 但是 java虚拟机又不信任这个SSL证书, 折腾了好久才折腾通过;
具体代码如下: (实际上就提供一个接口, 将我们之前生成的 subscription 返回, 可以顺便把 请求信息记录下)
搭建本地服务器
@app.route('/<re(".*"):path>', methods=['GET', 'POST', 'PUT', "DELETE", "PATCH"])
def welcome(path):
f = open('/Users/artemis/Downloads/keys/subscription.key', 'r')
sub_str = "".join(f.readlines())
return "VALID:SUCCESS" + "\r\n" + "-- SUBSCRIPTION" + "\r\n" + sub_str
if __name__ == "__main__":
'''
# 配置文件mySsl.conf:
# 生成私钥:
openssl genrsa -out server.key 4096
# 生成证书请求文件:
openssl req -new -sha256 -out server.csr -key server.key -config mySsl.conf
# 检查证书申请文件内容:
openssl req -text -noout -in server.csr
# 利用证书请求文件生成证书,执行如下命令:
openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt -extensions req_ext -extfile mySsl.conf
'''
'''
# 配置 JDK 信任证书
keystore=/Users/artemis/Library/Java/JavaVirtualMachines/azul-11.0.13/Contents/Home/lib/security/cacerts
keytool -delete -alias alias_dev -keystore $keystore -storepass changeit
keytool -import -file /Users/artemis/Documents/work_space/python/python_demo/tools/ssl/server.crt -alias alias_dev -storepass changeit -keystore $keystore
keytool -v -list -keystore $keystore -storepass changeit
'''
current_directory = os.path.dirname(os.path.abspath(__file__))
app.run(ssl_context=(
f'{current_directory}/tools/ssl/server.crt',
f'{current_directory}/tools/ssl/server.key'
), host='0.0.0.0', port=443)
配置好 host 文件, 然后我们再次复制 license,重启程序 注册;
可以看到 python web 服务, 收到请求了,请求信息如下:
* Running on https://127.0.0.1:443
* Running on https://172.18.20.57:443 (Press CTRL+C to quit)
{
"method": "GET",
"root": "https://dbeaver.com/",
"full_path": "/lmp/checkLicense?crc=1ae7edc9687e503c9fc6d079a91bca5f&product=dbeaver-ue&version=22.1&license=this%20is%20lice_id",
"headers": [
"X-Referrer: D1XXSUQM97WAZN",
"User-Agent: DBeaver Ultimate 22.1.0 [Mac OS X aarch64]",
"Host: dbeaver.com",
"Connection: Keep-Alive",
"Accept-Encoding: gzip",
"",
""
],
"args": {
"crc": "1ae7edc9687e503c9fc6d079a91bca5f",
"product": "dbeaver-ue",
"version": "22.1",
"license": "this is lice_id"
},
"form_data": {},
"raw_data": "",
"cookies": {}
}
127.0.0.1 - - [28/Jul/2022 17:11:48] "GET /lmp/checkLicense?crc=1ae7edc9687e503c9fc6d079a91bca5f&product=dbeaver-ue&version=22.1&license=this%20is%20lice_id HTTP/1.1" 200 -
然后在看看我们的程序;
终于注册成功 !!
后记
总体研究下来, 这个过程比之前的爆破要麻烦也复杂的多, 但是刺激啊....
仅供研究学习使用,请勿用于非法用途
注:若转载请注明来源(本贴地址)与作者信息。