Unsafe入门

介绍

sun.misc.Unsafe是Java底层API(仅限Java内部使用,反射可调用)提供的一个神奇的Java类,Unsafe提供了非常底层的内存、CAS、线程调度、类、对象等操作、Unsafe正如它的名字一样它提供的几乎所有的方法都是不安全的,本节只讲解如何使用Unsafe定义Java类、创建类实例。

获取unsafe对象

Unsafe是Java内部API,外部是禁止调用的,在编译Java类时如果检测到引用了Unsafe类也会有禁止使用的警告:Unsafe是内部专用 API, 可能会在未来发行版中删除

1
2
3
4
5
6
7
public static Unsafe getUnsafe() throws Exception{  
Class<?> aClass = Class.forName("sun.misc.Unsafe");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Unsafe unsafe= (Unsafe) declaredConstructor.newInstance();
return unsafe;
}
1
2
3
4
5
public static Unsafe getUnsafe() throws Exception{  
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe)theUnsafe.get(null);
}

以上这两种方式算是现在比较常用的获取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
public class Unsafe_Stu {  
public String name = "oceanzbz";
public int age = 20;
public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
Unsafe_Stu unsafeStu = new Unsafe_Stu();

//修改int类型的值
Field age1 = Class.forName("com.ocean.Unsafe_Stu").getDeclaredField("age");
long l = unsafe.objectFieldOffset(age1);
unsafe.putInt(unsafeStu,l,22);
System.out.println(unsafeStu.age);
//修改String类型
Field name1 = Unsafe_Stu.class.getDeclaredField("name");
long l1 = unsafe.objectFieldOffset(name1);
unsafe.putObject(unsafeStu,l1,"zbz");
System.out.println(unsafeStu.name);

}
//反射获取Unsafe对象

public static Unsafe getUnsafe() throws Exception{
Class<?> aClass = Class.forName("sun.misc.Unsafe");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Unsafe unsafe= (Unsafe) declaredConstructor.newInstance();
return 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
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分配时,此方法结果才是确定的)

获取系统信息

1
2
3
4
5
public native int addressSize();  
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int pageSize();
//内存页的大小,此值为2的幂次方。

线程调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
//尝试获取对象锁

获取对象

unsafe可以无视构造方法创建类的实例这个特性可以用来绕过一些检测,比如这里我们使用unsafe来获取一个unsafe对象只是为了演示

1
2
3
//创建对象绕过构造方法  
Object o = unsafe.allocateInstance(Unsafe.class);
System.out.println(o.getClass().getName())

其他方法

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
// 传入一个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);

创建VM Anonymous Class

VM Anonymous Class并不等同于匿名类,这种类具有以下几个特点参考
https://paper.seebug.org/1785

[!特点]
1、class名可以是已存在的class的名字,比如java.lang.File,即使如此也不会发生任何问题,java的动态编译特性将会在内存中生成名如 java.lang.File/13063602@38ed5306的class。 —将会使类名极具欺骗性
2、该class的classloader为null。 —在java中classloader为null的为来自BootstrapClassLoader的class,往往会被认定为jdk自带class
3、在JVM中存在大量动态编译产生的class(多为lamada表达式生成),这种class均不会落盘,所以不落盘并不会属于异常特征。
4、无法通过Class.forName()获取到该class的相关内容。 —严重影响通过反射排查该类安全性的检测工具
5、在部分jdk版本中,VM Anonymous Class甚至无法进行restransform。 —这也就意味着我们无法通过attach API去修复这个恶意类
6、该class在transform中的className将会是它的模板类名。 —这将会对那些通过attach方式检测内存马的工具造成极大的误导性

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

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;

import java.io.File;
import java.lang.reflect.Constructor;

public class vm_Anonymous_test {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("java.io.File");
CtMethod make = CtMethod.make(" public String toString() {java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");return null;}", ctClass);
make.setName("toString");
ctClass.addMethod(make);
byte[] bytecode = ctClass.toBytecode();
Unsafe unsafe = getUnsafe();
Class<?> aClass = unsafe.defineAnonymousClass(File.class, bytecode, null);
System.out.println(aClass.getName());
aClass.newInstance().toString();
}


public static Unsafe getUnsafe() throws Exception{
Class<?> aClass = Class.forName("sun.misc.Unsafe");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Unsafe unsafe= (Unsafe) declaredConstructor.newInstance();
return unsafe;
}
}

可以看到成功弹出计算器并且类名还很像jdk自身的类名迷惑性很大。defineAnonymousClass方法的第一个参数随便传入一个类对象即可,第二个参数需要传入一个类的字节码,这里使用javassist简单一点。第三个参数设置为null即可。执行后得到一个类对象,通过newInstance获取实例,再调用了匿名类的toString方法,弹个计算器。而后输出匿名类的类名和Unsafe的类名进行对比,可见,用defineAnonymousClass创建的类名后面,会有”/xxxxxxxx”,这里也算一个特征

class相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//静态属性的偏移量,用于在对应的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);

还有一些绕过RASP的方式因为还没学到那里暂时占个坑以后补,最后也来一个其他大佬的图

参考:
https://www.javasec.org/javase/Unsafe/
https://www.cnblogs.com/bitterz/p/15952315.html
https://paper.seebug.org/1785/
https://www.cnblogs.com/nice0e3/p/14102892.html