解密IDEA激活技术,阿里JVM工具Arths核心技术《JAVA agent》高级面试必备

613 阅读7分钟

IDEA过期了

哇擦,IDEA激活码竟然又过期了!毕竟之前都是付费的。 默默的我又掏出了某宝,准备付费。

!!!!所有的店铺都倒闭了!一夜之间全跟小姨子跑了!!!

这个时候贴心的老铁发来了信息,江湖之中还有这玩意儿?

看见agent这个单词,我大概知道他是怎么玩的了。 我们应该支持正版软件,正义的人儿一定反手一个举报,而我觉得我应该反编译看下他干了啥。

看见这位作者包的取名我就知道,他一定不同凡响。

因为当中反编译的类很多,我就不细细的去看了,毕竟太浪费时间,也不利于大家学习知识。

java agent技术以及绕过验证思路

java agent技术有人又叫他探针技术,反正我没理解为啥叫探针,我的理解是,可以在类加载的时候对字节码进行一些操作。

正如另外一位兄弟说的,找到序列号验证的地方,对字节码插入一个return true不就绕过验证了吗。

哈哈,没错就是这样,绕过验证就这么简单。

java agent技术实践

那我们赶快来实践一下吧,agent负责在类加载的时候触发修改字节码的入口, 真正的修改字节码需要用其他的技术,比如ASM,javassist,我们这里就用好上手快的javassist。

一些说在前面的话

java agent是通过附加的形式,将正在运行或者启动的java程序进行拦截,在JVM加载class的时候触发入口。 agent本身是另外一个Jar包。

JVM是通过agent这个jar包下的META-INF/MANIFEST.MF文件判断是否有agent类需要拦截在class加载的前面。

比如下面这个示例。

正式开始

我们现在要完成一个这样的功能,有一个Test002的类,这是一个消费者和生产者的测试类,我们现在要在这个类的每一个方法的前面和后面都调用我们修改的代码。

  • 1.我们新建一个DiTingAgent类用来做入口,加载类的时候触发。
  • 2.建立一个ClassTransformer用来做字节码转换,使用javassist。
  • 3.建立一个Hook2的类用来做钩子,我们可以正常的在编辑器中写代码。
  • 4.在运行Test002的时候加上agent参数。

首先我们创建一个空白的MAVEN项目,在POM文件中引入javassist

        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.8.0.GA</version>
        </dependency>

然后我们新建一个DiTingAgent入口类,代码如下:

package com.hsmap.diting;

import java.lang.instrument.Instrumentation;

/**
 * @author GavinKing
 * @ClassName: ParamReplace
 * @Description:参数替换
 * @date 2020/5/12
 */
public class DiTingAgent {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("======开始agent=========");
        System.out.println(agentArgs);
        System.out.println("======ClassTransformer=========");
        inst.addTransformer(new ClassTransformer());

    }

    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     * 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     *
     * @param agentOps
     * @param inst
     */
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("====premain 方法执行");
        System.out.println(agentOps);
        inst.addTransformer(new ClassTransformer());
    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst)
     * 则会执行 premain(String agentOps)
     *
     * @param agentOps
     */
    public static void premain(String agentOps) {
        System.out.println("====premain方法执行2====");
        System.out.println(agentOps);
    }

    public static void main(String[] args) {

    }

}

再创建一个用于修改字节码的类。

package com.hsmap.diting;

import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;

/**
 * @author GavinKing
 * @ClassName: ClassTransformer
 * @Description:类重载
 * @date 2020/5/13
 */
public class ClassTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        byte[] transformed = null;
        if(className == null || !className.contains("Test002")){
            return classfileBuffer;
        }
        System.out.println("匹配成功====:" + className);
        CtClass cl = null;
        try {
            ClassPool pool = ClassPool.getDefault();
            cl = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
            if (!cl.isInterface()) {
                CtBehavior[] methods = cl.getDeclaredBehaviors();
                for (int i = 0; i < methods.length; i++) {
                    CtBehavior method = methods[i];
                    //针对SERVICE_METHOD加入钩子函数
                    System.out.println("service方法:" + method.getName() + "修改字节码,注入钩子函数");
                    method.insertBefore("com.hsmap.diting.Hook2.before();");
                    //使用完成清除
                    method.insertAfter("com.hsmap.diting.Hook2.after();");
                }
                transformed = cl.toBytecode();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (cl != null) {
                cl.detach();
            }
        }
        return transformed;
    }
}

再创建一个钩子类:Hook2

package com.hsmap.diting;

/**
 * @author GavinKing
 * @ClassName: Hook
 * @Description:钩子函数
 * @date 2020/5/13
 */
public class Hook2 {

    /***
     * 注入参数钩子函数
     * @param
     */
    public static void before() {
        System.out.println("====before====");
    }

    public static void after() {
        System.out.println("====after====");
    }

}

在src/java路径下建立文件:META-INF/MANIFEST.MF,写上内容:

Manifest-Version: 1.0
Premain-Class: com.hsmap.diting.DiTingAgent
Can-Redefine-Classes: true

这里稍微解释一下,agent这个技术MANIFEST.MF中有哪些参数:

Premain-Class :包含 premain 方法的类(类的全路径名)

Agent-Class :包含 agentmain 方法的类(类的全路径名)

Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)

Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)

Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)

Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

感兴趣的具体可以翻下源码的解析,有更多详细的内容。

