在系统开发调试过程中,因为需要不停地修改代码导致需要不停地发布系统,而等待系统发布完成是个很“漫长而痛苦”的过程。有什么办法可以让修改的代码在不需要重新发布系统的情况下马上生效呢?JRebel就是一个可以解决此问题的组件,它是一个支持java应用热部署的JVM插件。有了JRebel,就可以为程序员节省很多宝贵时间。
JRebel就是一个好的东西,但是它不能被免费拥有。10人的团队使用JRebel一年,官网售价为$4150,你没有看错,是$不是¥!
JRebel字节码是经过混淆处理的许可证校验过程大致都一样,主要的文件是jrebel.jar、jrebel.lic。可以通过官网在线申请14天的使用注册码,来生成jrebel.lic。
01.准备工具
反编译工具:http://jd.benow.ca
编辑Java字节码的类库:http://www.javassist.org
用于IntelliJ IDEA的JRebel热部署插件:http://plugins.jetbrains.com/plugin/4441?pr=idea
JRebel试用注册码在线申请:https://zeroturnaround.com/software/jrebel/download
02.申请试用注册码
打开申请注册码的地址,随便填写申请信息,申请成功后会生成一个试用的注册码,先保存起来。
03.注册JRebel生成license.lic文件
IDEA中安装JRebel完成后,打开settings -> IDE settings -> JRebel -> Open activation dialog -> Paste license file from the clipboard,将上面的试用注册码粘贴到这里。确定后会显示注册成功,但是有限制时间。
关闭IDEA,将JRebel生成的license.lic和安装好的jrebel.jar提取出来,先放到C盘根目录。
jrebel.lic所在路径:C:\Users{当前用户名}.jrebel\jrebel.lic
jrebel.jar所在路径:C:\Users{当前用户名}.IntelliJIdea13\config\plugins\jr-ide-idea\lib\jrebel\jrebel.jar
04.反编译并查看UserLicense.java
用反编译工具jd-gui打开jrebel.jar,找到类:com.zeroturnaround.licensing.UserLicense,这是用户license所对应的类。此类同时还提供了分别从URL,byte数组以及文件中反序列化(deserialization)UserLicense对象的三个方法。
05.jrebel.lic授权信息
从上面的UserLicense截图可以看到,UserLicense类有5个成员变量,其中第一个是static,最后一个是transient,这两个变量在对象序列化(serialization)时是不会写入文件的,所以用户license主要包括:signature、license和dataMap。
使用类中的方法loadInstance(File paramFile)测试附件中的license文件jrebel.lic,发现反序列化(deserialization)后UserLicense对象中的signature和license有值,而dataMap的值为null。经过分析发现,把license反序列化到dataMap对象中,即可以得到用户License:java public static void main(String[] args) throws Exception{ UserLicense trailLicense = UserLicense.loadInstance(new File("jrebel.lic")); byte[] trailBytes = trailLicense.getLicense(); Map dataMap = deserialize(trailBytes); echoMap(dataMap); } private static void echoMap(Map dataMap){ StringBuilder builder = new StringBuilder(); SortedSet keys = new TreeSet(dataMap.keySet()); for (Object key : keys) { Object value = dataMap.get(key); builder.append(key).append("=").append(value).append('\n'); } System.out.print(builder); } private static void serialize(Object obj, OutputStream outputStream) { ObjectOutputStream out = new ObjectOutputStream(outputStream); out.writeObject(obj); out.close(); } private static byte[] serialize(Object obj) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(512); serialize(obj, baos); return baos.toByteArray(); } private static <T> T deserialize(final InputStream inputStream) { ObjectInputStream in = new ObjectInputStream(inputStream); T obj = (T) in.readObject(); in.close(); return obj; } private static <T> T deserialize(byte[] objectData) { return deserialize((new ByteArrayInputStream(objectData))); }
经过上面的处理,既可以清楚的看到保存在dataMap中的license信息:
06.重新写入jrebel.lic文件
license信息是存放在dataMap中的,所以可以读取dataMap并修改值,最简单的方法就是直接变成full版本,或者设置成永久试用。
试用版的dataMap的key并不全,部分key暂时不清楚其作用,还有一些隐藏key。
把dataMap序列化到文件中就可以生成了一个新的license文件。java public static void main(String[] args){ ... rebuild(dataMap); byte[] myBytes = serialize(dataMap); UserLicense myLicense = new UserLicense(); myLicense.setLicense(myBytes); myLicense.setSignature(trailLicense.getSignature()); serialize(myLicense, new FileOutputStream("out/jrebel.lic")); } /** * 重新生成授权证书明文信息 * * @param dataMap 授权证书 * @author cnblogs zoakerc */ public static void rebuild(Map dataMap) { dataMap.clear(); dataMap.put("Comment", "##### Cracked by zoakerc, For Study! Unlimited! Enjoy! #####"); dataMap.put("Name", "cnblogs zoakerc"); dataMap.put("Organization", "www.cnblogs.com/zoakerc"); dataMap.put("Product", "JRebel"); dataMap.put("Seats", "Unlimited"); dataMap.put("commercial", "true"); dataMap.put("uid", "www.cnblogs.com/zoakerc/p/4882480.html"); dataMap.put("version", "1.7"); dataMap.put("GeneratedOn", new Date()); dataMap.put("GeneratedBy", "cnblogs zoakerc"); } private static void echoMap(Map dataMap) {...} private static void serialize(Object obj, OutputStream outputStream) {...} private static byte[] serialize(Object obj) {...} private static <T> T deserialize(final InputStream inputStream) {...} private static <T> T deserialize(byte[] objectData) {...}
很显然我们已经得到了一个新的jrebel.lic,但是这个授权文件签名仍是原来的试用授权文件的签名,是无法通过SHA1withRSA签名校验的。
07.破解signature的SHA1withRSA签名验证
大家都知道,非对称加密算法主要是两个用途:一是签名(用自己的密钥签名,对方用自己的公钥验签),二是加密(用对方的公钥加密,对方用自己的密钥解密)。要破解签名,我能想到无非有以下的几种方式:
1. 获取对方的密钥(几乎不可能)
2. 了解license文件的结构,然后伪造签名。
比如:自己产生一对公钥和私钥,然后用自己的私钥对license进行签名,把签名信息放入到license中,同时用自己的公钥替换原有的jar包中公钥。
3. 找到验证签名的地方,让验签结果始终为true(伪造的license也可以通过验签),这种方式的难度在于找到验证签名的地方。
这次破解就是采用第3种方式,也是目前破解JRebel最简单的方式。
言归正传,简单查看了用户UserLicense的dataMap的结构,接下来就需要查找有哪些类使用了UserLicense类(验证签名肯定需要用到这个类)。最简单的方法是:把jrebel.jar全部反编译处理,Windows大小写敏感,会提示是否更改,点全部重命名即可,然后查找关键字UserLicense
查找的结果只有几个类使用到了UserLicense类,再在这几个类中查找关键字getSignature(),经过筛选,发现最终只剩下1个类(对于不的版本,经过混淆处理后类名不一样)。
对于jrebel 5.2.2是:com.zeroturnaround.javarebel.Av
对于jrebel 5.6.2是:com.zeroturnaround.javarebel.tP
对于jrebel 5.6.3是:com.zeroturnaround.javarebel.tR
接下来就是让验签的那个方法返回true即可。如果细心摸索,会发现所有返回boolean方法都要调用一个最基本的方法,那么只需要修改这个方法返回true即可,也就是修改用到了getSignature()得到UserLicense实体类的签名的那个方法。
(对于Class文件的修改,这时候就需要用到javassit或者cglib或者ASM。这里不再赘述)
使改静态验证方法的返回值一直为true,即可破解掉jrebel的签名验证机制。修改后的class示例:
08.替换jar包中的class文件
替换jar 包下面的class 文件,很多人会想到直接用压缩工具进行替换,在一般的情况下,是可行的,但是如果说这个jar 的代码经过混淆后,会有大小写不同,文件名是相同的,在windos下文件名是不区分大小写的。如果直接在压缩包内替换的话,你会发现,替换的并非是你想替换的那个文件。
现在有2中可以行的方案:
1. 在Linux下把jar包解压,替换,打成jar。这样比较麻烦。
2. 可以直接用Java jar 工具来替换。
jar uvf test.jar test.class
这样会直接把test.class 直接添加到jar包的根目录。
jar uvf test.jar com/test/test.class
这样就可以替换相应目录的class文件了。这里值得注意的是 test.class 必须放在com/test 文件下,要和jar的路径对应起来,不然会找不到这个文件或目录,jar包和com文件夹的上级在同一个目录。
3. 使用BCEL操作进行实现。
第2种手动方法最简单,但是需要进行手动操作。回到刚才生成的tR.class的最顶层包文件夹,使用CMD命令jar uvf jrebel.jar com/zeroturnaround/javarebel/tR.class替换掉原jar包中的tR.class
10.替换IntelliJ IDEA中对应文件
将生成的jrebel.jar、jrebel.lic替换掉原来的文件。
jrebel.jar替换掉C:\Users{当前用户名}.IntelliJIdea13\config\plugins\jr-ide-idea\lib\jrebel\jrebel.jar
jrebel.lic替换掉C:\Users{当前用户名}.jrebel\jrebel.lic,同时删除该目录其他文件
重新启动IDEA,会发现jrebel已经破解成功!
11.下篇准备写
下篇准备写破解过程中的笔记和替换掉jrebel中公钥实现JRebel破解的思路。