Unsafe绕过高版本JDK反射限制

前言

从JDK8迁移到更高版本的JDK时,在安全性方面做了很大的提升,从JDK9开始引入的模块化系统,到JDK16进一步加强。JDK的强封装是一个重要的特性,它旨在提高JDK的安全性和可维护性,同时减少对非标准、内部JDK实现细节的依赖。JDK17及以后的版本默认对Java本身代码使用强封装(Strong Encapsulation),这意味着使用反射访问JDK内部API的代码将不再被允许,任何对java.*代码中的非公共字段和方法进行反射将抛出InaccessibleObjectException异常

但是需要注意,sun.misc和sun.reflect包可供所有JDK版本(包括JDK17)中的工具和库进行反射

测试

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

public class Main {
public static void main(String[] args) throws Exception {
System.out.println("Java Version: " + System.getProperty("java.version"));
String evilClassBase64 = "yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgcAGwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAA9DYWxjdWxhdG9yLmphdmEMAAcACAcAHAwAHQAeAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAfACABAApDYWxjdWxhdG9yAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAACgAEAAsADQAMAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEQALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAABYACwAAAAQAAQAPAAEAEQAAAAIAEg==";
byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
((Class) method.invoke(ClassLoader.getSystemClassLoader(), "Calculator", bytes, 0, bytes.length)).newInstance();
}
}

在JDK8版本下,正常反射调用,且没有警告等信息。

在JDK9~JDK16版本下,正常反射调用,但是会出现警告信息。

