这是一段很典型的动态加载字节码操作,在jdk8中,可以执行任意代码。
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();
}
}
不过,随着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
文件来定义。这个文件位于模块的根目录下,包含了该模块的元数据。以下是一个示例:
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 处理相关的模块。
模块化可以比作建造一座房子,将其分成多个功能明确的房间。每个房间(模块)负责特定的任务,如客厅用于接待、厨房用于做饭等,这样使得整个房子(软件项目)结构清晰、易于理解和维护。模块之间互不干扰,减少了功能冲突,允许独立开发和测试,提升了开发效率。此外,模块化还可以按需加载,节省资源,并使得维护工作变得更加简单,只需修复出现问题的模块,而不影响其他部分。
影响
那么其实这次更新,其实并没有对上面代码造成什么影响,我们仍然可以动态加载字节码,只是会代码执行时会输出警告信息,提示你正在进行非法反射访问。这并不会阻止代码执行,但会提醒你这种做法在未来可能会被禁止。
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进行封装的两个理由:
对java代码进行反射是不安全的,比如可以调用ClassLoader的defineClass方法,这样在运行时候可以给程序注入任意代码。
java的这些非公开的api本身就是非标准的,让开发者依赖使用这个api会给JDK的维护带来负担。
所以从JDK 9开始就准备限制对java api的反射进行限制,直到JDK 17才正式禁用
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指令,也就是说并没有开放资源给外部模块反射访问。
下面是部分关键字的解释:
module
定义:用于声明一个模块。每个模块都有一个唯一的名称。
示例:
module com.example.myModule {
}
requires
定义:用来声明当前模块所依赖的其他模块。一个模块可以依赖于一个或多个模块。
示例:
module com.example.myModule {
requires com.example.otherModule;
}
exports
定义:用来声明当前模块希望公开的包。通过
exports
声明的包中的公共类和接口可以被其他模块访问。示例:
module com.example.myModule {
exports com.example.myModule.api;
}
opens
定义:与
exports
类似,但允许其他模块通过反射访问指定的包。适用于需要动态访问类和成员的情况,如序列化和依赖注入。示例:
module com.example.myModule {
opens com.example.myModule.internal to com.example.otherModule;
}
而我们动态类加载调用的是反射调用java.lang.ClassLoader#defineClass,为java.lang.*位于java.base模块下,而这个模块不允许外部模块反射调用,因此上面代码无法成功动态加载字节码。
绕过
当然,我们任然可以绕过上述限制。
根据报错信息,我们可以很容易追踪到问题出现在如下函数中。
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
的情况的总结:
同一模块:如果
callerModule
和declaringModule
是同一个模块(callerModule == declaringModule
),返回true
。未命名模块:如果
callerModule
是Object
类所在的模块(未命名模块),返回true
(callerModule == Object.class.getModule()
)。未命名模块的声明者:如果
declaringModule
不是命名模块(!declaringModule.isNamed()
),返回true
。公共类和导出的包:如果
declaringClass
是公共类(isClassPublic
为true
)并且其声明者模块导出了包(declaringModule.isExported(pn, callerModule)
):若成员是公共的(Modifier.isPublic(modifiers)
),返回true
。或者若成员是受保护的静态成员(Modifier.isProtected(modifiers) && Modifier.isStatic(modifiers)
)且调用者是声明者类的子类(isSubclassOf(caller, declaringClass)
),返回true
。开放的包:如果
declaringModule
对包(pn
)是开放的(declaringModule.isOpen(pn, callerModule)
),返回true
。
我们可以关注第一种情况,也就是callerModule
和declaringModule
是同一个模块这种情况。因为他的比较是获取调用类的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存在defineClass
和defineAnonymousClass
不过在jdk17之后都被删除了。就不讨论了,和ClassLoader#defineclass
功能一样。
我们关注下面这些方法:
objectFieldOffset
主要功能是使用反射来获取字段的偏移量
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
获取一个对象在特定内存偏移量上的当前值,并将其替换为一个新值。
public final Object getAndSetObject(Object o, long offset, Object newValue) {
return theInternalUnsafe.getAndSetReference(o, offset, newValue);
}
putObject
用于将一个对象(或引用)写入到指定对象的内存偏移量
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对象可以用下面代码
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
可以直接反射获得该类,因为Unsafe类所在的模块jdk.unsupported
的moudle-info.class
文件中,使用了opens指令开放了sun.misc
包,因为我们可以对该包下面的类进行反射操作,自然也包括sun.misc.Unsafe
那么我们来用上面的知识来编写jdk17以上版本的动态加载字节码。
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();
}
}