一、前言
在学习java反序列化漏洞的时候,看到有采用 notsoserial 的黑白名单javaagent机制实现对Java反序列化漏洞的修复
java 在运行时候提供参数
-agentlib:[=<选项>]
加载本机代理库 , 例如 -agentlib:hprof
另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:[=<选项>]
按完整路径名加载本机代理库
-javaagent:[=<选项>]
加载 Java 编程语言代理, 请参阅 java.lang.instrument
在JDK1.5之后Java 提供java.lang.instrument
支持,这是在rt.jar
中定义的一个包,该路径下有两个重要的类
Instrumentation
从函数的名称可以看出Instrumentation提供的功能:
1.
addTransformer
、removeTransFromer
:动态添加 or 移除ClassFileTranformer,功能是修改类字节码,具体实现将在demo体现2.
appendToBootstrapClassLoaderSearch
、appendToSystemClassLoaderSearch
:动态修改classpath 搜索结果3.
getAllLoadedClasses
:动态获取所有JVM
已加载的类3.
getInitiatedClasses
: 动态获取某个类加载器已实例化的所有类4.
redefineClasses
: 重新定义已加载类5.
retransFormClasses
: 重新加载某个已经被JVM加载过的类字节码6.
setNativeMethodPrefix
: 动态设置JNI前缀,可以实现Hook native方法ClassFileTransformer
:只有一个方法tranformer,就是返回修改的字节码
byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
Instrumentation
的实现依赖JVMTI,JVMTI (JVM Tool Interface)是Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。加载Agent的时机可以是目标JVM启动之时,也可以是在目标JVM运行时进行加载,而在目标JVM运行时进行Agent加载具备动态性,对于时机未知的Debug场景来说非常实用。
javaagent使用了 Instrumentation
的技术,使用 javaagent 需要几个步骤:
- 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class(JVM加载类后,执行main函数前,下面说的Agent模式) 选项或Agent-Class(执行main函数之后,下面说的Attach模式,类似与gdb的attach调试模式,在JDK1.6后支持),通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项,表示允许修改Class字节码。
- 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
- 创建一个Agent-Class 指定的类,类中包含 agentmain 方法,方法逻辑由用户自己确定。
- 将 premain,agentmain 的类和 MANIFEST.MF 文件打成 jar 包。
- 使用参数 -javaagent: jar包路径 启动要代理的应用或者通过程序指定jvm pid附加agent运行。
其中著名的RASP(Runtime Application Self Protect)就是采用java agent技术,下面以一个crack license的demo讲解java agent的两种模式
二、Demo演示
demo采用maven的项目构建工具
JDK 1.8u66
maven 3.63
pom.xml依赖:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun/tools -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
其中javassist 如官网说明
Javassist (JAVA programming ASSISTant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java.
是一个方便对字节流的class文件进行修改。
2.1 License.class:
每隔5s检查License是否过期,这里指定过期时间为 2020.01.01 12:00:00,以当前的时间比较总是超过日期,所以我们的工作是实现一个javaagent 修改License的检查逻辑。
package com.thonsun.sec.agent;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @Project: java-agent
* @Desc: 自动检测License合法性 --测试Javaagent 技术
* @Author: thonsun
* @Create: 2020/12/27 15:31
**/
public class License {
private static final SimpleDateFormat DATA_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static boolean checkLicense(String expireDate) throws ParseException {
//最迟的过期日期
Date date = DATA_FORMAT.parse(expireDate);
if(new Date().before(date)){
return false; //在过期日期前,有效License
}
return true;
}
public static void main(String[] args) {
final String expireDate = "2020-01-01 12:00:00";
new Thread(new Runnable() {
@Override
public void run() {
while (true){
String time = "==当前时间:" + DATA_FORMAT.format(new Date()) + "== ";
try {
if (checkLicense(expireDate)) {
System.err.println(time + "Licence 过期");
}else {
System.out.println(time + "Licence 正常 " + expireDate);
}
TimeUnit.SECONDS.sleep(5);
} catch (ParseException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
2.2 Agent实现
package com.thonsun.sec.agent;
import javassist.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
/**
* @Project: java-agent
* @Desc:
* @Author: thonsun
* @Create: 2020/12/27 15:54
**/
public class CrackLicense {
private static final String HOOK_CLASS = "com.thonsun.sec.agent.License";
//java agent
public static void premain(String args,final Instrumentation inst) {
loadAgent(args,inst);
}
//java attach
public static void agentmain(String args,final Instrumentation inst) {
loadAgent(args,inst);
}
//load agent
private static void loadAgent(String arg,final Instrumentation inst) {
ClassFileTransformer classFileTransformer = createClassFileTransformer();
// 添加自定义的Transformer,第二个参数true表示是否允许Agent Retransform,
// 需配合MANIFEST.MF中的Can-Retransform-Classes: true配置
inst.addTransformer(classFileTransformer,true);
for (Class clazz : inst.getAllLoadedClasses()) {
//重新加载HOOK_CLASS 类
String name = clazz.getName();
if (inst.isModifiableClass(clazz) && name.equals(HOOK_CLASS)) {
try {
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
//create classfile transformer
private static ClassFileTransformer createClassFileTransformer() {
return new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/",".");
if (className.equals(HOOK_CLASS)) {
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod checkLicenseMethod = ctClass.getDeclaredMethod(
"checkLicense",
new CtClass[]{classPool.getCtClass("java.lang.String")}
);
//方法前插入
//System.out.println("[javagent] Liscense expire " + $1);
checkLicenseMethod.insertBefore("System.out.println(\"[javagent] Liscense expire \" + $1);");
//修改返回值
checkLicenseMethod.insertAfter("return false;");
classfileBuffer = ctClass.toBytecode();
} catch (IOException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
};
}
}
三、Agent模式
将agent打包到javasec-agent.jar
3.1 添加MANIFEST.MF
Premain-Class: com.thonsun.sec.agent.CrackLicense
Agent-Class: com.thonsun.sec.agent.CrackLicense
Can-Redefine-Classes: true
Can-Retransform-Classes: true
3.2 MAVEN配置
<build>
<finalName>javasec-agent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven-jar-plugin.version}</version>
<configuration>
<archive>
<manifestFile>src/main/resources/${manifest-file.name}</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade-plugin.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>MANIFEST.MF</exclude>
<exclude>META-INF/maven/</exclude>
</excludes>
</filter>
</filters>
<artifactSet>
<includes>
<include>org.javassist:javassist:jar:*</include>
</includes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
整个项目结构
3.3 打包
mvn clean install
3.4 运行效果
没有加载agent时License的运行效果:
指定java 运行的参数加载上面打包好的javasec-agent.jar运行License程序
java -javaagent:target/javasec-agent.jar -classpath tartget/classes/ com.thonsun.sec.agent.License
可以看到License的逻辑已经改变
四、Attach模式
attach模式可以将agent以附加的模式加载到指定pid的jvm中,获取jvm中所有运行的进程可以通过java提供的命令 jps -l
在编程接口上jdk 提供了com.sun.tools.attach 支持,这个jar包在安装jdk就在本机的lib/tools.jar:
package com.thonsun.sec.agent;
import com.sun.tools.attach.*;
import java.io.IOException;
/**
* @Project: java-agent
* @Desc: 动态注入javagent 到 进程id
* @Author: thonsun
* @Create: 2020/12/27 16:55
**/
public class AttachAgent {
private static final String HOOK_NAME = "com.thonsun.sec.agent.License";
private static final String AGENT_PATH = "D:/MyCode/JavaCode/java-agent/demo01/target/javasec-agent.jar";
public static void main(String[] args) {
for (VirtualMachineDescriptor desc : VirtualMachine.list()) {
if (desc.displayName().equals(HOOK_NAME)) {
System.out.println(desc.id() +" "+ desc.displayName());
try {
//attach pid jvm
VirtualMachine vm = VirtualMachine.attach(desc.id());
//load agent
vm.loadAgent(AGENT_PATH);
vm.detach();
} catch (AttachNotSupportedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
}
}
}
}
在运行着License程序之外在启动AttachAgent