从零开始的RASP学习

Uncategorized
4.9k words

前置介绍

运行时应用程序自我保护(RASP)是一种在应用上运行的技术,旨在实时检测针对应用程序的攻击。它将防护功能“注入”到应用程序中,与应用程序融为一体,通过Hook少量关键函数来实时观测程序运行期间的内部情况。当应用出现可疑行为时,RASP根据当前上下文环境精准识别攻击事件,并给予实时阻断,使应用程序具备自我防护能力,而不需要进行人工干预。

Java中的RASP主要作用就是Hook掉了一些恶意类​​,比如Runtime​​、ProcessBuilder​其中​。Runtime.exec​​调用的是ProcessBuilder.start​​,ProcessBuilder.start​​的底层会调用ProcessImpl​​类。那么这时候只需要去Hook掉ProcessImpl​​就无法进行执行命令了。

Java Agent机制

提到RASP就绕不过Java Agent。一句话简单总结一下二者关系,Java Agent 是一种通用的机制,而RASP 是 Java Agent 专注于安全领域的一种特定用途

Java Agent 就像一个秘密特工,悄悄地在 Java 应用程序的幕后工作。它是一种特殊的软件,可以附加到运行中的 Java 进程,而无需修改应用程序的代码,即可对目标JVM造成影响,有点类似于之前写Frida的感觉,其中Java Agent有以下这么几个比较重要的特点或者说是功能需要我们掌握一下。

  1. 附加(Attachment):Java Agent 使用 -javaagent 命令行选项附加到运行中的 Java 进程。这个选项指定了 Java Agent JAR 文件的路径。当 Java 进程启动时,Agent 被加载。
  2. 字节码增强(Instrumentation):Java Agent 使用字节码增强技术来修改或增强 Java 类。它可以在方法前后添加代码、更改方法体,甚至创建新的类。
  3. 生命周期钩子(Lifecycle Hooks):Java Agent 有生命周期钩子,比如 premain 和 agentmain
    • premain:在应用程序的 main 方法之前执行。
    • agentmain:动态执行(例如,附加到运行中的进程时)。
  4. 类转换(Class Transformation):Agent 可以使用 ClassFileTransformer 转换类。它拦截类加载并修改字节码。

光看文字必然学不出来什么东西,让我们动手实践一下

Agent 实例(一)

通常情况下,我们可以用以下命令启动Agent机制。

1
java -javaagent:/path/to/myagent.jar -jar myapp.jar

j这里主要看到-javaagent这个参数,javaagent是java命令提供的一个参数,这个参数可以指定一个jar包,在真正的程序没有运行之前先运行指定的jar包。并且该jar包有两个要求:

  • jar包的MANIFEST.MF文件必须指定Premain-Class
  • Premain-Class指定的类必须实现premain()方法。

值得一提的是,这里的javaagent可以指定多个jar,jvm会依次执行不同的jar中PreMainclass#premain()方法,而premain方法则有两种定义方式

1
2
3
public static void premain(String agentArgs, Instrumentation inst)

public static void premain(String agentArgs)

此处会优先调用第一种写法。这种方法可以在JDK1.5及之后的版本使用。现在,我们开始着手创造第一个agent,打开idea随便起一个maven项目,并将以下内容写入pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.PreMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

之后再准备好PreMain.java,这个transform之前没有接触过,其参数所代表实际含义如下所示,有兴趣可以看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PreMain {  
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("agentArgs:" + agentArgs);
inst.addTransformer(new DefineTransformer(), true);
//通过调用 addTransformer 方法注册类转换器后,当类被加载到内存时,Instrumentation 实例会调用相应的类转换器的transform方法。
}

// ClassFileTransformer 是 Java Instrumentation API 的一部分,用于在类字节码加载到 JVM 之前对其进行修改。
static class DefineTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load Class: " + className); // 注意这里的输出
return classfileBuffer;
}
}
//transform 方法的主要作用是在类被加载到 JVM 之前提供一个机会来查看和/或修改类的字节码。这是 Java Instrumentation API 的核心部分
}
  1. ClassLoader loader:
    • 这是要转换的类的类加载器。如果类是由引导类加载器加载的,则此参数可能为 null。类加载器是 Java 中用于加载类文件(.class 文件)到 JVM 的组件。
  2. String className:
    • 完全限定的类名,这个名称表示当前正在被 JVM 加载的类。
  3. classBeingRedefined:
    • 如果正在重新定义的类不是 null,则这是一个对该类的引用。在正常类加载过程中,这个参数通常是 null。它只在使用了类重新定义或类重转换功能时才有值,这些功能允许在运行时动态修改类的定义。
  4. ProtectionDomain protectionDomain:
    • 这是类的保护域。保护域是与类关联的一组权限,这些权限由类的加载器在类加载时赋予。保护域可以包含从哪个位置加载类的信息、类签名的信息,以及类可以执行的操作的权限信息。
  5. byte[] classfileBuffer:
    • 这是类的原始字节码数组。这些是从 .class 文件中读取的未经修改的字节码。在 transform 方法中,我们可以分析这些字节码、修改它们或者返回原始字节码。

