Java openrasp學習記錄(二)

Author:tr1ple

主要分析以下四個部分:

1.openrasp agent

這裏主要進行插樁的定義,其pom.xml中定義了能夠當類重新load時重定義以及重新轉換

 

 

這裏定義了兩種插樁方式對應之前安裝時的獨立web的jar的attach或者修改啓動腳本添加rasp的jar的方式

 

 

其中init操作則需要將rasp.jar添加到Bootstrap路徑中,因爲後面修改字節碼時將涉及到bootstraploader加載的一些類,正常情況下由rasp位於System class path根據類加載機制是攔截不到的bootstrapclassloader的類加載路徑下的class,加入到Bootstrapclassloader的搜索路徑下以後,才能攔截到

 

 接着調用Moduleloader.load,通過選擇mode(premain或者agentmain),action(install或者uninstall),該類主要進行加載和初始化引擎模塊rasp-engine.jar

 

load方法將會根據選擇的action來new一個moduloader,傳入模式和inst

 

moduleLoader中將使用rasp引擎jar文件new一個ModuleContainer容器(static代碼塊主要完成獲取rasp.jar路徑以及設置moduleclassloader),然後啓動該引擎容器,傳入插樁方式mode和插樁實例inst

 

啓動引擎函數:

根據加載的agent\java\engine下面的主類來啓動rasp引擎

 

 

 

 也就是rasp-engine.jar的manifest.mf裏面所定義的EngineBoot類的start方法,模塊名爲rasp-engine,採用低版本的1.6.0_45打包可以兼容高版本

 

2.openrasp engine

主要的一些rasp具體的操作邏輯,包括hook操作

 根據第一部分初始化的最後一個階段調用rasp引擎模塊的start方法,對應Engineboot類,所以直接定位到該類:

public class EngineBoot implements Module { //該類是實現Moudle接口的,因此可以調用start方法

    private CustomClassTransformer transformer; //定義類轉換器

    @Override
    public void start(String mode, Instrumentation inst) throws Exception {
        System.out.println("\n\n" + //rasp打印標誌
                "   ____                   ____  ___   _____ ____ \n" +
                "  / __ \\____  ___  ____  / __ \\/   | / ___// __ \\\n" +
                " / / / / __ \\/ _ \\/ __ \\/ /_/ / /| | \\__ \\/ /_/ /\n" +
                "/ /_/ / /_/ /  __/ / / / _, _/ ___ |___/ / ____/ \n" +
                "\\____/ .___/\\___/_/ /_/_/ |_/_/  |_/____/_/      \n" +
                "    /_/                                          \n\n");
        try {
            Loader.load(); //加載v8引擎,用於解釋js
        } catch (Exception e) {
            System.out.println("[OpenRASP] Failed to load native library, please refer to https://rasp.baidu.com/doc/install/software.html#faq-v8-load for possible solutions.");
            e.printStackTrace();
            return;
        }
        if (!loadConfig()) { //進行rasp引擎的初始化配置
            return;
        }
        //緩存rasp的build信息
        Agent.readVersion();
        BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit);
        // 初始化js插件系統
        if (!JS.Initialize()) {
            return;
        }
        CheckerManager.init(); //初始化所有類型的checker,包括js插件檢測,java本地檢測,服務器基線檢測
        initTransformer(inst);
        if (CloudUtils.checkCloudControlEnter()) {
            CrashReporter.install(Config.getConfig().getCloudAddress() + "/v1/agent/crash/report",
                    Config.getConfig().getCloudAppId(), Config.getConfig().getCloudAppSecret(),
                    CloudCacheModel.getInstance().getRaspId());
        }
        deleteTmpDir();
        String message = "[OpenRASP] Engine Initialized [" + Agent.projectVersion + " (build: GitCommit="
                + Agent.gitCommit + " date=" + Agent.buildTime + ")]";
        System.out.println(message);
        Logger.getLogger(EngineBoot.class.getName()).info(message);
    }

    @Override
    public void release(String mode) {
        CloudManager.stop();
        CpuMonitorManager.release();
        if (transformer != null) {
            transformer.release();
        }
        JS.Dispose();
        CheckerManager.release();
        String message = "[OpenRASP] Engine Released [" + Agent.projectVersion + " (build: GitCommit="
                + Agent.gitCommit + " date=" + Agent.buildTime + ")]";
        System.out.println(message);
    }

    private void deleteTmpDir() {
        try {
            File file = new File(Config.baseDirectory + File.separator + "jar_tmp");
            if (file.exists()) {
                FileUtils.deleteDirectory(file);
            }
        } catch (Throwable t) {
            Logger.getLogger(EngineBoot.class.getName()).warn("failed to delete jar_tmp directory: " + t.getMessage());
        }
    }

    /**
     * 初始化配置
     *
     * @return 配置是否成功
     */
    private boolean loadConfig() throws Exception {
        LogConfig.ConfigFileAppender();  //初始化log4j的logger
        //單機模式下動態添加獲取刪除syslog
        if (!CloudUtils.checkCloudControlEnter()) {
            LogConfig.syslogManager();
        } else {
            System.out.println("[OpenRASP] RASP ID: " + CloudCacheModel.getInstance().getRaspId());
        }
        return true;
    }

    /**
     * 初始化類字節碼的轉換器
     *
     * @param inst 用於管理字節碼轉換器
     */
    private void initTransformer(Instrumentation inst) throws UnmodifiableClassException {
        transformer = new CustomClassTransformer(inst);
        transformer.retransform();
    }

}

