在这个笔记开始之前,前面的内容大家可以上网去搜索,也可以关注公众号我发给,前面笔记不是特别的重要,是一些javaAgent的基础,例如JavaAgent如何打包,Agent Jar组成的3个部分。还有一点笔记有些是个人总结的,有些是参考别人的。
这里使用到的主要是 javassist 和 javaAgent的学习,如果想看内存马怎么使用JavaAgent查杀的,可以直接跳到最后两个案例。
那么你也可以使用ASM,只是ASM的复杂程度需要和Class字节码指令去打交道,所以相对来说还是比较难的。
好了不说废话了,上笔记。
并不是所有的虚拟机 都支持command line(命令行)启动Java Agent。
-javaagent:jarpath[=options]
┌─── -javaagent:jarpath
┌─── Command-Line ───┤
│ └─── -javaagent:jarpath=options
Load-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:123456
LoadTimeAgent.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 boolean
removeTransformer(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 ───┤
│ └─── transform
class 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类型的参数。
void
redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
Classinfo:
public final class ClassDefinition{}
fields:
public final class ClassDefinition{
private final Class<?> mClass;
private final byte[] mClassFile;
}
Constructor:
public
ClassDefinition( 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文件进行修改。
void
retransformClasses(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 {
@Override
public 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(".", "/");
}
@Override
public 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) {
// 第二步,设置正则表达式:agentArgs
RegexUtils.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 classes
if (className.charAt(0) == '[') {
return false;
}
// convert the class name to external name
className = className.replace('/', '.');
// check for name pattern match
return pattern.matcher(className).matches();
}
紧接着我们来看DumpTransformer方法,这里是比较简单的,这里的话判断和上面是一样的,最后会返回一个Matcher,然后最后调用dump方法将字节码输出。
static class DumpTransformer implements ClassFileTransformer {
@Override
public 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的人。可以联系他价格美丽,报公众名字就行。