前几天和同学在排查一个线上问题时,发现一个有漏洞的HSF请求,急需对该HSF进行屏蔽,但是发现该系统未接入限流,这下懵逼了。但是一个同学灵机一动,使用了故障演练平台对该接口模拟hsf调用方异常。屏蔽了该请求。顿时对这个平台的技术产生了兴趣。故障演练平台。
粗略查看了它的手册,发现其使用了JavaAgent的技术。那么,什么是JavaAgent呢?
JavaAgent是基于JVMTI实现的。(从jdk1.5.0 版本加入 Java 虚拟机工具接口,用于监控JVM各项信息)。以下是JavaAgent的主要功能。
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
Agent_OnLoad
函数,如果agent是在启动的时候加载的,也就是在vm参数里通过-agentlib来指定,那在启动过程中就会去执行这个agent里的Agent_OnLoad
函数。Agent_OnAttach
函数,如果agent不是在启动的时候加载的,是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载agent,在加载过程中就会调用Agent_OnAttach
函数。Agent_OnUnload
函数,在agent做卸载的时候调用,不过貌似基本上很少实现它。/*
* JVMTI agent
*/
#include
#include
#include
#include
#include
#include
以上代码主要实现了2个方法,Agent_OnAttach是在程序在运行时加载,并打印所有的加载类。Agent_OnLoad是在程序启动时加载,打印一个语句。
然后对这个CPP进行编译。生成一个.so的动态库文件。
g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/darwin Agent.cpp -fPIC -shared -o libagent.so
然后是被agent的目标的类,并且对其进行编译
public class TestMain {
public static void main(String[] args) throws InterruptedException {
System.out.println("JVMTI agent Test start");
int i = 0;
while (i < 100) {
Thread.sleep(1000);
i++;
System.out.println(i);
}
}
}
然后开始尝试程序启动阶段的agent,使用以下指令进行运行
java -agentpath:/Users/archersblood/Desktop/libagent.so TestMain
可以看到日志文件的输出。
Agent_OnLoad success!
JVMTI agent Test start
1
2
3
4
5
6
7
8
9
10
11
12
这是程序在启动的时候进行agent了,jvm执行了动态库中的Agent_OnLoad代码。
import java.io.IOException;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
public class TestAgent {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException,
AgentInitializationException {
String pid = "1136"; // java进程pid
String agentPath = "/Users/archersblood/Desktop/libagent.so"; // agent.so的路径
String options = null;// 传入agent的参数
VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);
virtualMachine.loadAgentPath(agentPath, options);
virtualMachine.detach();
}
}
当运行TestMain的main函数的时候,ps aux | grep java 查看TestMain的PID,然后修改以上代码中的pid参数,让Agent加载到指定的java进程中。然后立马运行TestAgent,就会看到以下日志。
66
67
68
69
70
71
72
Agent_OnAttach success!
cls sig=Lagent/TestMain;
cls sig=Lorg/objectweb/asm/MethodWriter;
cls sig=Lorg/objectweb/asm/FieldWriter;
cls sig=[Lorg/objectweb/asm/Item;
cls sig=Lorg/objectweb/asm/Item;
cls sig=Lorg/objectweb/asm/ByteVector;
cls sig=Lorg/objectweb/asm/FieldVisitor;
cls sig=Lorg/objectweb/asm/MethodVisitor;
cls sig=Lorg/objectweb/asm/AnnotationVisitor;
cls sig=Lorg/objectweb/asm/ClassWriter;
cls sig=Lorg/objectweb/asm/ClassVisitor;
cls sig=Lagent/MyAgent;
cls sig=Ljava/lang/Void;
cls sig=Ljava/lang/Class$MethodArray;
cls sig=Lsun/launcher/LauncherHelper$FXHelper;
cls sig=[Lsun/launcher/LauncherHelper;
cls sig=Lsun/launcher/LauncherHelper;
cls sig=Lsun/usagetracker/UsageTrackerClient$3;
cls sig=Lsun/usagetracker/UsageTrackerClient$4;
cls sig=Lsun/usagetracker/UsageTrackerClient$1;
cls sig=Ljava/util/concurrent/atomic/AtomicBoolean;
cls sig=Lsun/usagetracker/UsageTrackerClient;
cls sig=Lsun/misc/PostVMInitHook;
cls sig=Ljava/lang/invoke/MethodHandleStatics$1;
cls sig=Ljava/lang/invoke/MethodHandleStatics;
cls sig=Ljava/lang/invoke/MemberName$Factory;
cls sig=Ljava/lang/ClassValue$Version;
cls sig=Ljava/lang/ClassValue$Identity;
cls sig=[Ljava/lang/ClassValue$Entry;
cls sig=Ljava/lang/ClassValue$Entry;
cls sig=Ljava/lang/invoke/MethodHandleImpl$4;
cls sig=Ljava/lang/ClassValue;
cls sig=[Ljava/lang/invoke/MethodHandle;
cls sig=Ljava/lang/invoke/MethodHandleImpl$3;
cls sig=Ljava/lang/invoke/MethodHandleImpl$2;
cls sig=Ljava/util/function/Function;
cls sig=Ljava/lang/invoke/MethodHandleImpl$1;
cls sig=Ljava/lang/invoke/MethodHandleImpl;
cls sig=Ljava/io/FileOutputStream$1;
cls sig=Ljava/lang/IllegalStateException;
cls sig=Ljava/util/concurrent/ConcurrentHashMap$ForwardingNode;
cls sig=Lsun/security/util/ManifestEntryVerifier;
cls sig=Ljava/io/ByteArrayOutputStream;
cls sig=Ljava/util/jar/JarVerifier$3;
cls sig=[Ljava/security/CodeSigner;
cls sig=Ljava/security/CodeSigner;
cls sig=Ljava/util/jar/JarVerifier;
cls sig=Lsun/misc/ASCIICaseInsensitiveComparator;
cls sig=Ljava/util/jar/Attributes$Name;
cls sig=Ljava/util/jar/Manifest$FastInputStream;
cls sig=Ljava/util/jar/Attributes;
cls sig=Lsun/misc/URLClassPath$JarLoader$2;
cls sig=Lsun/misc/IOUtils;
cls sig=Lsun/misc/ExtensionDependency;
计数器打印到一半时,执行了动态库中的Agent_OnAttach方法。
如图可知,mk修改业务java启动进程脚本(setenv.sh)就是增加了如下一行:
-javaagent:/usr/alisys/dragoon/libexec/monkeyking/mkagent.jar
-Dproject.name=sonar 表示业务java进程是sanar
先看以下代码
package agent;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.Instrumentation;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
public class MyAgent {
/**
* 该方法在main方法之前运行,与main方法运行在同一个JVM中
* 并被同一个System ClassLoader装载
* 被统一的安全策略(security policy)和上下文(context)管理
*
* @param agentOps
* @param inst
* @throws Exception
*/
public static void premain(String agentOps, Instrumentation inst) throws Exception {
System.out.println("=========premain方法执行========");
System.out.println(agentOps);
ClassWriter cw = new ClassWriter(0);
//通过visit方法确定类的头部信息
cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE,
"com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"});
//定义类的属性
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"LESS", "I", null, new Integer(-1)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"EQUAL", "I", null, new Integer(0)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"GREATER", "I", null, new Integer(1)).visitEnd();
//定义类的方法
cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd(); //使cw类已经完成
//将cw转换成字节数组写到文件里面去
byte[] data = cw.toByteArray();
File file = new File("/Users/archersblood/Desktop/test.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(data);
fout.close();
}
/**
* 如果不存在 premain(String agentOps, Instrumentation inst)
* 则会执行 premain(String agentOps)
*
* @param agentOps
* @author
*/
public static void premain(String agentOps) {
System.out.println("=========premain方法执行2========");
System.out.println(agentOps);
}
}
该代码使用了javaagent与 ASM(Java字节码操纵框架)技术,作用是在程序运行前生成一个com.asm3.Comparable接口,该agent默认是在程序main运行之前,执行premain方法。
Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
指定agent.MyAgent为PremainClass。同时将MyAgent与MF文件打包,我使用的是eclipse导出。然后使用以下指令(可以参考故障演练平台的配置)
java -javaagent:/Users/archersblood/Desktop/myAgent.jar TestMain
=========premain方法执行========
null
JVMTI agent Test start
1
2
3
4
5
6
7
8
9
10
11
12
13
最后,生成了一个test.class。对其反编译
javap -c test.class >test.txt
打开该文件,就是利用asm字节码操作生成的一个接口
public interface com.asm3.Comparable extends com.asm3.Mesurable {
public static final int LESS;
public static final int EQUAL;
public static final int GREATER;
public abstract int compareTo(java.lang.Object);
}
推测,可能故障演练平台上就是使用的就是javaagent+asm技术,在应用启动或运行时,通过修改类的字节码,模拟系统的各种故障。(以上都是瞎猜,猜错了我不负责,演练平台的同学别找我^o^)