DBeaver Ultimate edition 算法分析/本地服務器搭建
DBeaver Ultimate edition 算法分析
官方下載地址: https://dbeaver.com/download/ultimate/
當前分析版本號: dbeaver-ue-22.1.0
前記
前幾天出了 該軟件的 爆破分析; 機票 :
DBeaver Ultimate edition JAVA程序逆向分析
這次來嘗試下不一樣的...;
環境
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);// licenseIdfillString("this is lice_id", 16, buffer);// licenseType ULTIMATE('U', "Ultimate license"),buffer.put((byte) 85); // 'U' = 85// licenseIssueTimebuffer.putLong(SIMPLE_DATE_FORMAT.parse("2020-01-01").getTime());// licenseStartTimebuffer.putLong(SIMPLE_DATE_FORMAT.parse("2020-01-01").getTime());// licenseEndTimebuffer.putLong(SIMPLE_DATE_FORMAT.parse("2099-01-01").getTime());// flagsbuffer.putLong(250L);// proeudcIdfillString("dbeaver-ue", 16, buffer);// productVersionfillString("prod_version", 8, buffer);// ownerIdfillString("this is owner_id", 16, buffer);// ownerCompanyfillString("this is ownerCompany", 64, buffer);// ownerNamefillString("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 注冊一下試試;

哎,居然報錯了; 跟進去再看看,
發現個壞消息 , 原來除了用 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,
后面的也看不懂是啥, 只看到有個過期時間 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_strif __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 -
然后在看看我們的程序;
終于注冊成功 !!


后記
總體研究下來, 這個過程比之前的爆破要麻煩也復雜的多, 但是刺激啊....
僅供研究學習使用,請勿用于非法用途
注:若轉載請注明來源(本貼地址)與作者信息。