Java openrasp學習記錄(二)

tr1ple發表於2020-08-09

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的一些通用漏洞的檢測,這部分單獨將進行研究。

相關文章