准备完毕之后拿maven打个包

到这里我们的agent包就搞好了,再接着准备一下运行的jar包,

1
2
3
4
5
6
7
package org.example;  

public class Main {
public static void main(String[] args) {
System.out.println("Main.main() in test project");
}
}

同样还是用maven打包,只需要在<manifestEntries>标签下添加一个<Main-Class>即可。


运行结果如下,可以看到premain先按照逻辑输出了加载的各种类,然后再main执行,最后结束时,premain还加载了一些结束时需要的类,到此agent机制我们就算是碰到一点点门槛了。

Agent实例(二)

前面的方法需要在main函数启动前执行agent,但有些时候,jvm已经启动了,而且服务不能轻易暂停,这时候我们就可以引入此处要介绍的Attach机制了。jdk1.6之后在Instrumentation中添加了一种agentmain的代理方法,可以在main函数执行之后再运行。和premain函数一样,开发者可以编写一个包含agentmain函数的Java类,它也有两种写法:

1
2
3
public static void agentmain (String agentArgs, Instrumentation inst)
//还是第一种优先调用
public static void agentmain (String agentArgs)

attach机制的具体实现在com.sun.tools.attach中,这里介绍如下两个关键类:

  • VirtualMachine 字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等), loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

    代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。

  • VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能

具体实现过程: 通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

这里跟着实操一下,首先是起一个可以长时间运行的jar包,打包之类的过程不再细谈,直接看源码

1
2
3
4
5
6
7
8
package org.example;
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("Main.main() in test project start!!");
Thread.sleep(300000000);
System.out.println("Main.main() in test project end!!");
}
}

运行结果如下所示,这个32420的pid我们记录一下,之后要用到。

整完这个jar包,我们再整一个agent包,其中pom.xml部分把<Agent-Class>org.example.AgentMain</Agent-Class>添加到manifestEntries即可

1
2
3
4
5
6
public class AgentMain {  
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("agentmain start!");
System.out.println(instrumentation.toString());
}
}

打完Jar包之后,我们直接在idea里起个java程序,正儿八经开始我们的测试,这里可能会报不存在com.sun.tools,手动添加一下tools.jar就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;  

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 AttachTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine attach = VirtualMachine.attach("32420"); // 刚才的jvm的进程号
attach.loadAgent("F:\\RaspDemo2\\Myagent.jar");
attach.detach();
System.out.println("xxxx");
}
}

可以看到运行结果发生了改变,在Test.jar中成功执行了agentmain()的逻辑,拿到了Instrumentation对象。之后我们就可以通过对Instrumentation对象添加transformer类,来实现类转换(Class Transform),也就是在transform函数中结合修改字节码的方法(ASM、Javassist、cglib等)来进一步实现RASP。

RASP简易Demo

前置探索

在前面的例子中,我们可以通过Instrumentation对象来添加 Transformer来对对加载的类进行动态修改,那么显然我们可以通过对危险类进行动态修改监听来实现RASP。但实际情况确往往没有这么简单,我们可以用下面手敲下面几个例子来做个简单分析。

Main入口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {  
public static class A{
public void t(){System.out.println("Main$A.t()");}
}

public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("-------Main.main() start-------");
Runtime.getRuntime().exec("calc");
String a = "a";
System.out.println(a);
A a1 = new A();
a1.t();
System.out.println("-------Main.main() end-------");
}
}

PreMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PreMain {  
public static void premain(String agentArgs, Instrumentation inst) throws IOException {
System.out.println("++++++++Premain start++++++++");
System.out.println(ClassLoader.getSystemClassLoader().toString());
// 查看当前代理类是被哪个类加载器加载的
inst.addTransformer(new DefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
}

public static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className.toString() + " " + loader.toString());
// 类名 和 类加载器
System.out.println("");
return classfileBuffer;
}
}
}

按照之前的配置打jar包运行,结果如下

注意一下,这里的PreMain和Main这种我们自己编写的类都是由AppClassLoader来进行加载的,非常符合我们对于双亲委派的认知,但这种加载方式却会对我们RASP实现造成一定的干扰,我们跟着看下两个比较简单的例子。先将如下两个部分插入pom.xml对应位置中去

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
<plugin>  
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<!-- 指定Agent主类 -->
<manifestEntries>
<Premain-Class>org.example.PreMain</Premain-Class>
<Main-Class>org.example.Main</Main-Class>
<Agent-Class>org.example.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>


