JavaAgent(笔记1)

admin 2023年7月24日16:57:40评论7 views字数 26825阅读89分25秒阅读模式

在这个笔记开始之前,前面的内容大家可以上网去搜索,也可以关注公众号我发给,前面笔记不是特别的重要,是一些javaAgent的基础,例如JavaAgent如何打包,Agent Jar组成的3个部分。还有一点笔记有些是个人总结的,有些是参考别人的。

这里使用到的主要是 javassist 和 javaAgent的学习,如果想看内存马怎么使用JavaAgent查杀的,可以直接跳到最后两个案例。

那么你也可以使用ASM,只是ASM的复杂程度需要和Class字节码指令去打交道,所以相对来说还是比较难的。

好了不说废话了,上笔记。

agentArgs参数

并不是所有的虚拟机 都支持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

在Agent Jar中,根据META-INF/MANIFEST.MF文件中定义的Premain-Class属性来找到Agent Class。

例如我定义的是AgentMain,他就会去找AgentMain这个类以及找到他的premain方法。

Premain-Class: relaysec.agent.AgentMain

premain方法

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参数,我们使用命令行进行运行,注意这里传输参数的时候不能存在空格。

JavaAgent(笔记1)

可以看到这里打印出了agentArgs参数。

JavaAgent(笔记1)

解析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, ""}; }}

紧接着运行使用 : 分割。

JavaAgent(笔记1)

可以看到很清楚的解析出了agentArgs的参数。

JavaAgent(笔记1)

使用 = 进行分割,可以看到也是没有任何问题的。

JavaAgent(笔记1)

总结

  • 第一点,在命令行启动 Java Agent,需要使用 -javaagent:jarpath[=options] 选项,其中的 options 信息会转换成为 premain 方法的 agentArgs 参数。

  • 第二点,对于 agentArgs 参数的进一步解析,需要由我们自己来完成。

inst参数

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方法。

InstrumentationImpl

实现了Instrumentation接口:
public class InstrumentationImpl implements Instrumentation {}
loadClassAndCallPremain

sun.instrument.InstrumentationImpl 类当中,loadClassAndCallPremain 方法的实现非常简单,它直接调用了 loadClassAndStartAgent 方法:这个方法针对于premain方法调用的。

public class InstrumentationImpl implements Instrumentation {
   private void loadClassAndCallPremain(String classname, String optionsString) throws Throwable {
       loadClassAndStartAgent(classname, "premain", optionsString);
  }
}
loadClassAndCallAgentmain

这个方法针对于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 方法进行了调用。

Attach API

简介

在进行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包中,包含如下的类。

JavaAgent(笔记1)

这些类我们只需要关注VirtualMachine以及AttachProvider这两个类即可,其他的类都是一些异常类,还有一个类是VirtualMachineDescriptor,这个类就是对这几个字段(id,provider和display name的包装)。

JavaAgent(笔记1)

如何使用VirtualMachine类?

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断开链接。

JavaAgent(笔记1)

其他方法:

除了以上这些重要的方法之外还有一些其他的方法。

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对象。

VirtualMachineDescriptor类

这个类我们可以通过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;
}
AttachProvider

这个类是一个抽象类,它需要一个具体的实现类。

JavaAgent(笔记1)

在不同的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

JavaAgent(笔记1)

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包下。

java.lang.Instrument包中包含了哪些类

JavaAgent(笔记1)

JavaAgent(笔记1)

这里面的IllegalClassFormatException和UnmodifiableClassException这两个类都是Exception异常类的子类。

重点是如下的3个类或接口:

ClassDefinition ClassFileTransformer 接口Instrumentation 接口

在Agent Jar中,分别三个组成部分,MF文件 AgentClass(premain以及agentmain) ClassFileTransformer。

无论是agentmain或premain方法,它接收的第二个参数就是Instrumentation,真正去修改字节码的操作都是Instrumentation对象去完成的。

ClassFileTransformer:

在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

Instrumentation接口在 java.lang.instrument包中。

在Agent Jar中的Manifest文件中定义的这些属性配置,例如RedefineClassesSupported。

类似于我们在pom文件中定义的这种:

如下这3种属性,对应的着Instrumentation接口中的3个方法。

JavaAgent(笔记1)

boolean isRedefineClassesSupported();boolean isRetransformClassesSupported();boolean isNativeMethodPrefixSupported();

与ClassFileTransformer相关的方法。

void addTransformer(ClassFileTransformer transformer);boolean removeTransformer(ClassFileTransformer transformer);

关于针对目标JVM的相关方法。

