OpenRasp源码分析-java功能

OpenRasp源码分析-java功能

最近在做关于后台服务器和web的rasp这块工作,需要在OpenRasp的基础上做一个二次研发,为了能弄清其实现的技术细节,阅读了一下源码。记录的内容主要有以下几个点

  • RaspIntall.jar的功能
  • 启动执行流程
  • 核心原理
  • 检测规则
  • 其他
    整个OpenRasp的基本就是上述几个过程。其中重点关注的是启动执行流程,核心原理,检测规则三个点,下面就一个个的分析其实现。在分析的过程中我用了一个一些逆向工程的思路,当然也可以直接读源码,每个人都有自己的思路,能达到目的就可以了。

下面分析主要关注的java–>springboot–>DeserializationHook相关

RaspIntall.jar的功能

在其官方文档中,有提到了在启动服务之前,需要先安装agent,对于SpringBoot框架,使用如下方式安装

java -jar RaspInstall.jar -nodetect -install <spring_boot_folder> -backendurl http://XXX -appsecret XXX -appid XXX

其中backendurl appsecret appid可以通过启动rasp-cloud可以获取到,这里不是重点,先不去管。

这里使用jd-gui.jar查看RaspInstall.jar,根据参数

-nodetect -install <spring_boot_folder>

定位到MANIFEST.MF下的Main-Class值,如下

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: root
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_232
Main-Class: com.baidu.rasp.App

找到关键类com.baidu.rasp.App,经过分析后,来到关键掉一共

public static void operateServer(String[] args) throws RaspError, ParseException, IOException {
    showBanner();
    argsParser(args);
    checkArgs();
    if ("install".equals(install)) {
      File serverRoot = new File(baseDir);
      InstallerFactory factory = newInstallerFactory();
      Installer installer = factory.getInstaller(serverRoot, noDetect);
      if (installer != null) {
        installer.install();
      } else {
        throw new RaspError(RaspError.E10007);
      } 
    } else if ("uninstall".equals(install)) {
      File serverRoot = new File(baseDir);
      UninstallerFactory factory = newUninstallerFactory();
      Uninstaller uninstaller = factory.getUninstaller(serverRoot);
      if (uninstaller != null) {
        uninstaller.uninstall();
      } else {
        throw new RaspError(RaspError.E10007);
      } 
    } 
  }

进入到install中的调用,经过分析可以知道,程序是将rasp目录下的所有程序复制到指定的springboot_path下的rasp目录下,主要有以下几个文件

.
├── conf
│   └── openrasp.yml
├── logs
│   ├── alarm
│   │   └── alarm.log
│   ├── plugin
│   │   └── plugin.log
│   ├── policy_alarm
│   │   └── policy_alarm.log
│   └── rasp
│       └── rasp.log
├── plugins
├── rasp-engine.jar
└── rasp.jar
log文件是运行后自动生成的,可以忽略。

RaspInstall.jar主要功能:

  • 在目标路径创建rasp目录
  • 复制rasp.jar,rasp-engine.jar
  • 复制conf目录
  • 复制plugins目录
  • 生成配置信息conf/openrasp.yml文件

启动执行流程

启动openrasp的时候,使用如下命令

java -javaagent:/opt/spring-boot/rasp/rasp.jar -jar xxx.jar

指定javaagent:参数,在java中启动时指定了agent,则可以完成以下功能

javaagent 的主要功能如下:

可以在加载 class 文件之前做拦截,对字节码做修改
可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
还有其他一些小众的功能
获取所有已经加载过的类
获取所有已经初始化过的类(执行过 clinit 方法,是上面的一个子集)
获取某个对象的大小
将某个 jar 加入到 bootstrap classpath 里作为高优先级被 bootstrapClassloader 加载
将某个 jar 加入到 classpath 里供 AppClassloard 去加载
设置某些 native 方法的前缀,主要在查找 native 方法的时候做规则匹配

#参考 JVM 源码分析之 javaagent 原理完全解读
https://www.infoq.cn/article/javaagent-illustrated/

根据参考中的分析,可以知道,在启动的时候指定了javaagent之后,java程序会根据META-INF/MANIFEST.MF新找到

Premain-Class:xxxx

指定的类,接着调用premain方法,使用jd-gui.jar查看rasp.jar中的META-INF/MANIFEST.MF,内容如下

OpenRasp源码分析-java功能_第1张图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-it1rIYSE-1587637934982)(./imgs/rasp_manifest_mf.png)]

该类实现了premain方法,另一个是动态加载的方式,暂时不管,如下
OpenRasp源码分析-java功能_第2张图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQLZf4WM-1587637934986)(./imgs/init.png)]