<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

之后于org.example创建一个被代理的A类

1
2
3
4
5
6
7
package org.example;  

public class A {
public void call(){
System.out.println("The A is called!");
}
}

修改PreMain代码,对A类的call方法进行代理拦截,具体实现主要依赖javasist修改字节码来完成。

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
package org.example;  

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.io.IOException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;

public class PreMain {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("++++++++Premain start++++++++");
inst.addTransformer(new DefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
}

public static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if (className.equals("org/example/A")) {
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.get("org.example.A");
CtMethod method = ctClass.getDeclaredMethod("call");

method.insertBefore("{ System.out.println(\"Before call method\"); }"); // 在call方法执行前插入代码
byte[] byteCode = ctClass.toBytecode();
ctClass.detach();
return byteCode;
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
}

Main这块也做个小改,调用a.call()

1
2
3
4
5
6
7
8
9
10
package org.example;  
import java.io.IOException;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("-------Main.main() start-------");
A a = new A();
a.call();
System.out.println("-------Main.main() end-------");
}
}

运行结果如下,成功代理拦截A#call()

理论上通过这种方法,我们就可以实现对任意method的拦截修改,不过这里还是要改一改代码接着研究,Main的类源码如下所示,插了一段zip的压缩代码和经典弹计算机,重点看下我下面贴的这张图,此处的newFileSystem最终会调用到ZipFileSystemProvider#newFileSystem()

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
package org.example;  
import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws Exception {
System.out.println("-------Main.main() start-------");
A a = new A();
Runtime.getRuntime().exec("calc");
Path zipPath = Paths.get("example.zip");
URI uri = URI.create("jar:" + zipPath.toUri());
Map<String, String> env = new HashMap<>();
env.put("create", "true");
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env)) {
Path externalTxtFile = Paths.get("F:\\test.txt");
Path pathInZipfile = zipfs.getPath("/somefile.txt");
// Copy a file into the zip file
Files.copy(externalTxtFile, pathInZipfile, StandardCopyOption.REPLACE_EXISTING);
}
System.out.println("-------Main.main() end-------");
}
}

