jdk17模块化特性&绕过
1diot9 Lv4

模块化介绍

这是jdk8的库:

img

这是jdk17的库:

img

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

不过,jdk9时,模块化机制对我们进行反序列化没有实际影响,只是会打印警告而已,并不会直接报错中断。

模块化的强封装是从jdk17开始的。任何对java.*代码中的非public变量和方法进行反射会抛出InaccessibleObjectException异常。

但是每个模块中有一个module-info.java文件,里面定义了模块中的哪些包可以被外部访问,以及本模块依赖于哪些包。

1、export

img

通过 exports 声明的包中的公共类和接口可以被其他模块访问。

如果没有exports的话,你自己的类都没法去new

2、requires

img

表示依赖哪个模块。transitive表示隐式依赖。在这种情况下,任何使用了 java.desktop 模块的代码都能隐式的使用 java.xml 模块中的类。例如,如果 java.desktop 模块中某个方法返回了一个来自于 java.xml 模块中的类型,那么使用 java.desktop 模块的代码就间接依赖 java.xml 模块,如果没有 transitive 的话,那么使用 java.desktop 模块的模块就必须显式声明依赖 java.xml 模块才能正常编译,否则 java.xml 模块是不可见的。

3、opens

img

表示哪些包是对外开放的,即可以被反射调用。有to的话,就是指定对哪个模块开放。

1
2
3
4
5
6
7
8
9
10
11
public class Failed {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
byte[] bytes = Files.readAllBytes(Paths.get("Calc.class"));
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class clazzLoader = (Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length);
clazzLoader.newInstance();
// 抛出:Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int)
// throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @6f496d9f
}
}

上面这段代码,在jdk8可以实现构造方法执行,但在jdk17中,会报错。

从抛出的异常中可以知道,java.base模块中的java.lang包没有对uname模块开放,也就是没有对我们自定义的类开放。

源码分析

去看java.lang.reflect.AccessibleObject#checkCanSetAccessible(java.lang.Class, java.lang.Class, boolean)

img

这里有三种方法能返回true:

1、调用类(自己的类)的模块和被调用类的模块名相同

2、调用类的模块和Object类的模块相同,即java.base

3、被调用类的模块是未命名的

这里我们会选择第一种,因为callerModule是通过caller.getModule获取的,我们也许能直接修改里面的module字段。方法二也可以,但是不通用,setAccessible能通过,但是其他反射调用,比如newInstance的时候就不一定行。

img

往下看,还有三种方法能返回true。前两个的先决条件是:被调用类是public,被调用类所在的包被exports了

1、被调用的方法或字段是public的

2、被调用的方法或字段是protect-static的,且调用者是被调用者的子类

3、被调用的包对调用者是open的

这三种都不考虑,因为这些都是直接写死在代码里的,没有操作空间。

绕过方法

patchModule

要用到Unsafe类,这个类提供了类似C语言的指针操作,能够直接修改对象中的字段值。

首先,Unsafe类是opens的:

img

所以咱们可以直接在自己的类中使用。

Unsafe类中有几个关键方法:

img

objectFieldOffset:获取字段的偏移量

img

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

img

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

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

img

这里callerModule是通过caller.getModule()获取的,而caller是一个Class对象。当调用类,也就是我们自己的类加载到JVM时,会产生一个Class对象,这个Class对象是唯一的,里面的Module字段也是唯一的。所以,我们能够保证修改成功。

最终代码:

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
package com.test;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ModuleBypass {
public static void main(String[] args) throws Exception {
patchModule(ModuleBypass.class, ClassLoader.class);

byte[] bytes = Files.readAllBytes(Paths.get("Calc.class"));
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class clazzLoader = (Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length);
clazzLoader.newInstance();
}

public static void patchModule(Class current, Class target) throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

// 所有Class的数据结构都是一样的,相同字段的偏移量也是一样的
long l = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
Module targetModule = target.getModule();
unsafe.putObject(current, l, targetModule);
}
}

增加虚拟机参数选项

由于上面的过程只是为了能够在本地调用其他模块,所以也可以通过添加虚拟机参数选项的方法实现。

img

需要注意的是,如果反序列化过程中,有第三方组件对jdk中的模块进行反射调用,就不能通过这种方式去修改。否则,虽然你本地可以打通,但是远程是打不通的,因为远程的JVM可没有打开这个选项。上面的patchModule也是同样的道理。必须在反序列化过程中,执行patchModule才行。

补充

java.lang.reflect.Constructor#newInstance–>java.lang.reflect.AccessibleObject#checkAccess–>java.lang.reflect.AccessibleObject#verifyAccess–>java.lang.reflect.AccessibleObject#slowVerifyAccess–>jdk.internal.reflect.Reflection#verifyModuleAccess–>java.lang.Module#isExported(java.lang.String, java.lang.Module)–>java.lang.Module#implIsExportedOrOpen–>java.lang.Module#isStaticallyExportedOrOpen–>java.lang.Module#allows

newInstance反射最终是这样绕过的,所以上面patchModule才选择使调用者和被调用者的模块一致,而不是简单地使调用者的模块为java.base

这里的绕过可以看这篇文章:

https://jiecub3.github.io/zh/posts/java/chain/jdk17cc%E9%93%BE%E4%B8%8B%E5%88%A9%E7%94%A8templatesimpl/

参考

https://mp.weixin.qq.com/s/Nvra3OljzllryYg9L9yCFQ

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 61.3k 访客数 访问量