作者: xbear

0x01 什么是RASP

RASP(Runtime application self-protection)運行時應用自我保護。Gartner 在2014年應用安全報告里將 RASP 列為應用安全領域的關鍵趨勢,并將其定義為:

Applications should not be delegating most of their runtime protection to the external devices. Applications should be capable of self-protection (i.e., have protection features built into the application runtime environment).

RSAP 將自身注入到應用程序中,與應用程序融為一體,實時監測、阻斷攻擊,使程序自身擁有自保護的能力。并且應用程序無需在編碼時進行任何的修改,只需進行簡單的配置即可。

0x02 RASP 能做什么

RASP不但能夠對應用進行基礎安全防護,由于一些攻擊造成的應用程序調用棧調用棧具有相似性,還能夠對0day進行一定的防護。

除此之外,利用 RASP 也能夠對應用打虛擬補丁,修復官方未修復的漏洞。或者對應用的運行狀態進行監控,進行日志采集。

0x03 WAF VS RASP

傳統的 WAF 主要通過分析流量中的特征過濾攻擊請求,并攔截攜帶有攻擊特征的請求。WAF 雖然可以有效個過濾出絕大多數惡意請求,但是不知道應用運行時的上下文,必然會造成一定程度的誤報。并且 WAF 嚴重依賴于特征庫,各種花式繞過,導致特征編寫很難以不變應萬變。

RASP 的不同就在于運行在應用之中,與應用融為一體,可以獲取到應用運行時的上下文,根據運行時上下文或者敏感操作,對攻擊進行精準的識別或攔截。于此同時,由于 RASP 運行在應用之中,只要檢測點選取合理,獲取到的 payload 已經是解碼過的真實 payload ,可以減少由于 WAF 規則的不完善導致的漏報。

雖然 RASP 擁有 WAF 所不具有的一些優勢,但是否能夠代替 WAF 還有待商榷。畢竟 WAF 是成熟、快速、可以大規模部署的安全產品。兩者相互補充,將 WAF 作為應用外圍的防線,RASP 作為應用自身的安全防護,確保對攻擊的有效攔截。

0x04 實現基本思路

這里以 Java Rasp 的實現原理為例。Rasp 想要將自己注入到被保護的應用中,基本思路類似于 Java 中的 AOP 技術,將 RASP 的探針代碼注入到需要進行檢測的地方。

Java 的 AOP 主要可以從幾個層面來實現:

  • 編譯期
  • 字節碼加載前
  • 字節碼加載后

在編譯期進行 AOP 織入,一般需要編寫靜態代理,導致靈活性差,對原有的應用代碼有修改。

在字節碼加載后進行 AOP 織入,一般使用動態代理,為接口動態生成代理類。動態代理雖然靈活性高,但仍然需要使用相關的類庫,進行動態代理的配置,并融合到應用的源代碼中,不是理想的解決方案。

最后只剩下了在字節碼加載前進行 AOP 織入。在字節碼加載前進行織入,一般有兩種方法,重寫 ClassLoader 或利用 Instrumentation 。如果重寫 ClassLoader ,仍然對現有代碼進行了修改,不能做到對應用無侵入。所以只有利用 Java 的 Instrumentation 。

0x05 利用 Java Instrumentation 在 class 加載前插入修改機會

“java.lang.instrument”包的具體實現依賴于 JVMTI 。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機提供的,為 JVM 相關的工具提供的本地編程接口集合。在 Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,通過調用 JVMTI 當中 Java 類相關的函數來完成 Java 類的動態操作。

我們可以 Instrumentation 的代理,并讓其在 main 函數之前運行,這里需要實現的主要是 premain 函數。

public static void premain(String agentArgs, Instrumentation inst)
    throws ClassNotFoundException, UnmodifiableClassException {
    Console.log("init");
init();
inst.addTransformer(new ClassTransformer());
}
private static boolean init() {
Config.initConfig();
return true;
}

在 premain 函數中,我們將類轉換器添加到了 Instrumentation ,這樣在類加載前,我們便有機會對字節碼進行操作,織入 Rasp 的安全探針。