进入到init方法

OpenRasp源码分析-java功能_第3张图片

程序首先调用JarFileHelper.addJarToBootstrap(inst);rasp.jar添加到bootstrap的路径内,这样系统会优先加载这个jar文件到系统路径中,后续的Hook能完成预先的类加载埋点插桩,后面的核心逻辑功能分析有细致分析
OpenRasp源码分析-java功能_第4张图片

接着调用ModuleLoader.load(mode, action, inst);rasp-engine.jar加载并执行。进入到ModuleLoader.load程序调用了ModuleLoader创建一个loader的实例对象
OpenRasp源码分析-java功能_第5张图片

这里需要注意的是,在ModuleLoader内有一个静态代码块,这里负责初始化当前加载的根目录baseDirectory和系统加载器systemClassLoader
OpenRasp源码分析-java功能_第6张图片

可以看到在获取 systemClassLoader的时候,有一个判断是否是sun.misc.Launcher$ExtClassLoader,不是就获取其父类的classLoader
默认情况下,我们在写一个main方法内获取到的ClassLoader

    private static void showClassLoader(){
        ClassLoader loader =ClassLoader.getSystemClassLoader();
        System.out.println("Defaule ClassLoader Name:"+loader.getClass().getCanonicalName()+" \nparant Name: "+loader.getParent().getClass().getCanonicalName()+"\ngrand parent Name:"+loader.getParent().getParent().getClass().getCanonicalName());
    }

sun.misc.Launcher.AppClassLoader
对应的`parent`是
sun.misc.Launcher.ExtClassLoader
对应的grand parent:NullPointer

