Java安全之Unsafe類

Java安全之Unsafe類

0x00 前言

前面使用到的一些JNI編程和Javaagent等技術,其實在安全裏面的運用非常的有趣和微妙,這個已經說過很多次。後面還會發現一些比較有意思的技術,比如ASM和Unsafe這些。這下面就先來講解Unsafe這個類的使用和實際當中的一些運用場景。

0x01 Unsafe概述

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增強Java語言底層資源操作能力方面起到了很大的作用。使用該類可以獲取到底層的控制權,該類在sun.misc包,默認是BootstrapClassLoader加載的。

來看一下下面的兩張圖

Unsafe類是一個不能被繼承的類且不能直接通過new的方式創建Unsafe類實例。

這裡可以看到該構造方法是private所以說不能直接new該對象,裏面有一個getUnsafe()會返回Unsafe的實例。

@CallerSensitive
public static Unsafe getUnsafe() {
    // ----- 這裡去獲取當前類的ClassLoader加載器
    Class var0 = Reflection.getCallerClass();
    // ----- 判斷var0是不是BootstrapClassLoader
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        // ----- 否:拋出SecurityException異常
        throw new SecurityException("Unsafe");
    } else {
        // ----- 是:返回unsafe對象
        return theUnsafe;
    }
}

這裡是調用了isSystemDomainLoader來判斷是否為Bootstrap類加載器,如果是,可以正常獲取Unsafe實例,否則會拋出安全異常。

public static boolean isSystemDomainLoader(ClassLoader var0) {
    // ----- 重點是在這裡:
    // --- 當結果為true時:說明var0是Bootstrap類加載器,
    // -- 當結果為false時:說明var0是Extension || App || Custom 等類加載器
    // ----- 所以回到getUnsafe()函數,當這個函數返回false時,會直接拋異常,不允許加載Unsafe
    return var0 == null;
}

可以來測試一下

package com.UNsafe;

import sun.misc.Unsafe;

public class test {
    public static void main(String[] args) {
        Unsafe unsafe = Unsafe.getUnsafe();
        int i = unsafe.addressSize();
    }
}

0x02 Unsafe調用

前面說到Unsafe該類功能去進行直接調用,那麼這時候就會想到我們的反射機制。可以利用反射去直接調用。在這裏面也有兩種方式去進行反射調用。

調用方式一:

因為該類將他的實例化定義在theUnsafe成員變量裏面,所以可以使用反射直接獲取該變量的值。

package com.UNsafe;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class<?> aClass = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = aClass.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe o = (Unsafe)theUnsafe.get(null);
        int i = o.addressSize();
        System.out.println(i);

    }
}

結果:

8

調用方式二:

還有種方式就是反射調用getUnsafe()方法,該方法會直接返回UNsafe實例對象。那麼可以反射獲取該構造方法的實例,然後調用該方法

package com.UNsafe;

import sun.misc.Unsafe;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        Class<?> aClass = Class.forName("sun.misc.Unsafe");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe o = (Unsafe)declaredConstructor.newInstance();
        int i = o.addressSize();
        System.out.println(i);


    }
}

結果:

8

0x03 Unsafe功能

操作內存

public native long allocateMemory(long bytes);

	//分配內存, 相當於C++的malloc函數

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);

	//內存拷貝
public native Object getObject(Object o, long offset);

	//獲取給定地址值,忽略修飾限定符的訪問限制。與此類似操作還有: getInt,getDouble,getLong,getChar等
public native void putObject(Object o, long offset, Object x);

	//為給定地址設置值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等
public native byte getByte(long address);

//獲取給定地址的byte類型的值(當且僅當該內存地址為allocateMemory分配時,此方法結果為確定的)
public native void putByte(long address, byte x);

	//為給定地址設置byte類型的值(當且僅當該內存地址為allocateMemory分配時,此方法結果才是確定的)

獲取系統信息

public native int addressSize();  
//返回系統指針的大小。返回值為4(32位系統)或 8(64位系統)。
public native int pageSize();
//內存頁的大小,此值為2的冪次方。

線程調度

public native void unpark(Object thread);
// 終止掛起的線程,恢復正常.java.util.concurrent包中掛起操作都是在LockSupport類實現的,其底層正是使用這兩個方法

public native void park(boolean isAbsolute, long time);

// 線程調用該方法,線程將一直阻塞直到超時,或者是中斷條件出現。

@Deprecated
public native void monitorEnter(Object o);
//獲得對象鎖(可重入鎖)

@Deprecated
public native void monitorExit(Object o);
//釋放對象鎖

