作者:c0d3p1ut0s

0x00 前言

RASP(Runtime Application self-protection)是一種在運行時檢測攻擊并且進行自我保護的一種技術。關于RASP技術和RASP實踐,這是我寫的第二篇文章,上一篇文章在這里,是關于RASP技術在PHP中的實踐,無論是從設計思路還是技術實現來說,PHP RASP和OpenRASP Java實現都比較相似。有興趣的同學可以粗略看看,不必太關心技術細節,了解設計思路、工作原理即可,很多時候,對技術宏觀的把握對理解技術細節和設計思路非常有用。

0x01 RASP技術

關于RASP的發展、RASP與各種WAF的區別以及RASP的簡單原理可以看《一類PHP RASP的實現》這篇文章的RASP概念我的WAF世界觀這兩部分,這兩部分大約可以回答關于RASP技術的兩個問題,一是RASP技術是工作在哪一層,為了解決什么問題而存在的,二是它大致是怎么實現的,這里就不寫了。下面以OpenRASP為例分析一下Java RASP的實現。

0x02 JVMTI && Java Instrumentation

JVMTI是JVM提供的一些回調接口集。JVM在特定的狀態會執行特定的回調函數,開發者實現這些回調函數就可以實現自己的邏輯。Java Instrumentation就是利用JVMTI實現的。

Java Instrumentation是Java強大功能的一個體現,Java Instrumentation允許開發者訪問從JVM中加載的類,并且允許對它的字節碼做修改,加入我們自己的代碼,這些都是在運行時完成的。無需擔心這個機制帶來的安全問題,因為它也同樣遵從適用于Java類和相應的類加載器的安全策略。

0x03 OpenRASP簡要分析

OpenRASP以Java Instrumentation的方式工作在JVM層,它主要通過hook可能引發漏洞的關鍵函數,在這些關鍵函數執行之前添加安全檢查,根據上下文和關鍵函數的參數等信息判斷請求是否為惡意請求,并終止或繼續執行流。

Java Instrumentation允許開發者添加自定義的字節碼轉換器來對Java字節碼進行自定義的操作轉化,從而實現在不修改源代碼的情況下,實現AOP。當然,有一些開源的Java字節碼類庫幫助開發者操作Java字節碼。OpenRASP的開發者選擇了ASM這個框架,相比其他的框架,ASM的優點是更加底層、更加靈活,功能也更加豐富。

OpenRASP另一個值得稱道的做法是使用了js來編寫規則,通過Java語言實現的js引擎來執行腳本。OpenRASP官網關于為什么用JavaScript實現檢測的邏輯的解釋是 OpenRASP會支持PHP、DotNet、NodeJS、Python、Ruby等多種開發語言,為了避免在不同平臺上重新實現檢測邏輯,引入了插件系統; 選擇JS作為插件開發語言。當然,這是優點之一。筆者認為,另一個優點是使用JS插件系統可以很方便的支持熱部署。筆者曾經有幸參與某商業RASP產品的研發和測試,各語言規則的重復編寫和熱部署是兩個令我們頭痛的問題,而使用js引擎就可以很好的解決這兩個問題。

0x04 Talk is cheap

俗話說,Talk is cheap,show me the code. 下面簡要分析一下OpenRASP的代碼。OpenRASP是一個Java Instrumentation,它的入口是public static void premain(String agentArg, Instrumentation inst)函數,OpenRASP中的premain方法在com.fuxi.javaagent.Agent中。這個方法中主要的代碼如下

//........省略部分代碼........
JarFileHelper.addJarToBootstrap(inst);
//........省略部分代碼........
PluginManager.init();
initTransformer(inst);
//........省略部分代碼........

JarFileHelper.addJarToBootstrap(inst)的關鍵是JarFileHelper中inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))這行代碼,它的作用是將rasp.jar加入到bootstrap classpath里,優先其他jar被加載。在Java Instrumention的實現中,這行代碼應該是很常見的。為什么要這樣做呢?在Java中,Java類加載器分為BootstrapClassLoader、ExtensionClassLoader和SystemClassLoader。BootstrapClassLoader主要加載的是JVM自身需要的類,由于雙親委派機制的存在,越基礎的類由越上層的加載器進行加載,因此,如果需要在由BootstrapClassLoader加載的類的方法中調用由SystemClassLoader加載的rasp.jar,這違反了雙親委派機制。所以,而rasp.jar添加到BootstrapClassLoader的classpath中,由BootstrapClassLoader加載,就解決了這個問題。

接著是PluginManager.init(),初始化插件系統。PluginManager.init()的具體代碼如下:

JSContextFactory.init();
updatePlugin();
initFileWatcher();

JSContextFactory.init()的主要作用是初始化js引擎,這里使用的js引擎是Mozilla的Rhino,Mozilla旗下提供了各種語言的js引擎的成熟實現,例如用C/C++實現的js引擎SpiderMonkey等。JSContextFactory.init()先初始化了js引擎,執行了一堆js文件,筆者js水平有限,就不分析了。接著把jsstdout注入到js環境中,處理js環境中的輸出。把JSTokenizeSql和JSRASPConfig注入到RASP對象中,為js環境提供sql_tokenize方法,提供對SQL語句進行tokenize的能力。接下來updatePlugin()方法讀取插件目錄下的js文件,執行js腳本,加載插件。initFileWatcher()添加了文件監控,一旦插件目錄下的js文件發送變化,則調用updatePluginAsync()執行clean方法,執行js腳本,更新插件,實現熱部署功能。

