前言
久闻 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 类中相应的函数,进而触发我们在模块中编写的事件函数了。
参考文章
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!