目录
前言
准备
源码分析
1. manifest
2. agent分析
3. agent卸载逻辑
总结
笔者在很早前写了(231条消息) OpenRASP Java应用自我保护使用_fenglllle的博客-CSDN博客
实际上很多商业版的rasp工具都是基于OpenRASP的灵感来的,主要就是对核心的Java类通过Javaagent技术,对特定的方法注入字节码,做参数验证。核心技术就是Javaagent,那么分析OpenRASP的agent实现原理,即可明白主流的rasp实现逻辑。 在OpenRASP上优化部分实现逻辑就可以形成一个商业产品。
准备百度的admin端,执行./rasp-cloud -d
登录后拿到
appid、appsecret
在Java的靶机上配置agent和agent使用的参数
注意百度自己的一键注入代码jar,注入的agent在其他所有agent之前,agent的加载是有顺序的。
agent是mainfest定义的入口,可以在pom插件和java包内部看到
pom插件
jar包
以jvm参数方式为例,动态注入也差不多,rasp一般使用都是jvm参数启动时注入
com.baidu.openrasp.Agent
public static void premain(String agentArg, Instrumentation inst) {init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);}public static synchronized void init(String mode, String action, Instrumentation inst) {try {//添加jar文件到jdk的跟路径下,优先加载JarFileHelper.addJarToBootstrap(inst);//读取一些agent的数据readVersion();//核心代码ModuleLoader.load(mode, action, inst);} catch (Throwable e) {System.err.println("[OpenRASP] Failed to initialize, will continue without security protection.");e.printStackTrace();}}
ModuleLoader.load(mode, action, inst);
/*** 加载所有 RASP 模块** @param mode 启动模式* @param inst {@link java.lang.instrument.Instrumentation}*/public static synchronized void load(String mode, String action, Instrumentation inst) throws Throwable {if (Module.START_ACTION_INSTALL.equals(action)) {if (instance == null) {try {//安装instance = new ModuleLoader(mode, inst);} catch (Throwable t) {instance = null;throw t;}} else {System.out.println("[OpenRASP] The OpenRASP has bean initialized and cannot be initialized again");}} else if (Module.START_ACTION_UNINSTALL.equals(action)) {//卸载release(mode);} else {throw new IllegalStateException("[OpenRASP] Can not support the action: " + action);}}
看看new ModuleLoader逻辑
/*** 构造所有模块** @param mode 启动模式* @param inst {@link java.lang.instrument.Instrumentation}*/private ModuleLoader(String mode, Instrumentation inst) throws Throwable {// JBoss参数,实际上就是设置一些系统变量if (Module.START_MODE_NORMAL == mode) {setStartupOptionForJboss();}//构造对象,加载另外的jar engine.jar 初始化com.baidu.openrasp.EngineBootengineContainer = new ModuleContainer(ENGINE_JAR);//核心逻辑engineContainer.start(mode, inst);}
new ModuleContainer(ENGINE_JAR);
public ModuleContainer(String jarName) throws Throwable {try {//engine jar,openrasp有2个jar,单独加载,方便卸载,agent常用手段File originFile = new File(baseDirectory + File.separator + jarName);JarFile jarFile = new JarFile(originFile);Attributes attributes = jarFile.getManifest().getMainAttributes();jarFile.close();this.moduleName = attributes.getValue("Rasp-Module-Name");// com.baidu.openrasp.EngineBootString moduleEnterClassName = attributes.getValue("Rasp-Module-Class");//用classloader加载jarif (moduleName != null && moduleEnterClassName != null&& !moduleName.equals("") && !moduleEnterClassName.equals("")) {Class moduleClass;if (ClassLoader.getSystemClassLoader() instanceof URLClassLoader) {Method method = Class.forName("java.net.URLClassLoader").getDeclaredMethod("addURL", URL.class);method.setAccessible(true);method.invoke(moduleClassLoader, originFile.toURI().toURL());method.invoke(ClassLoader.getSystemClassLoader(), originFile.toURI().toURL());moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);//com.baidu.openrasp.EngineBoot对象module = (Module) moduleClass.newInstance();} else if (ModuleLoader.isCustomClassloader()) {moduleClassLoader = ClassLoader.getSystemClassLoader();Method method = moduleClassLoader.getClass().getDeclaredMethod("appendToClassPathForInstrumentation", String.class);method.setAccessible(true);try {method.invoke(moduleClassLoader, originFile.getCanonicalPath());} catch (Exception e) {method.invoke(moduleClassLoader, originFile.getAbsolutePath());}moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);//初始化com.baidu.openrasp.EngineBootmodule = (Module) moduleClass.newInstance();} else {throw new Exception("[OpenRASP] Failed to initialize module jar: " + jarName);}}} catch (Throwable t) {System.err.println("[OpenRASP] Failed to initialize module jar: " + jarName);throw t;}}
engineContainer.start(mode, inst);
public void start(String mode, Instrumentation inst) throws Exception {//帅气的时刻,打印logoSystem.out.println("\n\n" +" ____ ____ ___ _____ ____ \n" +" / __ \\____ ___ ____ / __ \\/ | / ___// __ \\\n" +" / / / / __ \\/ _ \\/ __ \\/ /_/ / /| | \\__ \\/ /_/ /\n" +"/ /_/ / /_/ / __/ / / / _, _/ ___ |___/ / ____/ \n" +"\\____/ .___/\\___/_/ /_/_/ |_/_/ |_/____/_/ \n" +" /_/ \n\n");try {//载入v8,调用c,实际上可以使用火狐引擎替代,百度的官方文档的图就是rhinoLoader.load();} 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()) {return;}//缓存rasp的build信息Agent.readVersion();BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit);// 初始化插件系统 调用v8,支持动态更新插件和jsif (!JS.Initialize()) {return;}//核心代码,加入检查点,就是哪些类的哪些方法需要checkCheckerManager.init();//字节码替换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);}
loadConfig()
private boolean loadConfig() throws Exception {//处理日志相关LogConfig.ConfigFileAppender();//单机模式下动态添加获取删除syslogif (!CloudUtils.checkCloudControlEnter()) {LogConfig.syslogManager();} else {System.out.println("[OpenRASP] RASP ID: " + CloudCacheModel.getInstance().getRaspId());}return true;}private void init() {this.configFileDir = baseDirectory + File.separator + CONFIG_DIR_NAME;String configFilePath = this.configFileDir + File.separator + CONFIG_FILE_NAME;try {//载入靶机的配置文件,里面配置了appid appsecret等loadConfigFromFile(new File(configFilePath), true);if (!getCloudSwitch()) {try {FileScanMonitor.addMonitor(baseDirectory, instance);} catch (JNotifyException e) {throw new ConfigLoadException("add listener on " + baseDirectory + " failed because:" + e.getMessage());}//支持动态刷新文件addConfigFileMonitor();}} catch (FileNotFoundException e) {handleException("Could not find openrasp.yml, using default settings: " + e.getMessage(), e);} catch (JNotifyException e) {handleException("add listener on " + configFileDir + " failed because:" + e.getMessage(), e);} catch (Exception e) {handleException("cannot load properties file: " + e.getMessage(), e);}String configValidMsg = checkMajorConfig();if (configValidMsg != null) {LogTool.error(ErrorType.CONFIG_ERROR, configValidMsg);throw new ConfigLoadException(configValidMsg);}}
JS.Initialize(),JS引擎初始化,js文件监听
public synchronized static boolean Initialize() {try {//v8初始化检查if (!V8.Initialize()) {throw new Exception("[OpenRASP] Failed to initialize V8 worker threads");}//logV8.SetLogger(new com.baidu.openrasp.v8.Logger() {@Overridepublic void log(String msg) {pluginLog(msg);}});//V8.SetStackGetter(new com.baidu.openrasp.v8.StackGetter() {@Overridepublic 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();//是否云端控制,比如js更新,插件更新if (!CloudUtils.checkCloudControlEnter()) {//更新插件UpdatePlugin();//js文件监听器InitFileWatcher();}return true;} catch (Exception e) {e.printStackTrace();LOGGER.error(e);return false;}}public synchronized static boolean UpdatePlugin(List scripts) {boolean rst = V8.CreateSnapshot(pluginConfig, scripts.toArray(), BuildRASPModel.getRaspVersion());if (rst) {try {//执行jsString jsonString = V8.ExecuteScript("JSON.stringify(RASP.algorithmConfig || {})","get-algorithm-config.js");Config.getConfig().setConfig(ConfigItem.ALGORITHM_CONFIG, jsonString, true);} catch (Exception e) {LogTool.error(ErrorType.PLUGIN_ERROR, e.getMessage(), e);}Config.commonLRUCache.clear();}return rst;}
CheckerManager.init();
public synchronized static void init() throws Exception {for (Type type : Type.values()) {checkers.put(type, type.checker);}}
检查的类是写在代码里,意味着新增类需要重启,不过一般是增加参数更新规则,新增类频率比较低
自定义转换器,retransform
private void initTransformer(Instrumentation inst) throws UnmodifiableClassException {transformer = new CustomClassTransformer(inst);transformer.retransform();}
这里很关键
public CustomClassTransformer(Instrumentation inst) {this.inst = inst;inst.addTransformer(this, true);//定义了哪些类需要类替换addAnnotationHook();}
addAnnotationHook();
private void addAnnotationHook() {//com.baidu.openrasp.hook HookAnnotation注解的类Set classesSet = AnnotationScanner.getClassWithAnnotation(SCAN_ANNOTATION_PACKAGE, HookAnnotation.class);for (Class clazz : classesSet) {try {Object object = clazz.newInstance();if (object instanceof AbstractClassHook) {addHook((AbstractClassHook) object, clazz.getName());}} catch (Exception e) {LogTool.error(ErrorType.HOOK_ERROR, "add hook failed: " + e.getMessage(), e);}}}
addHook((AbstractClassHook) object, clazz.getName());//实际上是缓存需要替换的类,还原的时候可以使用
关键还是transform方法
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {if (loader != null) {DependencyFinder.addJarPath(domain);}if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) {jspClassLoaderCache.put(className.replace("/", "."), new SoftReference(loader));}//关键点,使用javassist字节码增强for (final AbstractClassHook hook : hooks) {if (hook.isClassMatched(className)) {CtClass ctClass = null;try {ClassPool classPool = new ClassPool();addLoader(classPool, loader);ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));if (loader == null) {hook.setLoadedByBootstrapLoader(true);}//字节码增强了,包括Tomcat,jdk等类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;}
定义了一系列的埋点类替换
以com.baidu.openrasp.hook.file.FileHook为例
增加了对File的list方法调用前,对参数进行检查
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {String src = getInvokeStaticSrc(FileHook.class, "checkListFiles", "$0", File.class);insertBefore(ctClass, "list", "()[Ljava/lang/String;", src);}
openrasp的架构图和开源代码不对应,开源代码用的v8引擎 ,这里画的图是Rhino,是原生Java代码,不需要JNI
release(mode)
public static synchronized void release(String mode) {try {if (engineContainer != null) {System.out.println("[OpenRASP] Start to release OpenRASP");engineContainer.release(mode);engineContainer = null;} else {System.out.println("[OpenRASP] Engine is initialized, skipped");}} catch (Throwable throwable) {// ignore}}
engineContainer.release(mode);
public void release(String mode) {//任务线程停止CloudManager.stop();//CPU 监控线程停掉CpuMonitorManager.release();if (transformer != null) {transformer.release(); //类替换还原}JS.Dispose(); //JS检查还原CheckerManager.release(); //java 检查还原String message = "[OpenRASP] Engine Released [" + Agent.projectVersion + " (build: GitCommit="+ Agent.gitCommit + " date=" + Agent.buildTime + ")]";System.out.println(message);}public synchronized static void Dispose() {if (watchId != null) {boolean oldValue = HookHandler.enableHook.getAndSet(false);FileScanMonitor.removeMonitor(watchId);watchId = null;HookHandler.enableHook.set(oldValue);}}
以Tomcat Xss为例,其他同理,因为定义了Tomcat的请求的hook字节码拦截,在启动的源码已经分析过了
因为是字节码拦截,所以可以定义拦截级别,可以阻断请求
具体使用的方式,在启动的时候就定义了,比如http请求就有xss的风险,具体的拦截效率可以极大程度提升应用程序的性能,比如不存在xss的情况,比如没有前端页面,就不配置xss拦截
最终执行各个checker
checker检查,另外以FileHook代码注入检查为例
这个里面也会调用checker代码,然后检查参数
部分检查手段是JS检查
实际上逻辑很清晰了
字节码注入检查点
执行检查,根据逻辑,可以通过插件调用checker,也可以直接检查返回
执行checker的时候,可以通过JS引擎检查参数等(js引擎的js文件可以动态更新规则)
OpenRASP的源码就分析完成了,实际上代码并不复杂,但是思想很不错,在实际的使用过程中
要考虑性能影响,
要考虑是否阻断拦截对业务的影响
要考虑各个部件的使用逻辑,可以自定义,并且支持动态更新,毕竟字节码替换风险还是有的,所以一般更新检查的数据,所以引入了js引擎,通过js更新
最终还是要考虑实际需求,比如内网环境,毕竟开销很大。