前言

久闻 OpenRASP 大名,对其 Java 防护方式好奇,所以研究一下 RASP,看看它是怎么实现命令执行拦截的。


尝试

尝试用 javassist 修改 Runtime 类,结果发现主要会有两个问题:

  • 类加载器无法覆盖启动类加载器加载的类
  • 同个 Class 不能在同个类加载器中重复加载,要修改一个类就需要创建一个新的类加载器

不会写,只能先看看别人的实现了。

OpenRASP

相关知识

agent

参考文章1参考文章2,简单来说就是一个独立于应用程序的代理程序,可以在主程序运行之前运行,并用于执行 hook 等操作。

第二个参数 Instrumentation 的相关信息可以看这篇参考文章,简单仿照 OpenRASP 写份代码看看它的机制:

// Agent
public class Agent
{
    private static String getLocalJarPath() {
        URL localUrl = Agent.class.getProtectionDomain().getCodeSource().getLocation();
        String path = null;
        try {
            path = URLDecoder.decode(
                    localUrl.getFile().replace("+", "%2B"), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            System.err.println("[OpenRASP] Failed to get jarFile path.");
            e.printStackTrace();
        }
        System.out.println("Agent Jar at: " + path);
        return path;
    }

    public static void premain( String agentArgs, Instrumentation inst ) throws IOException {
        String localJarPath = getLocalJarPath();
        inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath));
        CustomClassTransformer customClassTransformer = new CustomClassTransformer(inst);
        customClassTransformer.retransform();
    }
}
// ClassTransformer
public class CustomClassTransformer implements ClassFileTransformer {
    private final Instrumentation inst;

    public CustomClassTransformer(Instrumentation inst) {
        this.inst = inst;
        inst.addTransformer(this, true);
    }

    public void retransform() {
        Class[] loadedClasses = inst.getAllLoadedClasses();
        System.out.println("----------      Loaded classes      ----------");
        for (Class clazz : loadedClasses) {
            if (inst.isModifiableClass(clazz)) {
                System.out.println("Loaded class: " + clazz.getName());
            }
        }
        System.out.println("----------      Loaded classes      ----------");
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("Transforming class: " + className);
        return classfileBuffer;
    }
}

pom.xml 中要注意的两个配置:

<Premain-Class>com.example.agent.Agent</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>

然后配置一下 IDEA 的启动参数,加上 -javaagent 为代理 jar 运行就好。

可以看到,在主程序加载类而触发 ClassTransformer 的 transform 之前,因为要运行代理程序,JVM 已经加载了一部分类,所以这些已加载的类就不会再次加载,不会触发 transform,我们也就无法用这种方式对他进行修改了。比如代理程序中解 URL 编码用到的 java.net.URLDecoder 类,我们在 loadedClasses 的输出中能看到,在 transform 的输出中却看不到了。

想要修改这些已加载的类,就需要在 retransform 函数中加一些处理,比如我要修改 URLDecoder 类:

if (inst.isModifiableClass(clazz) && clazz.getName().equals("java.net.URLDecoder")) {
    inst.retransformClasses(clazz);
}

可以看到 URLDecoder 重新进入了 transform,接下来尝试修改它的 decode 函数,打包的时候遇到了些问题,javassist 没有打包进来导致依赖缺失。使用 shade 插件打包时(shade 插件还可以通过 relocations 重命名防止冲突,shade 文档)又会有清单属性缺失、依赖没有打包等问题。

最后换了个 2020 版本的 IDEA,用的是下面这种写法(清单属性也可以写在 shade 中)成功的:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <executions>
        <execution>
            <phase>package</phase>
        </execution>
    </executions>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>com.example.agent.Agent</Premain-Class>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.4</version>
    <configuration>
        <!-- put your configurations here -->
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
        </execution>
    </executions>
</plugin>

然后在 transform 函数用 javassist 修改 decode:

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    if (className.equals("java/net/URLDecoder")) {
        ClassPool classPool = ClassPool.getDefault();
        try {
            CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
            CtMethod[] ctMethods = ctClass.getDeclaredMethods();
            for (CtMethod ctMethod : ctMethods) {
                if (ctMethod.getName().equals("decode") && ctMethod.getParameterTypes().length == 1) {
                    ctMethod.insertBefore("return \"You can't decode this string!\";");
                }
            }
            classfileBuffer = ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return classfileBuffer;
}

用主程序测试,可以看到 decode 函数已经被我们修改了。

卸载代理程序也很简单,用 retransform 将加载过的类处理一下,被修改过的类在从 Class 文件重新加载的时候就不会经过这个代理程序的修改了。

在存在多个 ClassTransformer 的情况下,transform 就会是个链式过程,上个 ClassTransformer 输出的字节码会成为下一个 ClassTransformer 的输入。

代理程序启动

跳过安装部分,直接看运行。

用 IDEA 打开 agent/java 目录查看逻辑代码(公开的源码中有注释,很好看,赞),看到作为入口的 Agent 类,其代理程序入口的 premain 函数如下:

/**
* 启动时加载的agent入口方法
*
* @param agentArg 启动参数
* @param inst     {@link Instrumentation}
*/
public static void premain(String agentArg, Instrumentation inst) {
    init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);
}