若想使用帶有 Instrumentation 代理的程序,需要在 JVM 的啟動參數中添加 -javaagent 啟動參數。

-javaagent:[編譯好的agent jar文件路徑]XXX.jar

0x06 利用 ClassTransformer 進行探針織入

在運行了 Instrumentation 代理的 Java 程序中,字節碼的加載會經過我們自定義的 ClassTransformer ,在這里我們可以過濾出我們關注的類,并對其字節碼進行相關的修改

public class ClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?>
classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws
IllegalClassFormatException {
byte[] transformeredByteCode = classfileBuffer;

if (Config.moudleMap.containsKey(className)) {
try {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor =
Reflections.createVisitorIns((String)Config.moudleMap.get(className).get("loadClass"),
writer, className);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
transformeredByteCode = writer.toByteArray();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return transformeredByteCode;
}
}

實現中使用了使用了 Map 將關注的類進行保存,一旦命中我們關心的類,便利用反射生成 asm 的ClassVisitor ,使用 asm 操作字節碼,進行探針織入,最終返回修改后的字節碼。

這里的 ClassVisitor 以 Struts 2 的 Ognl 表達式執行漏洞為例:

public class OgnlVisitor extends ClassVisitor {
public String className;

public OgnlVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM5, cv);
this.className = className;
}
@Override
    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if ("parseExpression".equals(name) &&
"(Ljava/lang/String;)Ljava/lang/Object;".equals(desc)) {
mv = new OgnlVisitorAdapter(mv, access, name, desc);
}
return mv;
}
}

在這個 ClassVisitor 中,我們關心的是 ognl.Ognl 類中的 parseExpression 方法。只要在 Ognl 的 parseExpression 執行之前對 Ognl 表達式中的惡意參數進行過濾,可以對 Struts 2 的 Ognl 表達式執行漏洞進行有效的防護。具體的字節碼操作封裝在了 OgnlVisitorAdapter 中。

public class OgnlVisitorAdapter extends AdviceAdapter {
public OgnlVisitorAdapter(MethodVisitor mv, int access, String name,
String desc) {
super(Opcodes.ASM5, mv, access, name, desc);
}
@Override
protected void onMethodEnter() {
        Label l30 = new Label();
        mv.visitLabel(l30);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC,
"xbear/javaopenrasp/filters/rce/OgnlFilter", "staticFilter",
"(Ljava/lang/Object;)Z", false);
Label l31 = new Label();
mv.visitJumpInsn(IFNE, l31);
Label l32 = new Label();
mv.visitLabel(l32);
mv.visitTypeInsn(NEW, "ognl/OgnlException");
mv.visitInsn(DUP);
mv.visitLdcInsn("invalid class in ognl expression because of security");
mv.visitMethodInsn(INVOKESPECIAL, "ognl/OgnlException", "<init>",
"(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitLabel(l31);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack, maxLocals);
}
}

在 ognl.Ognl 類中的 parseExpression 方法執行前,Hook 了方法的執行,跳轉至執行自定義的 OgnlFilter 。 OgnlFilter 中定義了如何對 Ognl 表達式進行過濾。如果出現了威脅的表達式,將進行log記錄并拋出異常,若正常將放過,繼續進行 parseExpression 。

0x07 結尾

這里介紹了 Java Rasp 實現的基本原理,除了 ognl.Ognl 類中的 parseExpression 方法加探針外,還有很多的地方可以加探針,比如:java/io/ObjectInputStreamjava/lang/ProcessBuildercom/mysql/jdbc/StatementImpl 等等。重點關注數據的關鍵流轉節點加入 Rasp 探針,進行安全過濾。

如果探針部署的足夠充分,可以有效的防御 XSS、CSRF、RCE、SQL 注入等 Web 攻擊。如果 Rasp 與云端結合,不但能夠采集應用的安全日志,也能夠對發現的漏洞進行迅速的修補,甚至抵御 0Day 攻擊。

Demo: https://github.com/xbeark/javaopenrasp

這里只實現了使用了 Instrumentation 的 premain 進行代理,其實還可以使用 agentmain 進行虛擬機啟動后的動態 instrument ,具體就不在這里研究啦~


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/330/