v8的引擎的初始化,調用的爲本地java代碼的initalize方法

    public synchronized static boolean Initialize() {
        try {
            if (!V8.Initialize()) {
                throw new Exception("[OpenRASP] Failed to initialize V8 worker threads");
            }
            V8.SetLogger(new com.baidu.openrasp.v8.Logger() { //設置v8的logger
                @Override
                public void log(String msg) {
                    PLUGIN_LOGGER.info(msg);
                }
            });
            V8.SetStackGetter(new com.baidu.openrasp.v8.StackGetter() { //設置v8獲取棧信息的getter方法,這裏獲得的棧信息,每一條信息包括類名、方法名和行號classname@methodname(linenumber)
                @Override
                public byte[] get() {
                    try {
                        ByteArrayOutputStream stack = new ByteArrayOutputStream();
                        JsonStream.serialize(StackTrace.getParamStackTraceArray(), stack);
                        stack.write(0);
                        return stack.getByteArray();
                    } catch (Exception e) {
                        return null;
                    }
                }
            });
            Context.setKeys();
            if (!CloudUtils.checkCloudControlEnter()) {
                UpdatePlugin(); //加載js插件到v8引擎中
                InitFileWatcher(); //啓動對js插件的文件監控,從而實現熱部署,動態的增刪js中的檢測規則
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error(e);
            return false;
        }
    }

updatePlugin:

其中涉及到rasp hook功能的開關,關於rasp繞過的一種方式就是通過反射關掉這個引擎

 

接着獲取到js插件的目錄plugins

 

默認就是official.js,檢測各種攻擊的邏輯就寫在裏面,用js寫實現熱部署,並加載到v8引擎中

InitFileWatcher:

這裏利用jnotify對js插件目錄進行監控,用的代碼是openrasp二次開發過的https://github.com/baidu-security/openrasp-jnotify

public synchronized static void InitFileWatcher() throws Exception {
        boolean oldValue = HookHandler.enableHook.getAndSet(false); 
        if (watchId != null) { //監聽器id
            FileScanMonitor.removeMonitor(watchId); //移除監聽器
            watchId = null;
        }
        watchId = FileScanMonitor.addMonitor(Config.getConfig().getScriptDirectory(), new FileScanListener() {
            @Override
            public void onFileCreate(File file) {
                if (file.getName().endsWith(".js")) {
                    UpdatePlugin();
                }
            }

            @Override
            public void onFileChange(File file) {
                if (file.getName().endsWith(".js")) {
                    UpdatePlugin();
                }
            }

            @Override
            public void onFileDelete(File file) {
                if (file.getName().endsWith(".js")) {
                    UpdatePlugin();
                }
            }
        });
        HookHandler.enableHook.set(oldValue);
    }

addMonitor將傳入監聽目錄和事件回調接口,最後返回監聽器id,其中mask定義了創建+刪除+修改三種模式,對應回調函數則重寫了OnfileCreate、OnfileChange、OnfileDelete三種方法,只要是後綴爲js的文件被創建、刪除或者修改了則調用UpdatePlugin方法重新讀取plugins目錄下的檢測js邏輯並重新加載到v8引擎中

 

 CheckerManager.init方法:

public class CheckerManager {

    private static EnumMap<Type, Checker> checkers = new EnumMap<Type, Checker>(Type.class);

    public synchronized static void init() throws Exception {
        for (Type type : Type.values()) {
            checkers.put(type, type.checker); //加載所有類型的檢測放入checkers,type.checker就是某種檢測對應的類
        }
    }

    public synchronized static void release() {
        checkers = null;
    }

    public static boolean check(Type type, CheckParameter parameter) {
        return checkers.get(type).check(parameter); //調用檢測類進行參數檢測
    }

}

包括使用js插件進行檢測的,對應的是類V8AttackChecker,就是調用V8引擎加載js進行檢測

本地檢測的兩種攻擊:

 

另外一些也是是本地的類檢查的,一些服務器安全配置檢查,數據庫連接以及日誌檢查

 

接着CheckManager.init結束以後,此時將初始換插樁用的轉換器

 自定義classTransformer:

/*
 * Copyright 2017-2020 Baidu Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.baidu.openrasp.transformer;

import com.baidu.openrasp.ModuleLoader;
import com.baidu.openrasp.config.Config;
import com.baidu.openrasp.dependency.DependencyFinder;
import com.baidu.openrasp.detector.ServerDetectorManager;
import com.baidu.openrasp.hook.AbstractClassHook;
import com.baidu.openrasp.messaging.ErrorType;
import com.baidu.openrasp.messaging.LogTool;
import com.baidu.openrasp.tool.annotation.AnnotationScanner;
import com.baidu.openrasp.tool.annotation.HookAnnotation;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.apache.log4j.Logger;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.ref.SoftReference;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;

/**
 * 自定義類字節碼轉換器,用於hook類的方法
 */
public class CustomClassTransformer implements ClassFileTransformer {
    public static final Logger LOGGER = Logger.getLogger(CustomClassTransformer.class.getName());
    private static final String SCAN_ANNOTATION_PACKAGE = "com.baidu.openrasp.hook"; //hook的類所在的包,hook的類都有對應的註解標註
    private static HashSet<String> jspClassLoaderNames = new HashSet<String>(); //保存要用到的一些類加載器
    private static ConcurrentSkipListSet<String> necessaryHookType = new ConcurrentSkipListSet<String>(); 
    private static ConcurrentSkipListSet<String> dubboNecessaryHookType = new ConcurrentSkipListSet<String>(); //dubbo要hook的類型
    public static ConcurrentHashMap<String, SoftReference<ClassLoader>> jspClassLoaderCache = new ConcurrentHashMap<String, SoftReference<ClassLoader>>();

    private Instrumentation inst;
    private HashSet<AbstractClassHook> hooks = new HashSet<AbstractClassHook>(); //各種攻擊對應的hook類的實例
    private ServerDetectorManager serverDetector = ServerDetectorManager.getInstance();

    public static volatile boolean isNecessaryHookComplete = false; //volatile修飾,保證多線程下該共享變量的可見性,值更改後立即刷新到主存,工作線程才能夠從內存中取到新的值
    public static volatile boolean isDubboNecessaryHookComplete = false; //dubbo的hook

    static {
        jspClassLoaderNames.add("org.apache.jasper.servlet.JasperLoader");  //類加載要用到的一些類加載器
        jspClassLoaderNames.add("com.caucho.loader.DynamicClassLoader");
        jspClassLoaderNames.add("com.ibm.ws.jsp.webcontainerext.JSPExtensionClassLoader");
        jspClassLoaderNames.add("weblogic.servlet.jsp.JspClassLoader");
        dubboNecessaryHookType.add("dubbo_preRequest");
        dubboNecessaryHookType.add("dubboRequest");
    }

    public CustomClassTransformer(Instrumentation inst) {
        this.inst = inst;
        inst.addTransformer(this, true);
        addAnnotationHook(); //在這要操作所有帶hook註解的類了,雖然看註解用上貌似效率慢一點,但是這裏用起來感覺還是很方便
    }

    public void release() {
        inst.removeTransformer(this);
        retransform();
    }

    public void retransform() {
        LinkedList<Class> retransformClasses = new LinkedList<Class>();
        Class[] loadedClasses = inst.getAllLoadedClasses();
        for (Class clazz : loadedClasses) {
            if (isClassMatched(clazz.getName().replace(".", "/"))) {
                if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.lang.invoke.LambdaForm")) {
                    try {
                        // hook已經加載的類,或者是回滾已經加載的類
                        inst.retransformClasses(clazz);
                    } catch (Throwable t) {
                        LogTool.error(ErrorType.HOOK_ERROR,
                                "failed to retransform class " + clazz.getName() + ": " + t.getMessage(), t);
                    }
                }
            }
        }
    }

    private void addHook(AbstractClassHook hook, String className) { //正常情況下將添加所有帶註解的hook點
        if (hook.isNecessary()) { //默認是false
            necessaryHookType.add(hook.getType()); //每種hook類對應一個type,例如讀文件、刪除文件、xxe、ognl
        }
        String[] ignore = Config.getConfig().getIgnoreHooks(); //拿到不hook的類名,支持配置的
        for (String s : ignore) {
            if (hook.couldIgnore() && (s.equals("all") || s.equals(hook.getType()))) { //hook點可以忽略
                LOGGER.info("ignore hook type " + hook.getType() + ", class " + className);
                return;
            }
        }
        hooks.add(hook);
    }

    private void addAnnotationHook() {
        Set<Class> classesSet = AnnotationScanner.getClassWithAnnotation(SCAN_ANNOTATION_PACKAGE, HookAnnotation.class); //取到所有帶HookAnnotaion.class註解的類
        for (Class clazz : classesSet) { 
            try {
                Object object = clazz.newInstance(); //實例化每種攻擊對應的hook類
                if (object instanceof AbstractClassHook) {
                    addHook((AbstractClassHook) object, clazz.getName());
                }
            } catch (Exception e) {
                LogTool.error(ErrorType.HOOK_ERROR, "add hook failed: " + e.getMessage(), e);
            }
        }
    }

    /**
     * 過濾需要hook的類,進行字節碼更改
     *
     * @see ClassFileTransformer#transform(ClassLoader, String, Class, ProtectionDomain, byte[])
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {  //transform也就是實際插樁生效的地方,loadclass到jvm中時觸發
        if (loader != null) {
            DependencyFinder.addJarPath(domain); 
//因爲用到的class可能是某個jar包中的,因此這裏根據當前保護域去找到當前load的class的絕對路徑,若其存在,則將對應的jar包加到loadedJarPath中 }
if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) { //如果當前的類加載器是jsp相關的類加載器 jspClassLoaderCache.put(className.replace("/", "."), new SoftReference<ClassLoader>(loader));
        //這裏用softReference對jsp相關的classloader進行弱引用封裝,SoftReference 所指向的對象,當沒有強引用指向它時,會在內存中停留一段的時間,
後面jvm再根據內存情況(堆上情況)和SoftReference.get來決定要不要回收該對象,弱引用封裝的對象通過get拿到對象的強引用再使用對象,這裏是爲了防止classloader內存泄露 }
for (final AbstractClassHook hook : hooks) { //對添加到hooks中的所有類別的hook點進行遍歷 if (hook.isClassMatched(className)) { //此時要判斷要hook的類名 CtClass ctClass = null; try { ClassPool classPool = new ClassPool(); //要用到javaassist技術改變字節碼了 addLoader(classPool, loader); //初始化class文件的搜索路徑 ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); if (loader == null) { hook.setLoadedByBootstrapLoader(true); } classfileBuffer = hook.transformClass(ctClass); if (classfileBuffer != null) { checkNecessaryHookType(hook.getType()); } } catch (IOException e) { e.printStackTrace(); } finally { if (ctClass != null) { ctClass.detach(); } } } } serverDetector.detectServer(className, loader, domain); return classfileBuffer; } private void checkNecessaryHookType(String type) { if (!isNecessaryHookComplete && necessaryHookType.contains(type)) { necessaryHookType.remove(type); if (necessaryHookType.isEmpty()) { isNecessaryHookComplete = true; } } if (!isDubboNecessaryHookComplete && dubboNecessaryHookType.contains(type)) { dubboNecessaryHookType.remove(type); if (dubboNecessaryHookType.isEmpty()) { isDubboNecessaryHookComplete = true; } } } public boolean isClassMatched(String className) { for (final AbstractClassHook hook : getHooks()) { if (hook.isClassMatched(className)) { return true; } } return serverDetector.isClassMatched(className); } private void addLoader(ClassPool classPool, ClassLoader loader) { classPool.appendSystemPath(); //添加jvm啓動時的一些搜索路徑比如擴展類,rt.jar或者classpath下的類 classPool.appendClassPath(new ClassClassPath(ModuleLoader.class)); if (loader != null) { classPool.appendClassPath(new LoaderClassPath(loader)); } } public HashSet<AbstractClassHook> getHooks() { return hooks; } }

hook的相關類

 

 判斷是不是某個註解的hook類對應的要進行插樁的class

 

3.openrasp安裝時的一些檢測代碼

其中App.java爲安裝rasp的主程序

 

根據nodetect選擇安裝模式:

nodetect模式下attach方法:

找到服務器對應的啓動腳本並修改

 

 

不同系統支持的平臺如下所示:

 

 operateServer主要在這個階段要完成的是:

1.根據不同的操作系統種類使用不同的工廠類,調用工廠類的getInstaller來根據nodetect參數判斷目標程序是否是以springboot型的獨立jar啓動選擇GenericInstaller模式安裝(此時將定義不需要修改啓動shell腳本去插入一下啓動rasp的配置項,直接使用attach模式根據提供的pid進行attach)。若nodetect爲false,則要探測一些服務器的標誌文件去判斷目標服務器種類拿到Installer的實例,後面則要根據不同服務器種類去修改相應的服務器的shell啓動腳本添加加載rasp的配置項

2.拿到GenericInstaller或者Installer後調用其install方法進行rasp的安裝,Installer的install調用中需要去找到服務器的啓動腳本添加配置項

4.openrasp的攻擊檢測插件,檢查攻擊的源碼

之前分析到rasp在初始化js插件時將會把plugins下的js文件加載到v8引擎中,來實現熱部署,這部分檢測邏輯代碼太多啦,這裏對於不同語言使用js來實現檢測邏輯,從而實現通用檢測,我只關心java相關的漏洞檢查,除了下面列出的一些CVE,還包括java的一些通用漏洞的檢測,這部分單獨將進行研究。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章