另一个入口 agentmain 也差不多:

/**
* attach 机制加载 agent
*
* @param agentArg 启动参数
* @param inst     {@link Instrumentation}
*/
public static void agentmain(String agentArg, Instrumentation inst) {
    init(Module.START_MODE_ATTACH, agentArg, inst);
}

一种是在 JVM 启动时启动的 normal 模式,另一种则是 JVM 启动后附着进去的 attach 模式。

两种模式都会进入 init 函数:

public static synchronized void init(String mode, String action, Instrumentation inst) {
    try {
        JarFileHelper.addJarToBootstrap(inst);
        readVersion();
        ModuleLoader.load(mode, action, inst);
    } catch (Throwable e) {
        System.err.println("[OpenRASP] Failed to initialize, will continue without security protection.");
        e.printStackTrace();
    }
}

addJarToBootstrap 会将该 jar 添加到启动类加载器检索路径:

/**
* 添加jar文件到jdk的跟路径下,优先加载
*
* @param inst {@link Instrumentation}
*/
public static void addJarToBootstrap(Instrumentation inst) throws IOException {
    String localJarPath = getLocalJarPath();
    inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath));
}

这样一来,代理程序就可以修改启动类加载器加载的类了,比如 java.lang.Runtime。

ModuleLoader.load 会加载所有的 RASP 模块:

/**
* 加载所有 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");
        }
    } 
    ...
}

new ModuleLoader 用于构造模块:

/**
* 构造所有模块
*
* @param mode 启动模式
* @param inst {@link java.lang.instrument.Instrumentation}
*/
private ModuleLoader(String mode, Instrumentation inst) throws Throwable {

    if (Module.START_MODE_NORMAL == mode) {
        setStartupOptionForJboss();
    }
    engineContainer = new ModuleContainer(ENGINE_JAR);
    engineContainer.start(mode, inst);
}

setStartupOptionForJboss 是判断是否为 JBOSS 环境并进行相关处理的函数,我们可以看到实际上加载的模块只有一个 rasp-engine.jar,进入 ModuleContainer 看看模块的构造方式:

public ModuleContainer(String jarName) throws Throwable {
    try {
        ...
        Attributes attributes = jarFile.getManifest().getMainAttributes();
        ...
        this.moduleName = attributes.getValue("Rasp-Module-Name");
        String moduleEnterClassName = attributes.getValue("Rasp-Module-Class");
        if (moduleName != null && moduleEnterClassName != null
            && !moduleName.equals("") && !moduleEnterClassName.equals("")) {
            Class moduleClass;
            if (ClassLoader.getSystemClassLoader() instanceof URLClassLoader) {
                ...
                moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);
                module = (Module) moduleClass.newInstance();
            } 
            ...
        }
    } 
...
}

构造完成之后的执行:

@Override
public void start(String mode, Instrumentation inst) throws Throwable {
    module.start(mode, inst);
}

可以看到模块的入口类来自配置文件中的 Rasp-Module-Class,我们看下 rasp-engine 模块的 pom.xml:

<manifestEntries>
    <Rasp-Module-Name>rasp-engine</Rasp-Module-Name>
    <Rasp-Module-Class>com.baidu.openrasp.EngineBoot</Rasp-Module-Class>
</manifestEntries>

所以下一步就是 EngineBoot 类的 start 函数,现在 OpenRASP 的启动已经基本完成了,接下来就是执行模块代码了。

模块启动

看看 EngineBoot 类的 start 函数:

@Override
public void start(String mode, Instrumentation inst) throws Exception {
    ...
    try {
        Loader.load();
    } catch (Exception e) {
        ...
    }
    if (!loadConfig()) {
        return;
    }
    //缓存rasp的build信息
    Agent.readVersion();
    BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit);
    // 初始化插件系统
    if (!JS.Initialize()) {
        return;
    }
    CheckerManager.init();
    initTransformer(inst);
    ...
}

前面的加载 v8 JavaScript 引擎、读取配置、缓存 build 信息、初始化插件系统等步骤先跳过,我们看到模块的初始化函数 initTransformer:

/**
* 初始化类字节码的转换器
*
* @param inst 用于管理字节码转换器
*/
private void initTransformer(Instrumentation inst) throws UnmodifiableClassException {
    transformer = new CustomClassTransformer(inst);
    transformer.retransform();
}

看看 CustomClassTransformer 类的构造函数:

public CustomClassTransformer(Instrumentation inst) {
    this.inst = inst;
    inst.addTransformer(this, true);
    addAnnotationHook();
}

将 Instrumentation 保存起来,然后将该类添加为一个 ClassFileTransformer(设置第二个参数 canRetransform 为 true 来让代理程序可以修改已经加载的类,用于卸载或者 attach 时),后面就可以通过其 transform 函数来修改类的字节码。

