Java agent学习


Agent

Java Agent是jdk1.5以后引入的在字节码这个层面对类和方法进行修改的技术,能够在不影响编译的情况下,修改字节码。可以理解spring的aop技术。

Java agent的实现方式主要有两种:

· premain方法,在JVM启动前

· agentmain方法,在JVM启动后加载

premain实现

首先创建一个类并实现premain方法

package com.vul.agent;

import java.lang.instrument.Instrumentation;

public class preagent {

public static void premain(String args, Instrumentation inst) {

​ for(int i =0 ; i<10; i++){

​ System.out.println(“调用了premain方法”);

​ }

}

}

创建完方法后在src/main/resources目录下创建META-INF/MANIFEST.MF内容如下其中Premain-Class的值是实现preagent方法的完整类名

img

再将整个项目打包成一个jar包

img

使用这个preagent的jar包只需在正常运行一个java程序前指定参数javaagent为要使用的这个jar包即可,如下图以打开jd-gui为例

img

这里可以看到启动JVM前在指定agent参数后先调用了premain方法再正常运行jd-gui

agentmain实现

agentmain的实现是在JVM启动后,在实战过程中我们不可能直接让目标重启JVM,所以在实战中注入内存马实现的就是agentmain这个方法。

实现agent

package com.vul.agent;

import java.lang.instrument.Instrumentation;

public class AgentMain {

public static void agentmain(String args, Instrumentation inst) {

​ for (int i=0; i<10; i++){

​ System.out.println(“this is agentmain”);

​ }

}

}

agentmain的实现过程与premain大致一样,需在MANIFEST.MF中另外指定Agent-Class并指定实现agentmain方法的完整类名。使用方法不再是启动前指定参数,需要用到jdk自带的tools.jar中VirtualMachine这个针对java虚拟机的API。Oracle官方给出的这个API方法的使用如下

img

还是以jd-gui为例,打开jd-gui并找到jd-gui的pid

img

再使用Oracle官方给出的VisualMachine方法加载agent到jd-gui的内存中

img

可以看到成功agentmain方法注入成功

修改字节码

Java Agent内存马注入其实就是在执行agentmain方法时通过Instrumentation参数修改字节码将内存马注册进目标进程中,agentmain方法的执行流程已经在前面了解过了,现在需要知道的就是怎么通过Instrumentation修改字节码

Instrumentation

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,premain和agentmain两个方法的第二个默认的参数是实现Instrumentation的对象,Instrumentation接口定义了十几个方法,用于拦截类加载时间,对字节码进行修改。可能会用到以下几种方法

//增加一个Class文件的转换器,将一个实现了ClassFileTransformer接口的对象注册成我们需要的transform

void addTransformer(ClassFileTransformer transformer);

//触发transformer

void retransformClasses(Class<?>… classes) throws UnmodifiableClassException;

//判断目标能否被修改

boolean isModifiableClass(Class<?> theClass);

//获取已加载在JVM中的class

Class[] getAllLoadedClasses();

另外在构造premain和使agentmain方法时不一定要用到Instrumentation但用了Instrumentation参数的方法优先级更高

ClassFileTransformer

可以看到addTransformer方法中传入参数是ClassFileTransformer对象,在ClassFileTransformer中我们可以重写transform类对javassist进行修改

javassist

Javassist (java programming A)是一个开源的分析、编辑和创建 Java 字节码的类库,它能在java程序运行时定义新的类并在JVM加载时修改类文件。

ClassPool

ClassPool是CtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。

CtClass

对指定class文件处理的类

ClassPool cp = ClassPool.getDefault();

CtClass ctc = cp.get(editClassName);

CtMethod

对通过CtClass获取到的类中的方法进行修改

CtMethod method = ctc.getDeclaredMethod(editMethod);

完整Demo

下面就来创建一个完整的实例

创建一个hello类,里面包含一个hello方法

1
2
3
4
5
6
7
8
9
public class hello {

public void hello() {

System.out.println("this is hello function");

}

}

创建一个测试类两次调用hello方法并用scanner暂停对比被agent修改前后的区别

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
public class hellotest {

public static void main(String[] args) {

hello h = new hello();

h.hello();

System.out.println("flag start...");

//标记暂停

Scanner sc = new Scanner(System.in);

sc.next();

//重新执行

hello h2 = new hello();

h2.hello();

System.out.println("flag ends....");

}

}

接下来就构造agentmmain,首先实现一个ClassFileTransformer重写transform在其中指定要修改的方法是hello类中的hello方法

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
public class TransformerDemo implements ClassFileTransformer {

public static final String editClassName = "com.vul.hello";

public static final String editMethod = "hello";

@Override

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

try {

ClassPool cp = ClassPool.getDefault();

if(classBeingRedefined !=null){

ClassClassPath ccp = new ClassClassPath(classBeingRedefined);

cp.insertClassPath(ccp);

}

//获取修改的类名

CtClass ctc = cp.get(editClassName);

//获取修改的方法名

CtMethod method = ctc.getDeclaredMethod(editMethod);

//设置被修改后的方法的内容

String source ="{System.out.println(\"hello method has been transformer\");}";

method.setBody(source);

byte[] bytes = ctc.toBytecode();

ctc.detach();

System.out.println();

return bytes;

}catch (Exception e){

e.printStackTrace();

}

return null;

}

}


在agentmain中通过Instrumentation调用addTransformer和retransformClasses

public class AgentMain {

public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {

​ //注册Transformer

​ inst.addTransformer(new TransformerDemo());

​ //获取JVM中类并遍历

​ Class[] classes = inst.getAllLoadedClasses();

​ for (Class aClass :classes){

​ //判断是否为想要修改的类

​ if (aClass.getName().equals(TransformerDemo.editClassName)){

​ System.out.println(aClass.getName());

​ //触发Transformer

​ inst.retransformClasses(aClass);

​ System.out.println(“agentmain注入标记,被修改的类的类名为:”+aClass.getName());

​ }

​ }

}

}

tips:因为用到了retransformClasses方法,还需要在MANIFEST.MF中设置Can-Retransform-Classes: true

将上面的agentmain打包成jar包后启动测试方法并在暂停的时候使用前面提到的VirtualMachine将agent注入进test中再继续执行test观察变化

img

img

可以看到在注入agentmain后hello被transform方法修改


文章作者: jokerscar
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jokerscar !
  目录