高版本JDK加载字节码分析

高版本JDK加载字节码分析

这是一段很典型的动态加载字节码操作,在jdk8中,可以执行任意代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;

public class evilByteClassloader {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
String evilClassBase64="Base64 encoding of malicious bytecode";
byte[] decode = Base64.getDecoder().decode(evilClassBase64);
Method defineClass =ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evilClassloader = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), decode, 0, decode.length);
evilClassloader.newInstance();
}
}

image-20241026173730868

不过,随着java版本的更新,这种方法被限制。

JDK9的模块化

Oracle官方文档:Java Platform, Standard Edition What’s New in Oracle JDK 9, Release 9

Java 9 引入了模块化系统(Project Jigsaw)。模块化的主要目的是提高 Java 的可维护性、可扩展性和安全性,同时改善大型应用程序和库的构建和管理。

  • 模块:模块是一个具有明确边界的代码单元,它包含了一组相关的包、类和资源。每个模块都有一个描述文件(module-info.java),用于声明模块的名称、依赖关系以及导出的包。

在 Java 9 中,每个模块都用一个 module-info.java 文件来定义。这个文件位于模块的根目录下,包含了该模块的元数据。以下是一个示例:

1
2
3
4
module com.example.myModule {
exports com.example.myModule.api; // 导出公共 API
requires java.sql; // 声明依赖于 java.sql 模块
}

Java 9 还将 JDK 本身进行了模块化

  • java.base:核心 Java 类库,所有模块都依赖于这个模块。
  • java.sql:与数据库连接相关的模块。
  • java.xml:与 XML 处理相关的模块。

模块化可以比作建造一座房子,将其分成多个功能明确的房间。每个房间(模块)负责特定的任务,如客厅用于接待、厨房用于做饭等,这样使得整个房子(软件项目)结构清晰、易于理解和维护。模块之间互不干扰,减少了功能冲突,允许独立开发和测试,提升了开发效率。此外,模块化还可以按需加载,节省资源,并使得维护工作变得更加简单,只需修复出现问题的模块,而不影响其他部分。

影响

那么其实这次更新,其实并没有对上面代码造成什么影响,我们仍然可以动态加载字节码,只是会代码执行时会输出警告信息,提示你正在进行非法反射访问。这并不会阻止代码执行,但会提醒你这种做法在未来可能会被禁止。

1
2
3
4
5
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by EvilClassLoader.evilByteClassloader (file:/D:/java_local/Temp/target/classes/) to method java.lang.ClassLoader.defineClass(byte[],int,int)
WARNING: Please consider reporting this to the maintainers of EvilClassLoader.evilByteClassloader
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

JDK17的强封装

Migrating From JDK 8 to Later JDK Releases

Some tools and libraries use reflection to access parts of the JDK that are meant for internal use only. This use of reflection negatively impacts the security and maintainability of the JDK. To aid migration, JDK 9 through JDK 16 allowed this reflection to continue, but emitted warnings about illegal reflective access. However, JDK 17 is strongly encapsulated, so this reflection is no longer permitted by default.

根据Oracle的文档,为了安全性,从JDK 17开始对java本身代码使用强封装,原文叫Strong Encapsulation。任何对java.*代码中的非public变量和方法进行反射会抛出InaccessibleObjectException异常。

JDK的文档解释了对java api进行封装的两个理由:

  1. 对java代码进行反射是不安全的,比如可以调用ClassLoader的defineClass方法,这样在运行时候可以给程序注入任意代码。
  2. java的这些非公开的api本身就是非标准的,让开发者依赖使用这个api会给JDK的维护带来负担。

所以从JDK 9开始就准备限制对java api的反射进行限制,直到JDK 17才正式禁用

1
2
3
4
5
6
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @404b9385
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at EvilClassLoader.evilByteClassloader.main(evilByteClassloader.java:13)

这段信息说明:

  • defineClass 方法被声明为 protected,并且它属于 java.base 模块。
  • java.base 模块没有将 java.lang 包开放给未命名的模块(即没有明确声明的模块)。

可以打开java.base模块的moudle-info.class文件看看就知道了,没有使用opens指令,也就是说并没有开放资源给外部模块反射访问。

下面是部分关键字的解释:

  1. module
  • 定义:用于声明一个模块。每个模块都有一个唯一的名称。
  • 示例
1
2
module com.example.myModule {
}
  1. requires
  • 定义:用来声明当前模块所依赖的其他模块。一个模块可以依赖于一个或多个模块。
  • 示例
    1
    2
    3
    module com.example.myModule {
    requires com.example.otherModule;
    }
  1. exports
  • 定义:用来声明当前模块希望公开的包。通过 exports 声明的包中的公共类和接口可以被其他模块访问。
  • 示例
    1
    2
    3
    module com.example.myModule {
    exports com.example.myModule.api;
    }
  1. opens
  • 定义:与 exports 类似,但允许其他模块通过反射访问指定的包。适用于需要动态访问类和成员的情况,如序列化和依赖注入。
  • 示例
    1
    2
    3
    module com.example.myModule {
    opens com.example.myModule.internal to com.example.otherModule;
    }

而我们动态类加载调用的是反射调用java.lang.ClassLoader#defineClass,为java.lang.*位于java.base模块下,而这个模块不允许外部模块反射调用,因此上面代码无法成功动态加载字节码。

绕过

当然,我们任然可以绕过上述限制。

根据报错信息,我们可以很容易追踪到问题出现在如下函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
throw new IllegalCallerException(); // should not happen
}

Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();

if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;