添加 hook

CustomClassTransformer 函数最后会调用 addAnnotationHook:

private void addAnnotationHook() {
    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 包下的类,然后遍历实例化,并调用 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);
}

这里有两个 Set 用来存放这些 hook,一般的 hook 都会存放在 hooks 这个 set 中。

修改类字节码

添加完 hook 之后,我们回到 EngineBoot 类的 initTransformer 函数中,接下来会调用的是 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);
                }
            }
        }
    }
}

isClassMatched 其实就是遍历 hooks 然后确定该类是否需要修改:

public boolean isClassMatched(String className) {
    for (final AbstractClassHook hook : getHooks()) {
        if (hook.isClassMatched(className)) {
            return true;
        }
    }
    return serverDetector.isClassMatched(className);
}

判断用的是该 hook 中的 isClassMatched 方法,以 DeserializationHook 类为例,其 isClassMatched 代码如下:

@Override
public boolean isClassMatched(String className) {
    return "java/io/ObjectInputStream".equals(className);
}

意思就是 hook 住 java.io.ObjectInputStream 这个类。

如果该类不在 hooks 中,就会从 detectors 中查找,这一步其实就是检测一些常见的服务端框架,用的是 com.baidu.openrasp.detector 包下面的类,大概有这几种类型:

private ServerDetectorManager() {
    detectors.add(new TomcatDetector());
    detectors.add(new JBossDetector());
    detectors.add(new JBossEAPDetector());
    detectors.add(new JettyDetector());
    detectors.add(new WeblogicDetector());
    detectors.add(new ResinDetector());
    detectors.add(new WebsphereDetector());
    detectors.add(new UndertowDetector());
    detectors.add(new DubboDetector());
    detectors.add(new SpringbootDetector());
    detectors.add(new TongWebDetector());
    detectors.add(new BESDetector());

}

如果是要 hook 且可修改的类,就会通过 inst.retransformClasses 触发 transform 修改字节码(因为该类已经被加载,所以需要用 retransformClasses 让其重新进行 transform;或者需要卸载代理,所以重新进行一遍 transform),我们最后看看修改字节码的 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<ClassLoader>(loader));
    }
    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);
                }
                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;
}

再次遍历 hooks,使用 javassist 将字节码转换为一个可修改的 CtClass 对象,然后调用 hook 的 transformClass 函数进行修改,同样以 DeserializationHook 为例,其没有实现 transformClass,所以执行的是父类的:

/**
* 转化目标类
*
* @param ctClass 待转化的类
* @return 转化之后类的字节码数组
*/
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 进行修改,然后返回修改后的字节码,我们继续看看 DeserializationHook 的 hookMethod 函数:

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

getInvokeStaticSrc 和 insertBefore 都是父类 AbstractClassHook 中的函数,其中 getInvokeStaticSrc 的作用是构造调用该 hook 下函数的代码块,insertBefore 则是将代码块插入函数中。DeserializationHook 类的 hookMethod 函数的意思就是,在 resolveClass 函数调用时先调用 checkDeserializationClass 函数进行类名校验。

至于防护命令执行的 ProcessBuilderHook 类,hook 的是 ProcessImpl 和 UNIXProcess 类的构造函数,然后调用 checkCommand 进行检验,最后调用的 checker 就是 COMMAND 类型的检测器 v8:

COMMAND("command", new V8AttackChecker(), 1 << 1)

具体干了什么,看不太懂,不知道用 v8 引擎对命令做了什么样的检测。

OpenRASP 的缺陷

在可以代码执行的情况下,可以直接关掉 OpenRASP 来执行命令。

JVM-Sandbox

启动

比较复杂的一个事件驱动框架,参考文章,相比 OpenRASP 更加细致,除了做安全防护,也可以做动态添加日志等需求。

实现方式同样使用的 agent 作为入口,然后将 Spy 类放入启动类加载器的检索路径:

// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
    getSandboxSpyJarPath(home)
    // SANDBOX_SPY_JAR_PATH
)));

接着启动 Jetty 服务器和 JVM 沙盒:

// install
classOfProxyServer
    .getMethod("bind", classOfConfigure, Instrumentation.class)
    .invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
// bind
initializer.initProcess(new Initializer.Processor() {
    @Override
    public void process() throws Throwable {
        LogbackUtils.init(
            cfg.getNamespace(),
            cfg.getCfgLibPath() + File.separator + "sandbox-logback.xml"
        );
        logger.info("initializing server. cfg={}", cfg);
        jvmSandbox = new JvmSandbox(cfg, inst);
        initHttpServer();
        initJettyContextHandler();
        httpServer.start();
    }
});

JvmSandbox 类的构造函数如下:

public JvmSandbox(final CoreConfigure cfg,
                final Instrumentation inst) {
        EventListenerHandler.getSingleton();
        this.cfg = cfg;
        this.coreModuleManager = SandboxProtector.instance.protectProxy(CoreModuleManager.class, new DefaultCoreModuleManager(
                cfg,
                inst,
                new DefaultCoreLoadedClassDataSource(inst, cfg.isEnableUnsafe()),
                new DefaultProviderManager(cfg)
        ));

        init();
    }