PreMain代码如下,拦截ZipFileSystemProvider#newFileSystem()

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
public class PreMain {  
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("++++++++Premain start++++++++");
inst.addTransformer(new DefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
}

public static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if (className.equals("com/sun/nio/zipfs/ZipFileSystemProvider")) {
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.get("com.sun.nio.zipfs.ZipFileSystemProvider");
CtMethod method = ctClass.getDeclaredMethod("newFileSystem");

method.insertBefore("{ System.out.println(\"Before call method\"); }"); // 在call方法执行前插入代码
byte[] byteCode = ctClass.toBytecode();
ctClass.detach();
return byteCode;
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
}

拦截是成功的,但让我们继续换一种拦截方式看看

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
public class PreMain {  
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("++++++++Premain start++++++++");
inst.addTransformer(new DefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
}

public static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if (className.equals("com/sun/nio/zipfs/ZipFileSystemProvider")) {
System.out.println("类名:"+className+" 类加载器:"+loader.toString());
System.out.println(ClassLoader.getSystemClassLoader());
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.get("com.sun.nio.zipfs.ZipFileSystemProvider");
CtMethod method = ctClass.getDeclaredMethod("newFileSystem");

// 在方法执行前插入对 org.example.B.before 的调用
String codeToInsert = "org.example.B.before();";
method.insertBefore(codeToInsert);

byte[] byteCode = ctClass.toBytecode();
ctClass.detach();
return byteCode;
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
}
---------------------------------------------------------------------------------
public class B {
public static void before(){
System.out.println("B called");
}
}
--------------------------------------------------------------------------------

发现提示ClassNotFound,这里的原因可以归结到双亲委派这一块。虽然同样都是用javaasist修改字节码,但法一的修改方式并不涉及到类加载层面的问题,而法二method.insertBefore("org.example.B.before();") 却并不尽然地对org.example.B进行了类加载。在法二形势下,用来加载ZipFileSystemExtClassloader被强行用来加载本该由AppClassLoader进行加载的org.example.B,依据双亲委派原则,ExtClassLoader在找不到类的情况只得向上委派给更高层级的BootstrapClassLoader,而无法将问题抛给更低层级的AppClassLoader,最终导致ClassNotFound

这里本来是想Hook一下ProcessBuilder类的,但发现在类加载hook的模式没有办法解决这个问题,诸如ProccessBuilder的系统类早在JVM启动初期就已经完成了加载,其加载进度相较于PreMain更加提前,故难以通过对类加载hook来对此种系统类进行修改。

Instrumentation详探

为了方便用户对JVM进行操作,JDK1.5之后引入了这个Instrumentation特性,通过Instrumentation的实例对象,可以对jvm进行一定的操作,例如修改字节码、插桩等等。

它的实现原理是JVMTI(JVM Tool Interface),即JVM向用户提供的操作jvm的接口。JVMTI是事件驱动的,当发生一定的处理逻辑时,才会调用回调接口,而这些接口可以让用户扩展一些逻辑。例如前面的transform函数调用,就是JVMTI监听到类加载,就会基于这个事件,回调instrumentation中的所有ClassTransformer.transform函数,进行类转换(Class transform)。所以我们可以理解为获得instrumentation对象,就可以实现对一个jvm的一定操作,获取的这个对象的方法就是前文提到的javaagent和attach方法。

然后以下是一些常用的Instrumentation方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
//注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer实例,相当于它们通过了redefineClasses方法进行重定义。布尔值参数canRetransform决定这里被重定义的类是否能够通过retransformClasses方法进行回滚。
void addTransformer(ClassFileTransformer transformer)
//相当于addTransformer(transformer, false),也就是通过ClassFileTransformer实例重定义的类不能进行回滚。
boolean removeTransformer(ClassFileTransformer transformer)
//移除(反注册)ClassFileTransformer实例。
void retransformClasses(Class<?>... classes)
//已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer的列表中进行处理。
void appendToBootstrapClassLoaderSearch(JarFile jarfile)
//指定 JAR 文件,放到Bootstrap ClassLoader搜索路径
void appendToSystemClassLoaderSearch(JarFile jarfile)
//将某个jar加入到Classpath里供AppClassloard去加载。
Class[] getAllLoadedClasses()//返回 JVM 当前加载的所有类的数组
Class[] getInitiatedClasses(ClassLoader loader)//获取所有已经被初始化过了的类。
boolean isModifiableClass(Class<?> theClass)//确定一个类是否可以被 retransformation 或 redefinition 修改
void redefineClasses(ClassDefinition... definitions)//重定义类,也就是对已经加载的类进行重定义,ClassDefinition类型的入参包括了对应的类型Class<?>对象和字节码文件对应的字节数组

JVMTIAgent(JVMTI Agent)是实现了 JVMTI 规范的代理程序,它是基于 JVMTI 接口开发的具体应用,简而言之,JVMTI 是一种规范和接口,定义了与 Java 虚拟机交互的方式,而 JVMTIAgent 则是基于 JVMTI 规范实现的具体代理程序,用于实现各种监控、调试和分析功能。像这里的Instrumentation就可以理解为一种JVMTI Agent,而以上两种JavaAgent例子其实就是两种不同的
instrument agent运行过程,我们可以用下面这种方式来描述他们。

一、启动时加载instrument agent过程:

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

二、运行时加载instrument agent过程:

通过 JVM 的attach机制来请求目标 JVM 加载对应的agent,过程大致如下:

  1. 创建并初始化JPLISAgent;
  2. 解析 javaagent 里 MANIFEST.MF 里的参数;
  3. 创建 InstrumentationImpl 对象;
  4. 监听 ClassFileLoadHook 事件;
  5. 调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。

最终解决方案

通过以上对于Instrumentation的具体学习,我们显然有了突破限制的思路。虽然没有办法在类加载阶段修改字节码,但我们可以等到所有类都加载进JVM之后再对字节码来进行修改,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
28
29
30
31
32
package org.example;  
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.io.IOException;

public class PreMain {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
// 先测试一次使用ProcessBuilder获取当前路径
System.out.println("\n");
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", "chdir");
Process process = processBuilder.start();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "gbk"));
System.out.println(bufferedReader.readLine());

// 添加ClassFileTransformer类
ProcessBuilderHook processBuilderHook = new ProcessBuilderHook(inst);
inst.addTransformer(processBuilderHook, true);

// 获取所有jvm中加载过的类
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class aClass : allLoadedClasses) {
if (inst.isModifiableClass(aClass) && !aClass.getName().startsWith("java.lang.invoke.LambdaForm")){
// 调用instrumentation中所有的ClassFileTransformer#transform方法,实现类字节码修改
inst.retransformClasses(new Class[]{aClass});
}
}
System.out.println("++++++++++++++++++hook finished++++++++++++++++++\n");
}
}

Main部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;  
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Main {
public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("main start!");
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", "calc");
Process process = processBuilder.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "gbk"));
System.out.println(bufferedReader.readLine());
}
}

运行结果如下,成功实现简易版的RASP

如果这里想用之前ZipFileSystem那里提到的类加载式拦截也不是不可以,但是记得加上这么一段代码,把我们的代理类加到BootstrapClassloader里面去,这里就不实操了

1
// localJarPath为代理jar包的绝对路径 inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))