本文最后更新于:几秒前
agent相关官方文档:https://docs.oracle.com/javase/10/docs/api/java/lang/instrument/package-summary.html
最早接触agent技术可以回溯到去年拟态的一道java沙箱的题目,用的阿里云的jvm-sandbox,通过premain的方式启动agent,在jvm底层对关键代码进行hook,从而能阻止命令执行等危险行为
那么这个javaagent到底是何方神圣?
Java Agent可以类比于Spring中的拦截器,在进入main函数之前执行一些特定的操作,比如在类加载的阶段进行动态修改jvm字节码(premain),或者在程序运行的过程对类进行附加操作来增强类(agentmain)
它通过java的Instrumentation API接口来实现,一些可能需要依赖于热部署的技术像rasp或者是jvm性能监控等,在实际业务中都可以通过agent来实现。
举个大家更熟悉的例子:使用idea进行java的debug

premain
在程序启动前使用-javaagent:agent.jar参数来加载agent,在启动时,会先加载agent的类并执行premain方法
那怎么用这个东西呢?
我们先创建一个非常基础的maven项目:

一个premain agent应该满足什么条件,官方文档已经给出了答案
- 在mainfest的清单中包含agent的主类
- 实现public static premain方法
一个基础的premain agent类需要实现premain方法,premain()方法有两种实现方式,jvm会优先调用第一种实现方式的premain()方法

创建一个简单的agent示例:
1 2 3 4 5 6 7 8 9
| package com.potato;
import java.lang.instrument.Instrumentation;
public class PremainAgent { public static void premain(String args, Instrumentation instrumentation){ System.out.println("这个agent调用了"+args+"参数"); } }
|
随后在resources/META-INF下创建MANIFEST.MF
最主要的是Premain-Class这个选项,用于指定agent的入口类;Can-Redefine-Classes和Can-Retransform-Classes,这两个属性控制Agent是否能重新定义和转换类,redefine是替换类的定义,而retransform是通过转换器修改字节码,后面两者后续会介绍,简易使用中不做赘述,主要还是通过Instrumentation的方法来对已加载的类进行操作
1 2 3 4 5
| Manifest-Version: 1.0 Premain-Class: com.potato.PremainAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
|
然后在pom中指定MAINFEST.MF清单的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?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.potato</groupId> <artifactId>agent</artifactId> <version>1.0-SNAPSHOT</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-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> </plugins> </build> </project>
|
或者直接在pom中写入mainfest参数即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.potato.PremainAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
|
最后mvn clean package打包
我们写一个简单的主程序
1 2 3 4 5 6 7
| package com.potato;
public class Main { public static void main(String[] args) { System.out.println("main method called"); } }
|
给主程序添加一个jvm参数

成功触发agent

当然,premain方式的启动有一个比较大的缺陷,如下图

如果我们的premain抛出了异常,那么整个jvm将中止,也就意味着后续的主程序也无法启动
agentmain
在jdk1.6引入了另一种agent启动方式,agentmain
也就是我们前言提到的在主程序运行过程中,通过attach方式注入jvm中,动态修改jvm字节码的方式
agent的打包方式和premain的类似
下图是条件

实现agentmain()方法:
1 2 3 4 5 6 7 8 9
| package com.potato;
import java.lang.instrument.Instrumentation;
public class AgentmainAgent { public static void agentmain(String args, Instrumentation instrumentation){ System.out.println("调用了agentmain,参数为"+args); } }
|
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class>com.potato.AgentmainAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
|
随后需要介绍两个有关类:
这个类提供了一个API,下面的一些静态方法能让我们直接在运行时动态注入
(若是jdk8及以下,请手动添加tools.jar依赖)
1 2 3 4 5 6 7
| VirtualMachine.attach():静态方法。传入一个JVM的PID作为参数,连接到该JVM上。返回一个VirtualMachine对象
virtualMachine.loadAgent():非静态方法,向JVM注册一个agent
VirtualMachine.list():静态方法,获取当前所有JVM的列表,返回一个List<VirtualMachineDescriptor>列表
virtualMachine.detach():非静态方法,解除与当前JVM虚拟机的绑定
|
该类用于表示一个特定的虚拟机,如上面VirtualMachine.list()返回的List<VirtualMachineDescriptor>
列表中,每一个元素都是一个特定的虚拟机
注入
我们环境采用的是jdk1.8,所以这里需要引入一下tools.jar依赖
idea直接导入即可