这里分为ClassLoader相关的 Class相关的,object相关的,module相关的。

ClassLoader相关的
void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile);
Class相关的
Class[] getAllLoadedClasses(); //获取所有已经被加载的这些类
Class[] getInitiatedClasses(ClassLoader loader); //获取某个Classloader加载的类
boolean isModifiableClass(Class<?> theClass); //判断当前加载的这个类是否可以被修改
void  redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;
boolean isRetransformClassesSupported();
Object相关的
long getObjectSize(Object objectToSize); //查看对象占用的内存空间
module相关的
isModifiableModule()
redefineModule()
小总结:
  • 第一点,理解 java.lang.instrument 包的主要作用:它让一个普通的 Jar 文件成为一个 Agent Jar。

  • 第二点,在 java.lang.instrument 包当中,有三个重要的类型:ClassDefinitionClassFileTransformerInstrumentation

Instrumentation-isXxxSupported

这里指的就是我们上面说到的这三个方法。

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。

JavaAgent(笔记1)

可以看到成功注入Agent并且打印输出了这几个配置属性的值。

JavaAgent(笔记1)

Instrumentation-添加和移除transformer

添加对应的是addTransformer方法, 这两个方法的本质是一样的。那么addTransformer的第二个参数决定你的transformer对象存储的位置以及它的功能发挥。

void addTransformer(ClassFileTransformer transformer, boolean canRetransform);void addTransformer(ClassFileTransformer transformer);

我们来看他的实现类也就是Instrumentation的实现类,它的实现类是InstrumentationImpl。

JavaAgent(笔记1)

那么我们定位到它的addTransformer方法,可以看到这里会进行判断canRetransform这个参数,如果为true的话,那么就会调用mRetransfomableTransformerManager的addTransformer方法,mRetransfomableTransformerManager对应的是TransformerManager类。也就是调用了TransformerManager的addTransformer方法。

如果值为false,那么就会调用mTransformerManager的addTransformer方法,mTransformerManager对应的也是TransformerManager。

JavaAgent(笔记1)

  • 如果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也不相同:

JavaAgent(笔记1)

总结

第一点,介绍了Instrumentation添加和移除ClassFileTransformer的两个方法。

第二点,介绍了ClassFileTransformer被调用的三个时机:load、redefine和retransform。

redefineClasses

redefineClasses方法是对目标类的重新定义,redefineClasses方法接受多个ClassDefinition类型的参数。

voidredefineClasses(ClassDefinition... definitions)    throws  ClassNotFoundException, UnmodifiableClassException;
ClassDefinition

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的内容更改。

JavaAgent(笔记1)

但是如果将Can-Redefine-Classes设置为false,那么就会报错。但是如果你使用的是mvn compile package的话,那么他不会替换掉这个属性值,所以我们这里必须使用mvn clean package才会更新属性。

 <Can-Redefine-Classes>false</Can-Redefine-Classes>

JavaAgent(笔记1)

当我们使用mvn clean package打包之后,再去加载Agent Jar的时候会显示报错。

这里报错表示的就是它不支持redefineClasses。

JavaAgent(笔记1)

细节之处

redeineClasses是进行替换的一个操作,就是将原来的字节码替换成新的字节码,retransformClasses是对原有的Class字节码文件进行修改,而并不是进行替换。

如果某个方法执行的时候,修改之后的方法会在下一个方法中执行。

静态初始化(class initialization)不会再次执行,不受 redeineClasses  方法的影响。

redefineClasses() 方法的功能是有限的,主要集中在对方法体(method body)的修改。

redefineClasses() 方法出现异常的时候,就相当于“什么都没有发生过”,不会对类产生影响。

retransformClasses

retransformClasses主要是对原有的Class文件进行修改。

voidretransformClasses(Class<?>... classes) throws UnmodifiableClassException;

示例-修改toString方法

premain方法

第一步 指定需要修改的类文件,第二步使用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);    }}
ClassFileTransformer方法
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(笔记1)

测试结果

JavaAgent(笔记1)

示例2-dump JVM中的类

首先打印出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(); } }}

JavaAgent(笔记1)

示例3-通过正则表达式dump JVM中的class字节码文件

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(); } } }
}

JavaAgent(笔记1)

到这里就结束了,那么如果有问题可以联系我Get__Post。

另外帮朋友招个HW的人。可以联系他价格美丽,报公众名字就行。

JavaAgent(笔记1)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月24日16:57:40
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JavaAgent(笔记1)http://cn-sec.com/archives/1903278.html

发表评论

匿名网友 填写信息