ja-netfilter 是一款 JavaAgent 框架,而 JavaAgent 是由 Java 提供一种动态代理机制,可以在 runtime 时修改 Class 字节码,从而实现代码注入操作。
ja-netfilter 对底层操作进行封装,提供插件系统(Plugin System)机制来向外部暴露简易的接口,使得注入操作更加的简单。
example 从 releases page 下载 ja-netfilter 最新的发布版本,解压该压缩包,其目录结构如下:
plugins:插件 jar 目录
config:插件配置目录
配置名称需要与插件 jar 名称相同
插件需要引入 ja-netfilter 作为依赖,然后实现其 PluginEntry 与 MyTransformer 接口,前者用于描述插件信息,后者则对字节码进行转换操作,换句话说,在这过程中可以对字节码进行注入。
.java 文件编译成 class 字节码后,通过 java 命令来运行,而该命令提供了一个 -javaagent 参数,用于在 Runtime 时对字节码进行操作。启动 ja-netfilter 需要添加 -javaagent:/absolute/path/to/ja-netfilter.jar 在命令参数后,完整的命令应是这样 java -javaagent:/absolute/path/to/ja-netfilter.jar -jar executable_jar_file.jar。
命令执行后,ja-netfilter 会随着虚拟机启动后进行调用,从 plugins 目录加载插件,根据插件的行为进行注入操作。
implement 在 pom.xml 的 maven-assembly-plugin 配置中,会添加如下的配置来限定 javaagent 入口类,根据如下配置,当 javaagent 启动时,会调用其 Launcher 类的 agentmain 方法。
JDK 文档指出,需在 manifest 文件指定 javaagent entry class,javaagent 会调用 entry class 的 agentmain 方法
see more details:https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html
1 2 3 4 5 6 7 8 9 <manifestEntries > <Built-By > neo</Built-By > <Premain-Class > com.janetfilter.core.Launcher</Premain-Class > <Agent-Class > com.janetfilter.core.Launcher</Agent-Class > <Main-Class > com.janetfilter.core.Launcher</Main-Class > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > <Can-Set-Native-Method-Prefix > true</Can-Set-Native-Method-Prefix > </manifestEntries >
看到 agentmain 方法,倘若开启了 debug mode,设置 debug 参数之后,随后就调用内部的 premain 方法。
1 2 3 4 5 6 7 public static void agentmain (String args, Instrumentation inst) { if (null == System.getProperty("janf.debug" )) { System.setProperty("janf.debug" , "1" ); } premain(args, inst, true ); }
premain 方法中,主要逻辑如下所示:
判断是否多次加载
打印 usage
获取到该 jar,用于 BootStrap Class Loader 进行搜索
创建 Environment 实例,初始化配置环境
调用 Initializer 的 init 方法进行后续初始化
1-5 都是一些前置操作,我们关注点应放在 init 方法上。
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 private static void premain (String args, Instrumentation inst, boolean attachMode) { if (loaded) { DebugInfo.warn("You have multiple `ja-netfilter` as javaagent." ); return ; } printUsage(); URI jarURI; try { loaded = true ; jarURI = WhereIsUtils.getJarURI(); } catch (Throwable e) { DebugInfo.error("Can not locate `ja-netfilter` jar file." , e); return ; } File agentFile = new File(jarURI.getPath()); try { inst.appendToBootstrapClassLoaderSearch(new JarFile(agentFile)); } catch (Throwable e) { DebugInfo.error("Can not access `ja-netfilter` jar file." , e); return ; } Initializer.init(inst, new Environment(agentFile, args, attachMode)); }
来到 init 方法,根据 environment 来创建 Dispatcher 实例,该 Dispatcher 设计运用了委托者模式,用于衔接 javaagent 与 ja-netfilter。随后创建 PluginManager 实例来加载 plugin 插件,并将 dispatcher 添加到 Instrumentation 中,因 dispatcher 实现了 ClassFileTransformer 接口,当进行 transformer 操作时候,会调用其 transform 方法。
之后根据 Dispatcher 的 getHookClassNames 来获取插件需要 HOOK 的类名,并遍历 Instrumentation 所有加载的类,如果类符合 HOOK 要求,那么调用其的 retransformClasses 方法进行 transformer 操作,最后来到 Dispatcher 的 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 public static void init (Instrumentation inst, Environment environment) { DebugInfo.useFile(environment.getLogsDir()); DebugInfo.info(environment.toString()); Dispatcher dispatcher = new Dispatcher(environment); new PluginManager(inst, dispatcher, environment).loadPlugins(); inst.addTransformer(dispatcher, true ); inst.setNativeMethodPrefix(dispatcher, environment.getNativePrefix()); Set<String> classSet = dispatcher.getHookClassNames(); for (Class<?> c : inst.getAllLoadedClasses()) { String name = c.getName(); if (!classSet.contains(name)) { continue ; } try { inst.retransformClasses(c); } catch (Throwable e) { DebugInfo.error("Retransform class failed: " + name, e); } } }
加载插件 PluginManager 的 loadPlugins 方法目的是为了从 plugins 目录下加载插件,首先获取到插件目录,之后加载目录下所有文件进行一个过滤操作,为了扫描出其中所有的 jar 包文件。
因加载插件是一个耗时操作,所以采用了线程池设计,每个 plugin file 会被包装成 PluginLoadTask 提交给线程 池,执行时会调用其 run 方法。当所有 task 都提交到线程池,会进行 shutdown 操作,随后等待线程池 30s 终止,倘若执行超时,则判定加载插件超时。
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 public void loadPlugins () { long startTime = System.currentTimeMillis(); File pluginsDirectory = environment.getPluginsDir(); if (!pluginsDirectory.exists() || !pluginsDirectory.isDirectory()) { return ; } File[] pluginFiles = pluginsDirectory.listFiles((d, n) -> n.endsWith(".jar" )); if (null == pluginFiles) { return ; } try { ExecutorService executorService = Executors.newCachedThreadPool(); for (File pluginFile : pluginFiles) { executorService.submit(new PluginLoadTask(pluginFile)); } executorService.shutdown(); if (!executorService.awaitTermination(30L , TimeUnit.SECONDS)) { throw new RuntimeException("Load plugin timeout" ); } DebugInfo.debug(String.format("============ All plugins loaded, %.2fs elapsed ============" , (System.currentTimeMillis() - startTime) / 1000D )); } catch (Throwable e) { DebugInfo.error("Load plugin failed" , e); } }
如上所述,加载插件逻辑会放在 PluginLoadTask 的 run 方法中。首先获取该 plugin jar 的 manifest 信息,从而获取 jar 的 entry class name,后续由 PluginClassLoader 来将 class name 加载成 class,如果该 entry class 未实现 PluginEntry 接口,说明该 jar 不是 plugin,则不需要后续操作。
获取插件的配置文件后,解析并用 PluginConfig 进行表示,然后调用 PluginEntry#init 对插件进行初始化,最后将 Plugin 的 MyTransformer 实现添加到 Dispatcher 中,从而完成插件加载化。
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 public void run () { try { if (pluginFile.getName().endsWith(environment.getDisabledPluginSuffix())) { DebugInfo.debug("Disabled plugin: " + pluginFile + ", ignored." ); return ; } JarFile jarFile = new JarFile(pluginFile); Manifest manifest = jarFile.getManifest(); String entryClass = manifest.getMainAttributes().getValue(ENTRY_NAME); if (StringUtils.isEmpty(entryClass)) { return ; } PluginClassLoader classLoader = new PluginClassLoader(jarFile); Class<?> klass = Class.forName(entryClass, false , classLoader); if (!Arrays.asList(klass.getInterfaces()).contains(PluginEntry.class)) { return ; } synchronized (inst) { inst.appendToBootstrapClassLoaderSearch(jarFile); } PluginEntry pluginEntry = (PluginEntry) Class.forName(entryClass).newInstance(); File configFile = new File(environment.getConfigDir(), pluginEntry.getName().toLowerCase() + ".conf" ); PluginConfig pluginConfig = new PluginConfig(configFile, ConfigParser.parse(configFile)); pluginEntry.init(environment, pluginConfig); dispatcher.addTransformers(pluginEntry.getTransformers()); DebugInfo.debug("Plugin loaded: {name=" + pluginEntry.getName() + ", version=" + pluginEntry.getVersion() + ", author=" + pluginEntry.getAuthor() + "}" ); } catch (Throwable e) { DebugInfo.error("Parse plugin info failed" , e); } }
执行插件 调用 Instrumentation#retransformClasses 后,会来到 Dispatcher#transform 方法,该方法会通过 classname 获取到该 class 所有的 MyTransformer,并循环遍历调用每个生命周期方法,最终得到 class 字节流进行返回即可。
要对 class 进行修改,需要借助 ASM,该库可以修改 class,并动态生成新的 class。
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 public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classFileBuffer) throws IllegalClassFormatException { do { if (null == className) { break ; } List<MyTransformer> transformers = transformerMap.get(className); if (null == transformers) { break ; } int order = 0 ; try { for (MyTransformer transformer : globalTransformers) { transformer.before(className, classFileBuffer); } for (MyTransformer transformer : globalTransformers) { classFileBuffer = transformer.preTransform(className, classFileBuffer, order++); } for (MyTransformer transformer : transformers) { classFileBuffer = transformer.transform(className, classFileBuffer, order++); } for (MyTransformer transformer : globalTransformers) { classFileBuffer = transformer.postTransform(className, classFileBuffer, order++); } for (MyTransformer transformer : globalTransformers) { transformer.after(className, classFileBuffer); } } catch (Throwable e) { DebugInfo.error("Transform class failed: " + className, e); } } while (false ); return classFileBuffer; }
reference