JavaAgent简述

javaAgent能干什么

提供了一种虚拟机级别支持的 AOP 实现方式,可以利用Instrumentation API实现一个Agent程序,在不修改程序源代码的情况下,在程序启动前修改类的定义。进而我们就拥有了获取当前应用的上下文,在应用运行中实时分析数据流以及调用栈的能力。比如: 我们熟知RSAP和IAST还有一些java类的软件破解都是基于这个做的

支持方式

支持Agent_OnLoad和Agent_OnAttach两种方式

1
2
3
4
5
6
7
8
9
10
/**Agent_OnLoad方式
* .
* 其jar包的manifest需要配置属性Premain-Class
*/
public static void premain(String agentArgs, Instrumentation inst);
/**
* Agent_OnAttach方式
* 其jar包的manifest需要配置属性Agent-Class
*/
public static void agentmain(String agentArgs, Instrumentation inst);

两种方式的对比

形式 Onload OnAttach
时间点 JDK1.5 JDK1.6
载入节点 程序运行前 程序运行时
指定 Premain-Class Agent-Class
载入形式 以vm参数的形式载入,在程序main方法执行之前执行 以Attach的pid方式载入,在Java程序启动后执行
入口方法 premain agentmain
修改方式 直接对Class修改 需要借助retransform进行类重新转换
存在形式 寄生、Jar包 寄生、Jar包

运作流程

Instrumentation 支持Agent_OnLoad和Agent_OnAttach两种方法,我们下面看看两种方式的运作流程

测试前

1>业务Server正常流程

  • 开发编写业务代码 .java

  • 运行时会进行编译,类似javac命令 将java文件转换成.class文件

  • 然后用classloader类加载器,将字节码文件加载到内存

  • 然后就可以在jvm上愉快的执行了

2>如果用Agnet的onload形式

  • 首先编写Onload方式的Agent.jar

  • 通过jvm -javaagent参数的形式加载Agent

  • Agent的premain入口方法,会用AppClassLoader去加载,并在Main方法前执行.从名字pre也可以看出来

  • 通过重写tranform方法对类进行拦截,并对类的字节码文件进行修改

  • 修改一般直接使用javassist、ASM等字节码操作框架进行修改

  • 修改后用classloader加载到内存中

  • 需要注意的是在agent在加载前,会有一些由BootStapClass类加载器的加载rt.jar等,一些基础的核心库先进行了加载。所以这些类是Hook不到的,解决方法是retranform后面再聊,和正常业务不同的地方就是,业务代码本来直接用AppClassLoader去加载的类,现在要过一遍Agent的拦截

3>如果用Agnet的onAttach形式

  • 因为这种方式为热部署的形式,所以业务代码首先会跑起来

  • 此时自己编写一个方法,用Java虚拟机通过全限定名的方式找见业务的进程PID(只要java程序跑起来,就会有一个 MAINFEST.NF文件指定的main方法,一般全限定名就是找这个类的)

  • 找见这个pid会通过socket的方式去把自己编写的agent附加到业务程序中

  • 因为业务程序已经都加载到内存中了,所以需要通过retransformClasses对类进行一个重新的转换

  • 入口方法为agentmain,重写retranform方法会对类进行拦截,拦截后对类的字节码文件进行修改

  • 修改一般直接使用javassist、ASM等字节码操作框架进行修改

  • 修改重新用AppClassLoader加载到内存中

  • 需要注意的是,这个热部署的方式虽然好,但是会有一些暗坑,比如第二次重新转换时,第一次的代码并没有消失,造成代码的重复插入,后面咱们再聊

Instrumentation的一些主要方法如下

测试前

  • addTransformer/ removeTransformer:注册/删除ClassFileTransformer
  • retransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明
  • redefineClasses:与如上类似,但不是重新进行转换处理,而是直接把处理结果(bytecode)直接给JVM
  • getAllLoadedClasses:获得当前已经加载的Class,可配合retransformClasses使用
  • getInitiatedClasses:获得由某个特定的ClassLoader加载的类定义
  • getObjectSize:获得一个对象占用的空间,包括其引用的对象
  • appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch:增加BootstrapClassLoader/SystemClassLoader的搜索路径
  • isNativeMethodPrefixSupported/setNativeMethodPrefix:支持拦截Native Method

Agent_OnLoad详细加载过程

  • 创建并初始化JPLISAgent
  • 监听VMInit事件,在vm初始化完成之后做下面的事情:
    • 创建InstrumentationImpl对象
    • 监听ClassFileLoadHook事件
    • 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法
  • 解析javaagent里MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的一些内容

