前言
JDK9版本开始引入Java平台模块系统JPMS(Java Platform Module System),详细介绍可以看Oracle官方对于JDK9的新特性说明:https://docs.oracle.com/javase/9/whatsnew/toc.htm
关于模块之间的访问权限:
通常Java的class类访问权限分为:public、protected、private和默认的包访问权限。JDK9引入模块概念后,这些概念就要和模块的区分一下,class的这些访问权限没有失效,但是只能在模块内生效。模块和模块之间如果想要外部访问到我们的类就需要显式导出一下也就是使用expoerts
module hello.world {
exports com.itranswarp.sample;
requires java.base;
requires java.xml;
}
demo如下:
import javax.management.loading.MLet;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Base64;
public class Main {
public static void main(String[] args) {
try {
String evilClassBase64 = "xxxx";
byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class cc = (Class) method.invoke(new MLet(new URL[0], Main.class.getClassLoader()), bytes, new Integer(0), new Integer(bytes.length));
cc.newInstance();
}catch (Exception e){
}
}
}
将上面的demo分别在JDK11
和JDK21
环境中测试
JDK 11
使用上述demo运行后会提示非法反射操作,并且会提示在未来的版本会完全禁用掉此类的不安全反射操作,但是不影响字节码的加载
JDK21
JDK17版本之后使用了强封装直接会ban掉非法反射,可以看报错提示java.base 模块中的 java.lang 包没有对未命名模块开放反射
Oracle官方文档给出的解释
https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-7BB28E4D-99B3-4078-BDC4-FC24180CE82B
这里就要去看一下JDK9之后模块化的一个指令
open, opens, opens…to 指令
在 Java 9 之前,我们可以通过反射技术来获取某个包下所有的类及其内部成员的信息,即使是 private 类型我们也能获取到,所以类信息并不是真的与外界完全隔离的。而模块系统的主要目标之一就是实现强封装,默认情况下,除非显式地导出或声明某个类为 public 类型,那么模块中的类对外部都是不可见的,模块化要求我们对外部模块应最小限度地暴露包的范围。open 相关的指令就是用来限制在运行时哪些类可以被反射技术探测到。
首先我们先看 opens 指令,语法如下:
opens package
opens 指令用于指定某个包下所有的 public 类都只能在运行时可被别的模块进行反射,并且该包下的所有的类及其乘员都可以通过反射进行访问。
opens…to 指令,语法如下:
opens package to modules
该指令用于指定某些特定的模块才能在运行时对该模块下特定包下的 public 类进行反射操作,to 后面跟逗号分隔的模块名称。
open 指令,语法如下:
open module moduleName{
}
该指令用于指定外部模块可以对该模块下所有的类在运行时进行反射操作。
也就是说JDK17+在开发的时候并没有将我们所需要的java.lang
开放反射权限,导致我们无法进行反射类加载,查看JDK源码中的module-info.class
定义,发现确实没有使用open
指令
后面看Oracle的官方文档发现,官方预留了sun.misc
和sun.reflect
两个包可以进行反射调用
后面看了下JDK21源码,在JDK的jdk.unsupported
模块下的module-info
中有声明
使用opens的指令使得两个包名下的类可以被反射。
Unsafe
关于Unsafe类的介绍可以看这篇文章:https://javaguide.cn/java/basis/unsafe.html
文中提到了它分别有defineClass
和defineAnonymousClass
两种方法可以加载字节码的方式。但是作者在实际JDK8、JDK11、JDK21的环境中测试中发现,JDK8同时存在两个方法,但是JDK11只剩defineAnonymousClass
一种方法,甚者JDK21两种方法都被移除了。。。。
下面是三个版本的JDK的Unsafe
类
JDK8
JDK11
JDK21
后面去查找相关文章发现在JDK 17移除了defineAnonymousClass
方法。
也就是说在 <JDK17之前都可以直接用defineAnonymousClass
这个方法去进行反射类加载操作。
Field field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
unsafe.defineAnonymousClass(Class.class,bytes,null).newInstance();
JDK17+的字节码加载
虽然JDK模块化后官方预留了sun.misc
和sun.reflect
两个包可以进行反射调用,但是JDK17+同时也删掉了Unsafe
的defineAnonymousClass
方法。这就导致前面的加载方式就失效了。
后面看了@Aiwin师傅在https://xz.aliyun.com/t/14048分享通过修改当前类的module为java.base
保持和java.lang.ClassLoader
同module下就可以打破模块化的限制,从而可以加载字节码文件。
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)) {
logIfExportedForIllegalAccess(caller, declaringClass);
return true;
}
// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
logIfExportedForIllegalAccess(caller, declaringClass);
return true;
}
}
// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
logIfOpenedForIllegalAccess(caller, declaringClass);
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;
}
从上述的方法中可以看到主要是检查了class的moudle,Unsafe类中恰好可以修改偏移量,将我们的类的module修改成基础module就可以绕过JDK17+版本的反射限制。
String evilClassBase64 = "xxxx";
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 = Main.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.putObject(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();