*本文原创作者:cck,本文属FreeBuf原创奖励计划,未经许可禁止转载
看文章看到javassist可以直接修改java字节码,之前没有尝试过,因为charles是用java写的跨平台抓包工具,之前我也用过,所以拿来进行测试!
Javassist是一个开源的分析、编辑和创建Java字节码的类库。
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。
关于java字节码的处理,目前有很多工具,如asm。不过这些都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令,可以采用javassist。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
Java编译器编译好Java文件之后,产生.class 文件在磁盘中。这种class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码。JVM虚拟机读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息,生成对应的 Class对象:
由于JVM通过字节码的二进制信息加载类的,那么,如果我们在运行期系统中,遵循Java编译系统组织.class文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样,就完成了在代码中,动态创建一个类的能力了
ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载 器非常相似, CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。 CtField:用来访问域 CtMethod :用来访问方法 CtConstructor:用来访问构造器
Constructor getConstructor(Class..c);获得某个公共的构造方法。 Constructor[] getConstructors();获得所有的构造方法。 Constructor getDeclaredConstructor(Class..c);获得某个构造方法。 Constructor[] getDeclaredConstructors();获得所有的构造方法 CtMethod 和CtConstructor 提供了 setBody() 的方法,可以替换方法或者构造函数里的所有内容
读取和输出字节码
ClassPool pool = ClassPool.getDefault(); //会从classpath中查询该类 CtClass cc = pool.get("test.Rectangle"); //设置.Rectangle的父类 cc.setSuperclass(pool.get("test.Point")); //输出.Rectangle.class文件到该目录中 cc.writeFile("c://"); //输出成二进制格式 //byte[] b=cc.toBytecode(); //输出并加载class 类,默认加载到当前线程的ClassLoader中,也可以选择输出的ClassLoader。 //Class clazz=cc.toClass();
这里可以看出,Javassist的加载是依靠ClassPool类,输出方式支持三种
使用javassist来编写的代码与java代码不完全一致,主要的区别在于 javassist提供了一些特殊的标记符(以开头),用来表示方法,构造函数参数、方法返回值等内容。示例:System.out.println(“Argument1:”+开头),用来表示方法,构造函数参数、方法返回值等内容。示例:System.out.println(“Argument1:”+1); 其中的$1表示第1个参数.
可以通过javassist来修改java类的方法,来修改其实现。如下所示:
ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("org.light.lab.JavassistTest"); CtMethod ctMethod = ctClass.getDeclaredMethod("test"); ctMethod.setBody("System.out.println(/"this method is changed dynamically!/");"); ctClass.toClass();
上面的方法即是修改一个方法的实现,当调用ctClass.toClass()时,修改后的类将被当前的ClassLoader加载并实例化。
Tips
类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)。每种类加载器都有设定好从哪里加载类。
package samples; /** /* 自定义一个类加载器,用于将字节码转换为class对象 */ public class MyClassLoader extends ClassLoader { public Class<?> defineMyClass( byte[] b, int off, int len) { return super.defineClass(b, off, len); } }
然后编译成Programmer.class文件,在程序中读取字节码,然后转换成相应的class对象,再实例化
import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; public class MyTest { public static void main(String[] args) throws IOException { //读取本地的class文件内的字节码,转换成字节码数组 File file = new File("."); InputStream input = new FileInputStream(file.getCanonicalPath()+"//bin//samples//Programmer.class"); byte[] result = new byte[1024]; int count = input.read(result); // 使用自定义的类加载器将 byte字节码数组转换为对应的class对象 MyClassLoader loader = new MyClassLoader(); Class clazz = loader.defineMyClass( result, 0, count); //测试加载是否成功,打印class 对象的名称 System.out.println(clazz.getCanonicalName()); //实例化一个Programmer对象 Object o= clazz.newInstance(); try { //调用Programmer的code方法 clazz.getMethod("code", null).invoke(o, null); } catch (IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { e.printStackTrace(); } } }
以上代码演示了,通过字节码加载成class 对象的能力
正文
我们在进行应用开发过程中有时候可以需要进行抓包测试数据,比如模拟服务端的下发数据和我们客户端的请求参数数据,特别是测试人员在进行测试的过程中都会进行抓包,当然我们在破解逆向的过程中也是需要用到抓包工具,因为我们抓到数据包可能就是我们破解的突破口,那么我们可能常用的都是Fiddler工具,但是这个工具有一个弊端就是只能在Windows系统中使用,但是还有一个厉害的工具就是跨平台抓包工具Charles,之所以他是跨平台的就是因为他使用Java语言开发的,而且也非常好用。但是这个工具有一个不好的地方就是有一个购买功能,如果不购买的话当然可以使用,但是有时间限制和各种提示,使用过程中也挺烦的,所以我决定把它破解了!
首先我们去官网下载一个最新版,我下载的是windows版
官网地址: https://www.charlesproxy.com/
开启界面有段字符,延迟几秒后进入主界面,我们点击购买功能
首先的思路也是老套路,先利用字符串作为入口,寻找可能的关键代码,这里我们利用开启界面的字符串,This is a 30 day trial version….
找到charles.jar,用jd-gui打开打开,全局搜索This is a 30 day trial version….
如下
发现一个showRegistrationStatus()方法,方法名没有被混淆,大致能判断此方法跟注册有关,并且是根据lcjx()方法的返回值来判断,为true则成功,false则显示showSharewareStatus()的内容,也就是This is a 30 day trial version….,接下来我们进入lcjx()来验证我们的推断!
在JD-gui里点击相应方法函数,可以知道目标的调用位置,这个可以省不少事,这里我们点击第一个框中JZlU,找到调用位置
它返回的值是调用了boolean变量JZlU,默认为false,此时我们推想一下逻辑,也就是说正常情况下默认是未注册的状态,所以这个值默认为false,如果我们要破解的话,是不是可以直接把这个变量给初始化为true呢?答案是可以的
我们利用kKPk的构造方法进行初始化变量
如果我们想在初始界面显示我们想要显示的字符怎么办呢,我们可以修改JZlU方法,使之返回我们想要的字符
下面贴出利用代码
import javassist.*; import java.io.IOException; public class javassivt { // 实例化类型池 public static ClassPool pool = ClassPool.getDefault(); public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException { // 获取默认类型池对象 pool.insertClassPath("K:/charles.jar"); // 从类型池中读取指定类型 CtClass oFTR = pool.get("com.xk72.charles.kKPk"); try {// 获取指定方法 CtMethod ct = oFTR.getDeclaredMethod("JZlU"); // 修改原方法 ct.setBody("return /"By.Ethan http://www.luckydog.top:4000 QQ:798993306/";"); // 为类设置构造器,获得全部的构造方法 CtConstructor[] cca = oFTR.getDeclaredConstructors(); cca[0].setBody("{this.yNVB = /"Cracked By Ethan http://www.luckydog.top:4000 QQ:798993306/";/nthis.JZlU = true;}"); cca[1].setBody("{this.yNVB = /"Cracked By Ethan http://www.luckydog.top:4000 QQ:798993306/";/nthis.JZlU = true;}"); //将上面构造好的类写入到指定的工作空间中 oFTR.writeFile("K:"); } catch (Exception e) { e.printStackTrace(); } }}
以上脚本实现了初始化yNVB,JZlU,并且重写了JZlU类,使之返回相应字符。
修改后相应代码如下
运行完进入输出目录运行命令,把修改的内容更新到jar文件
jar -uvf charles.jar com
用破解的charles.jar替换原来的charles.jar,运行
成功破解,在使用过程中也无任何弹出消息框,注册状态也显示已经注册!整个破解也就结束了!
除了以上方法我们也可以番外知识我们可以修改smali文件,所以思路就是把jar转化成dex文件,这个直接用dx命令即可,然后在把dex弄成smali文件直接修改即可,然后在打包回去,同样也可以实现!
动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术,在Java中有如下几种方式:
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
详细介绍见: https://blog.csdn.net/sinat_38259539/article/details/71799078?utm_source=blogxgwz0
动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。
Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本API允许你为脚本语言提供Java支持。
这种技术通过操作Java字节码的方式在JVM中生成新类或者对已经加载的类动态添加元素。
在静态语言中引入动态特性,主要是为了解决一些使用场景的痛点。其实完全使用静态编程也办的到,只是付出的代价比较高,没有动态编程来的优雅。例如依赖注入框架Spring使用了反射,而Dagger2 却使用了代码生成的方式(APT)。
此处我们主要说一下通过动态生成字节码的方式,其他方式可以自行查找资料。
操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit 。
ASM :直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。
Javassit 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。
应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM.当然如果开发的是一个基础类库,或者基础平台,还是直接使用ASM吧,相信从事这方面工作的开发者能力应该比较高。
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
与 BCEL 和 SERL 不同,ASM 提供了更为现代的编程模型。对于 ASM 来说,Java class 被描述为一棵树;使用 “Visitor” 模式遍历整个二进制结构;事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分,而不必了解 Java 类文件格式的所有细节:ASM 框架提供了默认的 “response taker”处理这一切。
详细介绍见: https://blog.csdn.net/zhuoxiuwu/article/details/78619645
构造方法是一种特殊的方法,它是一个与类同名且返回值类型为同名类类型的方法。对象的创建就是通过构造方法来完成,其功能主要是完成对象的初始化。当类实例化一个对象时会自动调用构造方法。构造方法和其他方法一样也可以重载。
构造方法的作用
为了初始化成员属性,而不是初始化对象,初始化对象是通过new关键字实现的
通过new调用构造方法初始化对象,编译时根据参数签名来检查构造函数,称为静态联编和编译多态
(参数签名:参数的类型,参数个数和参数顺序)
创建子类对象会调用父类构造方法但不会创建父类对象,只是调用父类构造方法初始化父类成员属性;
关于重载和子类调用父类的构造方法、构造方法的作用域、构造方法的访问级别等,
详见: https://www.cnblogs.com/lwj820876312/p/7231271.html
在此之前,我的对于修改java字节码的观念还是把jar文件转为dex文件,再把dex文件弄成smali文件,在smali层进行修改然后再重新打包,这样工作量会相对大一些,如果直接可以对java字节码操作,可以并且是用java源码来执行操作,便会方便好多,而这一切便源于javassist对于我们操作的封装,asm不同的是少了java层的操作封装,它是基于字节码的,所以它效率更高,但是使用起来也更为繁琐。
*本文原创作者:cck,本文属FreeBuf原创奖励计划,未经许可禁止转载