在前面发现不少技术都会去采用Java Agent该技术去作实现,比分说RASP和内存马(其中一种方式)、包括IDEA的这些破解都是基于Java Agent去作实现。下面来领略该技术的微妙所在。html
在JDK1.5版本开始,Java增长了Instrumentation(Java Agent API)
和JVMTI(JVM Tool Interface)
功能,该功能能够实现JVM再加载某个class文件对其字节码进行修改,也能够对已经加载的字节码进行一个从新的加载。Java Agent能够去实现字节码插桩、动态跟踪分析等。java
启动Java程序的时候添加-javaagent(Instrumentation API实现方式)
或-agentpath/-agentlib(JVMTI的实现方式)
参数shell
在1.6版本新增了attach(附加方式)方式,能够对运行中的Java进程
插入Agent
apache
方式一中只能在启动前去指定须要加载的Agent文件,而方式二能够在Java程序运行后根据进程ID进行动态注入Agent到JVM里面去。编程
Java Agent是一个Java里面命令的参数该参数内容能够指定一个jar包,该jar包内容有必定的规范windows
上面说到的这个premain方法会在运行main方法前被调用,也就是说在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。那么其实Java agent本质上就是一个Java的类,可是普通的Java类是以main方法做为程序入口点,而Java Agent则将premain
(Agent模式)和agentmain
(Attach模式)做为了Agent程序的入口。api
若是须要修改已经被JVM加载过的类的字节码,那么还须要设置在MANIFEST.MF
中添加Can-Retransform-Classes: true
或Can-Redefine-Classes: true
。数组
先来看看命令参数tomcat
-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个重要的接口 Instrumentation
和 ClassFileTransformer
先来看看Instrumentation接口中的内容
来看到上图,这是java.lang.instrument.Instrumentation
中的一些方法。借鉴一下javasec里面的一张图,该图片描述了各类方法的一个做用
java.lang.instrument.Instrumentation
的做用是用来监测运行在JVM中的Java API,利用该类能够实现以下功能:
ClassFileTransformer
(addTransformer/removeTransformer
),JVM会在类加载时调用Agent中注册的ClassFileTransformer
;classpath
(appendToBootstrapClassLoaderSearch
、appendToSystemClassLoaderSearch
),将Agent程序添加到BootstrapClassLoader
和SystemClassLoaderSearch
(对应的是ClassLoader类的getSystemClassLoader方法
,默认是sun.misc.Launcher$AppClassLoader
)中搜索;JVM
已加载的类(getAllLoadedClasses
);getInitiatedClasses
)。redefineClasses
)。JNI
前缀(setNativeMethodPrefix
),能够实现Hook native方法。retransformClasses
)。这里已经代表各大实现功能所对应的方法了。
java.lang.instrument.ClassFileTransformer
是一个转换类文件的代理接口,咱们能够在获取到Instrumentation
对象后经过addTransformer
方法添加自定义类文件转换器。
示例中咱们使用了addTransformer
注册了一个咱们自定义的Transformer
到Java Agent
,当有新的类被JVM
加载时JVM
会自动回调用咱们自定义的Transformer
类的transform
方法,传入该类的transform
信息(类名、类加载器、类字节码
等),咱们能够根据传入的类信息决定是否须要修改类字节码,修改完字节码后咱们将新的类字节码返回给JVM
,JVM
会验证类和相应的修改是否合法,若是符合类加载要求JVM
会加载咱们修改后的类字节码。
查看一下该接口
该接口中有只有一个transform方法,里面的参数内容对应的信息分别是:
ClassLoader loader 定义要转换的类加载器;若是是引导加载器,则为 null String className 加载的类名,如:java/lang/Runtime Class<?> classBeingRedefined 若是是被重定义或重转换触发,则为重定义或重转换的类;若是是类加载,则为 null ProtectionDomain protectionDomain 要定义或重定义的类的保护域 byte[] classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
重写transform
方法注意事项:
ClassLoader
若是是被Bootstrap ClassLoader(引导类加载器)
所加载那么loader
参数的值是空。ClassLoader
中能够正确的获取到,不然会报ClassNotFoundException
,好比修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)
时插入了咱们检测代码,那么咱们将必须保证FileInputStream
可以获取到咱们的检测代码类。JVM
类名的书写方式路径方式:java/lang/String
而不是咱们经常使用的类名方式:java.lang.String
。JVM
校验要求,若是没法验证类字节码会致使JVM
崩溃或者VerifyError(类验证错误)
。retransform
类(修改已被JVM
加载的类),修改后的类字节码不得新增方法
、修改方法参数
、类成员变量
。addTransformer
时若是没有传入retransform
参数(默认是false
)就算MANIFEST.MF
中配置了Can-Redefine-Classes: true
并且手动调用了retransformClasses
方法也同样没法retransform
。transform
时须要使用建立时的Instrumentation
实例。上面说的都是一些概念性的问题,如今去作一个Java agent的实现
来看一下实现的大体几个步骤
Premain-Class
类,而且里面包含premain
方法,方法逻辑由用户本身肯定premain
和 MANIFEST.MF
文件打包成一个jar包-javaagent: jar
参数包路径 启动要代理的方法。完成以上步骤后,启动程序的时候会去执行premain
方法,固然这个确定是优先于main方法执行的。可是难免会有一些系统类优先于javaagent进行执行。可是用户类这些确定是会被javaagent给拦截下来的。这么这时候拦截下来后就能够进行一个重写类等操做,例如使用ASM、javassist,cglib等等来改写实现类。在实现里面须要去些2个项目,一个是javaAgent的类,一个是须要JavaAagent须要去代理的类。在mian方法执行前去执行的一些代码。
建立一个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.Date
的convertToAbbr
方法去作一个改写使用setBody
插入新的内容,而后转换成字节码进行返回。
前面是使用在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
里面有两个重要的类。
来查看一下该包中的内容,这里有两个比较重要的类,分别是VirtualMachine
和VirtualMachineDescriptor
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)
Attach模式须要知道咱们运行的Java程序进程ID,经过Java虚拟机的进程注入方式实现能够将咱们的Agent程序动态的注入到一个已在运行中的Java程序中。咱们也可使用自带的Jps -l
命令去查看。
看到第一个16320进程估计就是IDEA的破解插件,使用的Java agent技术进行一个实现破解。
attach实现动态注入的原理以下:
VirtualMachine类的attach(pid)
方法,即可以attach到一个运行中的java进程上,以后即可以经过loadAgent(agentJarPath)
来将agent的jar包注入到对应的进程,而后对应的进程会调用agentmain方法。
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了。
编辑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(); } } } }
执行结果:
instrumentation.redefineClasses
,让JVM从新该Java类,这样咱们就可使用Agent机制修改该类的字节码了。下面拿一个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/
在中途中会遇到不少坑,好比tools.jar的jar包在windows下找不到,须要手工去Java jdk的lib目录下而后将该包手工进行添加进去。学习就是一个排坑的过程。假设用Java agent 须要在反序列化或者是直接打入内存马该怎么去实现?其实y4er师傅文中有提到过一些须要注意点和考虑到的点。这个后面再去作实现。