要attach()到jvm,第一步便是获取当前进程的jvm的pid
一种方法是通过上面介绍的两种类来获取
1 2 3 4 5
| List<VirtualMachineDescriptor> vmList = VirtualMachine.list(); for(VirtualMachineDescriptor descriptor : list){ if(descriptor.displayName().contains("com.potato.Main")) System.out.println(descriptor.id()); }
|
当我们并不清楚当前进程相关的一些信息比如类名之类的,也可以使用下面的方式来获取:
1 2 3
| String jvmName = ManagementFactory.getRuntimeMXBean().getName(); String pid = jvmName.split("@")[0]; System.out.println(pid);
|
运行主程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.potato;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.lang.management.ManagementFactory; import java.util.List;
public class Main { public static void main(String[] args) throws Exception{ System.out.println("main method called"); String jvmName = ManagementFactory.getRuntimeMXBean().getName(); String pid = jvmName.split("@")[0]; VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent("D:\\Desktop\\AgentmainAgent\\target\\AgentmainAgent-1.0-SNAPSHOT.jar","test"); vm.detach(); } }
|

动态修改字节码
这里最主要的需要关注前文提到的Instrumentation
接口下定义的一些方法
一些相对常用的方法做了标注,自行按照参数规则验证功能即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public interface Instrumentation { void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
Class[] getAllLoadedClasses();
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
|
其中最重要的便是涉及到Transformer的几个方法了,但是注意到有一个重定义方法redifineClasses(),有必要花一点点时间解释一下重定义和重转化是什么意思,重定义即绕过jvm的所有转化器,直接将字节码注入到原来的类中;而重转化则是重新调用所有的jvm转化器,然后调用transform()方法去进行一个重转化
这一篇文章我们先起一个头,简单地写一个能hook exec()方法的超级超级简约的rasp吧
首先我们写一个类去实现ClassFileTransformer类,并重写他的transform()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.potato;
import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class BlockRuntimeTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
return null; }
}
|
然后写入一点点逻辑,结合前面学习过的javassist来进行动态注入字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| package com.potato;
import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain;
public class BlockRuntimeTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("java/lang/Runtime")) try {
return modifyExecMethod(classfileBuffer); }catch (Exception e){ e.printStackTrace(); } return null; }
private byte[] modifyExecMethod(byte[] classfileBuffer) throws Exception {
ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(Class.forName("java.lang.Runtime")));
CtClass ctClass = classPool.get(Runtime.class.getName());
CtMethod[] ctMethods = ctClass.getDeclaredMethods("exec"); for (CtMethod ctMethod:ctMethods){ ctMethod.setBody("{ throw new SecurityException(\"泥在做什么!\"); }"); }
byte[] bytecode = ctClass.toBytecode(); ctClass.detach();
return bytecode;
} }
|
随后注入
1 2 3 4 5 6 7 8 9 10
| package com.potato;
import java.lang.instrument.Instrumentation;
public class PremainAgent { public static void premain(String args, Instrumentation instrumentation){ instrumentation.addTransformer(new BlockRuntimeTransformer(),true); } }
|
但是我程序就这么一跑,诶!怎么还是弹窗呢~
想一想其实也比较好理解,使用premain方式启动的时候,已经完成了部分java基础类的加载,导致了Runtime错过了此次的transform()转化
如何解决呢?思考一下其实也比较简单,那就让他重加载一次呗~
1 2 3 4 5 6 7 8 9 10 11
| package com.potato;
import java.lang.instrument.Instrumentation;
public class PremainAgent { public static void premain(String args, Instrumentation instrumentation) throws Exception{ instrumentation.addTransformer(new BlockRuntimeTransformer(),true); instrumentation.retransformClasses(Runtime.class); } }
|
但是这样搓出来的”rasp”,还是让命令成功执行了
是哪里没做到位呢?

经过一番测试我发现代码压根就无法走到ClassPool classPool = ClassPool.getDefault();
这一行后面!
那么问题可能是出在了agent包本身
在询问ai的过程中突然想到,会不会是javassist的依赖压根没有加入进来!
好家伙还真是,,

那么罪魁祸首就出在这:
我们将maven-jar-plugin改为maven-assembly-plugin,让依赖随之打包

修改pom的build字段重新打包
(记得修改javaagent参数的值为附带依赖的jar包)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.3.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.potato.PremainAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix> </manifestEntries> </archive> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
|
再次运行,成功拦截!

emmmm,但是好像还是轻松被远程类加载给绕过了啊!

万事开头难~这一篇先到这里,下一篇我们再继续琢磨琢磨
(这里的问题十分戏剧性,可以马上衔接到下一篇)