接下來是initTransformer(inst)方法,它調用inst.addTransformer(new CustomClassTransformer(), true)方法添加了CustomClassTransformer這個Class轉換器,這樣,每一個類的字節碼在加載之前都會調用CustomClassTransformer.transform(..)(參數省略)方法,對字節碼進行更改之后,字節碼被載入JVM中,接下來繼續類加載過程:加載->驗證->準備->解析->初始化。CustomClassTransformer類在初始化的時候創建了很多個ClassHook對象,代碼如下:

public CustomClassTransformer() {
        hooks = new HashSet<AbstractClassHook>();

        addHook(new WebDAVCopyResourceHook());
        addHook(new CoyoteInputStreamHook());
        addHook(new DeserializationHook());
        addHook(new DiskFileItemHook());
        addHook(new FileHook());
        //.....省略......
}

我們看一下CustomClassTransformer.transform(..)這個方法,如果當前加載的類是需要轉換的,即hook.isClassMatched(className)返回true,就會調用hook.transformClass(className, classfileBuffer)對字節碼進行轉化。transformClass的代碼在com.fuxi.javaagent.hook.AbstractClassHook中,如下所示:

public byte[] transformClass(String className, byte[] classfileBuffer) {
        try {
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, computeFrames() ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS);
            LOGGER.debug("transform class: " + className);
            ClassVisitor visitor = new RaspHookClassVisitor(this, writer);
            reader.accept(visitor, ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        } catch (RuntimeException e) {
            LOGGER.error("exception", e);
        }
        return null;
    }

這段代碼調用了ASM庫,關于ASM庫的詳情請看這里,為了方便讀者,筆者翻譯了一下,在這里。如上文中所說,ASM是一個強大的字節碼操作庫。它主要使用了設計模式中的訪問者模式,使用訪問者模式的好處是數據結構與數據操作分開。在ASM中哪些是數據結構呢?轉換前字節碼中類、方法、注釋、成員變量等就是數據結構,對這些字節碼的操作就是數據操作。在ASM中,開發者不需要關心ASM解析字節碼,遍歷類、方法、注釋等是怎樣實現,只需要知道ClassReader.accept()接受一個ClassVisitor實例作為參數,在ASM遍歷類、方法、注釋時,ASM會調用ClassVisitor.visitMethod()ClassVisitor.visitAnnotation()等方法。開發只需要重寫這些方法,就可以操作方法、注釋的字節碼。如上面的代碼所示,OpenRASP開發者創建了一個RaspHookClassVisitor類,重寫了visitvisitMethod方法。在visitMethod方法中,ClassHook的hookMethod方法被調用,下面以FileHook為例,看一下hookMethod方法:

public MethodVisitor hookMethod(int access, String name, String desc, String signature, String[] exceptions, MethodVisitor mv) {
        if (name.equals("listFiles")) {
            return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                @Override
                protected void onMethodEnter() {
                    loadThis();
                    invokeStatic(Type.getType(HookHandler.class),
                            new Method("checkListFiles", "(Ljava/io/File;)V"));
                }
            };
        }
        return mv;
    }

invokeStatic(Type.getType(HookHandler.class),new Method("checkListFiles", "(Ljava/io/File;)V")這行代碼調用了靜態方法HookHandler.checkListFiles來實現對java.io.File.listFiles方法的檢測。

各關鍵函數的檢測有些區別,我就不分析了,很多都是最后調用com.fuxi.javaagent.plugin.check進行檢測,而com.fuxi.javaagent.plugin.check最后調用了各js函數來檢測。代碼如下

checkProcess = processList.get(i);
function = checkProcess.getFunction();
try {
    tmp = function.call(this, scope, function, functionArgs);
} 
//......省略......

對檢測細節感興趣的可以一個一個跟。

0x05 關于OpenRASP規則的幾點說明

OpenRASP提供了一些官方插件,當然,相關的規則還是需要根據業務和開發水平來定制。開發者水平參差不齊,什么奇葩的實現方式都有。例如,筆者之前參與研發的RASP也有這么一條規則:禁止在SQL語句中出現常量比較操作。但是,這條規則在實際中卻有不少誤報。很多開發會這樣寫(用偽代碼表示一下):

String sql="select * from table where 1=1";
for(key,value in condition.items()){
    sql+=" and "+key+"="+"value"; 
}

開發會自己添加1=1,為了能方便的在循環中在SQL后面的where語句中直接加and,而1=1永遠為真,不影響后面的邏輯。所以說,規則永遠是和場景分不開的,同樣,不深入開發、不深入客戶很難做好安全產品。說白了,做安全,開發安全產品和軟件工程的目標是一致的,即讓大多數的Stakeholder(利益相關者)滿意。開發當然也是RASP產品的Stakeholder。好了,扯遠了。

OpenRASP中對SQL做了詞法分析來實現"零規則"檢測。實際上,詞法分析的主要作用是將SQL語句分割成數據和代碼。SQL注入的本質是代碼注入,有了詞法分析的幫助,Web層就可以判斷對SQL語句的字符串處理是否改變了SQL的邏輯。這是詞法分析為什么可以用來檢測SQL注入的原因,類似的,利用詞法分析也可以檢測其他代碼注入。不過把SQL的詞法分析做好并不容易,一個原因是SQL語句語法復雜,不同的數據庫有不同的語法,不同的數據庫還有不同的奇葩特性。從代碼來看OpenRASP的詞法分析暫時還沒有區分不同的數據庫。

RASP并不能高效地防御所有的漏洞,還是那句話,Web服務器、解釋器/JVM、數據庫、操作系統各有各的防御陣地,各有各的優勢,充分發揮它們的優勢,才能更好的做好安全防護。希望這篇文章能對想用RASP產品或者想研發RASP的公司有一些幫助。

0x06 Reference

0x07 關于作者


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