可以看到sun.misc.Launcher.AppClassLoader对应的父类是sun.misc.Launcher.ExtClassLoader,这两个的父类都是URLClassLoader,具体参考(https://blog.csdn.net/briblue/article/details/54973413)

   private static void showClassLoader(){
        ClassLoader loader =ClassLoader.getSystemClassLoader();
//        System.out.println("Defaule ClassLoader Name:"+loader.getClass().getCanonicalName()+" \nparant Name: "+loader.getParent().getClass().getCanonicalName()+"\ngrand parent Name:"+loader.getParent().getParent().getClass().getCanonicalName());
        ClassLoader module = loader.getParent();
        if (module instanceof URLClassLoader){
            System.out.println("Got it  "+module.getClass().getCanonicalName());
        }
    }
输出的结果
Got it  sun.misc.Launcher.ExtClassLoader

初始化完成后,调用ModuleLoader的构造函数,ModuleLoader的构造函数是一个私有的,在构造函数内通过调用ModuleContainer的构造函数获取一个engineContainer对象,接着调用start方法启动程序。在ModuleContainer内部通过解析rasp-engine.jar获取MANIFEST.MF指定的Rasp-Module-Class类,调用反射方法获取到rasp-engine.jar的入口类,接着使用获取到的类加载器指定反射调用newInstance()方法,执行调用模块对应的start方法。

OpenRasp源码分析-java功能_第7张图片

对应的Rasp-Module-Classrasp-engine.jarMETA-INF/MANIFEST.MF
OpenRasp源码分析-java功能_第8张图片

由于系统加载器不一定就是默认的加载器,所以在加载的时候做了区分,
一个就是判断是否为java.net.URLClassLoader这个加载器,另一个就是调用ModuleLoader.isCustomClassLoader函数判断是否是weblogic或者jdk9、10和11。
源码内对应的注释
OpenRasp源码分析-java功能_第9张图片
如果两个都不是,则这里就提示加载失败,根据前面的分析可以知道,此时调用的是URLClassLoader部分的逻辑,反之就利用反射初始化模块EngineBoot对象,最后调用start方法。由于EngineBoot实现的是Module接口,每个模块都有start release两个方法
OpenRasp源码分析-java功能_第10张图片

在准备完成后,通过engineContainer.start()的方式调用模块对应的start()
OpenRasp源码分析-java功能_第11张图片

启动正常的话,就能看到了如下输出
OpenRasp源码分析-java功能_第12张图片

核心原理

根据前面的启动流程可以知道,程序最后交给了EngineBoot类并执行了start方法。EngineBoot类主要完成以下几个工作

  • 加载openrasp_v8.so (这个是暂时不做重点关注)
  • 记载config文件 (这个是暂时不做重点关注)
  • 初始化rasp编译信息 (这个是暂时不做重点关注)
  • 缓存RASP的build信息 (这个是暂时不做重点关注)
  • 加载js插件(这个是暂时不做重点关注)
  • 加载java层的hook功能模块(重点关注)
  • 初始化加载或者回滚类(重点)

加载java层的hook功能模块

EngineBoot.start()内,在完成一些js和系统信息初始化后,接着来到了检测点的初始化,通过CheckerManager.init();来实现。在其内部通过加载com.baidu.openrasp.plugin.checker.CheckParameter. Typ内的枚举类,包括了如下类型的hook点

  // js插件检测
        SQL("sql", new V8AttackChecker(), 1),
        COMMAND("command", new V8AttackChecker(), 1 << 1),
        DIRECTORY("directory", new V8AttackChecker(), 1 << 2),
        REQUEST("request", new V8AttackChecker(), 1 << 3),
        READFILE("readFile", new V8AttackChecker(), 1 << 5),
        WRITEFILE("writeFile", new V8AttackChecker(), 1 << 6),
        FILEUPLOAD("fileUpload", new V8AttackChecker(), 1 << 7),
        RENAME("rename", new V8AttackChecker(), 1 << 8),
        XXE("xxe", new V8AttackChecker(), 1 << 9),
        OGNL("ognl", new V8AttackChecker(), 1 << 10),
        DESERIALIZATION("deserialization", new V8AttackChecker(), 1 << 11),
        WEBDAV("webdav", new V8AttackChecker(), 1 << 12),
        INCLUDE("include", new V8AttackChecker(), 1 << 13),
        SSRF("ssrf", new V8AttackChecker(), 1 << 14),
        SQL_EXCEPTION("sql_exception", new V8AttackChecker(), 1 << 15),
        REQUESTEND("requestEnd", new V8AttackChecker(), 1 << 17),
        DELETEFILE("deleteFile", new V8AttackChecker(), 1 << 18),
        MONGO("mongodb", new V8AttackChecker(), 1 << 19),
        LOADLIBRARY("loadLibrary", new V8AttackChecker(), 1 << 20),
        SSRF_REDIRECT("ssrfRedirect", new V8AttackChecker(), 1 << 21),
        RESPONSE("response", new V8AttackChecker(false), 1 << 23),

        // java本地检测
        XSS_USERINPUT("xss_userinput", new XssChecker(), 1 << 16),
        SQL_SLOW_QUERY("sqlSlowQuery", new SqlResultChecker(false), 0),

        // 安全基线检测
        POLICY_LOG("log", new LogChecker(false), 1 << 22),
        POLICY_MONGO_CONNECTION("mongoConnection", new MongoConnectionChecker(false), 0),
        POLICY_SQL_CONNECTION("sqlConnection", new SqlConnectionChecker(false), 0),
        POLICY_SERVER_TOMCAT("tomcatServer", new TomcatSecurityChecker(false), 0),
        POLICY_SERVER_JBOSS("jbossServer", new JBossSecurityChecker(false), 0),
        POLICY_SERVER_JBOSSEAP("jbossEAPServer", new JBossEAPSecurityChecker(false), 0),
        POLICY_SERVER_JETTY("jettyServer", new JettySecurityChecker(false), 0),
        POLICY_SERVER_RESIN("resinServer", new ResinSecurityChecker(false), 0),
        POLICY_SERVER_WEBSPHERE("websphereServer", new WebsphereSecurityChecker(false), 0),
        POLICY_SERVER_WEBLOGIC("weblogicServer", new WeblogicSecurityChecker(false), 0),
        POLICY_SERVER_WILDFLY("wildflyServer", new WildflySecurityChecker(false), 0),
        POLICY_SERVER_TONGWEB("tongwebServer", new TongwebSecurityChecker(false), 0),
        POLICY_SERVER_BES("bes", new BESSecurityChecker(false), 0);

加这些插件添加到一个EnumMap

/**
 * Created by tyy on 17-11-20.
 *
 * 用于管理 hook 点参数的检测
 */
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);
        }
    }

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

    public static boolean check(Type type, CheckParameter parameter) {
        return checkers.get(type).check(parameter);
    }
}

添加的检测类为

