前言
除了热佬和zhile,似乎没有人实现过其它插件,今天我就来实现一个。
目标
一个简单的janetfilter插件,功能是防止恶意插件读取我们的vmoptions文件,顺便增强下hideme
实现步骤
新建一个maven项目
pom文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.novitechie</groupId>
<artifactId>plugin-privacy</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
<compilerArgument>-XDignore.symbol.file</compilerArgument>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Built-By>novice.li</Built-By>
<JANF-Plugin-Entry>com.novitechie.PrivacyPlugin</JANF-Plugin-Entry>
</manifestEntries>
</archive>
<finalName>privacy</finalName>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.ja-netfilter</groupId>
<artifactId>ja-netfilter</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
上面的pom引入了ja-netfilter依赖,并且配置了一些编译打包选项,
如果你想自己实现一个插件,可以拷贝并修改artifactId
、Built-By
、JANF-Plugin-Entry
、finalName
等属性
实现PluginEntry
package com.novitechie;
import com.janetfilter.core.plugin.MyTransformer;
import com.janetfilter.core.plugin.PluginEntry;
import java.util.Arrays;
import java.util.List;
public class PrivacyPlugin implements PluginEntry {
@Override
public String getName() {
return "PRIVACY";
}
@Override
public String getAuthor() {
return "novice.li";
}
@Override
public List<MyTransformer> getTransformers() {
return Arrays.asList(new VMOptionsTransformer(),new PluginClassLoaderTransformer());
}
}
将该类的名称配置到pom.xml的JANF-Plugin-Entry属性中,目的是告诉ja-netfilter
编写Transformer
VMOptionsTransformer
Idea 读取vmoptions 文件路径的方法再 VMOptions.java的getUserOptionsFile方法
import com.janetfilter.core.plugin.MyTransformer;
import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
import jdk.internal.org.objectweb.asm.tree.*;
import static jdk.internal.org.objectweb.asm.Opcodes.*;
public class VMOptionsTransformer implements MyTransformer {
@Override
public String getHookClassName() {
return "com/intellij/diagnostic/VMOptions";
}
@Override
public byte[] transform(String className, byte[] classBytes, int order) throws Exception {
ClassReader reader = new ClassReader(classBytes);
ClassNode node = new ClassNode(ASM5);
reader.accept(node, 0);
for (MethodNode m : node.methods) {
if ("getUserOptionsFile".equals(m.name)) {
InsnList list = new InsnList();
list.add(new MethodInsnNode(INVOKESTATIC, "com/novitechie/StackTraceRule", "check", "()Z", false));
LabelNode labelNode = new LabelNode();
list.add(new JumpInsnNode(IFEQ,labelNode));
list.add(new InsnNode(ACONST_NULL));
list.add(new InsnNode(ARETURN));
list.add(labelNode);
m.instructions.insert(list);
}
}
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
node.accept(writer);
return writer.toByteArray();
}
}
上面的代码告诉ja-netfilter我们要修改VMOptions的字节码,在transform方法中,我们使用asm将一些字节码指令插入到方法执行的最开始,插入的字节码转成java类似如下
if (StackTraceRule.check()){
return null;
}
StackTraceRule 的代码如下,检测当前调用栈是否每个元素都有FileName,
某些插件使用混淆,就没有FileName
public class StackTraceRule {
public static boolean check() {
RuntimeException e = new RuntimeException();
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
if (stackTraceElement.getFileName() == null) {
return true;
}
}
return false;
}
}
PluginClassLoader Transformer
idea 加载 插件的class loader是PluginClassLoader ,我们修改这个类使得其无法加载到ja-netfilter的class
import com.janetfilter.core.plugin.MyTransformer;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.tree.*;
import static jdk.internal.org.objectweb.asm.Opcodes.*;
public class PluginClassLoaderTransformer implements MyTransformer {
@Override
public String getHookClassName() {
return "com/intellij/ide/plugins/cl/PluginClassLoader";
}
@Override
public byte[] transform(String className, byte[] classBytes, int order) throws Exception {
ClassReader reader = new ClassReader(classBytes);
ClassNode node = new ClassNode(ASM5);
reader.accept(node, 0);
for (MethodNode m : node.methods) {
if ("loadClass".equals(m.name)) {
InsnList list = new InsnList();
list.add(new VarInsnNode(ALOAD, 1));
list.add(new MethodInsnNode(INVOKESTATIC, "com/novitechie/LoadClassRule", "check", "(Ljava/lang/String;)V", false));
m.instructions.insert(list);
}
}
ClassWriter writer = new SafeClassWriter(null,null,ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
node.accept(writer);
return writer.toByteArray();
}
}
上面的代码将 LoadClassRule.check(className);
插入到了loadClass方法的最前面
LoadClassRule代码如下
public class LoadClassRule {
public static void check(String name) throws Exception {
if (name.startsWith("com.janetfilter")) {
throw new ClassNotFoundException(name);
}
}
}
如果className 是以com.janetfilter开头,则抛出异常
使用
mvn clean package
将产生的 privacy.jar
放到plugins-jetbrains目录即可
总结
编写插件需要了解字节码基础,和asm,对Java新手还是有一些难度的
源码放到了自建的git仓库中了,大家可以自己编译或者直接在release中下载已编译好的jar
惊喜
使用了该插件后,发现原本一直无法使用paid feature的插件竟然可以用了