• Java 安全之Java Agent


    Java 安全之Java Agent

    0x00 前言

    在前面发现很多技术都会去采用Java Agent该技术去做实现,比分说RASP和内存马(其中一种方式)、包括IDEA的这些破解都是基于Java Agent去做实现。下面来领略该技术的微妙所在。

    0x01 Java Agent 机制

    在JDK1.5版本开始,Java增加了Instrumentation(Java Agent API)JVMTI(JVM Tool Interface)功能,该功能可以实现JVM再加载某个class文件对其字节码进行修改,也可以对已经加载的字节码进行一个重新的加载。Java Agent可以去实现字节码插桩、动态跟踪分析等。

    Java Aget运行模式

    1. 启动Java程序的时候添加-javaagent(Instrumentation API实现方式)-agentpath/-agentlib(JVMTI的实现方式)参数

    2. 在1.6版本新增了attach(附加方式)方式,可以对运行中的Java进程插入Agent

    方式一中只能在启动前去指定需要加载的Agent文件,而方式二可以在Java程序运行后根据进程ID进行动态注入Agent到JVM里面去。

    0x02 Java Agent 概念

    Java Agent是一个Java里面命令的参数该参数内容可以指定一个jar包,该jar包内容有一定的规范

    1. jar包中的MANIFEST.MF 文件必须指定 Premain-Class 项
    2. Premain-Class 指定的那个类必须实现 premain() 方法

    上面说到的这个premain方法会在运行main方法前被调用,也就是说在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。那么其实Java agent本质上就是一个Java的类,但是普通的Java类是以main方法作为程序入口点,而Java Agent则将premain(Agent模式)和agentmain(Attach模式)作为了Agent程序的入口。

    如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: trueCan-Redefine-Classes: true

    先来看看命令参数

    命令参数:

    -agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
    	另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
    -agentpath:<pathname>[=<选项>]
    	按完整路径名加载本机代理库
    -javaagent:<jarpath>[=<选项>]
    	加载 Java 编程语言代理, 请参阅 java.lang.instrument
    

    上面说到的 java.lang.instrument 提供允许 Java 编程语言代理监测运行在 JVM 上的程序的服务。监测的机制是对方法的字节码的修改,在启动 JVM 时,通过指示代理类 及其代理选项 启动一个代理程序。

    该代理类必须实现公共的静态 premain 方法,该方法原理上类似于 main 应用程序入口点,并且premain 方法的前面也会有一定的要求,签名必须满足一下两种格式:

    public static void premain(String agentArgs, Instrumentation inst)
        
    public static void premain(String agentArgs)
    

    JVM会去优先加载带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中实现,可以来审计一下该代码

    例:

    public static void premain(String agentArgs, Instrumentation inst);
    

    参数详细说明:

    -javaagent:jarpath[=options]
    	jarpath 是指向代理程序 JAR 文件的路径。options 是代理选项。此开关可以在同一命令行上多次使用,从而创建多个代理程序。多个代	理程序可以使用同一 jarpath。代理 JAR 文件必须符合 JAR 文件规范。下面的清单属性是针对代理 JAR 文件定义的:
    Premain-Class
    	代理类。即包含 premain 方法的类。此属性是必需的,如果它不存在,JVM 将中止。注:这是类名,而不是文件名或路径。
    Boot-Class-Path
    	由引导类加载器搜索的路径列表。路径表示目录或库(在许多平台上通常作为 jar 或 zip 库被引用)。查找类的特定于平台的机制出现故障之后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件的语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。此属性是可选的。
    Can-Redefine-Classes
    	布尔值(true 或 false,与大小写无关)。能够重定义此代理所需的类。值如果不是 true,则被认为是 false。此属性是可选的,默认值为 false。
    代理 JAR 文件附加到类路径之后。
    

    在JDK里面有个rt.jar包中存在一个java.lang.instrument的包,这个包提供了Java运行时,动态修改系统中的Class类型的功能。但最关键的还是javaagent 。它可以在运行时重新接收外部请求,对class类型进行一个修改。

    这里面有2个重要的接口 InstrumentationClassFileTransformer

    Instrumentation接口

    先来看看Instrumentation接口中的内容

    来看到上图,这是java.lang.instrument.Instrumentation中的一些方法。借鉴一下javasec里面的一张图,该图片描述了各种方法的一个作用

    java.lang.instrument.Instrumentation的作用是用来监测运行在JVM中的Java API,利用该类可以实现如下功能:

    1. 动态添加或移除自定义的ClassFileTransformeraddTransformer/removeTransformer),JVM会在类加载时调用Agent中注册的ClassFileTransformer
    2. 动态修改classpathappendToBootstrapClassLoaderSearchappendToSystemClassLoaderSearch),将Agent程序添加到BootstrapClassLoaderSystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索;
    3. 动态获取所有JVM已加载的类(getAllLoadedClasses);
    4. 动态获取某个类加载器已实例化的所有类(getInitiatedClasses)。
    5. 重定义某个已加载的类的字节码(redefineClasses)。
    6. 动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法。
    7. 重新加载某个已经被JVM加载过的类字节码retransformClasses)。

    这里已经表明各大实现功能所对应的方法了。

    ClassFileTransformer接口

    java.lang.instrument.ClassFileTransformer是一个转换类文件的代理接口,我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

    示例中我们使用了addTransformer注册了一个我们自定义的TransformerJava Agent,当有新的类被JVM加载时JVM会自动回调用我们自定义的Transformer类的transform方法,传入该类的transform信息(类名、类加载器、类字节码等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给JVMJVM会验证类和相应的修改是否合法,如果符合类加载要求JVM会加载我们修改后的类字节码。

    查看一下该接口

    该接口中有只有一个transform方法,里面的参数内容对应的信息分别是:

    ClassLoader loader              	定义要转换的类加载器;如果是引导加载器,则为 null
    String   className           		加载的类名,如:java/lang/Runtime
    Class<?> classBeingRedefined 		如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
    ProtectionDomain protectionDomain   要定义或重定义的类的保护域
    byte[]  classfileBuffer     		类文件格式的输入字节缓冲区(不得修改)
    

    重写transform方法注意事项:

    1. ClassLoader如果是被Bootstrap ClassLoader(引导类加载器)所加载那么loader参数的值是空。
    2. 修改类字节码时需要特别注意插入的代码在对应的ClassLoader中可以正确的获取到,否则会报ClassNotFoundException,比如修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)时插入了我们检测代码,那么我们将必须保证FileInputStream能够获取到我们的检测代码类。
    3. JVM类名的书写方式路径方式:java/lang/String而不是我们常用的类名方式:java.lang.String
    4. 类字节必须符合JVM校验要求,如果无法验证类字节码会导致JVM崩溃或者VerifyError(类验证错误)
    5. 如果修改的是retransform类(修改已被JVM加载的类),修改后的类字节码不得新增方法修改方法参数类成员变量
    6. addTransformer时如果没有传入retransform参数(默认是false)就算MANIFEST.MF中配置了Can-Redefine-Classes: true而且手动调用了retransformClasses方法也一样无法retransform
    7. 卸载transform时需要使用创建时的Instrumentation实例。

    0x03 Java Agent 技术实现

    上面说的都是一些概念性的问题,现在去做一个Java agent的实现

    来看一下实现的大致几个步骤

    1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
    2. 创建指定的Premain-Class类,并且里面包含premain 方法,方法逻辑由用户自己确定
    3. premain MANIFEST.MF文件打包成一个jar包
    4. 使用 -javaagent: jar参数包路径 启动要代理的方法。

    完成以上步骤后,启动程序的时候会去执行premain 方法,当然这个肯定是优先于main方法执行的。但是不免会有一些系统类优先于javaagent进行执行。但是用户类这些肯定是会被javaagent给拦截下来的。这么这时候拦截下来后就可以进行一个重写类等操作,例如使用ASM、javassist,cglib等等来改写实现类。在实现里面需要去些2个项目,一个是javaAgent的类,一个是需要JavaAagent需要去代理的类。在mian方法执行前去执行的一些代码。

    JVM运行前运行

    创建一个Agent类,里面需要包含premain方法:

    package com.nice0e3;
    
    import java.lang.instrument.Instrumentation;
    
    public class Agent {
        public static void premain(String agentArgs, Instrumentation inst){
            System.out.println("agentArgs"+agentArgs);
            inst.addTransformer(new DefineTransformer(),true);//调用addTransformer添加一个Transformer
        }
    }
    

    DefineTransformer类:

    package com.nice0e3;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    
    public class DefineTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load class"+className); //打印加载的类
            return new byte[0];
        }
    }
    
    

    这里需要重写transform方法。也就是在加载的时候需要执行操作都会在该方法中进行实现。

    SRCMETA-INFMANIFEST.MF文件中添加内容:

    Manifest-Version: 1.0
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Premain-Class: com.nice0e3.Agent
    
    

    我这里用的是maven去做一个配置

    pom.xml:

      <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.1.0</version>
                    <configuration>
                        <archive>
                            <!--自动添加META-INF/MANIFEST.MF -->
                            <manifest>
                                <addClasspath>true</addClasspath>
                            </manifest>
                            <manifestEntries>
                                <Premain-Class>com.nice0e3.Agent</Premain-Class>
                                <Agent-Class>com.nice0e3.Agent</Agent-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            </manifestEntries>
                        </archive>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>6</source>
                        <target>6</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    

    编译成jar包后,再建立一个项目,配置加入-javaagent参数,-javaagent:outAgent1-1.0-SNAPSHOT.jar后面不能有多余的空格。

    编写一个main方法

    package com.test;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    public class test {
    
    
        public static void main(String[] args) throws IOException {
            System.out.println("main");
    
    
        }
    
    }
    
    

    这里可以看到打印了JVM加载的所有类。而main这个字符再Shutdown之前被打印了,最后面才去加载Shutdown这个也是比较重要的一个点,但是在这里不做赘述。

    前面说过transform方法,也就是在加载的时候需要执行其他的操作都会在该方法中进行实现。这是因为ClassFileTransformer中会去拦截系统类和自己实现的类对象,如果需要对某个类进行改写,就可以在拦截的时候抓住这个类使用字节码编译工具去实现。

    小案例

    这里来复制一个小案例

    import javassist.*;
    
    import java.io.IOException;
    import java.lang.instrument.ClassFileTransformer;
    import java.security.ProtectionDomain;
    
    /**
     * @author rickiyang
     * @date 2019-08-06
     * @Desc
     */
    public class MyClassTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
            // 操作Date类
            if ("java/util/Date".equals(className)) {
                try {
                    // 从ClassPool获得CtClass对象
                    final ClassPool classPool = ClassPool.getDefault();
                    final CtClass clazz = classPool.get("java.util.Date");
                    CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
                    //这里对 java.util.Date.convertToAbbr() 方法进行了改写,在 return之前增加了一个 打印操作
                    String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
                            "sb.append(name.charAt(1)).append(name.charAt(2));" +
                            "System.out.println("sb.toString()");" +
                            "return sb;}";
                    convertToAbbr.setBody(methodBody);
    
                    // 返回字节码,并且detachCtClass对象
                    byte[] byteCode = clazz.toBytecode();
                    //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                    clazz.detach();
                    return byteCode;
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            // 如果返回null则字节码不会被修改
            return null;
        }
    }
    

    这里是使用javassist去动态创建一个类,并且对java.util.DateconvertToAbbr方法去做一个改写使用setBody插入新的内容,然后转换成字节码进行返回。

    JVM运行后运行

    前面是使用在main方法运行之前,执行Instrument。而在JDK1.6以后新增的agentmain方法,可以实现在main方法执行以后进行插入执行。

    该方法和前面的permain类似,需要定义一个agentmain方法的类。

    public static void agentmain (String agentArgs, Instrumentation inst)
    
    public static void agentmain (String agentArgs)
    

    这个也是和前面的一样,有Instrumentation类型参数的运行优先级也是会比没有该参数的高。

    在Java JDK6以后实现启动后加载Instrument的是Attach api。存在于com.sun.tools.attach里面有两个重要的类。

    来查看一下该包中的内容,这里有两个比较重要的类,分别是VirtualMachineVirtualMachineDescriptor

    VirtualMachine:

    VirtualMachine可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用

    Attach :从 JVM 上面解除一个代理等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

    loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。

    Detach:从 JVM 上面解除一个代理(agent)

    手动获取Java程序进程

    Attach模式需要知道我们运行的Java程序进程ID,通过Java虚拟机的进程注入方式实现可以将我们的Agent程序动态的注入到一个已在运行中的Java程序中。我们也可以使用自带的Jps -l命令去查看。

    看到第一个16320进程估计就是IDEA的破解插件,使用的Java agent技术进行一个实现破解。

    attach实现动态注入的原理如下:

    VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

    代码自动获取Java程序进程

    package com.nice0e3;
    
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    
    import java.util.List;
    
    public class test {
        public static void main(String[] args) {
            List<VirtualMachineDescriptor> list = VirtualMachine.list();
            for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
                System.out.println(virtualMachineDescriptor+"
    "+virtualMachineDescriptor.id());
            }
        }
    }
    
    

    有了进程ID后就可以使用Attach API注入Agent了。

    动态注入Agent代码实现

    编辑pom.xml文件

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>com.nice0e3.Agent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
    

    Agent类:

    package com.nice0e3;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.lang.instrument.Instrumentation;
    import java.security.ProtectionDomain;
    
    public class Agent {
        public static void agentmain(String agentArgs, Instrumentation instrumentation) {
            instrumentation.addTransformer(new DefineTransformer(), true);
    
        }
    }
    

    DefineTransformer类:

    package com.nice0e3;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    
    public class DefineTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load class"+className);
            return classfileBuffer;
        }
    }
    
    

    编译成jar包后,编写一个main方法来进行测试

    main方法类:

    package com.test;
    
    import com.sun.tools.attach.*;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.List;
    
    
    public class test {
    
    
        public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
    
            System.out.println("main running");
            List<VirtualMachineDescriptor> list = VirtualMachine.list();
            for (VirtualMachineDescriptor vir : list) {
                System.out.println(vir.displayName());//打印JVM加载类名
                if (vir.displayName().endsWith("com.test.test")){
                    VirtualMachine attach = VirtualMachine.attach(vir.id());   //attach注入一个jvm id注入进去
                    attach.loadAgent("out\Agent1-1.0-SNAPSHOT.jar");//加载agent
                    attach.detach();
    
                }
            }
    
        }
    }
    

    执行结果:

    Tips:

    1. 已加载的Java类是不会再被Agent处理的,这时候我们需要在Attach到目标进程后调用instrumentation.redefineClasses ,让JVM重新该Java类,这样我们就可以使用Agent机制修改该类的字节码了。
    2. premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
    3. 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
      1. 新类和老类的父类必须相同;
      2. 新类和老类实现的接口数也要相同,并且是相同的接口;
      3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
      4. 新类和老类新增或删除的方法必须是private static/final修饰的;
      5. 可以修改方法体。

    破解IDEA小案例

    下面拿一个Javasec的里面的案例来做一个测试,复制该代码

    package com.test;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Creator: yz
     * Date: 2020/10/29
     */
    public class CrackLicenseTest {
    
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        private static boolean checkExpiry(String expireDate) {
            try {
                Date date = DATE_FORMAT.parse(expireDate);
    
                // 检测当前系统时间早于License授权截至时间
                if (new Date().before(date)) {
                    return false;
                }
            } catch (ParseException e) {
                e.printStackTrace();
            }
    
            return true;
        }
    
        public static void main(String[] args) {
            // 设置一个已经过期的License时间
            final String expireDate = "2020-10-01 00:00:00";
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            String time = "[" + DATE_FORMAT.format(new Date()) + "] ";
    
                            // 检测license是否已经过期
                            if (checkExpiry(expireDate)) {
                                System.err.println(time + "您的授权已过期,请重新购买授权!");
                            } else {
                                System.out.println(time + "您的授权正常,截止时间为:" + expireDate);
                            }
    
                            // sleep 1秒
                            TimeUnit.SECONDS.sleep(5);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    
    }
    
    

    这里是模拟了一个IDEA的检测激活功能。

    执行如下

    现在需要的就是将这个检测的激活的CrackLicenseTest这个类给HOOK掉。

    下面来编写一下代码。

    package com.nice0e3;
    
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    import java.io.ByteArrayInputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.UnmodifiableClassException;
    import java.net.URL;
    import java.security.ProtectionDomain;
    import java.util.List;
    
    /**
     * Creator: yz
     * Date: 2020/1/2
     */
    public class CrackLicenseAgent {
    
        /**
         * 需要被Hook的类
         */
        private static final String HOOK_CLASS = "com.anbai.sec.agent.CrackLicenseTest";
    
        /**
         * Java Agent模式入口
         *
         * @param args 命令参数
         * @param inst Instrumentation
         */
        public static void premain(String args, final Instrumentation inst) {
            loadAgent(args, inst);
        }
    
        /**
         * Java Attach模式入口
         *
         * @param args 命令参数
         * @param inst Instrumentation
         */
        public static void agentmain(String args, final Instrumentation inst) {
            loadAgent(args, inst);
        }
    
        public static void main(String[] args) {
            if (args.length == 0) {
                List<VirtualMachineDescriptor> list = VirtualMachine.list();
    
                for (VirtualMachineDescriptor desc : list) {
                    System.out.println("进程ID:" + desc.id() + ",进程名称:" + desc.displayName());
                }
    
                return;
            }
    
            // Java进程ID
            String pid = args[0];
    
            try {
                // 注入到JVM虚拟机进程
                VirtualMachine vm = VirtualMachine.attach(pid);
    
                // 获取当前Agent的jar包路径
                URL agentURL = CrackLicenseAgent.class.getProtectionDomain().getCodeSource().getLocation();
                String agentPath = new File(agentURL.toURI()).getAbsolutePath();
    
                // 注入Agent到目标JVM
                vm.loadAgent(agentPath);
                vm.detach();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 加载Agent
         *
         * @param arg  命令参数
         * @param inst Instrumentation
         */
        private static void loadAgent(String arg, final Instrumentation inst) {
            // 创建ClassFileTransformer对象
            ClassFileTransformer classFileTransformer = createClassFileTransformer();
    
            // 添加自定义的Transformer,第二个参数true表示是否允许Agent Retransform,
            // 需配合MANIFEST.MF中的Can-Retransform-Classes: true配置
            inst.addTransformer(classFileTransformer, true);
    
            // 获取所有已经被JVM加载的类对象
            Class[] loadedClass = inst.getAllLoadedClasses();
    
            for (Class clazz : loadedClass) {
                String className = clazz.getName();
    
                if (inst.isModifiableClass(clazz)) {
                    // 使用Agent重新加载HelloWorld类的字节码
                    if (className.equals(HOOK_CLASS)) {
                        try {
                            inst.retransformClasses(clazz);
                        } catch (UnmodifiableClassException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
        private static ClassFileTransformer createClassFileTransformer() {
            return new ClassFileTransformer() {
    
                /**
                 * 类文件转换方法,重写transform方法可获取到待加载的类相关信息
                 *
                 * @param loader              定义要转换的类加载器;如果是引导加载器,则为 null
                 * @param className           类名,如:java/lang/Runtime
                 * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
                 * @param protectionDomain    要定义或重定义的类的保护域
                 * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)
                 * @return 字节码byte数组。
                 */
                @Override
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                        ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    
                    // 将目录路径替换成Java类名
                    className = className.replace("/", ".");
    
                    // 只处理com.anbai.sec.agent.CrackLicenseTest类的字节码
                    if (className.equals(HOOK_CLASS)) {
                        try {
                            ClassPool classPool = ClassPool.getDefault();
    
                            // 使用javassist将类二进制解析成CtClass对象
                            CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
    
                            // 使用CtClass对象获取checkExpiry方法,类似于Java反射机制的clazz.getDeclaredMethod(xxx)
                            CtMethod ctMethod = ctClass.getDeclaredMethod(
                                    "checkExpiry", new CtClass[]{classPool.getCtClass("java.lang.String")}
                            );
    
                            // 在checkExpiry方法执行前插入输出License到期时间代码
                            ctMethod.insertBefore("System.out.println("License到期时间:" + $1);");
    
                            // 修改checkExpiry方法的返回值,将授权过期改为未过期
                            ctMethod.insertAfter("return false;");
    
                            // 修改后的类字节码
                            classfileBuffer = ctClass.toBytecode();
                            File classFilePath = new File(new File(System.getProperty("user.dir"), "src\main\java\com\nice0e3\"), "CrackLicenseTest.class");
    
                            // 写入修改后的字节码到class文件
                            FileOutputStream fos = new FileOutputStream(classFilePath);
                            fos.write(classfileBuffer);
                            fos.flush();
                            fos.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
    
                    return classfileBuffer;
                }
            };
        }
    
    }
    

    这个不知道为啥自己做的时候没有成功,贴一张成功的图过来。

    补充一张原理图

    .

    参考文章

    https://www.cnblogs.com/rickiyang/p/11368932.html
    https://javasec.org/javase/JavaAgent/JavaAgent.html
    https://y4er.com/post/javaagent-tomcat-memshell/
    

    0x04 结尾

    在中途中会遇到很多坑,比如tools.jar的jar包在windows下找不到,需要手工去Java jdk的lib目录下然后将该包手工进行添加进去。学习就是一个排坑的过程。假设用Java agent 需要在反序列化或者是直接打入内存马该怎么去实现?其实y4er师傅文中有提到过一些需要注意点和考虑到的点。这个后面再去做实现。

  • 相关阅读:
    C#--web中上传图片与浏览
    win通过ssh访问virtualbox虚拟中的debian
    【转】win10中下载安装mysql5.7
    [转发]centos7利用crontab定时检测杀死cpu使用率超过80%的进程
    MySQL 重要参数 innodb_flush_log_at_trx_commit 和 sync_binlog
    查询正在执行的sql语句
    php图片等比例缩放
    excel 导入 sqlserver 字符串被截取为255长度解决方案
    查询阻塞的sql
    centos7安装mariadb10遇到的问题解决
  • 原文地址:https://www.cnblogs.com/nice0e3/p/14086165.html
Copyright © 2020-2023  润新知