[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.local.XssChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.local.SqlResultChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.LogChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.MongoConnectionChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.SqlConnectionChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.TomcatSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.JBossSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.JBossEAPSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.JettySecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.ResinSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.WebsphereSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.WeblogicSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.WildflySecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.TongwebSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.BESSecurityChecker

完成插件添加后,接着调用initTransformer(inst);函数开始执行插桩。

插桩的实现是通过CustomClassTransformer implements ClassFileTransformer来实现。在分析构造函数前,这里需要关注两个变量,一个是hooks另一个是serverDetector

  • hooks用来保存需要插桩的类,例如DeserializationHook
  • serverDetector服务器的探测器,这个类也是需要通过CustomClassTransformer检测服务器并针对不同的服务器做不同的处理。

包括的类型有

   private ServerDetectorManager() {
        detectors.add(new TomcatDetector());//"org/apache/catalina/Server"
        detectors.add(new JBossDetector());//"org/jboss/Main"
        detectors.add(new JBossEAPDetector());//"org/jboss/modules/Main"
        detectors.add(new JettyDetector());//"org/eclipse/jetty/server/Server"
        detectors.add(new WeblogicDetector());//"weblogic/servlet/internal/WebAppServletContext"
        detectors.add(new ResinDetector());//"com/caucho/server/resin/Resin"
        detectors.add(new WebsphereDetector());// "org/eclipse/core/launcher/Main"
        detectors.add(new UndertowDetector());//"io/undertow/server/HttpHandler"
        detectors.add(new DubboDetector());//"com/alibaba/dubbo/rpc/filter/GenericFilter"
        detectors.add(new SpringbootDetector());//"org/apache/catalina/startup/Bootstrap"
        detectors.add(new TongWebDetector());// "com/tongweb/web/thor/Server"
        detectors.add(new BESDetector());//"com/bes/enterprise/webtier/Server"

    }

构造函数内完成了以下功能

  • 将当前的CustomClassTransformer注册
 inst.addTransformer(this, true);
  • 添加hook点
    对于每个类型的检测进行关键类的方法插桩,在其内部函数
  addAnnotationHook();//添加hook点

重点关注这个addAnnotationHook();方法。

 private void addAnnotationHook() {
    //SCAN_ANNOTATION_PACKAGE="com.baidu.openrasp.hook"
        Set<Class> 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);
            }
        }
    }

在程序内首先扫描了com.baidu.openrasp.hook下的带有HookAnnotation注解的类,这里以DeserializationHook为例,其他的都一样。如下是DeserializationHook的源码

/**
 * Created by tyy on 6/21/17.
 * 反序列化漏洞检测hook
 */
@HookAnnotation
public class DeserializationHook extends AbstractClassHook {
    /**
     * (none-javadoc)
     *
     * @see com.baidu.openrasp.hook.AbstractClassHook#getType()
     */
    @Override
    public String getType() {
        return "deserialization";
    }

    /**
     * (none-javadoc)
     *
     * @see com.baidu.openrasp.hook.AbstractClassHook#isClassMatched(String)
     */
    @Override
    public boolean isClassMatched(String className) {
        return "java/io/ObjectInputStream".equals(className);
    }

    /**
     * (none-javadoc)
     *
     * @see com.baidu.openrasp.hook.AbstractClassHook#hookMethod(CtClass)
     */
    @Override
    protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
        String src = getInvokeStaticSrc(DeserializationHook.class, "checkDeserializationClass",
                "$1", ObjectStreamClass.class);
        insertBefore(ctClass, "resolveClass", "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;", src);
    }

    /**
     * 反序列化监检测点
     *
     * @param objectStreamClass 反序列化的类的流对象
     */
    public static void checkDeserializationClass(ObjectStreamClass objectStreamClass) {
        if (objectStreamClass != null) {
            String clazz = objectStreamClass.getName();
            if (clazz != null) {
                HashMap<String, Object> params = new HashMap<String, Object>();
                params.put("clazz", clazz);
                HookHandler.doCheck(CheckParameter.Type.DESERIALIZATION, params);
            }
        }

    }


}

可以看到DeserializationHook存在注解类HookAnnotation,因此会被扫描到。在获取了所有的hook插入点类之后,开始执行程序插桩过程。

迭代获取获取回来的classesSet类集合,并将每个类执行newInstance方法

Object object = clazz.newInstance();

在获取到对应的实例对象值后,判断获取的类是否是AbstractClassHook实例,从DeserializationHook内可以看到,这个类继承自AbstractClassHook,这个类是所有的hook点插桩的基类。判断之后就开始调用addhook函数执行插桩类的存储,后续使用。

 private void addHook(AbstractClassHook hook, String className) {
        if (hook.isNecessary()) {
            necessaryHookType.add(hook.getType());
        }
        String[] ignore = Config.getConfig().getIgnoreHooks();
        for (String s : ignore) {
            if (hook.couldIgnore() && (s.equals("all") || s.equals(hook.getType()))) {
                LOGGER.info("ignore hook type " + hook.getType() + ", class " + className);
                return;
            }
        }
        //保存需要插桩的类
        hooks.add(hook);
    }

