目录
前言:
(一)类加载机制
0x01 ClassLoader 类
0x02 loadClass()方法的流程
0x03 自定义的类加载器
0x04 loadClass()方法与 Class.forName 的区别
0x05 URLClassLoader
(二)Java 动态代理
0x01 静态代理
0x02 动态代理
1.Proxy 类
2.InvocationHandler 接口
0x03 CGLib 代理
(三)Javassist 动态编程
前言:
Java 程序是由 class 文件组成的一个完整的应用程序。在程序运行时,并不会一 次性加载所有的 class 文件进入内存,而是通过 Java 的类加载机制( ClassLoader )进 行动态加载,从而转换成 java.lang.Class 类的一个实例。
(一)类加载机制
0x01 ClassLoader 类
ClassLoader 是一个抽象类,主要的功能是通过指定的类的名称,找到或生成对 应的字节码,返回一个 java.lang.Class 类的实例。开发者可以继承 ClassLoader 类来 实现自定义的类加载器。
ClassLoader
类中和加载类相关的方法如图 1-1
所示
0x02 loadClass()方法的流程
前面曾介绍过 loadClass() 方法可以加载类并返回一个 java.lang.Class 类对象。通 过如下源码可以看出,当 loadClass() 方法被调用时,会首先使用 findLoadedClass() 方 法判断该类是否已经被加载,如果未被加载,则优先使用加载器的父类加载器进行 加载。当不存在父类加载器,无法对该类进行加载时,则会调用自身的 findClass() 方法,可以重写 findClass() 方法来完成一些类加载的特殊要求。该方法的代码如 下所示:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//省略
}
if (c == null) {
//省略
c = findClass(name);
//省略
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
0x03 自定义的类加载器
根据loadClass() 方法的流程,可以发现通过重写 findClass() 方法,利用 defineClass() 方法来将字节码转换成 java.lang.class 类对象,就可以实现自定义的类加载器。示例 代码如下所示:
public class DemoClassLoader extends ClassLoader {
private byte[] bytes ;
private String name = "";
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, In
stantiationException {
String clzzName = "com.test.Hello";
byte[] testBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 52, 0, 28, 10, 0, 6, 0, 14, 9,
0, 15, 0, 16, 8, 0, 17, 10, 0, 18, 0, 19, 7,
0, 20, 7, 0, 21, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0,
3, 40,
//省略
};
DemoClassLoader demo = new DemoClassLoader(clzzName,testBytes);
Class clazz = demo.loadClass(clzzName);
Constructor constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(obj);
}
public DemoClassLoader(String name, byte[] bytes){
this.name = name;
this.bytes = bytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
{
if(name.equals(this.name)) {
defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}
}
- 该示例代码的执行结果如图 3-1 所示:
0x04 loadClass()方法与 Class.forName 的区别
loadClass()方法只对类进行加载,不会对类进行初始化。 Class.forName 会默认对 类进行初始化。当对类进行初始化时,静态的代码块就会得到执行,而代码块和构 造函数则需要适合的类实例化才能得到执行,示例代码如下所示:
public class Dog {
static {
System.out.println("静态代码块执行");
}
{
System.out.println("代码块执行");
}
public Dog(){
System.out.println("构造方法执行");
}
}
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("Dog");
ClassLoader.getSystemClassLoader().loadClass("Dog");
}
}
- 该示例代码的执行结果如图 4-1 所示
0x05 URLClassLoader
URLClassLoader 类是 ClassLoader 的一个实现,拥有从远程服务器上加载类的能力。通过 URLClassLoader 可以实现对一些 WebShell 的远程加载、对某个漏洞的深入利用。
(二)Java 动态代理
0x01 静态代理
所谓静态代理,顾名思义,当确定代理对象和被代理对象后,就无法再去代理 另一个对象。同理,在 Java 静态代理中,如果我们想要实现另一个代理,就需要重 新写一个代理对象,其原理如图 5-1 所示
总而言之,在静态代理中,代理类和被代理类实现了同样的接口,代理类同时 持有被代理类的引用。当我们需要调用被代理类的方法时,可以通过调用代理类的 方法实现,静态代理的实现如图 5-2 所示:
0x02 动态代理
静态代理的优势很明显,即允许开发人员在不修改已有代码的前提下完成一些 增强功能的需求。但是静态代理的缺点也很明显,它的使用会由于代理对象要实现 与目标对象一致的接口,从而产生过多的代理类,造成冗余;其次,大量使用静态 代理会使项目不易维护,一旦接口增加方法,目标对象与代理对象就要进行修改。 而动态代理的优势在于可以很方便地对代理类的函数进行统一的处理,而不用修改 每个代理类中的方法。对于我们信息安全人员来说,动态代理意味着什么呢?实际 上, Java 中的“动态”也就意味着使用了反射,因此动态代理其实是基于反射机制 的一种代理模式。
如图 5-3
所示,动态代理与静态代理的区别在于,通过动态代理可以实现多个需
求。动态代理其实是通过实现接口的方式来实现代理,具体来说,动态代理是通过
Proxy
类创建代理对象,然后将接口方法“代理”给
InvocationHandler
接口完成的。
动态代理的关键有两个,即上文中提到的 Proxy 类以及 InvocationHandler 接口, 这是我们实现动态代理的核心。
1.Proxy 类
在 JDK 中, Java 提供了 :
- Java.lang.reflect.InvocationHandler 接口
- Java.lang.reflect.Proxy 类
这两个类相互配合,其中 Proxy 类是入口。 Proxy 类是用来创建一 个代理对象的类,它提供了很多方法:
- static Invocation Handler get Invocation Handler (Object proxy) :该方法主要用于获取指定代理对象所关联的调用程序。
- static Class<?> get Proxy Class (ClassLoader loader, Class<?>... interfaces) :该方法主要用于返回指定接口的代理类。
- static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, Invocation Handler h):该方法主要返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
- static boolean is Proxy Class (Class<?> cl):当且仅当指定的类通过 get ProxyClass 方法或 newProxyInstance 方法动态生成为代理类时,返回 true。该方法的可靠性对于使用它做出安全决策而言非常重要,所以它的实现不应仅测试相关的类是否可以扩展 Proxy
在上述方法中,最常用的是 newProxyInstance 方法,该方法的作用是创建一个代 理类对象,它接收 3 个参数: loader 、 interfaces 以及 h ,各个参数含义如下。
- loader:这是一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象对生成的代理类进行加载。
- interfaces:这是代理类要实现的接口列表,表示用户将要给代理对象提供的接口信息。如果提供了这样一个接口对象数组,就是声明代理类实现了这些接口,代理类即可调用接口中声明的所有方法。
- h:这是指派方法调用的调用处理程序,是一个 InvocationHandler 对象,表示当动态代理对象调用方法时会关联到哪一个 InvocationHandler 对象上,并最终由其调用。
2.InvocationHandler 接口
Java.lang.reflect InvocationHandler,主要方法为 Object invoke ( Object proxy,Method method, Object[] args ),该方法定义了代理对象调用方法时希望执行的动作, 用于集中处理在动态代理类对象上的方法调用。 Invoke 有 3 个参数: proxy 、 method 、 args ,各个参数含义如下:
- proxy:在其上调用方法的代理实例。
- method:对应于在代理实例上调用的接口方法的 Method 实例。 Method 对象的声明类将是在其中声明方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口。
- args:包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为 null。基本类型的参数被包装在适当基本包装器类(如 Java.lang.Integer 或 Java.lang.Boolean)的实例中。
0x03 CGLib 代理
CGLib( Code Generation Library )是一个第三方代码生成类库,运行时在内存中 动态生成一个子类对象,从而实现对目标对象功能的扩展。动态代理是基于 Java 反 射机制实现的,必须实现接口的业务类才能使用这种办法生成代理对象。而 CGLib 则基于 ASM 机制实现,通过生成业务类的子类作为代理类。 与动态代理相比,动态代理只能基于接口设计,对于没有接口的情况, JDK 方 式无法解决,而 CGLib 则可以解决这一问题;其次, CGLib 采用了非常底层的字节 码技术,性能表现也很不错。
(三)Javassist 动态编程
在了解 Javassist 动态编程之前,首先来了解一下什么是动态编程。动态编程是 相对于静态编程而言的一种编程形式,对于静态编程而言,类型检查是在编译时完 成的,但是对于动态编程来说,类型检查是在运行时完成的。因此所谓动态编程就 是绕过编译过程在运行时进行操作的技术。
那么动态编程可以解决什么样的问题呢?其实动态编程做的事情,静态编程也
可以做到,但相对于动态编程来说,静态编程要实现动态编程所实现的功能,过程
会比较复杂。一般来说,在依赖关系需要动态确认或者需要在运行时动态插入代码
的环境中,需要使用动态编程。
Java 字节码以二进制形式存储在
class
文件中,每一个
class
文件都包含一个
Java
类或接口。
Javassist
就是一个用来处理
Java
字节码的类库,其主要优点在于简
单、便捷。用户不需要了解虚拟机指令,就可以直接使用
Java
编码的形式,并且可
以动态改变类的结构,或者动态生成类。
Javassist 中最为重要的是 ClassPool、CtClass 、CtMethod 以及 CtField 这 4 个类:
- ClassPool:一个基于 HashMap 实现的 CtClass 对象容器,其中键是类名称,值是表示该类的 CtClass 对象。默认的 ClassPool 使用与底层 JVM 相同的类路径,因此在某些情况下,可能需要向 ClassPool 添加类路径或类字节。
- CtClass:表示一个类,这些 CtClass 对象可以从 ClassPool 获得。
- CtMethods:表示类中的方法。
- CtFields:表示类中的字段。
Javassist 官方文档中给出的代码示例如下:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
这段程序首先获取 ClassPool
的实例,它主要用来修改字节码,里面存储着基于
二进制文件构建的
CtClass
对象,它能够按需创建出
CtClass
对象并提供给后续处理
流程使用。当需要进行类修改操作时,用户需要通过
ClassPool
实例的
.get()
方法获取
CtClass
对象。
我们可以从上面的代码中看出,ClassPool 的 getDefault() 方法将会查找系统默认 的路径来搜索 test.Rectable 对象,然后将获取到的 CtClass 对象赋值给 cc 变量。 这里仅是构造 ClassPool 对象以及获取 CTclass 的过程,具体的 Javassist 的使用 流程如图 3-1 所示。
操作 Java 字节码有两个比较流行的工具,即 Javassist 和 ASM 。 Javassist 的优点 是提供了更高级的 API,无须掌握字节码指令的知识,对使用者要求较低,但同时其 执行效率相对较差; ASM 则直接操作字节码指令,执行效率高,但要求使用者掌握 Java 类字节码文件格式及指令,对使用者的要求比较高。安全人员能够利用 Javassist 对目标函数动态注入字节码代码。通过这种方式, 我们可以劫持框架的关键函数,对中间件的安全进行测试,也可以劫持函数进行攻 击阻断。此外,对于一些语言也可以很好地进行灰黑盒测试。