可以看到这里实例化了好几个类,DefaultProviderManager 类与主题无关跳过, DefaultCoreLoadedClassDataSource 类的构造函数如下:

public DefaultCoreLoadedClassDataSource(final Instrumentation inst,
                                    final boolean isEnableUnsafe) {
    this.inst = inst;
    this.isEnableUnsafe = isEnableUnsafe;
}

将 Instrumentation 保存了起来,后面可能会用来修改字节码。然后看看 DefaultCoreModuleManager 类的构造函数:

public DefaultCoreModuleManager(final CoreConfigure cfg,
                                final Instrumentation inst,
                                final CoreLoadedClassDataSource classDataSource,
                                final ProviderManager providerManager) {
    this.cfg = cfg;
    this.inst = inst;
    this.classDataSource = classDataSource;
    this.providerManager = providerManager;

    // 初始化模块目录
    this.moduleLibDirArray = mergeFileArray(
        StringUtils.isBlank(cfg.getSystemModuleLibPath())
            ? new File[0]
            : new File[]{new File(cfg.getSystemModuleLibPath())},
        cfg.getUserModuleLibFilesWithCache()
    );
}

同样将 Instrumentation 保存了起来,还从两个配置(SYSTEM_MODULE_LIB、USER_MODULE_LIB)中获取模块(getSystemModuleLibPath 获取的是 SYSTEM_MODULE_LIB 配置的文件夹,而 getUserModuleLibFilesWithCache 获取是 USER_MODULE_LIB 配置的用户模块,有目录和文件两种形式,文件要以 jar 结尾,目录则会列目录一次并读取其中的 jar 文件):

private File[] mergeFileArray(File[] aFileArray, File[] bFileArray) {
    final List<File> _r = new ArrayList<File>();
    _r.addAll(Arrays.asList(aFileArray));
    _r.addAll(Arrays.asList(bFileArray));
    return _r.toArray(new File[]{});
}

这两个配置在默认情况下的值可以看看官方的入门模块编写教程

SYSTEM_MODULE_LIB : /Users/vlinux/opt/sandbox/module
USER_MODULE_LIB : ~/.sandbox-module;

应该分别是 JVM-Sandbox 安装目录和用户目录下的 .sandbox-module 目录,所以我们编写好的模块打包成 jar 放到这里就 ok 了。

初始化完成 coreModuleManager 后,会调用 init:

private void init() {
    doEarlyLoadSandboxClass();
    SpyUtils.init(cfg.getNamespace());
}

前一个函数 doEarlyLoadSandboxClass 是加载沙盒内部类的,我们看后一个跟 Spy 有关的函数:

// SpyUtils
public synchronized static void init(final String namespace) {
    if (!Spy.isInit(namespace)) {
        Spy.init(namespace, EventListenerHandler.getSingleton());
    }
}
// Spy
public static void init(final String namespace,
                        final SpyHandler spyHandler) {
    namespaceSpyHandlerMap.putIfAbsent(namespace, spyHandler);
}

将一个看名字就很重要的单例模式类 EventListenerHandler 放进了 Map 中,具体有什么用要后面在看了。

回到前面的 bind 函数,初始化完 Jetty 和沙盒之后,就开始加载模块了:

// 初始化加载所有的模块
try {
    jvmSandbox.getCoreModuleManager().reset();
} catch (Throwable cause) {
    logger.warn("reset occur error when initializing.", cause);
}

加载模块

这是明显是调用了 coreModuleManager 的 reset 函数,不过我们仔细看其初始化方式,可以发现他其实是个 protectProxy 生成的代理:

public <T> T protectProxy(final Class<T> protectTargetInterface,
                        final T protectTarget) {
    return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{protectTargetInterface}, new InvocationHandler() {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final int enterReferenceCount = enterProtecting();
            try {
                return method.invoke(protectTarget, args);
            } finally {
                final int exitReferenceCount = exitProtecting();
                assert enterReferenceCount == exitReferenceCount;
                if (enterReferenceCount != exitReferenceCount) {
                    logger.warn(...);
                }
            }
        }
    });
}

调用 reset 要先经过这里的 invoke,不过其实也没有做什么额外的处理,我们看看 DefaultCoreModuleManager 的 reset 函数:

@Override
public synchronized CoreModuleManager reset() throws ModuleException {

    logger.info("resetting all loaded modules:{}", loadedModuleBOMap.keySet());

    // 1. 强制卸载所有模块
    unloadAll();

    // 2. 加载所有模块
    for (final File moduleLibDir : moduleLibDirArray) {
        // 用户模块加载目录,加载用户模块目录下的所有模块
        // 对模块访问权限进行校验
        if (moduleLibDir.exists() && moduleLibDir.canRead()) {
            new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
                .load(
                    new InnerModuleJarLoadCallback(),
                    new InnerModuleLoadCallback()
                );
        } else {
            logger.warn("module-lib not access, ignore flush load this lib. path={}", moduleLibDir);
        }
    }

    return this;
}

