JAVA Agent实现RASP技术(一)---premain
RASP技术全称(Runtime Application Self Protect)运行态应用自防护技术,是Gartner提出未来应用安全领域的重要技术,由应用自身做好危险函数执行的防护和阻断。RASP技术的实现基于Hook技术,通过反射、代理等技术,监控应用运行的细节内容(精确到类的加载,方法的执行),并阻断危险动作的运行。基于RASP技术,可以使应用在非侵入的可情况下,实现漏洞的整改与防护。
基于JAVA的RASP防护,目前普遍从java.lang.instrument
入手,其自JDK1.5后引入,主要目的是为开发人员提供监控和记录整个JVM运行的方法。
JAVA Instrumentation Agent
JAVA中基于Instrumentation
类约定俗成了两个类方法名,premain和agentmain。官方文档见https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/package-summary.html。
- premain方法
- 定义方式:
public static void premain(String agentArgs, Instrumentation inst)
- 使用方法:将agent.jar在应用运行前通过
java -javaagent:<jar 路径> -jar 应用程序.jar
,绑定JVM,实现class文件的修改。
- agentmain方法
- 定义方式:
public static void agentmain(String agentArgs, Instrumentation inst)
- 使用方法:希望修改的类正在运行状态,我们将agent.jar文件主动运行,agent程序会获取目前所有运行的JVM,通过过滤条件,找到希望修改的jvm-id,然后通过
VirtualMachine.attach()
方法,将agent注入,实现class文件的修改。
类都是classloader载入的,使用agent后,修改动作是在如下场景时生效:
-
addTransformer,当class被装载时(loadclasss),与ClassFileTransformer(我们需要实现其transform方法)结合,转换单个class文件(可以根据拿到的classname进行匹配)
-
redefineClasses,支持多个class文件的转换(自己指定好要转换哪些类,比如类aaa,即aaa.class)
接下来,介绍一下premain的实现示例。
premain-agent
测试目标程序
- Main.java
package org.gongz;
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
People p = new People();
p.setAge(18);
p.setName("杨超越");
System.out.println(p.toString());
}
}
- People.java
package org.gongz;
public class People {
private int age;
private String name;
public int getAge() {
return age;
}
@Override
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
通过maven将项目打成一个jar包,并在pom.xml中指明主类,防止运行报错。可参考https://blog.csdn.net/londa/article/details/115098901学习maven打包相关知识。
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.gongz</groupId>
<artifactId>helloWorld</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Main-Class>org.gongz.Main</Main-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
打包成helloWorld.jar,测试目标程序修改前运行效果如下:
premain agent程序
- PreAgent.java
package org.gongz;
import java.lang.instrument.Instrumentation;
/*
Instrumentation类可以与JVMI通讯,实现类加载时获取类信息,当然也可以改变类信息。
改变类信息就涉及到修改加载到内存中的java的字节码(class文件)
修改字节码工具有两个,一个是javassist,较为人性化,效率稍低。
另一个是asm,偏底层,复杂但效率高。
修改class字节码是通过实现ClassFileTransformer接口,并将实现类注册到Instrumentation中。
这样一旦有class的加载或者重载,这个修改就会生效。
*/
public class PreAgent {
// premain方法
public static void premain(String args, Instrumentation inst) throws Exception {
inst.addTransformer(new ClassHacker(), true);
}
}
- ClassHacker.java
package org.gongz;
import javassist.*;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ClassHacker implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("org/gongz/People")){
System.out.println("loader:"+loader);
System.out.println("className:"+className);
String loadName = className.replaceAll("/", ".");
try {
// 使用类加载器和原始class文件二进制码创建pool、获取类
ClassPool classPool = new ClassPool();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
// 网上找到其他教程大多使用下面这种方法获取class文件,我试了好像也可以
// stackoverflow中我找到类上面的获取方式,我觉得也挺合理的,试了也可以,就是用来上面的代码
// ClassPool classPool = ClassPool.getDefault();
// CtClass ctClass = classPool.get(loadName);
// 获取类中的方法
CtMethod ctMethod = ctClass.getDeclaredMethod("setName");
ctMethod.insertAfter("this.name=\"鞠婧祎\";");
byte[] newClassBytes = ctClass.toBytecode();
ctClass.detach();
return newClassBytes;
} catch (Throwable e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
注意,由于修改class文件这里使用了javassist包,所以我们要在maven的pom.xml中引入依赖,同时打包时要注意将依赖一起打到jar包中,否则agent程序运行会出现javassist找不到的情况。
这里还有一个坑点,我之前在网上看的资料引入javassist时,都会使用<groupId>javassist</groupId>
这个groupId,而最新的Id应该是org.javassist
。这导致maven中匹配的版本不对,找不到包,只有版本非常低的。而版本非常低的javassist无法修改较新版本的jdk的class文件。这里推荐大家使用新版本的javassist,避免遇到无法找到类,或者无法修改等各种各样奇怪的问题。
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.gongz</groupId>
<artifactId>premainDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>org.gongz.PreAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
通过maven打包,会生成两个agent.jar的包,一个带依赖、一个不带依赖,我们选择后缀带依赖的包。
运行测试
java -jar -javaagent:./premainDemo-1.0-SNAPSHOT-jar-with-dependencies.jar helloWorld-1.0-SNAPSHOT.jar
进行agent运行和修改,得到以下结果,我们可以看到人名已经被修改成我们希望的值了。
总结
基于上述的操作方法,我们可以获取到类的详细信息,甚至修改类的逻辑代码。有了这样的方式,我们就可以将其运用到java的rasp技术中去,在agent中定义需要观察的类和方法,对其进行日志记录和监控,发现威胁时直接修改类的代码组织其运行,具体我也在学习中,这里只做一个简单的实现。
通过这次实验,我也发现了java通过premain和agentmain给了开发者非常大的权利去控制其他jvm的行为,这种权限最先是希望开发者更好的监控程序的运行,反之也可以被运用于安全防护和破坏,还是非常具有危险性的一个功能。