CustomClassTransformer构造函数完成后,下一步就是调用retransform函数。这里先整理下到目前为止的工作

  • 注册CustomClassTransformer到系统内
  • 扫描com.baidu.openrasp.hook带有HookAnnotation类型注解的类
  • 判断扫描得到的类是否是AbstractClassHook
  • 添加需要插桩的类到hooks内,hooks是一个HashSet

retransform内,首先通过Instrumentation来获取所有加载的类,

然后调用isClassMatched将获取的类和需要插桩的类比较,如果是复合想要插桩的类,则返回True否则,就调用serverDetector.isClassMatched做判断,返回true的类都是要被插桩或者 特殊处理的类,将会调用inst.retransformClasses(clazz);来hook或者回滚已经被加载的类,这里主要的功能就是初始化已经被回滚已经被加载的类。

 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);
                    }
                }
            }
        }
    }

    //
    public boolean isClassMatched(String className) {
        for (final AbstractClassHook hook : getHooks()) {
            if (hook.isClassMatched(className)) {
                return true;//判断是否在类Hook点内
            }
        }
		//另一种是判断是否在存在服务器类型内
        return serverDetector.isClassMatched(className);
    }

到这里EngineBoot.start()方法分析完成了,接着是运行时的修改流程

运行时插桩

由于CustomClassTransformer继承自ClassFileTransformer,当类被加载的时候,会调用transform函数,原型如下

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {}

其中classfileBuffer就是类的字节码,类加载进来后会将字节码保存在这个参数内,重写transform函数,当类被加载的时候,通过这个函数传递类的字节码进来,在这里执行插桩过程,如下

 /**
     * 过滤需要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 {
        if (loader != null) {
            DependencyFinder.addJarPath(domain);
        }
        if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) {
            jspClassLoaderCache.put(className.replace("/", "."), new SoftReference<ClassLoader>(loader));
        }
        for (final AbstractClassHook hook : hooks) {
            if (hook.isClassMatched(className)) {
                CtClass ctClass = null;
                try {
                    ClassPool classPool = new ClassPool();
                    addLoader(classPool, loader);//设置classPool参数,添加到运行环境内
                    ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));//从字节数组内回复一个完整的类对象
                    if (loader == null) {
                        hook.setLoadedByBootstrapLoader(true);//设置是从bootstrapLoader加载的类
                    }
                    classfileBuffer = hook.transformClass(ctClass);//修改类字节码,返回一个byte[]修改好的类字节数组
                    if (classfileBuffer != null) {
                        checkNecessaryHookType(hook.getType());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (ctClass != null) {
                        ctClass.detach();
                    }
                }
            }
        }
        serverDetector.detectServer(className, loader, domain);
		//返回修改后的字节码
        return classfileBuffer;//
    }

从程序中可以看到,利用hooks来过滤不需要插桩的类,符合条件的类将进入动态插桩流程,这里和之前的java动态修改字节码技术基础中的内容一致,重点进入到classfileBuffer = hook.transformClass(ctClass);

进入到transformClass方法后,先调用了hookMethod执行插桩,接着放回类的字节码

  public byte[] transformClass(CtClass ctClass) {
        try {
            hookMethod(ctClass);
            return ctClass.toBytecode();
        } catch (Throwable e) {
            if (Config.getConfig().isDebugEnabled()) {
                LOGGER.info("transform class " + ctClass.getName() + " failed", e);
            }
        }
        return null;
    }

继续进入到hookMethod,发现是一个抽象的方法,则需要在子类内实现,这里关注反序列化的检测,因此查看DeserializationHookhookMethod的实现如下

   protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
        String src = getInvokeStaticSrc(DeserializationHook.class, "checkDeserializationClass",
                "$1", ObjectStreamClass.class);
        insertBefore(ctClass, "resolveClass", "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;", src);
    }

根据java动态修改字节码的知识可以知道,getInvokeStaticSrc返回的是一个方法的字符串,如下

 /**
     * 获取调用静态方法的代码字符串
     *
     * @param invokeClass 静态方法所属的类
     * @param methodName  静态方法名称
     * @param paramString 调用传入的参数字符串,按照javassist格式
     * @return 整合之后的代码
     */
    public String getInvokeStaticSrc(Class invokeClass, String methodName, String paramString, Class... parameterTypes) {
        String src;
        String invokeClassName = invokeClass.getName();

        String parameterTypesString = "";
        if (parameterTypes != null && parameterTypes.length > 0) {
            for (Class parameterType : parameterTypes) {
                if (parameterType.getName().startsWith("[")) {
                    parameterTypesString += "Class.forName(\"" + parameterType.getName() + "\"),";
                } else {
                    parameterTypesString += (parameterType.getName() + ".class,");
                }
            }
            parameterTypesString = parameterTypesString.substring(0, parameterTypesString.length() - 1);
        }
        if (parameterTypesString.equals("")) {
            parameterTypesString = null;
        } else {
            parameterTypesString = "new Class[]{" + parameterTypesString + "}";
        }
        if (isLoadedByBootstrapLoader) {
			//被bootstrap加载的类,插入的代码方式
            src = "com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass(\"" + invokeClassName + "\").getMethod(\"" + methodName +
                    "\"," + parameterTypesString + ").invoke(null";
            if (!StringUtils.isEmpty(paramString)) {
                src += (",new Object[]{" + paramString + "});");
            } else {
                src += ",null);";
            }
            src = "try {" + src + "} catch (Throwable t) {if(t.getCause() != null && t.getCause().getClass()" +
                    ".getName().equals(\"com.baidu.openrasp.exceptions.SecurityException\")){throw t;}}";
        } else {
            src = invokeClassName + '.' + methodName + "(" + paramString + ");";
            src = "try {" + src + "} catch (Throwable t) {if(t.getClass()" +
                    ".getName().equals(\"com.baidu.openrasp.exceptions.SecurityException\")){throw t;}}";
        }
        return src;
    }