unloadAll 的卸载过程先放一边,我们看加载过程,遍历之前加载到的模块,然后实例化一个 ModuleLibLoader 类并调用其 load 函数进行加载:

ModuleLibLoader(final File moduleLibDir,
                final Information.Mode mode) {
    this.moduleLibDir = moduleLibDir;
    this.mode = mode;
}

void load(final ModuleJarLoadCallback mjCb,
          final ModuleJarLoader.ModuleLoadCallback mCb) {
    // 开始逐条加载
    for (final File moduleJarFile : listModuleJarFileInLib()) {
        try {
            mjCb.onLoad(moduleJarFile);
            new ModuleJarLoader(moduleJarFile, mode).load(mCb);
        } catch (Throwable cause) {
            logger.warn("loading module-jar occur error! module-jar={};", moduleJarFile, cause);
        }
    }
}

listModuleJarFileInLib 是个遍历读取目录下 jar 文件的过程(因为 SYSTEM_MODULE_LIB 配置中的目录还没有展开),然后调用 mjCb.onLoad,这个实际是 DefaultProviderManager 的 loading 函数,跳过。

看看 ModuleJarLoader 类的 load 函数:

ModuleJarLoader(final File moduleJarFile,
                final Information.Mode mode) {
    this.moduleJarFile = moduleJarFile;
    this.mode = mode;
}

void load(final ModuleLoadCallback mCb) throws IOException {
    boolean hasModuleLoadedSuccessFlag = false;
    ModuleJarClassLoader moduleJarClassLoader = null;
    logger.info("prepare loading module-jar={};", moduleJarFile);
    try {
        moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);
        final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(moduleJarClassLoader);
        try {
            hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
        } finally {
            Thread.currentThread().setContextClassLoader(preTCL);
        }
    } finally {
        if (!hasModuleLoadedSuccessFlag
            && null != moduleJarClassLoader) {
            logger.warn("loading module-jar completed, but NONE module loaded, will be close ModuleJarClassLoader. module-jar={};", moduleJarFile);
            moduleJarClassLoader.closeIfPossible();
        }
    }
}

用自定义的 ModuleJarClassLoader 加载了模块 jar 并将其设置为线程类加载器,这样可以防止类冲突,然后调用了 loadingModules:

private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
                            final ModuleLoadCallback mCb) {
    final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>();
    final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);
    final Iterator<Module> moduleIt = moduleServiceLoader.iterator();
    while (moduleIt.hasNext()) {
        final Module module;
        try {
            module = moduleIt.next();
        } catch (Throwable cause) {
            logger.warn("...");
            continue;
        }
        final Class<?> classOfModule = module.getClass();
        // 判断模块是否实现了@Information标记
        if (!classOfModule.isAnnotationPresent(Information.class)) {
            logger.warn("...");
            continue;
        }
        final Information info = classOfModule.getAnnotation(Information.class);
        final String uniqueId = info.id();
        // 判断模块ID是否合法
        if (StringUtils.isBlank(uniqueId)) {
            logger.warn("...");
            continue;
        }

        // 判断模块要求的启动模式和容器的启动模式是否匹配
        if (!ArrayUtils.contains(info.mode(), mode)) {
            logger.warn("...");
            continue;
        }
        try {
            if (null != mCb) {
                mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);
            }
        } catch (Throwable cause) {
            logger.warn("...");
            continue;
        }
        loadedModuleUniqueIds.add(uniqueId);
    }
    ...
    return !loadedModuleUniqueIds.isEmpty();
}

从 jar 中并遍历读取所有实现了 Module 接口的类(也就是我们编写的模块代码),检查是否定义了唯一的 ID 后调用 mCb.onLoad,mCb 实际上是一个 InnerModuleLoadCallback 对象:

/**
* 用户模块加载回调
*/
final private class InnerModuleLoadCallback implements ModuleJarLoader.ModuleLoadCallback {
    @Override
    public void onLoad(final String uniqueId,
        final Class moduleClass,
        final Module module,
        final File moduleJarFile,
        final ModuleJarClassLoader moduleClassLoader) throws Throwable {

        // 如果之前已经加载过了相同ID的模块,则放弃当前模块的加载
        if (loadedModuleBOMap.containsKey(uniqueId)) {
            final CoreModule existedCoreModule = get(uniqueId);
            logger.info("");
            return;
        }

        // 需要经过ModuleLoadingChain的过滤
        providerManager.loading(
            uniqueId,
            moduleClass,
            module,
            moduleJarFile,
            moduleClassLoader
        );

        // 之前没有加载过,这里进行加载
        logger.info("..."");

        // 这里进行真正的模块加载
        load(uniqueId, module, moduleJarFile, moduleClassLoader);
    }
}

ModuleLoadingChain 实际上是空的,看下一个 load 函数:

/**
* 加载并注册模块
* <p>1. 如果模块已经存在则返回已经加载过的模块</p>
* <p>2. 如果模块不存在,则进行常规加载</p>
* <p>3. 如果模块初始化失败,则抛出异常</p>
*
* @param uniqueId          模块ID
* @param module            模块对象
* @param moduleJarFile     模块所在JAR文件
* @param moduleClassLoader 负责加载模块的ClassLoader
* @throws ModuleException 加载模块失败
*/
private synchronized void load(final String uniqueId,
                                final Module module,
                                final File moduleJarFile,
                                final ModuleJarClassLoader moduleClassLoader) throws ModuleException {

    if (loadedModuleBOMap.containsKey(uniqueId)) {
        logger.debug("module already loaded. module={};", uniqueId);
        return;
    }

    logger.info("");

    // 初始化模块信息
    final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);

