前言 久闻 OpenRASP 大名,对其 Java 防护方式好奇,所以研究一下 RASP,看看它是怎么实现命令执行拦截的。
尝试 尝试用 javassist 修改 Runtime 类,结果发现主要会有两个问题:
类加载器无法覆盖启动类加载器加载的类
同个 Class 不能在同个类加载器中重复加载,要修改一个类就需要创建一个新的类加载器
不会写,只能先看看别人的实现了。
相关知识 agent 参考文章1 ,参考文章2 ,简单来说就是一个独立于应用程序的代理程序,可以在主程序运行之前运行,并用于执行 hook 等操作。
第二个参数 Instrumentation 的相关信息可以看这篇参考文章 ,简单仿照 OpenRASP 写份代码看看它的机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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(); } }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 中要注意的两个配置:
1 2 <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 类:
1 2 3 if (inst.isModifiableClass(clazz) && clazz.getName().equals("java.net.URLDecoder" )) { inst.retransformClasses(clazz); }
可以看到 URLDecoder 重新进入了 transform,接下来尝试修改它的 decode 函数,打包的时候遇到了些问题,javassist 没有打包进来导致依赖缺失。使用 shade 插件打包时(shade 插件还可以通过 relocations 重命名防止冲突,shade 文档 )又会有清单属性缺失、依赖没有打包等问题。
最后换了个 2020 版本的 IDEA,用的是下面这种写法(清单属性也可以写在 shade 中)成功的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <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 > </configuration > <executions > <execution > <phase > package</phase > <goals > <goal > shade</goal > </goals > </execution > </executions > </plugin >
然后在 transform 函数用 javassist 修改 decode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @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 函数如下:
1 2 3 4 5 6 7 8 9 public static void premain (String agentArg, Instrumentation inst) { init(START_MODE_NORMAL, START_ACTION_INSTALL, inst); }
另一个入口 agentmain 也差不多:
1 2 3 4 5 6 7 8 9 public static void agentmain (String agentArg, Instrumentation inst) { init(Module.START_MODE_ATTACH, agentArg, inst); }
一种是在 JVM 启动时启动的 normal 模式,另一种则是 JVM 启动后附着进去的 attach 模式。
两种模式都会进入 init 函数:
1 2 3 4 5 6 7 8 9 10 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 添加到启动类加载器检索路径:
1 2 3 4 5 6 7 8 9 public static void addJarToBootstrap (Instrumentation inst) throws IOException { String localJarPath = getLocalJarPath(); inst.appendToBootstrapClassLoaderSearch(new JarFile (localJarPath)); }
这样一来,代理程序就可以修改启动类加载器加载的类了,比如 java.lang.Runtime。
ModuleLoader.load 会加载所有的 RASP 模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 用于构造模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 看看模块的构造方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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(); } ... } } ... }
构造完成之后的执行:
1 2 3 4 @Override public void start (String mode, Instrumentation inst) throws Throwable { module .start(mode, inst); }
可以看到模块的入口类来自配置文件中的 Rasp-Module-Class,我们看下 rasp-engine 模块的 pom.xml:
1 2 3 4 <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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override public void start (String mode, Instrumentation inst) throws Exception { ... try { Loader.load(); } catch (Exception e) { ... } if (!loadConfig()) { return ; } Agent.readVersion(); BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit); if (!JS.Initialize()) { return ; } CheckerManager.init(); initTransformer(inst); ... }
前面的加载 v8 JavaScript 引擎、读取配置、缓存 build 信息、初始化插件系统等步骤先跳过,我们看到模块的初始化函数 initTransformer:
1 2 3 4 5 6 7 8 9 private void initTransformer (Instrumentation inst) throws UnmodifiableClassException { transformer = new CustomClassTransformer (inst); transformer.retransform(); }
看看 CustomClassTransformer 类的构造函数:
1 2 3 4 5 public CustomClassTransformer (Instrumentation inst) { this .inst = inst; inst.addTransformer(this , true ); addAnnotationHook(); }
将 Instrumentation 保存起来,然后将该类添加为一个 ClassFileTransformer(设置第二个参数 canRetransform 为 true 来让代理程序可以修改已经加载的类,用于卸载或者 attach 时),后面就可以通过其 transform 函数来修改类的字节码。
添加 hook CustomClassTransformer 函数最后会调用 addAnnotationHook:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 进行添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 { inst.retransformClasses(clazz); } catch (Throwable t) { LogTool.error(ErrorType.HOOK_ERROR, "failed to retransform class " + clazz.getName() + ": " + t.getMessage(), t); } } } } }
isClassMatched 其实就是遍历 hooks 然后确定该类是否需要修改:
1 2 3 4 5 6 7 8 public boolean isClassMatched (String className) { for (final AbstractClassHook hook : getHooks()) { if (hook.isClassMatched(className)) { return true ; } } return serverDetector.isClassMatched(className); }
判断用的是该 hook 中的 isClassMatched 方法,以 DeserializationHook 类为例,其 isClassMatched 代码如下:
1 2 3 4 @Override public boolean isClassMatched (String className) { return "java/io/ObjectInputStream" .equals(className); }
意思就是 hook 住 java.io.ObjectInputStream 这个类。
如果该类不在 hooks 中,就会从 detectors 中查找,这一步其实就是检测一些常见的服务端框架,用的是 com.baidu.openrasp.detector 包下面的类,大概有这几种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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,所以执行的是父类的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 函数:
1 2 3 4 5 6 @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:
1 COMMAND("command" , new V8AttackChecker (), 1 << 1 )
具体干了什么,看不太懂,不知道用 v8 引擎对命令做了什么样的检测。
OpenRASP 的缺陷 在可以代码执行的情况下,可以直接关掉 OpenRASP 来执行命令。
启动 比较复杂的一个事件驱动框架,参考文章 ,相比 OpenRASP 更加细致,除了做安全防护,也可以做动态添加日志等需求。
实现方式同样使用的 agent 作为入口,然后将 Spy 类放入启动类加载器的检索路径:
1 2 3 4 5 inst.appendToBootstrapClassLoaderSearch(new JarFile (new File ( getSandboxSpyJarPath(home) )));
接着启动 Jetty 服务器和 JVM 沙盒:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 classOfProxyServer .getMethod("bind" , classOfConfigure, Instrumentation.class) .invoke(objectOfProxyServer, objectOfCoreConfigure, inst); 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 类的构造函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 类的构造函数如下:
1 2 3 4 5 public DefaultCoreLoadedClassDataSource (final Instrumentation inst, final boolean isEnableUnsafe) { this .inst = inst; this .isEnableUnsafe = isEnableUnsafe; }
将 Instrumentation 保存了起来,后面可能会用来修改字节码。然后看看 DefaultCoreModuleManager 类的构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 文件):
1 2 3 4 5 6 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 []{}); }
这两个配置在默认情况下的值可以看看官方的入门模块编写教程 :
1 2 SYSTEM_MODULE_LIB : /Users/vlinux /opt/sandbox /module USER_MODULE_LIB : ~/ .sandbox-module ;
应该分别是 JVM-Sandbox 安装目录和用户目录下的 .sandbox-module 目录,所以我们编写好的模块打包成 jar 放到这里就 ok 了。
初始化完成 coreModuleManager 后,会调用 init:
1 2 3 4 private void init () { doEarlyLoadSandboxClass(); SpyUtils.init(cfg.getNamespace()); }
前一个函数 doEarlyLoadSandboxClass 是加载沙盒内部类的,我们看后一个跟 Spy 有关的函数:
1 2 3 4 5 6 7 8 9 10 11 public synchronized static void init (final String namespace) { if (!Spy.isInit(namespace)) { Spy.init(namespace, EventListenerHandler.getSingleton()); } }public static void init (final String namespace, final SpyHandler spyHandler) { namespaceSpyHandlerMap.putIfAbsent(namespace, spyHandler); }
将一个看名字就很重要的单例模式类 EventListenerHandler 放进了 Map 中,具体有什么用要后面在看了。
回到前面的 bind 函数,初始化完 Jetty 和沙盒之后,就开始加载模块了:
1 2 3 4 5 6 try { jvmSandbox.getCoreModuleManager().reset(); } catch (Throwable cause) { logger.warn("reset occur error when initializing." , cause); }
加载模块 这是明显是调用了 coreModuleManager 的 reset 函数,不过我们仔细看其初始化方式,可以发现他其实是个 protectProxy 生成的代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override public synchronized CoreModuleManager reset () throws ModuleException { logger.info("resetting all loaded modules:{}" , loadedModuleBOMap.keySet()); unloadAll(); 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 函数进行加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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(); if (!classOfModule.isAnnotationPresent(Information.class)) { logger.warn("..." ); continue ; } final Information info = classOfModule.getAnnotation(Information.class); final String uniqueId = info.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 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 { if (loadedModuleBOMap.containsKey(uniqueId)) { final CoreModule existedCoreModule = get(uniqueId); logger.info("" ); return ; } providerManager.loading( uniqueId, moduleClass, module , moduleJarFile, moduleClassLoader ); logger.info("..." "); // 这里进行真正的模块加载 load(uniqueId, module, moduleJarFile, moduleClassLoader); } }
ModuleLoadingChain 实际上是空的,看下一个 load 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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 ); injectResourceOnLoadIfNecessary(coreModule); callAndFireModuleLifeCycle(coreModule, MODULE_LOAD); coreModule.markLoaded(true ); markActiveOnLoadIfNecessary(coreModule); loadedModuleBOMap.put(uniqueId, coreModule); callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED); }
看到注入 @Resource 资源的部分,先看 injectResourceOnLoadIfNecessary 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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(); ... 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 for (final Method method : MethodUtils.getMethodsListWithAnnotation(classOfModule, Command.class)) { final Command commandAnnotation = method.getAnnotation(Command.class); if (null == commandAnnotation) { continue ; } String cmd = appendSlash(commandAnnotation.value()); final String pathOfCmd = "/" + uniqueId + cmd; if (StringUtils.equals(path, pathOfCmd)) { return method; } }
想起一开始看到的 Jetty 服务器和启用模块时需要加上的 -d 参数,我们就能明白,前面分析的只是他的一部分处理过程,在 @Resource
注入完成后模块激活之前,应该还有一个与 Jetty 服务器通信以加载 SandboxClassFileTransformer 的操作,也就是文档中所说:
1 为了实现沙箱模块的动态热插拔,容器客户端和沙箱动态可插拔容器采用HTTP协议进行通讯,底层用Jetty8作为HTTP服务器。
找一找他在哪里,最后可以找到 sandbox.sh,首先是 -d 参数:
1 2 3 4 5 6 7 8 9 10 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:
1 2 3 4 5 6 7 8 9 10 11 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @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 对象并添加到模块中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer ( watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace); coreModule.getSandboxClassFileTransformers().add(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public byte [] transform(final ClassLoader loader, final String internalClassName, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte [] srcByteCodeArray) { SandboxProtector.instance.enterProtecting(); try { 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 private byte [] _transform(final ClassLoader loader, final String internalClassName, final Class<?> classBeingRedefined, final byte [] srcByteCodeArray) { 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 ; } 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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 等函数的字节码:
1 invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnBefore);
打完桩后,字节码执行到某个流程,就会触发 Spy 类中相应的函数,进而触发我们在模块中编写的事件函数了。
参考文章 OpenRASP 底层代码解读