@Deprecated
public native boolean tryMonitorEnter(Object o);
//嘗試獲取對象鎖

操作對象

// 傳入一個Class對象並創建該實例對象,但不會調用構造方法
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

// 獲取字段f在實例對象中的偏移量
public native long objectFieldOffset(Field f);

// 返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
// 靜態屬性的偏移量,用於在對應的Class對象中讀寫靜態屬性
public native long staticFieldOffset(Field f);

// 獲得給定對象偏移量上的int值,所謂的偏移量可以簡單理解為指針指向該變量;的內存地址,
// 通過偏移量便可得到該對象的變量,進行各種操作
public native int getInt(Object o, long offset);
// 設置給定對象上偏移量的int值
public native void putInt(Object o, long offset, int x);

// 獲得給定對象偏移量上的引用類型的值
public native Object getObject(Object o, long offset);
// 設置給定對象偏移量上的引用類型的值
public native void putObject(Object o, long offset, Object x););

// 設置給定對象的int值,使用volatile語義,即設置後立馬更新到內存對其他線程可見
public native void putIntVolatile(Object o, long offset, int x);
// 獲得給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。
public native int getIntVolatile(Object o, long offset);

// 與putIntVolatile一樣,但要求被操作字段必須有volatile修飾
public native void putOrderedInt(Object o, long offset, int x);

這裡allocateInstance 這個方法很有意思,可以不調用該構造方法,然後去獲取一個傳入對象的實例。

那麼在安全中會怎麼去使用到該方法呢?假設一個場景,某個類的構造方法被HOOK了,該構造方法是private修飾也不能直接去進行new該對象。如果這時候不能使用 反射的機制去進行一個調用,那麼這時候就可以使用到該方法進行繞過。

小案例:

定義一個Persion類,並且構造方法為private修飾。

package com.demo2;

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private  int age;

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private Person() {

    }

    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

編寫調用測試代碼:

package com.UNsafe;

import com.demo2.Person;
import sun.misc.Unsafe;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        Class<?> aClass = Class.forName("sun.misc.Unsafe");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe unsafe = (Unsafe)declaredConstructor.newInstance();
        Person person = (Person)unsafe.allocateInstance(Person.class);
        person.setAge(20);
        person.setName("nice0e3");
        System.out.println(person);
    }
}

執行結果:

Person{name='nice0e3', age=20}

不採用反射和new的反射調用構造方法。

Class相關操作

//靜態屬性的偏移量,用於在對應的Class對象中讀寫靜態屬性
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);

這裏面的defineClass方法也很有意思,在前面的學習中應該會對defineClass方法有比較深刻的印象,比如命令執行 Java的webshell工具實現、還有jsp的一些免殺都會利用到ClassLoaderdefineClass這個方法去將位元組碼給還原成一個類。那麼在這裡的這個defineClass的作用上面也說明了,也是可以去定義一個匿名類,並且可以動態去進行一個創建。假設一個場景ClassLoader.defineClass不可用後就可以使用Unsafe.defineClass

動態加載類案例

package com.UNsafe;

import com.demo2.Person;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import sun.misc.Unsafe;

import javax.xml.soap.SAAJResult;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;

public class test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, CannotCompileException, IOException, NotFoundException {
        String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String Classname ="com.nice0e3.Commandtest";
        ClassPool classPool= ClassPool.getDefault();
        classPool.appendClassPath(AbstractTranslet);
        CtClass payload=classPool.makeClass("com.nice0e3.Commandtest");
        payload.setSuperclass(classPool.get(AbstractTranslet));
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");

        byte[] bytes=payload.toBytecode();
        //獲取系統加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        //創建默認保護域
        ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), null, systemClassLoader, null);
        Class<?> aClass = Class.forName("sun.misc.Unsafe");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe unsafe = (Unsafe)declaredConstructor.newInstance();
        Class<?> aClass1 = unsafe.defineClass(Classname, bytes, 0, bytes.length, systemClassLoader, protectionDomain);
        Object o = aClass1.newInstance();
        

    }
}

在JDK 11版本以後就移除了該方法。但是前面說到的defineAnonymousClass方法還是存在也可以進行使用。

參考文章

//www.cnblogs.com/rickiyang/p/11334887.html
//javasec.org/javase/Unsafe/

0x04 結尾

在這裏面由上面的案例可以看出來,結合了分析利用鏈的時候學習的Javassist動態生成類,然後去做轉換成位元組碼Unsafe去進行加載,這其實也能想到一些有趣的利用場景。