    // 注入@Resource资源
    injectResourceOnLoadIfNecessary(coreModule);

    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);

    // 设置为已经加载
    coreModule.markLoaded(true);

    // 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
    markActiveOnLoadIfNecessary(coreModule);

    // 注册到模块列表中
    loadedModuleBOMap.put(uniqueId, coreModule);

    // 通知生命周期,模块加载完成
    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);

}

看到注入 @Resource 资源的部分,先看 injectResourceOnLoadIfNecessary 函数:

private void injectResourceOnLoadIfNecessary(final CoreModule coreModule) throws ModuleException {
try {
    final Module module = coreModule.getModule();
    for (final Field resourceField : FieldUtils.getFieldsWithAnnotation(module.getClass(), Resource.class)) {
        final Class<?> fieldType = resourceField.getType();
        ...
        // ModuleEventWatcher对象注入
        else if (ModuleEventWatcher.class.isAssignableFrom(fieldType)) {
            final ModuleEventWatcher moduleEventWatcher = coreModule.append(
                new ReleaseResource<ModuleEventWatcher>(
                    SandboxProtector.instance.protectProxy(
                        ModuleEventWatcher.class,
                        new DefaultModuleEventWatcher(inst, classDataSource, coreModule, cfg.isEnableUnsafe(), cfg.getNamespace())
                )
                ) {
                    @Override
                    public void release() {
                        logger.info("release all SandboxClassFileTransformer for module={}", coreModule.getUniqueId());
                        final ModuleEventWatcher moduleEventWatcher = get();
                        if (null != moduleEventWatcher) {
                            for (final SandboxClassFileTransformer sandboxClassFileTransformer
                                    : new ArrayList<SandboxClassFileTransformer>(coreModule.getSandboxClassFileTransformers())) {
                                moduleEventWatcher.delete(sandboxClassFileTransformer.getWatchId());
                            }
                        }
                    }
                });

            writeField(
                resourceField,
                module,
                moduleEventWatcher,
                true
            );
        }
        ...
    }
}

只保留下关键的 ModuleEventWatcher对象注入部分。

遍历了有 @Resource 注释的成员,然后调用 coreModule.append 在模块下追加了新的资源,资源中还注册了一个 release 函数用于模块卸载,最后调用 writeField 将数据写入成员。

激活模块

资源注入完成之后,看 markActiveOnLoadIfNecessary 函数激活模块,来到 active 函数:

@Override
public synchronized void active(final CoreModule coreModule) throws ModuleException {

    // 如果模块已经被激活,则直接幂等返回
    if (coreModule.isActivated()) {
        logger.debug("module already activated. module={};", coreModule.getUniqueId());
        return;
    }

    logger.info("active module, module={};class={};module-jar={};",
        coreModule.getUniqueId(),
        coreModule.getModule().getClass().getName(),
        coreModule.getJarFile()
    );

    // 通知生命周期
    callAndFireModuleLifeCycle(coreModule, MODULE_ACTIVE);

    // 激活所有监听器
    for (final SandboxClassFileTransformer sandboxClassFileTransformer : coreModule.getSandboxClassFileTransformers()) {
        EventListenerHandler.getSingleton().active(
            sandboxClassFileTransformer.getListenerId(),
            sandboxClassFileTransformer.getEventListener(),
            sandboxClassFileTransformer.getEventTypeArray()
        );
    }

    // 标记模块为:已激活
    coreModule.markActivated(true);
}

可以看到,active 函数会将我们编写的模块中的所有 sandboxClassFileTransformer 加入单例模式的 EventListenerHandler 对象中,那么问题来了,我们一路上似乎没看到有添加 sandboxClassFileTransformer 的处理?

全局搜索一下,可以看到只有一个地方, DefaultModuleEventWatcher 类的 watch 函数中有这个操作。而调用 watch 的地方同样只有一个,EventWatchBuilder 类的 build 函数。一路往上推,onWatch(BuildingForBehavior)、onBehavior(BuildingForClass),我们会发现,这跟示例里面的代码顺序是一样的,而且示例中使用的 moduleEventWatcher 也就是 @Resource 注入的成员,即一个 DefaultModuleEventWatcher 对象。

我们再搜索 @Command 注释,会发现只有一个地方存在,ModuleHttpServlet 类的 matchingModuleMethod 函数:

// 查找@Command注解的方法
for (final Method method : MethodUtils.getMethodsListWithAnnotation(classOfModule, Command.class)) {
    final Command commandAnnotation = method.getAnnotation(Command.class);
    if (null == commandAnnotation) {
        continue;
    }
    // 兼容 value 是否以 / 开头的写法
    String cmd = appendSlash(commandAnnotation.value());
    final String pathOfCmd = "/" + uniqueId + cmd;
    if (StringUtils.equals(path, pathOfCmd)) {
        return method;
    }
}

想起一开始看到的 Jetty 服务器和启用模块时需要加上的 -d 参数,我们就能明白,前面分析的只是他的一部分处理过程,在 @Resource

注入完成后模块激活之前,应该还有一个与 Jetty 服务器通信以加载 SandboxClassFileTransformer 的操作,也就是文档中所说:

为了实现沙箱模块的动态热插拔,容器客户端和沙箱动态可插拔容器采用HTTP协议进行通讯,底层用Jetty8作为HTTP服务器。

找一找他在哪里,最后可以找到 sandbox.sh,首先是 -d 参数:

d)
    OP_DEBUG=1
    ARG_DEBUG=${OPTARG}
    ;;
