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