1
2
3
4
5
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.example.Main (file:/Users/alphag0/Desktop/Demo/target/classes/) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int)
WARNING: Please consider reporting this to the maintainers of org.example.Main
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及以上版本下,反射调用失败,抛出异常java.lang.reflect.InaccessibleObjectException。

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(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @4bde3f8a
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 org.example.Main.main(Main.java:12)

绕过

Unsafe

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

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
public final class Unsafe {

static {
Reflection.registerMethodsToFilter(Unsafe.class, Set.of("getUnsafe"));
}

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();
private static final jdk.internal.misc.Unsafe theInternalUnsafe = jdk.internal.misc.Unsafe.getUnsafe();

/**
* Provides the caller with the capability of performing unsafe
* operations.
*
* <p>The returned {@code Unsafe} object should be carefully guarded
* by the caller, since it can be used to read and write data at arbitrary
* memory addresses. It must never be passed to untrusted code.
*
* <p>Most methods in this class are very low-level, and correspond to a
* small number of hardware instructions (on typical machines). Compilers
* are encouraged to optimize these methods accordingly.
*
* <p>Here is a suggested idiom for using unsafe operations:
*
* <pre> {@code
* class MyTrustedClass {
* private static final Unsafe unsafe = Unsafe.getUnsafe();
* ...
* private long myCountAddress = ...;
* public int getCount() { return unsafe.getByte(myCountAddress); }
* }}</pre>
*
* (It may assist compilers to make the local variable {@code final}.)
*
* @throws SecurityException if the class loader of the caller
* class is not in the system domain in which all permissions
* are granted.
*/
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}

......

使用Unsafe类时,可以通过两个方法获取其实例。

  • 从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
1
2
## 其中path为调用Unsafe相关方法的类所在jar包路径
java -Xbootclasspath/a: ${path}
  • 通过反射获取单例对象theUnsafe。
1
2
3
4
5
6
7
8
9
10
11
private static Unsafe reflectGetUnsafe() {
try {
Class<?> name = Class.forName("sun.misc.Unsafe");
Field field = name.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}

如下图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。

在Class相关部分主要提供Class和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。

1
2
3
4
5
6
7
8
9
10
11
12
// 获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
// 获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
// 判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
public native boolean shouldBeInitialized(Class<?> c);
// 检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
// 定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
// 定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

在对象操作部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
public native long objectFieldOffset(Field f);
// 获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
// 给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
// 从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
public native Object getObjectVolatile(Object o, long offset);
// 存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
public native void putObjectVolatile(Object o, long offset, Object x);
// 有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
public native void putOrderedObject(Object o, long offset, Object x);
// 绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

Bypass

在上文提到了sun.misc和sun.reflect包可供所有JDK版本(包括JDK17)中的工具和库进行反射,而在sun.misc包下就有着Unsafe类。那么该如何利用Unsafe来打破JDK17及以上的强封装module限制呢?

在Java中,setAccessible是一个用于改变Java反射时对私有属性或方法访问限制的方法。它是java.lang.reflect.AccessibleObject类的一个方法,该类是Field、Method和Constructor等类的超类。setAccessible(true)方法允许绕过Java的访问控制检查,从而访问私有(private)或受保护(protected)的属性和方法。

跟进setAccessible方法,首先调用AccessibleObject类的静态方法checkPermission,该方法检查当前的安全策略是否允许改变访问控制;如果不允许,会抛出SecurityException。

1
2
3
4
5
public void setAccessible(boolean flag) {
AccessibleObject.checkPermission();
if (flag) checkCanSetAccessible(Reflection.getCallerClass());
setAccessible0(flag);
}
1
2
3
4
5
6
7
8
9
10
static void checkPermission() {
@SuppressWarnings("removal")
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// SecurityConstants.ACCESS_PERMISSION is used to check
// whether a client has sufficient privilege to defeat Java
// language access control checks.
sm.checkPermission(SecurityConstants.ACCESS_PERMISSION);
}
}

接着,当设置非公共字段或方法的访问权限为true时,会调用checkCanSetAccessible方法,这个方法检查调用setAccessible方法的类是否有权限改变访问控制。Reflection.getCallerClass()方法获取调用setAccessible方法的类,不包括匿名内部类。

跟进java.lang.reflect.AccessibleObject#checkCanSetAccessible方法,可以看到,callerModule获取调用者的模块,declaringModule获取声明成员(方法或字段)的类的模块,如果调用者的模块与声明成员的类的模块相同,或者调用者是未知模块(Object.class.getModule()通常返回null),则允许访问。

因此,可以尝试利用Unsafe类来修改当前类的module属性和java.*下类的module属性一致来进行绕过。

在Unsafe类中,存在方法getAndSetObject,该方法是一个用于原子操作的方法,它主要用于在多线程环境下对对象的字段进行安全的更新操作,类似于反射赋值,可以利用其修改调用类的module。

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

利用Unsafe绕过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
27
28
29
30
import sun.misc.Unsafe;

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

public class Main {
public static void main(String[] args) throws Exception {
System.out.println("Java Version: " + System.getProperty("java.version"));

Class<?> aClass = Class.forName("sun.misc.Unsafe");
Field theUnsafe = aClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Module module = Object.class.getModule();
Class<Main> mainClass = Main.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(mainClass, offset, module);
// unsafe.putObject(mainClass, offset, module);


String evilClassBase64 = "yv66vgAAAD0AIgoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCgAIAAkHAAoMAAsADAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwgADgEAEm9wZW4gLWEgQ2FsY3VsYXRvcgoACAAQDAARABIBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7BwAUAQATamF2YS9pby9JT0V4Y2VwdGlvbgcAFgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uCgAVABgMAAUAGQEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgcAGwEABEV2aWwBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQAhABoAAgAAAAAAAgABAAUABgABABwAAAAhAAEAAQAAAAUqtwABsQAAAAEAHQAAAAoAAgAAAAQABAAGAAgAHgAGAAEAHAAAAFQAAwABAAAAF7gABxINtgAPV6cADUu7ABVZKrcAF7+xAAEAAAAJAAwAEwACAB0AAAAWAAUAAAAKAAkADQAMAAsADQAMABYADgAfAAAABwACTAcAEwkAAQAgAAAAAgAh";
byte[] bytes = Base64.getDecoder().decode(evilClassBase64);

Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
((Class)method.invoke(ClassLoader.getSystemClassLoader(), "Evil", bytes, 0, bytes.length)).newInstance();

}
}

参考

Migrating From JDK 8 to Later JDK Releases

Java魔法类:Unsafe应用解析

JDK17+反射限制绕过