在这个笔记开始之前,前面的内容大家可以上网去搜索,也可以关注公众号我发给,前面笔记不是特别的重要,是一些javaAgent的基础,例如JavaAgent如何打包,Agent Jar组成的3个部分。还有一点笔记有些是个人总结的,有些是参考别人的。
这里使用到的主要是 javassist 和 javaAgent的学习,如果想看内存马怎么使用JavaAgent查杀的,可以直接跳到最后两个案例。
那么你也可以使用ASM,只是ASM的复杂程度需要和Class字节码指令去打交道,所以相对来说还是比较难的。
好了不说废话了,上笔记。
并不是所有的虚拟机 都支持command line(命令行)启动Java Agent。
-javaagent:jarpath[=options]┌─── -javaagent:jarpath┌─── Command-Line ───┤│ └─── -javaagent:jarpath=optionsLoad-Time Instrumentation ───┤│ ┌─── MANIFEST.MF - Premain-Class: lsieun.agent.LoadTimeAgent└─── Agent Jar ──────┤└─── Agent Class - premain(String agentArgs, Instrumentation inst)
例如:
java -cp ./target/classes/ -javaagent:./target/TheAgent.jar=this-is-a-long-message sample.Program在Agent Jar中,根据META-INF/MANIFEST.MF文件中定义的Premain-Class属性来找到Agent Class。
例如我定义的是AgentMain,他就会去找AgentMain这个类以及找到他的premain方法。
Premain-Class: relaysec.agent.AgentMain
public class LoadTimeAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// ...
}
}那么这里命令行启动指定的options就是我们的agentArgs的值。
例如: 这里的this-is-a-long-message就是agentArgs的值。
java -cp ./target/classes/ -javaagent:./target/TheAgent.jar=this-is-a-long-message sample.Program如下图,这里进行打印输出agentArgs参数,我们使用命令行进行运行,注意这里传输参数的时候不能存在空格。
可以看到这里打印出了agentArgs参数。
我们传入的信息,一般情况下是以key-value的形式,有人喜欢用;分割,有人喜欢用=分割。
例如:
username:admin,password:123456
username=root,password:123456LoadTimeAgent.java
如下代码就是对agentArgs进行了解析,当我们拿到agentArgs参数之后,然后通过循环进行一个一个取出。
package lsieun.agent;import java.lang.instrument.Instrumentation;public class LoadTimeAgent {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Premain-Class: " + LoadTimeAgent.class.getName());System.out.println("agentArgs: " + agentArgs);System.out.println("Instrumentation Class: " + inst.getClass().getName());if (agentArgs != null) {String[] array = agentArgs.split(",");int length = array.length;for (int i = 0; i < length; i++) {String item = array[i];String[] key_value_pair = getKeyValuePair(item);String key = key_value_pair[0];String value = key_value_pair[1];String line = String.format("|%03d| %s: %s", i, key, value);System.out.println(line);}}}private static String[] getKeyValuePair(String str) {{int index = str.indexOf("=");if (index != -1) {return str.split("=", 2);}}{int index = str.indexOf(":");if (index != -1) {return str.split(":", 2);}}return new String[]{str, ""};}}
紧接着运行使用 : 分割。
可以看到很清楚的解析出了agentArgs的参数。
使用 = 进行分割,可以看到也是没有任何问题的。
第一点,在命令行启动 Java Agent,需要使用 -javaagent:jarpath[=options] 选项,其中的 options 信息会转换成为 premain 方法的 agentArgs 参数。
第二点,对于 agentArgs 参数的进一步解析,需要由我们自己来完成。
Instrumentation是一个接口,那么它的实现类是那个?它的实现类是InstrumentationImpl
是谁调用了PreMainTraceAgent的premain方法呢?Instrumentation调用的premain
测试代码:
public static void premain(String agentArgs, Instrumentation _inst){
System.out.println("agentArgs:" + agentArgs);
System.out.println("Instrumentation Class" + _inst.getClass().getName());
Exception ex = new Exception("Exception from PreMainTraceAgent1");
ex.printStackTrace(System.out);
_inst.addTransformer(new DefineTransformer());
}结果输出:
可以看到他的实现类是InstrumentationImpl,并且通过反射进行调用premain方法,这里通过了InstrumentationImpl调用了premain方法。
public class InstrumentationImpl implements Instrumentation {}在 sun.instrument.InstrumentationImpl 类当中,loadClassAndCallPremain 方法的实现非常简单,它直接调用了 loadClassAndStartAgent 方法:这个方法针对于premain方法调用的。
public class InstrumentationImpl implements Instrumentation {
private void loadClassAndCallPremain(String classname, String optionsString) throws Throwable {
loadClassAndStartAgent(classname, "premain", optionsString);
}
}这个方法针对于agentmain方法进行调用的。
public class InstrumentationImpl implements Instrumentation {
private void loadClassAndCallAgentmain(String classname, String optionsString) throws Throwable {
loadClassAndStartAgent(classname, "agentmain", optionsString);
}
}我们跟进去loadClassAndStartAgent方法。那么我们传进去的值就是premain对应的就是methodname这个字段,第一步,从自身的方法定义中,去寻找目标方法:先找带有两个参数的方法;如果没有找到,则找带有一个参数的方法。如果第一步没有找到,则进行第二步。第二步,从父类的方法定义中,去寻找目标方法:先找带有两个参数的方法;如果没有找到,则找带有一个参数的方法。
之后就会通过反射进行调用。
public class InstrumentationImpl implements Instrumentation {
// Attempt to load and start an agent
private void loadClassAndStartAgent(String classname, String methodname, String optionsString) throws Throwable {
ClassLoader mainAppLoader = ClassLoader.getSystemClassLoader();
Class<?> javaAgentClass = mainAppLoader.loadClass(classname);
Method m = null;
NoSuchMethodException firstExc = null;
boolean twoArgAgent = false;
// The agent class must have a premain or agentmain method that
// has 1 or 2 arguments. We check in the following order:
//
// 1) declared with a signature of (String, Instrumentation)
// 2) declared with a signature of (String)
// 3) inherited with a signature of (String, Instrumentation)
// 4) inherited with a signature of (String)
//
// So the declared version of either 1-arg or 2-arg always takes
// primary precedence over an inherited version. After that, the
// 2-arg version takes precedence over the 1-arg version.
//
// If no method is found then we throw the NoSuchMethodException
// from the first attempt so that the exception text indicates
// the lookup failed for the 2-arg method (same as JDK5.0).
try {
m = javaAgentClass.getDeclaredMethod(methodname,
new Class<?>[]{
String.class,
java.lang.instrument.Instrumentation.class
}
);
twoArgAgent = true;
} catch (NoSuchMethodException x) {
// remember the NoSuchMethodException
firstExc = x;
}
if (m == null) {
// now try the declared 1-arg method
try {
m = javaAgentClass.getDeclaredMethod(methodname, new Class<?>[]{String.class});
} catch (NoSuchMethodException x) {
// ignore this exception because we'll try
// two arg inheritance next
}
}
if (m == null) {
// now try the inherited 2-arg method
try {
m = javaAgentClass.getMethod(methodname,
new Class<?>[]{
String.class,
java.lang.instrument.Instrumentation.class
}
);
twoArgAgent = true;
} catch (NoSuchMethodException x) {
// ignore this exception because we'll try
// one arg inheritance next
}
}
if (m == null) {
// finally try the inherited 1-arg method
try {
m = javaAgentClass.getMethod(methodname, new Class<?>[]{String.class});
} catch (NoSuchMethodException x) {
// none of the methods exists so we throw the
// first NoSuchMethodException as per 5.0
throw firstExc;
}
}
// the premain method should not be required to be public,
// make it accessible so we can call it
// Note: The spec says the following:
// The agent class must implement a public static premain method...
setAccessible(m, true);
// invoke the 1 or 2-arg method
if (twoArgAgent) {
m.invoke(null, new Object[]{optionsString, this});
}
else {
m.invoke(null, new Object[]{optionsString});
}
// don't let others access a non-public premain method
setAccessible(m, false);
}
}第一点,在 premain 方法中,Instrumentation 接口的具体实现是 sun.instrument.InstrumentationImpl 类。
第二点,查看 Stack Trace,可以看到 sun.instrument.InstrumentationImpl.loadClassAndCallPremain 方法对 LoadTimeAgent.premain 方法进行了调用。
在进行Dynamic Instrumentation的时候,需要使用到Attach Api,它允许一个JVM连接到另外一个JVM。
Attach API是java 1.6引入的。
在java8版本 com.sun.tools.attach位于JDK_HOME/lib/tools.jar文件
在java9版本之后 com.sun.tools.attch包位于jdk.attach模块
在com.sun.tools.attach包中,包含如下的类。
这些类我们只需要关注VirtualMachine以及AttachProvider这两个类即可,其他的类都是一些异常类,还有一个类是VirtualMachineDescriptor,这个类就是对这几个字段(id,provider和display name的包装)。
1.与目标JVM建立socks链接,获取一个VirtualMachine对象,这里的目标指的是你要注入的那个类。
2.使用VirtualMachine对象,可以将Agent Jar加载到agent VM上,也可以从目标JVM中获取一些属性信息。
3.与目标JVM断开链接。
如下图:
首先建立链接,需要使用到VirtualMachine类的attach方法,这里有两个重载的方法,第一个方法接收一个id参数,也就是目标JVM的进程id,这里可以使用jps命令进行查看。第二个参数接收一个VirtualMachineDescriptor对象,在这个对象中可以获取到id,displayName相关的值。
建立链接之后,需要调用loadAgent方法,加载Agent Jar包到目标的JVM上,这里也有两个重载的方法,第一个loadAgentJar里面只有一个String类型的参数,表示Agent Jar包的路径,第二个里面有两个String类型的参数,第一个参数表示Jar包的路径,第二个参数表示agentmain方法的agentArgs参数,就跟我们上面介绍那个AgentAgrs参数是一样的。
紧接着可以通过getAgentProperties以及getSystemProperties方法获取目标JVM的相关属性信息。
最后通过detach方法与目标的JVM断开链接。
其他方法:
除了以上这些重要的方法之外还有一些其他的方法。
list方法,返回一组VirtualMachineDescriptor对象,返回的这组对象中表示所有潜在的目标对象,也就是说我们可以把可以连接的目标对象遍历出来,然后进行判断。
public static List<VirtualMachineDescriptor> list() {ArrayList var0 = new ArrayList();List var1 = AttachProvider.providers();Iterator var2 = var1.iterator();while(var2.hasNext()) {AttachProvider var3 = (AttachProvider)var2.next();var0.addAll(var3.listVirtualMachines());}return var0;}
provider方法,它返回一个AttachProvider对象。
这个类我们可以通过VirtualMachine类的list方法来获取到VirtualMachineDescriptor类。
这个类中主要有3个方法。
id这个方法返回的是目标JVM的ID。
public String id() {
return this.id;
}displayName方法这里返回的是目标JVM上面运行的类名,那么这里我们就可以进行判断是否是我们需要注入目标的那个类。
public String displayName() {
return this.displayName;
}public AttachProvider provider() {
return this.provider;
}这个类是一个抽象类,它需要一个具体的实现类。
在不同的JVM平台上,它对应的AttachProvider实现是不一样的。
例如:
Linux: sun.tools.attach.LinuxAttachProviderWindows: sun.tools.attach.WindowsAttachProvider 那么这里的代码我们应该都可以看的懂了。
import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;/*** @author rickiyang* @date 2019-08-16* @Desc*/public class TestAgentMain {public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {//获取当前系统中所有 运行中的 虚拟机System.out.println("running JVM start ");List<VirtualMachineDescriptor> list = VirtualMachine.list();for (VirtualMachineDescriptor vmd : list) {//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid//然后加载 agent.jar 发送给该虚拟机System.out.println(vmd.displayName());if (vmd.displayName().equals("Hello")) {VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());virtualMachine.loadAgent("/Users/relay/Downloads/JavaAgentTest/target/TheAgent.jar");virtualMachine.detach();}}}}
输出结果:
可以看到成功注入Agent
Instrumentation API
Instrumentation类中它定义了一些规范,例如Manifest当中的Premain-Class和Agent-Class属性,再例如premain和agentmain方法,这些规范是Agent Jar必须遵守的。
它定义了一些类和接口,例如Instrumentation和ClassFileTransformer,这些类和接口允许我们在Agent jar当中实现修改某些类的字节码。
简单来说就是 这些规范让一个普通的.Jar文件成为Agent Jar,接着Agent jar就可以在目标JVM中对加载的类进行修改等等操作。
Instrumentation的包在java.lang.Instrument包下。
这里面的IllegalClassFormatException和UnmodifiableClassException这两个类都是Exception异常类的子类。
重点是如下的3个类或接口:
ClassDefinition 类ClassFileTransformer 接口Instrumentation 接口
在Agent Jar中,分别三个组成部分,MF文件 AgentClass(premain以及agentmain) ClassFileTransformer。
无论是agentmain或premain方法,它接收的第二个参数就是Instrumentation,真正去修改字节码的操作都是Instrumentation对象去完成的。
在Agent Jar中可以提供对ClassFileTransformer的实现以及对transform方法重写,然后对我们目标的Class文件进行修改。
transform的返回值是一个byte类型的数组。如果我们对目标的字节码进行了修改那么就返回修改之后的byte[]数组,如果我们不想修改的话,那么就返回null即可。
这里最重要的是className和classfileBuffer这两个参数。
className表示目标的类名,这个属性主要是做判断的,比如判断这个类是不是我需要修改的那个类。
classfileBuffer表示修改返回的byte类型数组,就是修改之后的数据存储在classfilebuffer中。
byte[]transform( ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)throws IllegalClassFormatException;
loader:如果参数为null,那么表示使用bootstrap loader。
className:表示internal Class Name 例如java/util/List
ClassfileBuffer:一定不要修改它的原有内容,可以复制一份,在复制的集成商将进行更改。
返回值:如果返回null,则表示没有修改。
Instrumentation接口在 java.lang.instrument包中。
在Agent Jar中的Manifest文件中定义的这些属性配置,例如RedefineClassesSupported。
类似于我们在pom文件中定义的这种:
如下这3种属性,对应的着Instrumentation接口中的3个方法。
boolean isRedefineClassesSupported();boolean isRetransformClassesSupported();boolean isNativeMethodPrefixSupported();
与ClassFileTransformer相关的方法。
void addTransformer(ClassFileTransformer transformer);boolean removeTransformer(ClassFileTransformer transformer);
关于针对目标JVM的相关方法。
这里分为ClassLoader相关的 Class相关的,object相关的,module相关的。
void appendToBootstrapClassLoaderSearch(JarFile jarfile);void appendToSystemClassLoaderSearch(JarFile jarfile);
Class[] getAllLoadedClasses(); //获取所有已经被加载的这些类
Class[] getInitiatedClasses(ClassLoader loader); //获取某个Classloader加载的类
boolean isModifiableClass(Class<?> theClass); //判断当前加载的这个类是否可以被修改
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
boolean isRetransformClassesSupported();long getObjectSize(Object objectToSize); //查看对象占用的内存空间isModifiableModule()
redefineModule()第一点,理解 java.lang.instrument 包的主要作用:它让一个普通的 Jar 文件成为一个 Agent Jar。
第二点,在 java.lang.instrument 包当中,有三个重要的类型:ClassDefinition、ClassFileTransformer 和 Instrumentation。
这里指的就是我们上面说到的这三个方法。
boolean isRedefineClassesSupported();
boolean isRetransformClassesSupported();
boolean isNativeMethodPrefixSupported();首先第一个方法isRedefineClassesSupported,这个方法是判断JVM虚拟机是否支持Redefine。如果虚拟机支持的话,也就是返回true的话,那么再去判断我们的Agent Jar中配置的Can-Redefine-Classes是否是true。
boolean isRedefineClassesSupported();那么接下来的2个方法也是一样,首先判断JVM虚拟机是否支持,如果支持话,那么再去判断AgentJar中值是否为true。
简单来说分为3步:
1.判断JVM虚拟机是否支持该功能。
2.判断java Agent jar内的MANIFEST.MF文件里的属性是否为true。
3.在一个JVM实例中,多次调用某个isXxxxSupported()方法,该方法的返回值是不会有任何改变的。
示例:
这里在agentmain方法中写的,打成AgentJar之后,然后使用Attach 注入到目标的JVM。
可以看到成功注入Agent并且打印输出了这几个配置属性的值。
添加对应的是addTransformer方法, 这两个方法的本质是一样的。那么addTransformer的第二个参数决定你的transformer对象存储的位置以及它的功能发挥。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);void addTransformer(ClassFileTransformer transformer);
我们来看他的实现类也就是Instrumentation的实现类,它的实现类是InstrumentationImpl。
那么我们定位到它的addTransformer方法,可以看到这里会进行判断canRetransform这个参数,如果为true的话,那么就会调用mRetransfomableTransformerManager的addTransformer方法,mRetransfomableTransformerManager对应的是TransformerManager类。也就是调用了TransformerManager的addTransformer方法。
如果值为false,那么就会调用mTransformerManager的addTransformer方法,mTransformerManager对应的也是TransformerManager。
如果canRetransform的值为true,我们就将transformer对象称为retransformation capable transformer
如果canRetransform的值为false,我们就将transformer对象称为retransformation incapable transformer
第一点,两个addTransformer方法两者本质上是一样的。
第二点,第二个参数canRetransform影响第一个参数transformer的存储位置。
移除对应的是removeTransformer,在这个方法中,我们可以看到如果传递过来的ClassFileTransformer为空的话,那么他就会抛出异常。紧接着调用findTransformerManager去查找transformer,因为它不知道传递过来的到底是那个transformer,有可能是mTransformerManager,也有可能是mRetransfomableTransformerManager。
public synchronized booleanremoveTransformer(ClassFileTransformer transformer) {if (transformer == null) {throw new NullPointerException("null passed as 'transformer' in removeTransformer");}TransformerManager mgr = findTransformerManager(transformer);if (mgr != null) {mgr.removeTransformer(transformer);if (mgr.isRetransformable() && mgr.getTransformerCount() == 0) {setHasRetransformableTransformers(mNativeAgent, false);}return true;}return false;}
removeTransformer有两种情况调用,第一种情况,我们要处理的Class很明确,那就尽早调用removeTransformer方法,让ClassFileTransformer影响的范围最小化。这种情况一般在agentmain方法中使用较多。
public static void agentmain(String agentArgs, Instrumentation instrumentation) {DefineTransformer transformer = new DefineTransformer();System.out.println("123");instrumentation.addTransformer(transformer, true);Class<?> cls = null;try {cls = Class.forName("Hello");instrumentation.retransformClasses(cls);} catch (Exception e) {throw new RuntimeException(e);}finally {instrumentation.removeTransformer(transformer);}}
第二种情况,想处理的Class不明确,可以不调用removeTransformer方法。这一类在premain方法中使用较多。
public static void premain(String agentArgs, Instrumentation _inst){System.out.println("agentArgs:" + agentArgs);System.out.println("Instrumentation Class" + _inst.getClass().getName());Exception ex = new Exception("Exception from PreMainTraceAgent1");ex.printStackTrace(System.out);_inst.addTransformer(new DefineTransformer());}
当我们将ClassFileTransformer添加到Instrumentation之后,ClassFileTransformer类当中的transform方法什么时候执行的呢?
那么对于ClassFileTransformer.transformer方法调用的时机有3种。
1.类加载的时候会进行调用。
2.调用Instrumentation.redefineClasses方法的时候。
3.调用Instrumentation.retransformClasses方法的时候。
redefine和retransform两个概念,它们与类的加载状态有关系:
对于正在加载的类进行修改,它属于define和transform的范围。
对于已经加载的类进行修改,它属于redefine和retransform的范围。
对于已经加载的类(loaded class),redefine侧重于以“新”换“旧”,而retransform侧重于对“旧”的事物进行“修补”。
┌─── define: ClassLoader.defineClass┌─── loading ───┤│ └─── transformclass state ───┤│ ┌─── redefine: Instrumentation.redefineClasses└─── loaded ────┤└─── retransform: Instrumentation.retransformClasses
再者,触发的方式不同:
load,是类在加载的过程当中,JVM内部机制来自动触发。
redefine和retransform,是我们自己写代码触发。
最后,就是不同的时机(load、redefine、retransform)能够接触到的transformer也不相同:
第一点,介绍了Instrumentation添加和移除ClassFileTransformer的两个方法。
第二点,介绍了ClassFileTransformer被调用的三个时机:load、redefine和retransform。
redefineClasses方法是对目标类的重新定义,redefineClasses方法接受多个ClassDefinition类型的参数。
voidredefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;
Classinfo:
public final class ClassDefinition{}fields:
public final class ClassDefinition{private final Class<?> mClass;private final byte[] mClassFile;}
Constructor:
publicClassDefinition( Class<?> theClass,byte[] theClassFile) {if (theClass == null || theClassFile == null) {throw new NullPointerException();}mClass = theClass;mClassFile = theClassFile;}
Methods:
public Class<?>getDefinitionClass() {return mClass;}public byte[]getDefinitionClassFile() {return mClassFile;}
示例-替换Object类toString方法
首先使用javassist生成class文件。
package relaysec.agent;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.net.URL;import java.lang.Object;public class ObjectTest {public static void main(String[] args) throws Exception{URL resource = ObjectTest.class.getClassLoader().getResource("");String file = resource.getFile();System.out.println("文件存储路径:"+file);ClassPool classPool = ClassPool.getDefault();CtClass ctClass = classPool.get("java.lang.Object");CtMethod toString = ctClass.getDeclaredMethod("toString");toString.setBody("return \"This is an object.\";");ctClass.writeFile(file + "/data");}}
生成之后然后通过redefineClasses方法来重新定义Object类。
package relaysec.agent;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;import java.lang.instrument.ClassDefinition;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class PreMainTraceAgent2 {public static void main(String[] args) {System.out.println(PreMainTraceAgent2.class.getResourceAsStream(""));}public static void premain(String agentArgs, Instrumentation inst){System.out.println("agentArgs:" + agentArgs);try {Class<?> clazz = Object.class;if (inst.isModifiableClass(clazz)) {InputStream in = new FileInputStream("/Users/relay/Downloads/JavaAgentTest/target/classes/data/java/lang/Object.class");int available = in.available();byte[] bytes = new byte[available];in.read(bytes);ClassDefinition classDefinition = new ClassDefinition(clazz, bytes);inst.redefineClasses(classDefinition);}} catch (Exception e) {e.printStackTrace();}}}
ObjectTest测试类:
public class ObjectTest {public static void main(String[] args) {Object obj = new Object();System.out.println(obj.toString());}}
结果: 可以看到成功将toString的内容更改。
但是如果将Can-Redefine-Classes设置为false,那么就会报错。但是如果你使用的是mvn compile package的话,那么他不会替换掉这个属性值,所以我们这里必须使用mvn clean package才会更新属性。
<Can-Redefine-Classes>false</Can-Redefine-Classes>当我们使用mvn clean package打包之后,再去加载Agent Jar的时候会显示报错。
这里报错表示的就是它不支持redefineClasses。
redeineClasses是进行替换的一个操作,就是将原来的字节码替换成新的字节码,retransformClasses是对原有的Class字节码文件进行修改,而并不是进行替换。
如果某个方法执行的时候,修改之后的方法会在下一个方法中执行。
静态初始化(class initialization)不会再次执行,不受 redeineClasses 方法的影响。
redefineClasses() 方法的功能是有限的,主要集中在对方法体(method body)的修改。
当redefineClasses() 方法出现异常的时候,就相当于“什么都没有发生过”,不会对类产生影响。
retransformClasses主要是对原有的Class文件进行修改。
voidretransformClasses(Class<?>... classes) throws UnmodifiableClassException;
第一步 指定需要修改的类文件,第二步使用inst:添加transformer --> retransform --> 移除transformer。
public static void premain(String agentArgs, Instrumentation inst){System.out.println("agentArgs:" + agentArgs);String className = "java.lang.Object";DefineTransformer defineTransformer = new DefineTransformer();inst.addTransformer(defineTransformer,true);try {Class<?> clazz = Class.forName(className);boolean isModifable = inst.isModifiableClass(clazz);if (isModifable){inst.retransformClasses(clazz);}} catch (Exception e) {e.printStackTrace();}finally {inst.removeTransformer(defineTransformer);}}
static class DefineTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {try{if ("java/lang/Object".equals(className)){ClassPool cp = ClassPool.getDefault();CtClass ctClass = cp.get("java.lang.Object");CtMethod toString = ctClass.getDeclaredMethod("toString");toString.setBody("return \"this is relaysec\";");byte[] bytes = ctClass.toBytecode();return bytes;}}catch (Exception e){e.printStackTrace();}return null;}
记住一定要打开Can-Retransform-Classes这个选项。一定要设置为true,否则他会报错不支持。
首先打印出javaagent后面的参数,也就是options,然后将DumpTransformer对象创造出来,这个DumpTransformer对象继承了ClassFileTransformer类,我们就是通过这个类进行字节码的修改的,然后将我们创建出来的DumpTransformer加进去,紧接着判断JVM是否支持retransformClasses,如果支持那么就调用retransformClasses进行修改原有的字节码,最后调用removeTransformer进行移除。
package relaysec.agent;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;import java.util.Objects;public class PreMainTraceAgent4 {public static void main(String[] args) {System.out.println(PreMainTraceAgent4.class.getResourceAsStream(""));}public static void premain(String agentArgs, Instrumentation inst){System.out.println("agentArgs:" + agentArgs);String className = "java.lang.Object";DumpTransformer defineTransformer = new DumpTransformer(className);inst.addTransformer(defineTransformer,true);try {Class<?> clazz = Class.forName(className);boolean isModifable = inst.isModifiableClass(clazz);if (isModifable){inst.retransformClasses(clazz);}} catch (Exception e) {e.printStackTrace();}finally {inst.removeTransformer(defineTransformer);}}}
紧接着我们来看DumpTransformer类,首先他会将我们在上面传进来的类名传递给transform的className这个字段,然后接着判断是否是我们要修改的那个类,如果是的话,那么进行替换将斜杠替换成点,再加上时间 + .class,替换完成之后,调用DumpUtils.dump方法,进行输出字节码文件。
static class DumpTransformer implements ClassFileTransformer {private final String internalName;public DumpTransformer(String internalName) {Objects.requireNonNull(internalName);this.internalName = internalName.replace(".", "/");}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {try{System.out.println(internalName);if (className.equals(internalName)){String timeStamp = DateUtils.getTimeStamp();String filename = className.replace("/", ".") + "." + timeStamp + ".class";DumpUtils.dump(filename,classfileBuffer);}}catch (Exception e){e.printStackTrace();}return null;}}
dumpUtils.java
这里是比较简单的,首先构造出文件的路径,然后就是创建一个FIle对象,最后通过文件输出流,将文件保存。
package relaysec.agent;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;public class DumpUtils {private static final String USER_DIR = System.getProperty("user.dir");private static final String WORK_DIR = USER_DIR + File.separator + "dump";private DumpUtils() {throw new UnsupportedOperationException();}public static void dump(String filename, byte[] bytes) {String filepath = WORK_DIR + File.separator + filename;File f = new File(filepath);File dirFile = f.getParentFile();if (!dirFile.exists()) {if (!dirFile.mkdirs()) {System.out.println("Make Directory Failed: " + dirFile);return;}}try (FileOutputStream fout = new FileOutputStream(filepath);BufferedOutputStream bout = new BufferedOutputStream(fout)) {bout.write(bytes);bout.flush();System.out.println("file:///" + filepath);} catch (IOException e) {e.printStackTrace();}}}
agentmain.java
注意这里是agentmain,需要通过attach的loadAgent方法进行加载Agent Jar。
首先它调用了RegexUtils.setPattern方法传进去一个正则表达式,这里是通过attach的携带的参数进行传递的。
传递进去之后他会进行判断这个正则表达式如果不为null,那么调用Pattern类的compile方法,返回一个Pattern对象。这块代码在后面。然后new一个DumpTransformer对象,紧接着调用addTransformer添加Instrumentation。
紧接着调用getAllLoadedClasses方法,将我们JVM中所有加载的Class字节码文件,存储在一个classes数组中。
接着进行循环classes这个数组,然后通过Class对象的getName方法获取到这些加载在JVM内存中的class名称。
然后紧接着判断这些名称中,是否前缀包含如下的名称,例如java,javax,jdk,sun,com.cun,这些等等,这里其实就是一个过滤的操作,就是将这些带有这些标识的字节码文件,不dump出来。
紧接着然后调用instrumentation的isModifiableClass方法当前加载的这个类是否可以被修改。
紧接着调用正则工具类中的isCandidate方法,将我们的className传递进去,这个方法首先会判断我们上面传递过来的正则是否等于null,如果不等于null,那么就调用chAt(0),取我们ClassName的第一个字符,如果等于[ 的话返回false。
否则进行调用replace进行替换,将 / 替换成 .
最后调用matcher方法进行匹配,最后返回。
那么回到agentmain方法,接下来进行判断我们的类如果可以被修改,并且我们的正则返回是true,那么就调用candidates的add方法将我们的class存储起来,这里的candidates是List集合,在上面我们定义的List集合。
然后就调用isEmpty判断我们这个集合是否为空,如果不为空的话,那么就调用retransformClasses方法进行字节码修改。
public static void agentmain(String agentArgs, Instrumentation inst) {// 第二步,设置正则表达式:agentArgsRegexUtils.setPattern(agentArgs);// 第三步,使用inst:进行re-transform操作ClassFileTransformer transformer = new DumpTransformer();inst.addTransformer(transformer, true);try {Class<?>[] classes = inst.getAllLoadedClasses();List<Class<?>> candidates = new ArrayList<>();for (Class<?> c : classes) {String className = c.getName();// 这些if判断的目的是:不考虑JDK自带的类if (className.startsWith("java")) continue;if (className.startsWith("javax")) continue;if (className.startsWith("jdk")) continue;if (className.startsWith("sun")) continue;if (className.startsWith("com.sun")) continue;if (className.startsWith("[")) continue;boolean isModifiable = inst.isModifiableClass(c);boolean isCandidate = RegexUtils.isCandidate(className);if (isModifiable && isCandidate) {candidates.add(c);}}System.out.println("candidates size: " + candidates.size());if (!candidates.isEmpty()) {inst.retransformClasses(candidates.toArray(new Class[0]));}} catch (Exception ex) {ex.printStackTrace();} finally {inst.removeTransformer(transformer);}}
正则表达式的代码:
public static void setPattern(String regex) {if (regex != null) {pattern = Pattern.compile(regex);}else {pattern = Pattern.compile(".*");}}public static boolean isCandidate(String className) {if (pattern == null) return false;// ignore array classesif (className.charAt(0) == '[') {return false;}// convert the class name to external nameclassName = className.replace('/', '.');// check for name pattern matchreturn pattern.matcher(className).matches();}
紧接着我们来看DumpTransformer方法,这里是比较简单的,这里的话判断和上面是一样的,最后会返回一个Matcher,然后最后调用dump方法将字节码输出。
static class DumpTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {if (RegexUtils.isCandidate(className)) {String timeStamp = DateUtils.getTimeStamp();String filename = className.replace("/", ".") + "." + timeStamp + ".class";DumpUtils.dump(filename, classfileBuffer);}return null;}}
测试类:
这里的loadAgent方法需要我们去传递第二个参数,也就是正则表达式,我这里直接传递是Hello,你也可以传递相关正则表达式,到这里我们如果去查杀内存马的时候是不是就有思路了呢???
import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;/*** @author rickiyang* @date 2019-08-16* @Desc*/public class TestAgentMain {public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {//获取当前系统中所有 运行中的 虚拟机System.out.println("running JVM start ");List<VirtualMachineDescriptor> list = VirtualMachine.list();for (VirtualMachineDescriptor vmd : list) {//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid//然后加载 agent.jar 发送给该虚拟机System.out.println(vmd.displayName());if (vmd.displayName().equals("Hello")) {VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());virtualMachine.loadAgent("/Users/relay/Downloads/JavaAgentTest/target/TheAgent.jar","Hello");virtualMachine.detach();}}}}
到这里就结束了,那么如果有问题可以联系我Get__Post。
另外帮朋友招个HW的人。可以联系他价格美丽,报公众名字就行。