• Java openrasp学习记录-2


    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

     

    启动引擎函数:

    根据加载的agentjavaengine下面的主类来启动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("
    
    " + //rasp打印标志
                    "   ____                   ____  ___   _____ ____ 
    " +
                    "  / __ \____  ___  ____  / __ \/   | / ___// __ \
    " +
                    " / / / / __ \/ _ \/ __ \/ /_/ / /| | \__ \/ /_/ /
    " +
                    "/ /_/ / /_/ /  __/ / / / _, _/ ___ |___/ / ____/ 
    " +
                    "\____/ .___/\___/_/ /_/_/ |_/_/  |_/____/_/      
    " +
                    "    /_/                                          
    
    ");
            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的一些通用漏洞的检测,这部分单独将进行研究。

  • 相关阅读:
    Devexpress GridView 数据格式化显示
    贝叶斯算法
    贝叶斯算法
    科普 | 7 个步骤入门区块链
    科普 | 7 个步骤入门区块链
    漫画:Spring Boot、Java多线程、分布式、Java架构的高级架构师进阶之路
    漫画:Spring Boot、Java多线程、分布式、Java架构的高级架构师进阶之路
    ARP协议工作原理[同网段及跨网段]
    ARP协议工作原理[同网段及跨网段]
    过滤DataTable数据的Select()方法
  • 原文地址:https://www.cnblogs.com/tr1ple/p/12918942.html
Copyright © 2020-2023  润新知