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方法。也就是在加载的时候需要执行操作都会在该方法中进行实现。

SRC\META-INF\MANIFEST.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:out\Agent1-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+"\n"+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,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,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,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师傅文中有提到过一些需要注意点和考虑到的点。这个后面再去做实现。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 连接 连接池产生原因 连接池实现原理 小结 TEMPERANCE:Eat not to dullness;drink not to elevation.节制
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 一个优秀的工程师和一个普通的工程师的区别,不是满天飞的架构图,他的功底体现在所写的每一行代码上。-- 毕玄 1. 命名风格 【书摘】类名用 UpperCamelC
今天犯了个错:“接口变动,伤筋动骨,除非你确定只有你一个人在用”。哪怕只是throw了一个新的Exception。哈哈,这是我犯的错误。一、接口和抽象类类,即一个对象。先抽象类,就是抽象出类的基础部分,即抽象基类(抽象类)。官方定义让人费解,但是记忆方法是也不错的 —包含抽象方法的类叫做抽象类。接口
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket一、引子文件,作为常见的数据源。关于操作文件的字节流就是 —FileInputStream&amp;FileOutputStream。
作者:泥沙砖瓦浆木匠网站:http://blog.csdn.net/jeffli1993个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节。交流QQ群:【编程之美 365234583】http://qm.qq.com/cgi-bin/qm/qr?k=FhFAoaWwjP29_Aonqz
本文目录 线程与多线程 线程的运行与创建 线程的状态 1 线程与多线程 线程是什么? 线程(Thread)是一个对象(Object)。用来干什么?Java 线程(也称 JVM 线程)是 Java 进程内允许多个同时进行的任务。该进程内并发的任务成为线程(Thread),一个进程里至少一个线程。 Ja
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket在面向对象编程中,编程人员应该在意“资源”。比如?1String hello = &quot;hello&quot;; 在代码中,我们
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 这是泥瓦匠的第103篇原创 《程序兵法:Java String 源码的排序算法(一)》 文章工程:* JDK 1.8* 工程名:algorithm-core-le
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 一、父子类变量名相同会咋样? 有个小故事,今天群里面有个人问下面如图输出什么? 我回答:60。但这是错的,答案结果是 40 。我知错能改,然后说了下父子类变
作者:泥瓦匠 出处:https://www.bysocket.com/2021-10-26/mac-create-files-from-the-root-directory.html Mac 操作系统挺适合开发者进行写代码,最近碰到了一个问题,问题是如何在 macOS 根目录创建文件夹。不同的 ma
作者:李强强上一篇,泥瓦匠基础地讲了下Java I/O : Bit Operation 位运算。这一讲,泥瓦匠带你走进Java中的进制详解。一、引子在Java世界里,99%的工作都是处理这高层。那么二进制,字节码这些会在哪里用到呢?自问自答:在跨平台的时候,就凸显神功了。比如说文件读写,数据通信,还
1 线程中断 1.1 什么是线程中断? 线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,通过其他线程调用了该线程的 方法,使得该线程中断标志位属性改变。 深入思考下,线程中断不是去中断了线程,恰恰是用来通知该线程应该被中断了。具体是一个标志位属性,
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want需求 项目在设计表的时候,要处理并发多的一些数据,类似订单号不能重复,要保持唯一。原本以为来个时间戳,精确到毫秒应该不错了。后来觉得是错了,测试环境下很多一
纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 加微信:bysocket01
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want.文章Points:1、介绍RESTful架构风格2、Spring配置CXF3、三层初设计,实现WebService接口层4、撰写HTTPClient 客户
Writer :BYSocket(泥沙砖瓦浆木匠)什么是回调?今天傻傻地截了张图问了下,然后被陈大牛回答道“就一个回调…”。此时千万个草泥马飞奔而过(逃哈哈,看着源码,享受着这种回调在代码上的作用,真是美哉。不妨总结总结。一、什么是回调回调,回调。要先有调用,才有调用者和被调用者之间的回调。所以在百
Writer :BYSocket(泥沙砖瓦浆木匠)一、什么大小端?大小端在计算机业界,Endian表示数据在存储器中的存放顺序。百度百科如下叙述之:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加
What is a programming language? Before introducing compilation and decompilation, let&#39;s briefly introduce the Programming Language. Programming la
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket泥瓦匠喜欢Java,文章总是扯扯Java。 I/O 基础,就是二进制,也就是Bit。一、Bit与二进制什么是Bit(位)呢?位是CPU
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocket一、前言 泥瓦匠最近被项目搞的天昏地暗。发现有些要给自己一些目标,关于技术的目标:专注很重要。专注Java 基础 + H5(学习) 其他操作系统,算法,数据结构当成课外书博览。有时候,就是那样你越是专注方面越