0x00 前言
原本打算把这部分内容放到基础篇章的,但随着我的学习,发现这一部分内容其实并不基础,于是又单独开了字节码篇章。这一篇章的主要内容为Javassist、ASM这两个字节码第三方库的使用,字节码文件的结构以及字节码相关技术,在内存马注入中应该也会有应用吧~
本文为JAVA安全系列文章第二十二篇,学习javassist的基本使用
0x01 认识javassist
一、介绍
Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。而Javaassist 就是一个用来处理 Java 字节码的类库。
下面是来自javassist官网的介绍,本人只是做了点小小的翻译:
Javassist是一个用于在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类,并在 JVM 加载类文件时对其进行修改。
与其他字节码编辑器不同的是,Javassist 提供了两种级别的 API:源代码级别和字节码级别。
源代码级API使得用户不需要了解Java 字节码规范就可以编辑类文件。整个 API 仅使用 Java 语言的语法进行设计。用户甚至可以在源文本中插入指定的字节码,Javassist即时编译它。
字节码级 API 允许用户直接编辑类文件。
对于安全的学习,我们常使用的是源码级API,下面就来了解下源码级API中我们常使用的类和方法。
二、源码级API常见类和方法
1.Ctclass和ClassPool的关系
CtClass(compile-time class,编译时类)是一个class文件在代码中的抽象表现形式 。可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。一个 CtClass对象可以处理一个 class 文件。
ClassPool是CtClass对象的容器,它根据需要读取类文件来构造CtClass对象,并且保存CtClass 对象方便以后使用。要修改类的定义,用户首先必须从ClassPool对象获取表示该类的CtClass对象的引用。ClassPool中的get()用于此目的。通常我们会使用ClassPool的静态方法getDefault()获取到一个ClassPool对象。
从实现的角度来讲,ClassPool是一个CtClass对象的哈希表,它使用类名作为键。ClassPool中的get()在哈希表中查找与指定键相关联的CtClass对象。如果没有找到这样的CtClass对象,get()将读取一个类文件来构造一个新的CtClass对象,该对象记录在哈希表中,然后作为get()的结果值返回。
需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass
的detach()
方法以释放内存。
2.ClassPool常用方法
getDefault : 返回默认的ClassPool ,是单例模式的,一般通过该方法创建我们的ClassPool对象;
appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
makeClass:创建一个新的类
toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
3.Ctclass常用方法
CtClass理解为编译时类,它的API与Class的API相似而又有不同:
addField、addMethod、addConstructor:添加字段、方法、构造器
getField(s)、getMethod(s)、getConstructor(s):获取非private的编译时字段、方法、构造器对象
getDeclaredField(s)、getDeclaredMethod(s)、getDeclaredConstructor(s):获取声明的编译时字段、方法、构造器,包括私有的
setModifiers:设置(字段,方法,构造器等的)修饰符
等等,另外的一些常用API:
freeze : 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该class从ClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass。
4.CtMethod、CtNewMethod常用方法
对于一个类,相比于字段,我们更关心的是它的方法,相应的javassist中有CtMethod、CtNewMethod来修改/创建方法
(1)CtMethod常用方法
insertBefore : 在方法的起始位置插入代码;
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt : 在指定的位置插入代码;
setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make : 创建一个新的方法。
(2)CtNewMethod常用方法
getter , setter:创建public的getter,setter方法
make:创建一个新的方法
上面列举的类都在javassist包下,其他类和方法不一一列举,关于方法的使用和传入参数参考官方API文档:
https://www.javassist.org/html/index.html
0x02 javassist的基本使用
上面列举了一些常见API,下面就实际敲代码来使用下。
不过,在实际敲代码前我们需要在maven中导入javassist包,通过mvn仓库,发现用得最多的好像是3.27.0-GA版本:
于是我环境中导入的是这个包,我的JDK版本是java1.8.0_111。
另外,敲代码前先附上一张从别处Copy过来的javassist使用流程图:
一、使用javassist创建一个class文件
下面的代码可以从无到有产生一个.class文件:
package javassist_;
/*
* @author walker1995
* @version v1.0
*/
import javassist.*;
public class CreatePersonClass {
public static void createPerson() throws Exception {
//1.获取一个ClassPool对象
ClassPool pool = ClassPool.getDefault();
//2.使用ClassPool对象创建一个新类
CtClass cc = pool.makeClass("javassist_.Person");
//3.增加一个字段private String name,初始值为"walker"
CtField field = new CtField(pool.get("java.lang.String"), "name", cc);
field.setModifiers(Modifier.PRIVATE);
cc.addField(field,CtField.Initializer.constant("walker"));
//4.生成getter、setter方法
cc.addMethod(CtNewMethod.getter("getName",field));
cc.addMethod(CtNewMethod.setter("setName",field));
//5.添加无参构造器
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"walker\";}");
cc.addConstructor(cons);
//6.添加有参构造器
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},cc);
// $0 = this $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);
//7.创建一个名为personInfo的方法,无参数,无返回值,输出name值
CtMethod method = new CtMethod(CtClass.voidType, "personInfo", new CtClass[]{}, cc);
method.setModifiers(Modifier.PUBLIC);
method.setBody("{System.out.println(name);}");
cc.addMethod(method);
//8.将创建的类编译为.class文件
cc.writeFile("F:\\JAVA\\javassist_asm\\src\\main\\java");
}
public static void main(String[] args) {
try {
createPerson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
setBody()中我们使用了一些符号,更多符号请看官方教程:
http://www.javassist.org/tutorial/tutorial2.html
运行上面的代码会在指定位置产生.class文件,内容如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package javassist_;
public class Person {
private String name = "walker";
public String getName() {
return this.name;
}
public void setName(String var1) {
this.name = var1;
}
public Person() {
this.name = "walker";
}
public Person(String var1) {
this.name = var1;
}
public void personInfo() {
System.out.println(this.name);
}
}
二、调用生成的类对象
1.读取.class文件的方式反射调用
我们希望创建的class文件能够在别的类中被调用,代码如下:
package javassist_;
/*
* @author walker1995
* @version v1.0
*/
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Method;
public class InvokePerson {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//通过.class文件获取CtClass对象,需要添加类路径
pool.appendClassPath("F:\\JAVA\\javassist_asm\\src\\main\\java\\");
//1.这里通过get获取Person的CtClass对象
CtClass cc = pool.get("javassist_.Person");
//2.使用toClass加载Person的CtClass对象并实例化
Object person = cc.toClass().newInstance();
//3.通过反射获取setter方法对象并调用
Method setName = person.getClass().getMethod("setName", String.class);
setName.invoke(person, "沃克");
//4.通过反射获取personInfo方法对象并调用
Method info = person.getClass().getMethod("personInfo");
info.invoke(person);
}
}
2.通过接口的方式调用
有时在我们的工程中其实并没有类对象,使用反射的方式比较麻烦,并且开销也很大。
那么如果类对象可以抽象为一些方法的合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。
比如上面的Person类,我们新建一个PersonI接口类:
package javassist_;
/*
* @author walker1995
* @version v1.0
*/
public interface PersonI {
void setName(String name);
String getName();
void personInfo();
}
然后通过接口方式调用:
package javassist_;
/*
* @author walker1995
* @version v1.0
*/
import javassist.ClassPool;
import javassist.CtClass;
public class InvokePersonI {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("F:\\JAVA\\javassist_asm\\src\\main\\java\\");
CtClass ctClass = pool.get("javassist_.Person");
//获取接口
CtClass ctInterface = pool.get("javassist_.PersonI");
//使代码生成的类实现接口PersonI
ctClass.setInterfaces(new CtClass[]{ctInterface});
//通过接口直接调用,强转
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println("当前name为:" + person.getName());
person.setName("Walker 沃克");
person.personInfo();
}
}
可以清晰看到,使用这种方式比反射更轻松。
三、修改现有的类对象
开发中遇到较多的使用场景应该是修改已有的类。比如常见的日志切面,权限切面。
目前有一个如下类:
package javassist_;
/*
* @author walker1995
* @version v1.0
*/
public class PersonService {
public void personFly(){
System.out.println("oh my god,I can fly!");
}
}
我们希望在原本的personFly前后执行其他代码,且还想增加一个joinFriend的方法,修改代码如下:
package javassist_;
/*
* @author walker1995
* @version v1.0
*/
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import java.lang.reflect.Method;
public class ModifyPersonService {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
//此处通过.java文件类文件获取CtClass对象,并不需要添加类路径
// classPool.appendClassPath("F:\\JAVA\\javassist_asm\\src\\main\\java\\");
CtClass ctClass = classPool.get("javassist_.PersonService");
//修改personFly方法
CtMethod personFly = ctClass.getDeclaredMethod("personFly");
personFly.insertBefore("System.out.println(\"起飞之前准备降落伞\");");
personFly.insertAfter("System.out.println(\"成功落地...\");");
//新增joinFriend方法
CtMethod joinFriend = new CtMethod(CtClass.voidType, "joinFriend", new CtClass[]{}, ctClass);
joinFriend.setModifiers(Modifier.PUBLIC);
joinFriend.setBody("{System.out.println(\"I want to be your friend\");}");
ctClass.addMethod(joinFriend);
//实例化PersonService并反射调用personFly和joinFriend方法
Object personService = ctClass.toClass().newInstance();
Method personFlyMethod = personService.getClass().getMethod("personFly", new Class[]{});
personFlyMethod.invoke(personService);
System.out.println();
Method joinFriendMethod = personService.getClass().getMethod("joinFriend", new Class[]{});
joinFriendMethod.invoke(personService);
}
}
此处需要注意的是:insertBefore() 和 setBody()中的语句,如果是单行语句可以直接用双引号,如果是多行语句需要用{}
括起来。javassist只接受单个语句或用大括号括起来的语句块。
0x03 总结
本文主要了解了javassist源码级API的一些常用类和方法,并通过实际敲代码来学习了其基本使用。建议各位读者在理解了javassist的使用逻辑后都动手敲敲代码。
关于javassist后续学习的建议的是:
API使用详情参官方提供的在线API文档:https://www.javassist.org/html/,
关于javassist的设计逻辑和更多使用,参考官方教程:https://www.javassist.org/tutorial/tutorial.html
对于这类东西的学习,还是多看看官方文档。一方面,国内文章都来源于官方文档,只不过一手文章的作者加入了自己的一些理解,其他也都是相互抄,本文也是在看了一部分官方文档和国内文章来写的,所以本文不知道是第几手文章了(狗头)
另一方面,如果想搞安全研究一类的工作,阅读英文文档会是一个必不可少的内容,我英文也很菜,多看多学吧。
参考:
https://www.javassist.org/html/
https://www.javassist.org/tutorial/tutorial.html
https://juejin.cn/post/7078681608206680094
Java安全系列文集
第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用
第8篇:JAVA安全|Gadget篇:TransformedMap CC1链
第10篇:JAVA安全|Gadget篇:LazyMap CC1链
第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链
第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造
第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链
第17篇:JAVA安全|Gadget篇:CC2 CC4链—Commons-Collections4.0下的特有链
第19篇:JAVA安全|Gadget篇:Ysoserial CB1链
第20篇:JAVA安全|Gadget篇:shiro无依赖利用与常见shiro利用工具对比浅析
第21篇:JAVA安全|Gadget篇:JDK原生链—JDK7u21
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~