实现一个简单的ja-netfilter插件-也许对某些插件有用,已被DCMA

前言

除了热佬和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依赖,并且配置了一些编译打包选项,
如果你想自己实现一个插件,可以拷贝并修改artifactIdBuilt-ByJANF-Plugin-EntryfinalName 等属性

实现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新手还是有一些难度的
源码 放到了github
https://github.com/novice88/plugin-privacy/tree/main

惊喜

使用了该插件后,发现原本一直无法使用paid feature的插件竟然可以用了
image

109 个赞

其实还有一些插件作者的。之前github仓库没被封的时候可以看到引用,现在看不到了。

26 个赞

:joy: ,我还真没注意到

5 个赞

好像还是会报There is no valid license in your account.

4 个赞

学习一下,感谢大佬的分享~

3 个赞

idea 的到期时间不要超过2026年

5 个赞

另外我只测试了最新版本

4 个赞

这几天接触了原理后一直想实现一个插件,确实一点头绪没有,仿写都没有能力,还是大佬们强啊

6 个赞

大佬,我用的是最新版本,然后激活码用的是jetbra.in里的rainbow激活码,到期时间没有超过2026年,是激活码的问题嘛

4 个赞

请问这个2026又是咋判断的啊?

5 个赞

可以供你参考,我是把大佬release 的jar包下载到热老的plugins-jetbrains,重启ide,再打开神秘插件的设置页就可以了

6 个赞

好的我去试试,谢谢哈

5 个赞

想请问下用的是哪里的激活码

5 个赞

idea和那个插件都是用的热老,原本是自己根据前序教程生成的许可证,但是许可时间超出了大佬说的2026,所以我又换回来了,您知道为什么不能2026么?

6 个赞

https://jetbra.noviceli.win/
这里可以生成激活码,自定义到期时间
需要和我之前在油猴脚本生成插件激活码一样需要配置一下

10 个赞

说到自定义,对了大佬,请问下您这个网站是什么轮子么?还是要自己仿写一个

6 个赞

你可以参考这个帖子,我是自己用rust写的

5 个赞

学习学习

4 个赞

大佬,我即使把时间自定义成2024,还是会显示There is no valid license in your account.想请问下是什么原因呢

4 个赞

谢谢大佬

6 个赞