聲明
CLion程序版權爲jetBrains所有、註冊碼授權爲jetBrains及其付費用戶所有,本篇只從興趣出發,研究其註冊碼生成算法。
- 不會釋出任何完整的源代碼.
- 網上查了下,已有註冊機,所以想要key的同學不要找我:p
背景
打算學習cocos2dx,奈何vim只會ggvG,被jetBrains慣壞了,找到了CLion,試了下,果然神器,我等菜鳥正好可以拿來愉快地學習書寫c++了。
但是,試用版有30天的限制,又沒有學生授權,懶得折騰,看下它的註冊算法吧。
本篇用到的主要工具和命令:
- jd-gui 1.2
- jdb
- zip/unzip
- jar
找出註冊算法的代碼段
先用jd-gui看下反編譯的效果,大概找了下,看到這個MainImpl
類:
反編譯的效果還是可以,不過注意到一些類和方法的名稱被混淆了,如上圖的a.a().a(new a.a_())...
.
嘗試直接建立java文件,發現很多類缺失,比較難補,遂放棄。
嘗試用java attach
和agent
方式來dump運行時的class,比較難偷,遂放棄。
換換思路:
CLion啓動後,如果未輸入正確的註冊碼,或者沒有選擇試用,會打開一個對話框,提示輸入註冊碼。
如下圖:
如果選擇試用,先進入主界面,打開關於對話框,可以看到部分授權信息。
如下圖:
好,從這兩個點出發,大概找找相關代碼,縮小分析範圍:
分析MainImpl啓動前後的類加載信息
修改$CLION_HOME/bin
下的clion.sh
:
在啓動參數中,加入
-verbose
.
重新啓動CLion,收集到MainImpl
加載前後的一些信息,如下圖:
繼續翻看,又發現如下一些信息:
發現
com.intellij.ide.a
包和com.intellij.a
包嚴重混淆,並且在MainImpl加載前後出現.
可以初步判定這些包是分析重點.
分析AboutPopup類的信息
打開關於/About
對話框,觀察比對代碼,發現如下信息:
在粗略分析相關代碼後,跟蹤到這裏:
LicensingFacade
是個抽象類,跟一些實現,發現只有一個子類:
接着跟,又發現一個抽象類bb
:
這個類在com.intellij.ide.a.g
包下,其中一段關鍵代碼:
public void a(q.a_ parama_)
{
boolean bool = GraphicsEnvironment.isHeadless();
f();
a(this.f);
/// 這裏的u()是關鍵
for (s locals : u())
{
if (locals.d() == null) {
locals.c();
}
com.intellij.a.b.e locale = locals.d();
if (locale != null)
{
g_ localg_ = a(parama_, locals);
if (localg_ != g_.SKIP)
{
UpdateChecker.addUpdateRequestParameter("license", a(locale));
if (localg_ != g_.OK) {
break;
}
int i1 = (!bool) && (a(locale, locale instanceof l)) ? 1 : 0;
if (i1 == 0)
{
a(parama_, locals, locale);
return;
}
break;
}
}
}
if (bool)
{
b.error("No valid license found");
System.exit(-1);
}
else
{
b(parama_);
}
}
接着看com.intellij.ide.a.g.bb.u()
方法:
private s[] u()
{
s[] arrayOfs = v();
Object localObject1 = null;
for (Object localObject3 : arrayOfs)
{
try
{
((s)localObject3).c();
if (((s)localObject3).d() != null) {
/// 這裏的b()是關鍵
((s)localObject3).b();
} else {
continue;
}
}
catch (Exception localException)
{
continue;
}
localObject1 = localObject3;
break;
}
if (localObject1 != null)
{
??? = new ArrayList(Arrays.asList(arrayOfs));
((ArrayList)???).remove(localObject1);
((ArrayList)???).add(0, localObject1);
return (s[])((ArrayList)???).toArray(new s[((ArrayList)???).size()]);
}
return arrayOfs;
}
中間的過程比較繁瑣,考驗的是耐心、推測、和筆記功夫。
通過相關代碼的類型分析、方法分析、參數分析、變量分析,逐步縮小關鍵代碼段的分析範圍,同時增加對混淆代碼的熟悉。
分析註冊碼算法解密驗證的重點包
結合上面的對於MainImpl
加載前後的類信息分析,重點關注com.intellij.a.g
這個包.
最終跟到這樣一個類com.intellij.a.g.c
:
package com.intellij.a.g;
import java.math.BigInteger;
public class c
{
/// 這個a是關鍵,通過後面的實驗,這是RSA加密算法的公鑰部分的n .
private static BigInteger a = new BigInteger("8eeb1420b7d8b90aa3b95c7ff73628e46e12c19dc91531be5f517a54b042e99c17445ce7b23834a3ec80d2691b463231be43aab7e897cc334bc9b8bb9f0d55f5", 16);
private static BigInteger b = new BigInteger("10001", 16);
/// 這個方法是關鍵
/// 通過推測和實驗,可以確定paramString1和paramString2是用戶名和代碼,或者相反。
public static h a(String paramString1, String paramString2)
throws a
{
// 這個h類型是關鍵,記錄了註冊碼類型、產品類型、用戶名、過期時間、維護到期時間、主版本號、小版本號等關鍵信息
h localh = new h();
// 這裏m.a(,,,)方法完成第一次解密
byte[] arrayOfByte = m.a(paramString1, localh, b, a);
// m.a(,,,)方法驗證註冊碼解密後的字節信息,從中提取關鍵屬性
m.a(arrayOfByte, localh, paramString2, 14);
// 繼續驗證
m.b(arrayOfByte, localh, paramString2, -1);
return localh;
}
}
爲了確定靜態分析過程的正確性,使用jdb
來看一下簡單的調用棧:
Idea Main Thread 1.0.4#CL-141.874.66, eap:false[1] wherei 0x6f4
/// 可以確定,每次加載MainImpl,調用其start方法,都會來下面這個方法中驗證註冊碼,這是迄今爲止最爲關鍵的發現
[1] com.intellij.a.g.c.a (c.java:18), pc = 13
[2] com.intellij.ide.a.b.b$0.b (b$0.java:71), pc = 2
[3] com.intellij.ide.a.b.b$0.a (b$0.java:67), pc = 3
[4] com.intellij.a.b.d.x (d.java:34), pc = 12
[5] com.intellij.a.b.d.t (d.java:13), pc = 1
[6] com.intellij.a.b.b.s (b.java:40), pc = 10
[7] com.intellij.a.b.b.c (b.java:33), pc = 1
[8] com.intellij.ide.a.g.t.h (t.java:108), pc = 41
[9] com.intellij.ide.a.g.t.a (t.java:27), pc = 8
[10] com.intellij.ide.a.g.m.b (m.java:29), pc = 27
// bb類是個很重要的類,後續分析
[11] com.intellij.ide.a.g.bb.u (bb.java:152), pc = 48
[12] com.intellij.ide.a.g.bb.a (bb.java:78), pc = 18
[13] com.intellij.idea.MainImpl$1.start (MainImpl.java:45), pc = 19
[14] com.intellij.idea.StartupUtil.prepareAndStart (StartupUtil.java:117), pc = 115
[15] com.intellij.idea.MainImpl.start (MainImpl.java:40), pc = 9
[16] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method)
[17] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100
[18] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6
[19] java.lang.reflect.Method.invoke (Method.java:497), pc = 56
[20] com.intellij.ide.plugins.PluginManager$2.run (PluginManager.java:91), pc = 53
[21] java.lang.Thread.run (Thread.java:745), pc = 11
分析註冊碼字節數組
/// 我們重點跟一下com.intellij.a.g.c.a()方法:
package com.intellij.a.g;
import java.math.BigInteger;
public class c
{
private static BigInteger a = new BigInteger("8eeb1420b7d8b90aa3b95c7ff73628e46e12c19dc91531be5f517a54b042e99c17445ce7b23834a3ec80d2691b463231be43aab7e897cc334bc9b8bb9f0d55f5", 16);
private static BigInteger b = new BigInteger("10001", 16);
/// 重點跟這個方法
public static h a(String paramString1, String paramString2)
throws a
{
h localh = new h();
byte[] arrayOfByte = m.a(paramString1, localh, b, a);
m.a(arrayOfByte, localh, paramString2, 14);
m.b(arrayOfByte, localh, paramString2, -1);
return localh;
}
}
在上面的方法中,類型h
很關鍵,這裏的字符串沒有混淆,給我們推測註冊碼結構提供了極大的便利 O^. ^O.
/// 這裏的一些字段和變量的意思,已經給出來了
public String toString() {
StringBuffer var1 = new StringBuffer();
var1.append("\n");
var1.append("user name:" + this.b + "\n");
var1.append("customer id:" + this.c + "\n");
var1.append("product id:" + j.a(this.d) + "\n");
var1.append("license type:" + i.a(this.e) + "\n");
var1.append("major version:" + this.f + "\n");
var1.append("minor version:" + this.g + "\n");
var1.append("generationDate:" + this.h + "\n");
var1.append("expirationDate:" + this.i + "\n");
return var1.toString();
}
static {
a.b = "Fake";
a.d = 0;
a.e = 1;
a.f = 1;
a.g = 0;
a.h = d.a(1995, 1, 1);
a.i = d.a(1995, 1, 2);
}
跟這個方法:byte[] arrayOfByte = m.a(paramString1, localh, b, a);
protected static byte[] a(String var0, h var1, BigInteger var2, BigInteger var3) throws a {
Matcher var4 = c.matcher(var0);
var0 = var4.replaceAll("").trim();
// 10是ASCII碼中的"\n"換行符
// 13是ASCII碼中的"\r"回車符
// 下面的代碼告訴我們一個重要信息:
// 加密的註冊碼至少包含1個\n(linux)或者\r(windox|mac)
int var5 = var0.indexOf(10);
if(var5 < 0) {
var5 = var0.indexOf(13);
}
if(var5 < 0) {
throw new a();
} else {
// 45是ASCII碼中的"-"
// 重要信息:加密的註冊碼至少包含一個"-",且出現在"\n"或"\r"之前
String var6 = var0.substring(0, var5);
//"-"
int var7 = var6.indexOf(45);
if(var7 > 0) {
try {
// 從[xx-??\n???]中提取xx,並賦值給var1.c
// 根據上面的h類型的信息,這裏的xx是cutomerId,所以我們有了:
// license : customerId-??\n???
var1.c = Integer.parseInt(var6.substring(0, var7));
} catch (NumberFormatException var9) {
throw new a();
}
} else {
var1.c = -1;
}
// 這裏可以確定傳入的var2是公鑰的n,var3是公鑰的e
// 同時根據RSADecoder我們也找到了對應的RSAEncoder,後續直接拷貝RSAEncoder過來,作註冊碼加密用^^
return (new RSADecoder(var2, var3, 64)).decode(var0.substring(var5 + 1));
}
}
跟這個方法:m.a(arrayOfByte, localh, paramString2, 14);
static void a(byte[] var0, h var1, String var2, int var3) throws a {
//這裏,我們根據傳入的var3 = 14,得知:
//license的長度爲14個字節
if(var0.length != var3) {
byte[] var4;
if(var0.length == var3 + 1) {
if(var0[0] != 0) {
throw new a();
}
var4 = new byte[var3];
System.arraycopy(var0, 1, var4, 0, var3);
var0 = var4;
} else {
if(var0.length >= var3) {
throw new a();
}
var4 = new byte[var3];
System.arraycopy(var0, 0, var4, var3 - var0.length, var0.length);
var0 = var4;
}
}
// 這裏我們根據分析,得到:
//var2 : userName , var1.c : customerId , var3 : 14
// 假設lic是個14字節長的字節數組,我們有:
//lic[12] = customerId & 0xFF;
//lic[13] = customerId >> 8 & 0xFF;
if(var2 != null) {
/*
下面的o.a(,,)方法的實現:
可以直接把這段代碼copy到我們做註冊碼的地方^^
public static short a(String var0, int var1, byte[] var2) {
CRC32 var3 = new CRC32();
int var4;
if(var0 != null) {
for(var4 = 0; var4 < var0.length(); ++var4) {
char var5 = var0.charAt(var4);
var3.update(var5);
}
}
var3.update(var1);
var3.update(var1 >> 8);
var3.update(var1 >> 16);
var3.update(var1 >> 24);
for(var4 = 0; var4 < var2.length - 2; ++var4) {
byte var6 = var2[var4];
var3.update(var6);
}
return (short)((int)var3.getValue());
}
*/
short var5 = o.a(var2, var1.c, var0);
// 這裏得到:
// lic[12] = crc32 & 0xFF
// lic[13] = crc32 >> 8 & 0xFF
if(var0[var3 - 2] != (byte)(var5 & 255)) {
throw new a();
}
if(var0[var3 - 1] != (byte)(var5 >> 8 & 255)) {
throw new a();
}
}
努力,繼續跟這個方法:m.b(arrayOfByte, localh, paramString2, -1);
static void b(byte[] var0, h var1, String var2, int var3) throws a {
try {
/** 對比
var1.append("user name:" + this.b + "\n");
var1.append("customer id:" + this.c + "\n");
var1.append("product id:" + j.a(this.d) + "\n");
var1.append("license type:" + i.a(this.e) + "\n");
var1.append("major version:" + this.f + "\n");
var1.append("minor version:" + this.g + "\n");
var1.append("generationDate:" + this.h + "\n");
var1.append("expirationDate:" + this.i + "\n");
*/
// licTypeId是註冊碼類型的Id,prodId是產品類型的Id
/**
* lic[0] = licTypeId << 4 + (prodId & 0xFF);
*/
var1.b = var2;
var1.d = var0[0] & 15;
var1.e = var0[0] >> 4;
var1.g = var0[1] >> 4;
//這裏得到generationDate信息,也就是now了,以毫秒錶示,並帶符號右移16位,然後按4字節存儲
/**
* long now = System.currentTimeMillis() >> 16;
* lic[2] = now & 0xFF;
* lic[3] = now >> 8 & 0xFF;
* lic[4] = now >> 16 & 0xFF;
* lic[5] = now >> 24 & 0xFF;
*/
long var4 = ((long)var0[2] & 255L) + (((long)var0[3] & 255L) << 8) + (((long)var0[4] & 255L) << 16) + (((long)var0[5] & 255L) << 24) << 16;
var1.h = new Date(var4);
/*
* 注意到上面有句 :var1.g = var0[1] >> 4;
* 結合下面的代碼,我們有:
* lic[1] = minorVer >> 4 + (majorVer & 0xFF);
*/
var1.f = var0[1] & 15;
/**
* lic[6] = delta & 0xFF;
* lic[7] = delta >> 8 & 0xFF;
*/
int var6 = (var0[6] & 255) + ((var0[7] & 255) << 8);
if(var6 != 0) {
//expire date
var1.i = new Date(var4 + (long)var6 * 24L * 60L * 60L * 1000L);
}
//var3:-1
//lic[10] = any
//lic[11] = any
int var7 = var3 > -1?var3:(var0[10] & 255) + ((var0[11] & 255) << 8);
if(var6 != 0) {
var7 = var6;
}
//var1.l = maintenanceDueDate ,維護到期時間
var1.l = new Date(var4 + (long)var7 * 24L * 60L * 60L * 1000L);
} catch (ArrayIndexOutOfBoundsException var8) {
throw new a();
}
}
得到產品信息和註冊碼類型信息
現在我們基本拿到了註冊碼的字節信息,還差一點,產品信息和註冊碼類型信息這裏處理的是Id,那麼實際信息在哪裏?
還記得我們前面說過一個bb
類吧,對它的子類進行分析,可以得到很多類似這樣的代碼:
static a a(com.intellij.ide.a.r var0, q var1) {
return new a(var0, "AppCode", 8, Products.APPCODE, 3, 30, "6", var1, new g() {
public h b(String var1, String var2) throws com.intellij.a.g.a {
return com.intellij.a.g.b.a(var1, var2);
}
});
}
看來jetBrains的很多產品的加密算法都是差不多,真是比較懶啊.
通過分析bb
類所在的包com.intellij.ide.a.a
,以及com.intellij.a.g
包,我們得到如下有用信息:
以及:
寫出註冊碼生成算法
- 關鍵字節數組,14字節長:
- CRC校驗:
package rsa;
import java.util.zip.CRC32;
public class GroupUtil {
public static final int a = 12;
public static final int b = 14;
public GroupUtil() {
}
public static short computeCRC32(String userName, int customerId, byte[] licBytes) {
CRC32 crc32 = new CRC32();
if(userName != null) {
for(int i = 0; i < userName.length(); ++i) {
char var5 = userName.charAt(i);
crc32.update(var5);
}
}
crc32.update(customerId);
crc32.update(customerId >> 8);
crc32.update(customerId >> 16);
crc32.update(customerId >> 24);
for(int i = 0; i < licBytes.length - 2; i++) {
byte var6 = licBytes[i];
crc32.update(var6);
}
return (short)((int)crc32.getValue());
}
- RSA加密,可直接拷貝反編譯代碼
- 註冊碼生成
測試註冊碼
目前的問題是,我們沒有私鑰,怎麼搞?
可以隨意生成一對RSA的公鑰/私鑰,然後用我們生成的公鑰的n
,替代class文件中的n
,這裏的n
是modulus
.
看看clion.jar
中的com.intellij.a.g.c.class
的信息:
注意右邊ASCII字符的部分,上文有提到這個類的n
,我們可以替換之。
怎麼替換呢?
方法很多,jar、zip、sed,編程方式(javassist、asm、zip)都可以搞定,這個留給讀者吧:]