这样我们的基本工作就完成了,接下来就是把我们的代码达成一个jar包了。因为我在测试的时候发现,直接使用idea打包依赖没有打包进去,找不到javassite类,所以使用maven的打包插件,在pom文件中加入打包插件。

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <!-- get all project dependencies -->
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <!-- MainClass in mainfest make a executable jar -->
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.hsmap.diting.DiTingAgent</Premain-Class>
                            <Agent-Class>com.hsmap.diting.DiTingAgent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <!--                            <Boot-Class-Path>C:/Users/Administrator/.m2/repository/javassist/javassist/3.8.0.GA/javassist-3.8.0.GA.jar</Boot-Class-Path>-->
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- bind to the packaging phase -->
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

使用Maven的package命令,就能正常打包了,此处我们得到了一个含依赖的JAR包diting-jar-with-dependencies.jar

在Test002这个程序,启动时加入-javaagent:xxxxx.jar,配置agent包的全路径,这个时候启动Test002就生效了。

整个过程就变成这样了:

我们看下成果:

所有的方法在启动的时候都注入了钩子函数,打完收工!!

这样大家可以YY更加丰富的应用场景了。

运行时挂载

上面的例子,是在java程序启动的时候进行挂载,那能不能在程序运行的时候进行挂载呢?就好像我们平常使用 Arthas(阿里开源的JVM工具)一样,选择进程编号,就能让程序进行挂载。

答案肯定是可以的,需要用到VirtualMachine这个类。看名字就知道是虚拟机,具体的使用可以看一下源码docs.oracle.com/javase/7/do…

简单的说下思路,我们需要通过控制台获取一个正在运行的JAVA进程ID,然后加载这个运行的虚拟机信息,再把agent.jar挂载上去,实现代码如下:

package com.hsmap.diting;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;
import java.util.Scanner;

public class Start {
    public static void main(String[] args) throws IOException {
        VirtualMachine virtualMachine = null;
        try {
            Scanner scanner = new Scanner(System.in);
            String pid = scanner.nextLine();
            virtualMachine = VirtualMachine.attach(pid);
            virtualMachine.loadAgent("/Users/wangboxun/dev/diting/target/diting-jar-with-dependencies.jar");
        } catch (AttachNotSupportedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (AgentLoadException e) {
            e.printStackTrace();
        } catch (AgentInitializationException e) {
            e.printStackTrace();
        } finally {
            if (null != virtualMachine) {
                virtualMachine.detach();
            }
        }

    }
}

去掉agent参数,运行Test002,在使用JPS -l 找他的 PID。

运行,Start类,输入PID

已经执行了agent方法,但是竟然没有对类进行字节码转换!

静静的思考下,agent是在class加载的时候进行拦截,我们运行了main方法,相当于Test002自然已经加载过了,所以不会对他进行字节码篡改,这个时候我们只好写一个静态内部类,睡眠20秒之后,再调静态内部类(调用时加载),这就能测试一下我们是否成功挂载了。

Test002代码如下:

package com.tt.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Test002 {
    static Integer SIZE = 10;

    /***
    * 静态内部类
    **/
    public static class Collection {
        List<String> list = new ArrayList<>();
        volatile int count = 0;

        public synchronized boolean put(String s) throws InterruptedException {
            Thread.sleep(3000);
            if (count >= SIZE) {
                //仓库满了停止生产
                System.out.println("满了暂停生产");
                return false;
            }
            count++;
            System.out.println(Thread.currentThread().getName() + "开始生产:" + s + "当前总数:" + count);
            list.add(s);
            return true;
        }

        public synchronized boolean pop() throws InterruptedException {
            Thread.sleep(3000);
            if (count < 1) {
                //消费失败,需要生产
                System.out.println("空了暂停消费");
                return false;
            }
            String s = list.get(0);
            System.out.println(Thread.currentThread().getName() + "开始消费:" + s + "当前总数:" + list.size());
            list.remove(0);
            count--;
            return true;
        }

        public int getCount() {
            return count;
        }
    }

    public static class Test000 {
        private static void sout() {
            System.out.println("静态内部类打印拉=======");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000);

        Collection list = new Collection();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                while (true) {
                    //没有了开始生产
                    String news = "资讯" + System.currentTimeMillis() + new Random().nextInt(1000);
                    while (true) {
                        try {
                            if (!list.put(news)) {
                                break;
                            }
                            ;
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        news = "资讯" + System.currentTimeMillis() + new Random().nextInt(1000);
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "生产线程" + i).start();
        }

        for (int i = 0; i <= 10; i++) {
            new Thread(() -> {
                while (true) {
                    while (true) {
                        try {
                            if (!list.pop()) {
                                break;
                            }
                            ;
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "消费线程" + i).start();
        }
    }
}

结果如下:

结束

至此我们已经可以使用java agent 技术 加上 javassist技术对class进行篡改,并且可以在启动时或者运行时挂载agent的jar包。

作者使用agent技术完成了真实生产场景的参数抽离开发,大概需求是一些系统里面的参数是写死的变量,但是为了实现系统的解耦,需要对写死的代码进行抽离,使得这些参数可以灵活的使用,大家可以开动脑瓜想想怎么解决。

另外arths这个工具,也是基于agent技术去做的,大家想想,为什么我们不能篡改已经加载过的类,而arths可以呢?是不是牵涉到 classloader,嘿嘿,再聊下去就又一堆东西了,大家可以多思考下,欢迎点赞留言讨论。