Agent_OnAttach详细加载过程

  • 创建并初始化JPLISAgent

  • 解析javaagent里MANIFEST.MF里的参数

  • 创建InstrumentationImpl对象

  • 监听ClassFileLoadHook事件

  • 调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Agent-Class类的agentmain方法

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:
premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
新类和老类的父类必须相同。
新类和老类实现的接口数也要相同,并且是相同的接口。
新类和老类访问符必须一致。
新类和老类字段数和字段名要一致。
新类和老类新增或删除的方法必须是private static/final修饰的。
可以修改方法体。

下面会分别用这两种方式来使用ASM框架写演示Demo

09Onload方式代码实现

Agent.java
premain方法优先级很高,除了少量的系统类外,会先加载premain方法。在premain方法中加入一个转换类AgentTransform

通过addTransformer方法,注册一个ClassFileTransformer的实现类

1
2
3
4
5
6
7
8
9
10
11
package com.screw;

import java.lang.instrument.Instrumentation;

public class Agent {
//Onload形式的premain入口方法,会在main方法之前被调用
public static void premain(String agentArgs, Instrumentation inst) {

inst.addTransformer(new AgentTransform());
}
}

AgentTransform实现了ClassFileTransformer接口,在类加载时会回调ClassFileTransformer接口

重写transform方法,并对CalssName进行拦截也就是类的全限定名

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
package com.screw;

import org.objectweb.asm.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;


public class AgentTransform implements ClassFileTransformer {


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

className = className.replace("/", ".");

try {
//此处拦截了含有MyApp的类
if (className.contains("MyApp")) {
//开发者在此自定义做字节码操作,将传入的字节码修改后返回
System.out.println("Load class: " + className);

ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
//创建一个自定义ClassVisitor,方便后续ClassReader的遍历通知
ClassVisitor classVisitor = new TestClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
classfileBuffer = classWriter.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}


}

TestClassVisitor.java

重写visitMethod方法,也就是对MyApp类中的方法进行修改

紧接着就是一些ASM框架的一些操作

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
package com.screw;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;



public class TestClassVisitor extends ClassVisitor {

public TestClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
//拦截MyApp类中,方法名为sayhello的方法
if ("sayhello".equals(name)) {
//System.out.println(name + "方法的描述符是:" + desc);

return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
public void visitCode() {
//这块注入一个输出语句
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("注入Agent方法");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.visitCode();
}
};
}
return mv;
}
}

测试前结果:
测试前

测试后结果:
测试前

10.OnAttach方式

Agent.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.screw;


import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;


public class Agent {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
CustomClassTransformer transformer = new CustomClassTransformer(inst);
transformer.retransform();
}
}

CustomClassTransformer.java 转换类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.screw;


import org.objectweb.asm.*;


import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.LinkedList;


public class CustomClassTransformer implements ClassFileTransformer {
private Instrumentation inst;
public CustomClassTransformer(Instrumentation inst) {
this.inst = inst;
inst.addTransformer(this, true);
}


@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("In Transform");
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
// return super.visitMethod(i, s, s1, s2, strings);
final MethodVisitor mv = super.visitMethod(i, s, s1, s2, strings);
if ("sayhello".equals(s)) {
return new MethodVisitor(Opcodes.ASM5, mv) {
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("注入Agent方法");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
};
}
return mv;
}
};
cr.accept(cv, ClassReader.EXPAND_FRAMES);
classfileBuffer = cw.toByteArray();
return classfileBuffer;
}


public void retransform() throws UnmodifiableClassException {
LinkedList<Class> retransformClasses = new LinkedList<Class>();
Class[] loadedClasses = inst.getAllLoadedClasses();
for (Class clazz : loadedClasses) {
if ("com.screw.attach.MyApp".equals(clazz.getName())) {
if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.lang.invoke.LambdaForm")) {
inst.retransformClasses(clazz);
}
}
}
}
}

MyApp.java 简单的App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.screw.attach;


import java.util.Scanner;


public class MyApp {
public static void main(String[] args) {
while (1==1){
System.out.print("输入");
Scanner scan = new Scanner(System.in);
System.out.println(scan.next());
new MyApp().sayhello();
}
}


public void sayhello(){
System.out.println("he!!!!!!");

}
}

测试attach前:
测试前

编写Attach测试类,进行attach操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.screw.attach;

import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;

public class AttachTest {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("MyApp")) {
String pid = vmd.id();
System.out.println("注入的进程号是: " +pid);
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
virtualMachine.loadAgent("/Users/screw/Documents/SelfCode/AttachAgent/target/Agent.jar", "Attach!");
System.out.println("ok");
virtualMachine.detach();
}
}
}
}

Attach进行attach测试:
测试前

查看MyApp,发现已经修改了sayhello方法
测试前

方法描述符怎么看:
通过java自带的 javap -verbose TestAttachDemo即可查看
方法描述符

总结:
Attach优点是热部署,但是缺点是每次部署都会添加一次注入代码。会插入多次重复代码
测试前

文章作者: Screw
文章链接: http://screwsec.com/2019/11/10/JavaAgent%E7%AE%80%E8%BF%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Screw's blog