...
# -d debug
if [[ -n ${OP_DEBUG} ]]; then
    sandbox_debug_curl "module/http/${ARG_DEBUG}"
    exit
fi

然后是 sandbox_debug_curl:

function sandbox_debug_curl() {
  #local host=$(echo "${SANDBOX_SERVER_NETWORK}" | awk -F ";" '{print $1}')
  #local port=$(echo "${SANDBOX_SERVER_NETWORK}" | awk -F ";" '{print $2}')
  local host=${SANDBOX_SERVER_NETWORK%;**}
  local port=${SANDBOX_SERVER_NETWORK#**;}
  if [[ "$host" == "0.0.0.0" ]]; then
    host="127.0.0.1"
  fi
  curl -N -s "http://${host}:${port}/sandbox/${TARGET_NAMESPACE}/${1}" ||
    exit_on_err 1 "target JVM ${TARGET_JVM_PID} lose response."
}

所以通信其实用的就是 curl。

插入事件

模块激活的流程搞清楚了,现在看看模块中定义的事件是怎么插入的。

先看生成 SandboxClassFileTransformer,一步步设置 Class 和 Behavior 然后来到 onWatch:

@Override
public EventWatcher onWatch(AdviceListener adviceListener) {
    eventTypeSet.add(BEFORE);
    eventTypeSet.add(RETURN);
    eventTypeSet.add(THROWS);
    eventTypeSet.add(IMMEDIATELY_RETURN);
    eventTypeSet.add(IMMEDIATELY_THROWS);
    return build(
        new AdviceAdapterListener(adviceListener),
        toProgressGroup(progresses),
        eventTypeSet.toArray(EMPTY)
    );
}

private EventWatcher build(final EventListener listener,
                           final Progress progress,
                           final Event.Type... eventTypes) {
    final int watchId = moduleEventWatcher.watch(
        toEventWatchCondition(),
        listener,
        progress,
        eventTypes
    );
    return new EventWatcher() {
        final List<Progress> progresses = new ArrayList<Progress>();
        @Override
        public int getWatchId() {
            return watchId;
        }
        @Override
        public IBuildingForUnWatching withProgress(Progress progress) {
            if (null != progress) {
                progresses.add(progress);
            }
            return this;
        }
        @Override
        public void onUnWatched() {
            moduleEventWatcher.delete(watchId, toProgressGroup(progresses));
        }
    };
}

最后来到 watch 函数中生成一个 SandboxClassFileTransformer 对象并添加到模块中:

// 给对应的模块追加ClassFileTransformer
final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer(
    watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace);

// 注册到CoreModule中
coreModule.getSandboxClassFileTransformers().add(sandClassFileTransformer);

//这里addTransformer后,接下来引起的类加载都会经过sandClassFileTransformer
inst.addTransformer(sandClassFileTransformer, true);

// 查找需要渲染的类集合
final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(matcher);
logger.info("watch={} in module={} found {} classes for watch(ing).",
    watchId,
    coreModule.getUniqueId(),
    waitingReTransformClasses.size()
);

可以看到,这里添加了 Agent 技术用到的 Transformer,每个监听器都有对应的一个 Transformer,所以后面插桩的时候可以根据监听器 ID 回调到相应的事件处理函数。后面还调用了 reTransformClasses,让 Spy 可以修改一些需要修改但是已加载的类。

再看添加监视器时的 EventListenerHandler.getSingleton().active:

/**
* 注册事件处理器
*
* @param listenerId 事件监听器ID
* @param listener   事件监听器
* @param eventTypes 监听事件集合
*/
public void active(final int listenerId,
                    final EventListener listener,
                    final Event.Type[] eventTypes) {
    mappingOfEventProcessor.put(listenerId, new EventProcessor(listenerId, listener, eventTypes));
    logger.info("");
}

放进 Map 里,其中最主要的 listener 是一个 AdviceAdapterListener 对象,里面有 before、afterThrowing 等等待被重写的函数。

激活事件

可以像我一样,从 mappingOfEventProcessor.get 开始回溯,也可以想到 Agent 的技术实现,直接找到 ClassTransformer,也就是 SandboxClassFileTransformer 类:

@Override
public byte[] transform(final ClassLoader loader,
                            final String internalClassName,
                            final Class<?> classBeingRedefined,
                            final ProtectionDomain protectionDomain,
                            final byte[] srcByteCodeArray) {

    SandboxProtector.instance.enterProtecting();
    try {
        // 这里过滤掉Sandbox所需要的类|来自SandboxClassLoader所加载的类|来自ModuleJarClassLoader加载的类
        // 防止ClassCircularityError的发生
        if (SandboxClassUtils.isComeFromSandboxFamily(internalClassName, loader)) {
        return null;
    }
    return _transform(
        loader,
        internalClassName,
        classBeingRedefined,
        srcByteCodeArray
    );
    } catch (Throwable cause) {
        logger.warn("");
        return null;
    } finally {
        SandboxProtector.instance.exitProtecting();
    }
}

下一步是 _transform:

private byte[] _transform(final ClassLoader loader,
                            final String internalClassName,
                            final Class<?> classBeingRedefined,
                            final byte[] srcByteCodeArray) {
    // 如果未开启unsafe开关,是不允许增强来自BootStrapClassLoader的类
    if (!isEnableUnsafe
            && null == loader) {
        logger.debug("transform ignore {}, class from bootstrap but unsafe.enable=false.", internalClassName);
        return null;
    }

    final ClassStructure classStructure = getClassStructure(loader, classBeingRedefined, srcByteCodeArray);
    final MatchingResult matchingResult = new UnsupportedMatcher(loader, isEnableUnsafe).and(matcher).matching(classStructure);
    final Set<String> behaviorSignCodes = matchingResult.getBehaviorSignCodes();

    // 如果一个行为都没匹配上也不用继续了
    if (!matchingResult.isMatched()) {
        logger.debug("transform ignore {}, no behaviors matched in loader={}", internalClassName, loader);
        return null;
    }

    // 开始进行类匹配
    try {
        final byte[] toByteCodeArray = new EventEnhancer().toByteCodeArray(
            loader,
            srcByteCodeArray,
            behaviorSignCodes,
            namespace,
            listenerId,
            eventTypeArray
        );
        if (srcByteCodeArray == toByteCodeArray) {
            logger.debug("transform ignore {}, nothing changed in loader={}", internalClassName, loader);
            return null;
        }

        // statistic affect
        affectStatistic.statisticAffect(loader, internalClassName, behaviorSignCodes);

        logger.info("transform {} finished, by module={} in loader={}", internalClassName, uniqueId, loader);
        return toByteCodeArray;
    } catch (Throwable cause) {
        logger.warn("transform {} failed, by module={} in loader={}", internalClassName, uniqueId, loader, cause);
        return null;
    }
}

matcher 是用 onClass、onBehavior 输入的类名函数名生成的一个过滤器,具体可以看看 toEventWatchCondition 函数。

然后是 toByteCodeArray:

@Override
public byte[] toByteCodeArray(final ClassLoader targetClassLoader,
                                final byte[] byteCodeArray,
                                final Set<String> signCodes,
                                final String namespace,
                                final int listenerId,
                                final Event.Type[] eventTypeArray) {
    // 返回增强后字节码
    final ClassReader cr = new ClassReader(byteCodeArray);
    final ClassWriter cw = createClassWriter(targetClassLoader, cr);
    final int targetClassLoaderObjectID = ObjectIDs.instance.identity(targetClassLoader);
    cr.accept(
        new EventWeaver(
            ASM7, cw, namespace, listenerId,
            targetClassLoaderObjectID,
            cr.getClassName(),
            signCodes,
            eventTypeArray
        ),
        EXPAND_FRAMES
    );
    return dumpClassIfNecessary(cr.getClassName(), cw.toByteArray());
}

这里就是用 ASM 实现增强字节码的地方了,参考文章,EventWeaver 就是继承 ClassVisitor 的打桩类,类中复写了 visitMethod(函数名 match 也是在这个函数中),这个函数会返回一个继承了 AdviceAdapter 类的 ReWriteMethod 对象,里面实现了 onMethodEnter 等流程控制方法,可以看到里面插入了调用 spyMethodOnBefore 等函数的字节码:

invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnBefore);

打完桩后,字节码执行到某个流程,就会触发 Spy 类中相应的函数,进而触发我们在模块中编写的事件函数了。


参考文章

OpenRASP 底层代码解读


Web Java RASP

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

SpringBoot Thymeleaf 模板注入
(WebLogic)Coherence 反序列化漏洞