27

从Charles破解历程了解Javassist使用

 4 years ago
source link: https://www.tuicool.com/articles/IZjY3mu
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

*本文原创作者: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对象:

6jiuemF.jpg!web

在运行期的代码中生成二进制字节码

由于JVM通过字节码的二进制信息加载类的,那么,如果我们在运行期系统中,遵循Java编译系统组织.class文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样,就完成了在代码中,动态创建一个类的能力了

mQZNfuB.jpg!web

基本功能

重要的类

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/

安装并打开软件

a67fimM.jpg!web

开启界面有段字符,延迟几秒后进入主界面,我们点击购买功能

yUNVz27.jpg!web

首先的思路也是老套路,先利用字符串作为入口,寻找可能的关键代码,这里我们利用开启界面的字符串,This is a 30 day trial version….

找到charles.jar,用jd-gui打开打开,全局搜索This is a 30 day trial version….

6BnuAnf.jpg!web

如下

M7ZnymJ.jpg!web

发现一个showRegistrationStatus()方法,方法名没有被混淆,大致能判断此方法跟注册有关,并且是根据lcjx()方法的返回值来判断,为true则成功,false则显示showSharewareStatus()的内容,也就是This is a 30 day trial version….,接下来我们进入lcjx()来验证我们的推断!

m2eeEzY.jpg!web

在JD-gui里点击相应方法函数,可以知道目标的调用位置,这个可以省不少事,这里我们点击第一个框中JZlU,找到调用位置

iaE32eq.jpg!web

它返回的值是调用了boolean变量JZlU,默认为false,此时我们推想一下逻辑,也就是说正常情况下默认是未注册的状态,所以这个值默认为false,如果我们要破解的话,是不是可以直接把这个变量给初始化为true呢?答案是可以的

我们利用kKPk的构造方法进行初始化变量

Q7VFBjR.jpg!web

如果我们想在初始界面显示我们想要显示的字符怎么办呢,我们可以修改JZlU方法,使之返回我们想要的字符

UZJVvuy.jpg!web

下面贴出利用代码

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类,使之返回相应字符。

修改后相应代码如下

ZRzyyiz.jpg!web

iEjaYbb.jpg!web

a6bUJbI.jpg!web

运行完进入输出目录运行命令,把修改的内容更新到jar文件

jar -uvf charles.jar com

用破解的charles.jar替换原来的charles.jar,运行

f6fmiyU.jpg!web

uYvYniM.jpg!web

成功破解,在使用过程中也无任何弹出消息框,注册状态也显示已经注册!整个破解也就结束了!

除了以上方法我们也可以番外知识我们可以修改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原创奖励计划,未经许可禁止转载


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK