作者:yueji0j1anke
首发于公号:剑客古月的安全屋
字数:4251
阅读时间: 25min
声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。本文章内容纯属虚构,如遇巧合,纯属意外
目录
-
前言
-
RASP
-
实现demo
-
深化
-
总结
0x00 前言
之前讲eBPF,感觉底层实现原理上实现有点像rasp,转过头来发现自己好像没怎么设计学习到rasp,特此补充该专题,扩大一些知识面
0x01 RASP
1.什么是rasp
全名Runtime application self-protection,将防护功能注入应用程序,通过少数hook函数检测程序运行,并实时施行阻断
这里我以java rasp举例(别的咱也不懂)
之前将内存马的时候,说过java语言的instrumentation功能,我们可以利用此编写agent,通过premain和agentmain加入检测类。
核心功能点为instrument下的classfiletransfomrer和instrumentation api功能点,
classfiletransfomrer允许在类被加载(或重新定义)之前修改其字节码,而instrumentation用于注册 ClassFileTransformer
,并控制类加载、重新定义等行为,通过类中的transformer检测字节码文件中是否存在一些恶意的类。
简要的来说,其可以通过注入jvm完成对应用的实时监控与阻断
0x02 实现Demo
1.premain
javaagent是java命令提供的一个参数,这个参数可以指定一个jar包,在真正的程序没有运行之前先运行指定的jar包。并且对jar包有两个要求:
-
jar包的MANIFEST.MF文件必须指定Premain-Class
-
Premain-Class指定的类必须实现premain()方法。
简要来说,该premain方法会在java指定的main函数运行之前启动
同时,在premain方法执行时,获取的Instrumentation对象会加载类,包括main方法需要加载的各种类,但抓取不到系统类。这也给了我们机会结合ASM、javassist、cglib方式就可以实现对类的改写或者插桩
大致步骤如下
1.创建premain-class指定类,继承premain方法
2.打包成jar文件
3.启动java,指定参数 -javaagent:xx.jar,即可执行premain函数
下面开始实施demo
1.1 初步尝试
结构如下
rasp-demo/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── rasp/
│ └── example/
│ ├── premain.java // premain 入口类
├── resources/
│ └── META-INF/
│ └── MANIFEST.MF // 指定 Premain-Class
└── pom.xml // Maven 构建文件(或 build.gradle)
pom文件加入配置信息
<build>
<plugins>
<!-- 配置生成 agent jar 的 manifest -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.rasp.demo.PreMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!-- 普通 main 方法入口 -->
<Main-Class>com.rasp.demo.DemoApplication</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<!-- Spring Boot 插件(如果有主程序用 Spring Boot) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
premain方法如下
随后创建个main
用maven打包后,使用java命令行运行
java -javaagent:.agent-0.0.1-SNAPSHOT.jar -jar .demo-0.0.1-SNAPSHOT.jar agentArgs:null
可以看到明确加载的类,有一个初步demo,我们继续深化一下
2.agentmain
premain需要在main函数启动前执行agent,但大多数时候,服务不重启需要持续运行,这个时候如何对jvm中的类进行修改呢。agentmain启动!!
跟了下源码,具体流程如下
通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法
对应底层则是native方法
首先我们启动一个长时间的jvm
打包成jar文件运行
写agentmain方法
pom文件记得添加配置
<Agent-Class>com.rasp.demo.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
随后利用attach功能注入进程(jps命令可查看)
0x03 深化
前面我们只是用instrumentation功能完成了premain和agentmain的初步功能,并没有涉及到任何防护。接下来我们需要通过其添加transformer获取所有加载的类,并对有危险类的方法进行参数获取,判断是否存在攻击
1.premain
我们这里接着前面premain的尝试,进行深化
main方法调用
packagecom.rasp.demo;
importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
importjava.io.IOException;
publicclassDemoApplication {
publicstaticclassDemoA{
publicvoidggg(){
System.out.println("Demo A类方法被调用了");
}
}
publicstaticvoidmain(String[] args) throwsInterruptedException, IOException {
System.out.println("-------主方法main调用开始-------");
Runtime.getRuntime().exec("calc");
Stringa="a";
System.out.println(a);
DemoAdemoA=newDemoA();
demoA.ggg();
System.out.println("-------主方法main调用结束-------");
}
}
premain调用
packagecom.rasp.demo;
importjava.lang.instrument.ClassFileTransformer;
importjava.lang.instrument.IllegalClassFormatException;
importjava.lang.instrument.Instrumentation;
importjava.security.ProtectionDomain;
publicclassPreMain {
publicstaticvoidpremain(StringagentArgs, Instrumentationinst){
System.out.println("++++++++Premain start++++++++");
System.out.println(ClassLoader.getSystemClassLoader().toString()); // 查看当前代理类是被哪个类加载器加载的
inst.addTransformer(newDefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
}
staticclassDefineTransformerimplementsClassFileTransformer{
@Override
publicbyte[] transform(ClassLoaderloader, StringclassName, Class<?>classBeingRedefined, ProtectionDomainprotectionDomain, byte[] classfileBuffer) throwsIllegalClassFormatException {
System.out.println(className.toString() +" "+loader.toString()); // 类名 和 类加载器
System.out.println("n");
returnclassfileBuffer;
}
}
}
javaagent执行可以看到
并没有获取到runtime等执行具体类,我们的transformer没有截获到,这也涉及到了类加载机制,这里简单介绍一下
Java 的类加载器采用 双亲委派机制,即:
-
AppClassLoader
(应用类加载器) → 委派给ExtClassLoader
(扩展类加载器) → -
ExtClassLoader
→ 委派给BootstrapClassLoader
(引导类加载器)
假设你在 transform
方法中修改了 java.lang.String
的某个方法,插入了对你自己写的 MyLogger.log()
的调用,但 MyLogger
是由 AppClassLoader
加载的:
❗ 然而
String
是由BootstrapClassLoader
加载的,它不能找到AppClassLoader
加载的类(因为双亲委派机制是“向上找”,不会向下)。
所以运行时会报错:
正确解决方案:
inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath));
-
inst
是Instrumentation
实例; -
appendToBootstrapClassLoaderSearch()
是Instrumentation
提供的一个方法; -
它的作用是:将指定的 jar 包添加到 Bootstrap ClassLoader 的搜索路径中。
解决原因:
-
你自己写的代理类
MyLogger
会被 BootstrapClassLoader 加载; -
那些系统类(比如
java.lang.String
)就可以调用你的代理类,不会再报NoClassDefFoundError
。
1.1深化:
接下来我们尝试修改字节码,原理如下:ClassFileTransformer#transform中返回新的字节码,该字节码可以被覆盖重写
这里我们检测ProcessBuilder调用
publicstaticvoidmain(String[] args) throwsInterruptedException, IOException {
System.out.println("-------主方法main调用开始-------");
ProcessBuilderprocessBuilder=newProcessBuilder();
processBuilder.command("cmd", "/c", "ls");
Processprocess=processBuilder.start();
InputStreaminputStream=process.getInputStream();
BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(inputStream, "gbk"));
System.out.println(bufferedReader.readLine());
System.out.println("-------主方法main调用结束-------");
}
对应的premain拦截
publicstaticvoidpremain(StringagentArgs, Instrumentationinst) throwsIOException, UnmodifiableClassException {
System.out.println("n");
ProcessBuilderprocessBuilder=newProcessBuilder();
System.out.println("[info] 我是未拦截前的调用哦~~~~");
processBuilder.command("cmd", "/c", "chdir");
Processprocess=processBuilder.start();
BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(process.getInputStream(), "gbk"));
System.out.println(bufferedReader.readLine());
// 添加ClassFileTransformer类
ProcessBuilderHookprocessBuilderHook=newProcessBuilderHook(inst);
inst.addTransformer(processBuilderHook, true);
// 获取所有jvm中加载过的类,对已加载类进行重新转换
Class[] allLoadedClasses=inst.getAllLoadedClasses();
for (ClassaClass : allLoadedClasses) {
if (inst.isModifiableClass(aClass) &&!aClass.getName().startsWith("java.lang.invoke.LambdaForm")){
// 调用instrumentation中所有的ClassFileTransformer#transform方法,实现类字节码修改
inst.retransformClasses(newClass[]{aClass});
}
}
System.out.println("++++++++++++++++++hook finished+n");
}
这里我们在没拦截前进行一次调用尝试。
packagecom.rasp.demo.ClassHook;
importjava.io.IOException;
importjava.lang.instrument.ClassFileTransformer;
importjava.lang.instrument.Instrumentation;
importjava.security.ProtectionDomain;
importjavassist.*;
publicclassProcessBuilderHookimplementsClassFileTransformer {
privateInstrumentationinst;
privateClassPoolclassPool;
publicProcessBuilderHook(Instrumentationinst){
this.inst=inst;
this.classPool=newClassPool(true);
}
publicbyte[] transform(ClassLoaderloader, StringclassName, Class<?>classBeingRedefined, ProtectionDomainprotectionDomain, byte[] classfileBuffer) {
if (className.equals("java/lang/ProcessBuilder")){
CtClassctClass=null;
try {
// 找到ProcessBuilder对应的字节码
ctClass=this.classPool.get("java.lang.ProcessBuilder");
// 获取所有method
CtMethod[] methods=ctClass.getMethods();
// $0代表this,这里this = 用户创建的ProcessBuilder实例对象
Stringsrc=
"System.out.println("[Hook] 拦截到命令执行: ");"+
"for (Object arg : $0.command()) {"+
" System.out.println("参数: " + arg);"+
"}"+
"if ($0.command().get(0).equals("cmd")) {"+
" System.out.println("[Hook] 检测到 cmd 执行,已拦截。");"+
" return null;"+
"}";
for (CtMethodmethod : methods) {
// 找到start方法,并插入拦截代码
if (method.getName().equals("start")){
method.insertBefore(src);
break;
}
}
classfileBuffer=ctClass.toBytecode();
}
catch (NotFoundException|CannotCompileException|IOExceptione) {
e.printStackTrace();
}
finally {
if (ctClass!=null){
ctClass.detach();
}
}
}
returnclassfileBuffer;
}
}
这里hook processbulider参数和命令执行,并进行拦截检测
pom文件对应修改
最后效果展示
java -javaagent:.demo-0.0.1-SNAPSHOT-jar-with-dependencies.jar -jar .client-0.0.1-SNAPSHOT.jar
最终效果如下
agentmain同理,只是需要main函数去实现注入,这里不过多演示。
0x04 总结
本篇出了最基本的rasp代码,在真实的生产环境中,插件的可延展性、对生产环境造成的cpu占有率影响、是否支持重启等都需要有多方面的考量。后续将根据一些开源产品和好的工具针对此方面作讲解
原文始发于微信公众号(剑客古月的安全屋):Java安全-RASP基础讲解&代码Demo
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论