从Charles破解历程了解Javassist使用

2019-06-15 77103人围观 ,发现 3 个不明物体 工具

*本文原创作者:cck,本文属FreeBuf原创奖励计划,未经许可禁止转载

题记

看文章看到javassist可以直接修改java字节码,之前没有尝试过,因为charles是用java写的跨平台抓包工具,之前我也用过,所以拿来进行测试!

简介

Javassist是一个开源的分析、编辑和创建Java字节码的类库。

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。

关于java字节码的处理,目前有很多工具,如asm。不过这些都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令,可以采用javassist。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

原理介绍

class文件简介及加载

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反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.

详细介绍见:https://blog.csdn.net/sinat_38259539/article/details/71799078?utm_source=blogxgwz0

动态编译

动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。

调用JavaScript引擎

Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本API允许你为脚本语言提供Java支持。

动态生成字节码

这种技术通过操作Java字节码的方式在JVM中生成新类或者对已经加载的类动态添加元素。

动态编程解决什么问题

在静态语言中引入动态特性,主要是为了解决一些使用场景的痛点。其实完全使用静态编程也办的到,只是付出的代价比较高,没有动态编程来的优雅。例如依赖注入框架Spring使用了反射,而Dagger2 却使用了代码生成的方式(APT)。

Java中如何使用

此处我们主要说一下通过动态生成字节码的方式,其他方式可以自行查找资料。

操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit 。

ASM :直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。

Javassit 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。

应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM.当然如果开发的是一个基础类库,或者基础平台,还是直接使用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

Think one Think

在此之前,我的对于修改java字节码的观念还是把jar文件转为dex文件,再把dex文件弄成smali文件,在smali层进行修改然后再重新打包,这样工作量会相对大一些,如果直接可以对java字节码操作,可以并且是用java源码来执行操作,便会方便好多,而这一切便源于javassist对于我们操作的封装,asm不同的是少了java层的操作封装,它是基于字节码的,所以它效率更高,但是使用起来也更为繁琐。

*本文原创作者:cck,本文属FreeBuf原创奖励计划,未经许可禁止转载

相关推荐
发表评论

已有 3 条评论

取消
Loading...

这家伙太懒,还未填写个人描述!

7 文章数 9 评论数 0 关注者

特别推荐

推荐关注

活动预告

填写个人信息

姓名
电话
邮箱
公司
行业
职位
css.php