CLion注册码算法逆向分析实录(纯研究)
2015-06-24 13:03
309 查看
声明
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)都可以搞定,这个留给读者吧:]
最终效果:
相关文章推荐
- 【译】VisionMobile:2015 IOT大趋势(六)未来在此,开发者拥抱IoT
- android string.xml 中显示特殊符号
- hdu 1010 Tempter of the Bone
- 基于二元语法模型的中文分词
- C#实现任意角度旋转图片(方法2)
- UITableView的上下滑动与自己添加的手势冲突
- shell脚本中一些常见的特殊符号
- C#实现任意角度旋转图片(方法1)
- tcp_wrap之实例
- Json-lib - java.util.Date 转换问题
- tcp_wrap之基础知识
- JAVA获取文件MD5值
- python socket之tcp服务器与客户端demo
- 将yuv转换为Opencv中的Ipl
- python socket之tcp服务器与客户端demo
- ural 1014 Product of Digits
- 用汇编语言实现:以十进制形式输出双精度整型数
- OC-多个自定义对象的归档与解归档
- Shell编程中if的语法和常见判断用法
- 利用rating-input PreviewWidget来对事物进行评价及打分