浅析高版本JDK反射类加载问题

前言

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", String.class, 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分别在JDK11JDK21环境中测试

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.miscsun.reflect两个包可以进行反射调用

后面看了下JDK21源码,在JDK的jdk.unsupported模块下的module-info中有声明

使用opens的指令使得两个包名下的类可以被反射。

Unsafe

关于Unsafe类的介绍可以看这篇文章:https://javaguide.cn/java/basis/unsafe.html

文中提到了它分别有defineClassdefineAnonymousClass两种方法可以加载字节码的方式。但是作者在实际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.miscsun.reflect两个包可以进行反射调用,但是JDK17+同时也删掉了UnsafedefineAnonymousClass方法。这就导致前面的加载方式就失效了。

后面看了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();

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值