Featured image of post Unsafe绕过反射限制

Unsafe绕过反射限制

Unsafe绕过反射限制

先进Unsafe类源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public final class Unsafe {

    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }

    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();


    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

这里用getUnsafe方法获取会出问题,通过反射获取

1
2
3
4
Class c = Unsafe.class;
Field field =  c.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

内存操作

主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: 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);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

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;

加载字节码demo

我们之前加载字节码是这样操作的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

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

public class TestClassLoader {
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\TemplateClassLoader\\EvilClassLoader.class"));
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,"_name","calc");
        setFieldValue(templatesImpl,"_bytecodes",new byte[][]{code});
        setFieldValue(templatesImpl,"_tfactory",new TransformerFactoryImpl());
        templatesImpl.newTransformer();
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target,value);
    }
}

现在我们更改反射的方法,改用unsafe类内置的方法

先是实例化的操作,可以用allocateInstance方法来代替

1
TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);

后面改的就是反射的方式,这里用putObject来代替

这里我想写成跟前面一样工具函数的形式

先看putObject的构造函数

1
public native void putObject(Object o, long offset, Object x);

中间这个参数是内存偏移量,可以用unsafe的objectFieldOffset获取

1
2
3
4
5
public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }

最后重写的代码如下:

 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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.misc.Unsafe;

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

public class Test {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);

        byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\TemplateClassLoader\\EvilClassLoader.class"));

        TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);

        setFieldValue(templatesImpl,"_name","calc");
        setFieldValue(templatesImpl,"_bytecodes",new byte[][]{code});
        setFieldValue(templatesImpl,"_tfactory",new TransformerFactoryImpl());
        templatesImpl.newTransformer();
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
}

绕过jdk17+对反射限制

jdk17启动了强封装,java.* 的非公共字段和方法都无法反射获取调用了。

但是sun.miscsun.reflect包下的我们是可以正常反射的,所以有个关键的类就可以拿来用,就是 Unsafe

同时注意 JDK17下Unsafe类下的 defineClassdefineAnonymousClass 已被移除,且从jdk9开始存在的另一个Unsafe类jdk.internal.misc.Unsafe 也是强封装的,和 java.* 包下的一样。

现在我们就要利用Unsafe来打破这个强封装module限制

定位到setAccessible这里

1
2
3
4
5
6
@Override
@CallerSensitive
    public void setAccessible(boolean flag) {
        if (flag) checkCanSetAccessible(Reflection.getCallerClass());
        setAccessible0(flag);
    }

给非public字段和方法设置true时,调用checkCanSetAccessible检查对应的类

然后跟进,跟到checkCanSetAccessible方法

 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
private boolean checkCanSetAccessible(Class<?> caller,
                                          Class<?> declaringClass,
                                          boolean throwExceptionIfDenied) {
        if (caller == MethodHandle.class) {
            throw new IllegalCallerException();   // should not happen
        }

        if (caller == null) {
            // No caller frame when a native thread attaches to the VM
            // only allow access to a public accessible member
            boolean canAccess = Reflection.verifyPublicMemberAccess(declaringClass, declaringClass.getModifiers());
            if (!canAccess && throwExceptionIfDenied) {
                throwInaccessibleObjectException(caller, declaringClass);
            }
            return canAccess;
        }

        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 = ((Member)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;
            }
        }

可以看到这里是判断我们是否有权限去修改目标字段或方法的访问权限。

只要判断我们调用的类和目标类是一个module,或者调用类的module和Object类的module一样,就可以有修改权限

现在我们尝试用Unsafe修改当前类的module属性和 java.* 下类的module属性一致来绕过

Object类的module用getModule方法获取

Unsafe类中有个 getAndSetObject 方法,其和反射赋值功能差不多,利用这个修改调用类的module

1
2
3
4
5
6
7
8
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 addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);

只要在低版本的payload加上这一串代码继续反射就行了

使用 Hugo 构建
主题 StackJimmy 设计