假设返回来的字符串src如下

try{
	com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass("DeserializationHook").getMethod("checkDeserializationClass","$1").invoke(null,new Object[]{ObjectStreamClass.class});
	或者是
		com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass("java.io.ObjectInputStream").getMethod("resolveClass","java.io.ObjectStreamClass").invoke(null,null);
}catch(Throwable t) {
if(t.getCause() != null && t.getCause().getClass().getName().equals(\"com.baidu.openrasp.exceptions.SecurityException\"))

{throw t;}
}
}

接着调用插桩方法insertBefore执行插桩,如下

public void insertBefore(CtClass ctClass, String methodName, String desc, String src)
            throws NotFoundException, CannotCompileException {

		/**
			例如反序列化Hook方法,
		ctClass="io.ObjectInputStream"
		methodName="resolveClass"
		des="(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;" 方法描述
		src="checkDeserializationClass"封装好的方法体

		//2020-04-20新增注释

		**/

        LinkedList<CtBehavior> methods = getMethod(ctClass, methodName, desc, null);
        if (methods != null && methods.size() > 0) {
            insertBefore(methods, src);//核心插桩函数
        } else {
            if (Config.getConfig().isDebugEnabled()) {
                LOGGER.info("can not find method " + methodName + " " + desc + " in class " + ctClass.getName());
            }
        }

    }

最后完成插桩

  private void insertBefore(LinkedList<CtBehavior> methods, String src)
            throws CannotCompileException {
        for (CtBehavior method : methods) {
            if (method != null) {
                insertBefore(method, src);
            }
        }
    }

  /**
     * 在目标类的目标方法的入口插入相应的源代码
     *
     * @param method 目标方法
     * @param src    源代码
     */
    public void insertBefore(CtBehavior method, String src) throws CannotCompileException {
        try {
            method.insertBefore(src);
            LOGGER.info("insert before method " + method.getLongName());
        } catch (CannotCompileException e) {
            LogTool.traceError(ErrorType.HOOK_ERROR,
                    "insert before method " + method.getLongName() + " failed: " + e.getMessage(), e);
            throw e;
        }
    }

这里就完成了对关键类的插桩逻辑分析,当发生序列化检测的时候,程序会先调用到插桩函数,然后在执行原始函数,下面进行逻辑分析。

调用检测逻辑

还是以DeserializationHook为例,在java/io/ObjectInputStream类中的resolveClass方法的入口处加入了

checkDeserializationClass.checkDeserializationClass(ObjectInputStream input);

因此发生java.io.ObjectInputStream.resolveClass之后,首先调用checkDeserializationClass,此时检测逻辑将进入到HookHandler.doCheck(CheckParameter.Type.DESERIALIZATION, params);内。

来到HookHandler内,需要关注几个变量

  • requestCache 每个线程的请求缓存 类型是ThreadLocal 如果不清除的可以自行了解下
  • responseCache 每个线程响应的缓存 类型是ThreadLocal
  • enableHook 是否开启开关 类型是ThreadLocal
    可以看到以上几个关键变量都是ThreadLocal类型的,目的就是为了多线程支持,避免造成线程间的请求和响应出现混乱。

在清除了几个变量之后,再次进入到doCheck,首先判断是否开启了hook,如果开启了,则调用doCheckWithoutRequest函数,在doCheckWithoutRequest内经过一些校验检测后,最后来到


doRealCheckWithoutRequest(type, params);

这里的type=DESERIALIZATION("deserialization", new V8AttackChecker(), 1 << 11) ,param={"clazz":"ObjectStreamClass"},说明是反序列化检测类型,调用CheckParameter构建一个检测参数,然后通过

 isBlock = CheckerManager.check(type, parameter);

来调用检测逻辑,继续来到CheckerManager.check函数

public static boolean check(Type type, CheckParameter parameter) {
        return checkers.get(type).check(parameter);
    }

最后返回是否决定阻止返回,可以知道这里的checkers就是在一开始就初始化的类,最后通过一个类型获取对应的检测器去检测。

还有一个问题就是,这里的reqeustCache和responseCache并没有被DeserializationHook赋值,而是通过了SpringFormContentHook类的rebuildRequest进行赋值的

 public static void rebuildRequest(Object requestWrapper) {
        if (HookHandler.requestCache.get() != null) {
            String requestId = HookHandler.requestCache.get().getRequestId();
            HookHandler.requestCache.set(new HttpServletRequest(requestWrapper, requestId));
        }

    }

这个类也是在CustomClassTransformer阶段的transform执行代码插桩。

isBlock=true的时候,进入到handleBlock,这里的处理方式很简单粗暴,直接抛出异常

private static void handleBlock(CheckParameter parameter) {
        SecurityException securityException = new SecurityException("Request blocked by OpenRASP");
        if (responseCache.get() != null) {
            responseCache.get().sendError(parameter);
        }
        throw securityException;
    }

这里就差不多分析完成了整个检测调用流程,当然还有一些其他细节,例如响应头的设置等都在HookHandle类中,只是每个检测点请求的不同,调用的时机也不一样,有需要可以再细读其他细节。接下来就是检测细节的分析在,这里以反序列化检测为例进行分析。

反序列化检测细节

根据前面的流程分析可以知道,此时程序调用了反序列化的检测器

DESERIALIZATION("deserialization", new V8AttackChecker(), 1 << 11),

调用的了check阶段,调用位置在com.baidu.openrasp.plugin.checker.AbstractChecker内

    public boolean check(CheckParameter checkParameter) {
        List<EventInfo> eventInfos = checkParam(checkParameter);
        boolean isBlock = false;
        if (eventInfos != null) {
            for (EventInfo info : eventInfos) {
                if (info.isBlock()) {
                    isBlock = true;
                }
                dispatchCheckEvent(info);
            }
        }
        isBlock = isBlock && canBlock;
        return isBlock;
    }

其中的checkParam的实现如下

 public List<EventInfo> checkParam(CheckParameter checkParameter) {
        return JS.Check(checkParameter);
    }

当返回的eventInfos不为空并且info.isBlock()==true,则当前的请求会被墙掉。

继续跟进到JS.Check内,这里的V8是在EngineBoot.start()阶段就初始化了,继续跟进到check函数内,先获取检测类型,这里是DESERIALIZATION,调用JsonStream.serialize函数将请求的参数序列化,然后将请求的参数读取出来保存在out内。进入到实际判断逻辑阶段。

首先判断检测类型,如果是DIRECTORY READFILE WRITEFILE SQL SSRF类型的,则统一处理,否则,开始调用V8.Check(..)函数,参数分别是

public static byte[] Check(String type, byte[] params, int params_size, Context context, boolean new_request,
            int timeout);

其中Context对象是

public class Context extends com.baidu.openrasp.v8.Context {

    public AbstractRequest request = null;

    public static void setKeys() {
        setStringKeys(new String[]{"path", "method", "url", "querystring", "protocol", "remoteAddr", "appBasePath",
                "requestId", "appId", "raspId", "hostname", "source", "target", "clientIp"});
        setObjectKeys(new String[]{"json", "server", "parameter", "header", "nic"});
        setBufferKeys(new String[]{"body"});
    }

    public Context(AbstractRequest request) {
        this.request = request;
    }

    public String getString(String key) {
        if (key.equals("path"))
            return getPath();
        if (key.equals("method"))
            return getMethod();
        if (key.equals("url"))
            return getUrl();
        if (key.equals("querystring"))
            return getQuerystring();
        if (key.equals("appBasePath"))
            return getAppBasePath();
        if (key.equals("protocol"))
            return getProtocol();
        if (key.equals("remoteAddr"))
            return getRemoteAddr();
        if (key.equals("requestId"))
            return getRequestId();
        if (key.equals("appId"))
            return getAppId();
        if (key.equals("raspId"))
            return getRaspId();
        if (key.equals("hostname"))
            return getHostname();
        if (key.equals("source"))
            return getSource();
        if (key.equals("target"))
            return getTarget();
        if (key.equals("clientIp"))
            return getClientIp();
        return null;
    }

    public byte[] getObject(String key) {
        if (key.equals("json"))
            return getJson();
        if (key.equals("header"))
            return getHeader();
        if (key.equals("parameter"))
            return getParameter();
        if (key.equals("server"))
            return getServer();
        if (key.equals("nic"))
            return getNic();
        return null;
    }

    public byte[] getBuffer(String key) {
        if (key.equals("body"))
            return getBody();
        return null;
    }

    public String getPath() {
        try {
            return request.getRequestURI().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getMethod() {
        try {
            return request.getMethod().toLowerCase().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getUrl() {
        try {
            return request.getRequestURL().toString().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getQuerystring() {
        try {
            return request.getQueryString().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getAppBasePath() {
        try {
            return request.getAppBasePath().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getProtocol() {
        try {
            return request.getProtocol().toLowerCase().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getRemoteAddr() {
        try {
            return request.getRemoteAddr().toString();
        } catch (Exception e) {
            return "";
        }
    }

    public String getRequestId() {
        try {
            return request.getRequestId().toString();
        } catch (Exception e) {
            return "";
        }
    }

    // TODO: update openrasp-v8, accept string body
    public byte[] getBody() {
        try {
            return escape(request.getStringBody());
        } catch (Exception e) {
            return null;
        }
    }

    public byte[] getJson() {
        try {
            String contentType = request.getContentType();
            if (contentType != null && contentType.contains("application/json")) {
                return getBody();
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }

    public byte[] escape(String src) throws UnsupportedEncodingException {
        char j;
        StringBuilder tmp = new StringBuilder();
        for (int i = 0; i < src.length(); i++) {
            j = src.charAt(i);
            if (j < 256)
                tmp.append(j);
            else {
                tmp.append("\\u");
                tmp.append(Integer.toString(j, 16));
            }
        }
        return tmp.toString().getBytes("UTF-8");
    }

    public byte[] getHeader() {
        try {
            Enumeration<String> headerNames = request.getHeaderNames();
            if (headerNames == null || !headerNames.hasMoreElements()) {
                return null;
            }
            HashMap<String, String> headers = new HashMap<String, String>();
            while (headerNames.hasMoreElements()) {
                String key = headerNames.nextElement();
                String value = request.getHeader(key);
                headers.put(key.toLowerCase(), value);
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            JsonStream.serialize(headers, out);
            out.write(0);
            return out.getByteArray();
        } catch (Exception e) {
            return "{}".getBytes();
        }
    }

    public byte[] getParameter() {
        try {
            Map<String, String[]> parameters = request.getParameterMap();
            if (parameters == null || parameters.isEmpty()) {
                return null;
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            JsonStream.serialize(parameters, out);
            out.write(0);
            return out.getByteArray();
        } catch (Exception e) {
            return null;
        }
    }

    public byte[] getServer() {
        try {
            Map<String, String> server = ApplicationModel.getApplicationInfo();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            JsonStream.serialize(server, out);
            out.write(0);
            return out.getByteArray();
        } catch (Exception e) {
            return "{}".getBytes();
        }
    }

    public String getAppId() {
        try {
            return Config.getConfig().getCloudAppId();
        } catch (Exception e) {
            return "";
        }
    }

    public String getRaspId() {
        try {
            return CloudCacheModel.getInstance().getRaspId() != null ? CloudCacheModel.getInstance().getRaspId() : "";
        } catch (Exception e) {
            return "";
        }
    }

    public String getHostname() {
        try {
            return OSUtil.getHostName();
        } catch (Exception e) {
            return "";
        }
    }

    public byte[] getNic() {
        try {
            List<NicModel> nic = OSUtil.getIpAddress();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            JsonStream.serialize(nic, out);
            out.write(0);
            return out.getByteArray();
        } catch (Exception e) {
            return "{}".getBytes();
        }
    }

    public String getSource() {
        try {
            return request.getRemoteAddr();
        } catch (Exception e) {
            return "";
        }
    }

    public String getTarget() {
        try {
            return request.getLocalAddr();
        } catch (Exception e) {
            return "";
        }
    }

    public String getClientIp() {
        try {
            return request.getClientIp();
        } catch (Exception e) {
            return "";
        }
    }
}

保存的整个请求的完整字段信息。
params是请求的参数,被序列化为了字节,便于在V8引擎内检测。

程序最终来到V8.Check(String type, byte[] params, int params_size, Context context, boolean new_request, int timeout)函数,这个是一个Native的,需要分析下对应的实现。

待更新native的实现分析

后续在补上这块,因为涉及到V8引擎的执行原理,暂且留着。

你可能感兴趣的:(rasp,java)