String pn = declaringClass.getPackageName();
int modifiers;
if (this instanceof Executable) {
modifiers = ((Executable) this).getModifiers();
} else {
modifiers = ((Field) this).getModifiers();
}

// class is public and package is exported to caller
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
// member is public
if (Modifier.isPublic(modifiers)) {
return true;
}

// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
return true;
}
}

// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
return true;
}

if (throwExceptionIfDenied) {
// not accessible
String msg = "Unable to make ";
if (this instanceof Field)
msg += "field ";
msg += this + " accessible: " + declaringModule + " does not \"";
if (isClassPublic && Modifier.isPublic(modifiers))
msg += "exports";
else
msg += "opens";
msg += " " + pn + "\" to " + callerModule;
InaccessibleObjectException e = new InaccessibleObjectException(msg);
if (printStackTraceWhenAccessFails()) {
e.printStackTrace(System.err);
}
throw e;
}
return false;
}

这段代码是 Java 中反射机制的一部分,主要用于检查一个类或成员(如方法、字段)是否可以通过反射访问。它的功能是决定一个调用者(caller)是否有权限访问某个声明者的类或成员(declaringClass)。

在这段代码中,checkCanSetAccessible 方法有多个条件可以返回 true,表示调用者可以访问指定的类或成员。以下是所有能返回 true 的情况的总结:

  1. 同一模块:如果 callerModuledeclaringModule 是同一个模块(callerModule == declaringModule),返回 true
  2. 未命名模块:如果 callerModuleObject 类所在的模块(未命名模块),返回 truecallerModule == Object.class.getModule())。
  3. 未命名模块的声明者:如果 declaringModule 不是命名模块(!declaringModule.isNamed()),返回 true
  4. 公共类和导出的包:如果 declaringClass 是公共类(isClassPublictrue)并且其声明者模块导出了包(declaringModule.isExported(pn, callerModule)):若成员是公共的(Modifier.isPublic(modifiers)),返回 true。或者若成员是受保护的静态成员(Modifier.isProtected(modifiers) && Modifier.isStatic(modifiers))且调用者是声明者类的子类(isSubclassOf(caller, declaringClass)),返回 true
  5. 开放的包:如果 declaringModule 对包(pn)是开放的(declaringModule.isOpen(pn, callerModule)),返回 true

我们可以关注第一种情况,也就是callerModuledeclaringModule 是同一个模块这种情况。因为他的比较是获取调用类的Class对象的Moudle属性去和声明类的Class对象的module属性去做一个比较,即我们正在运行的当前类和ClassLoader类的Class对象的module属性做比较,如果我们可以修改当前类的Class对象的module属性,将其设置和ClassLoader类的Class对象的module属性一样,那就可以返回true,即运行当前类反射调用ClassLoader类的defineclass方法。

那么如何做到了,先别急,我们需要引入一个类,因为上面的操作将基于这个类的方法来实现。

Unsafe类

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。——节选:Java 魔法类 Unsafe 详解 | JavaGuide

这个类正好提供了修改类的Class对象的module。

Unsafe存在defineClassdefineAnonymousClass不过在jdk17之后都被删除了。就不讨论了,和ClassLoader#defineclass功能一样。

我们关注下面这些方法:

objectFieldOffset

主要功能是使用反射来获取字段的偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
public long objectFieldOffset(Field f) {
if (f == null) {
throw new NullPointerException();
}
Class<?> declaringClass = f.getDeclaringClass();
if (declaringClass.isHidden()) {
throw new UnsupportedOperationException("can't get field offset on a hidden class: " + f);
}
if (declaringClass.isRecord()) {
throw new UnsupportedOperationException("can't get field offset on a record class: " + f);
}
return theInternalUnsafe.objectFieldOffset(f);
}

getAndSetObject

获取一个对象在特定内存偏移量上的当前值,并将其替换为一个新值。

1
2
3
public final Object getAndSetObject(Object o, long offset, Object newValue) {
return theInternalUnsafe.getAndSetReference(o, offset, newValue);
}

putObject

用于将一个对象(或引用)写入到指定对象的内存偏移量

1
2
3
public void putObject(Object o, long offset, Object x) {
theInternalUnsafe.putReference(o, offset, x);
}

putObject和getAndSetObject方法功能很相似,不过getAndSetObject会返回被替换的旧值,当然里面细节也有所不同,比如getAndSetObject会使用原子操作保障同时执行读取和写入。不过就我们修改类的Class对象的moudle属性来说,两者都可以。

我一直在说类的Class对象的module,也就是说module这个属性是在Class类里面的,当类加载到虚拟机的时候,会生成该类的Class对象,并且其有且仅有一个存在。

获取一个Unsafe对象可以用下面代码

1
2
3
4
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

可以直接反射获得该类,因为Unsafe类所在的模块jdk.unsupportedmoudle-info.class文件中,使用了opens指令开放了sun.misc包,因为我们可以对该包下面的类进行反射操作,自然也包括sun.misc.Unsafe

image-20241028025725610

那么我们来用上面的知识来编写jdk17以上版本的动态加载字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;

public class JDKbypass {
public static void main(String[] args) throws Exception {
String evilClassBase64 = "Base64 encoding of malicious bytecode";
byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = JDKbypass.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
//使用putObject方法来设置moudle
unsafe.putObject(currentClass, offset, baseModule);
//使用getAndSetObject方法来设置moudle
//unsafe.getAndSetObject(currentClass,offset,baseModule);
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
((Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length)).newInstance();
}
}

高版本JDK加载字节码分析
http://example.com/2024/10/28/高版本JDK加载字节码分析/
作者
cmisl
发布于
2024年10月28日
许可协议