Java安全-RASP基础讲解&代码Demo

admin 2025年5月15日10:14:05评论3 views字数 8535阅读28分27秒阅读模式

作者: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方法如下

Java安全-RASP基础讲解&代码Demo

随后创建个main

Java安全-RASP基础讲解&代码Demo

用maven打包后,使用java命令行运行

java -javaagent:.agent-0.0.1-SNAPSHOT.jar -jar .demo-0.0.1-SNAPSHOT.jar agentArgs:null

Java安全-RASP基础讲解&代码Demo

可以看到明确加载的类,有一个初步demo,我们继续深化一下

2.agentmain

premain需要在main函数启动前执行agent,但大多数时候,服务不重启需要持续运行,这个时候如何对jvm中的类进行修改呢。agentmain启动!!

跟了下源码,具体流程如下

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

对应底层则是native方法

首先我们启动一个长时间的jvm

Java安全-RASP基础讲解&代码Demo

打包成jar文件运行

写agentmain方法

Java安全-RASP基础讲解&代码Demo

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命令可查看)

Java安全-RASP基础讲解&代码Demo

Java安全-RASP基础讲解&代码Demo

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[] argsthrowsInterruptedExceptionIOException {
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(StringagentArgsInstrumentationinst){
System.out.println("++++++++Premain start++++++++");
System.out.println(ClassLoader.getSystemClassLoader().toString());  // 查看当前代理类是被哪个类加载器加载的
inst.addTransformer(newDefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
    }

staticclassDefineTransformerimplementsClassFileTransformer{
@Override
publicbyte[] transform(ClassLoaderloaderStringclassNameClass<?>classBeingRedefinedProtectionDomainprotectionDomainbyte[] classfileBufferthrowsIllegalClassFormatException {
System.out.println(className.toString() +"    "+loader.toString());  // 类名 和 类加载器
System.out.println("n");
returnclassfileBuffer;
        }
    }
}

javaagent执行可以看到

Java安全-RASP基础讲解&代码Demo

并没有获取到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[] argsthrowsInterruptedExceptionIOException {
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(StringagentArgsInstrumentationinstthrowsIOExceptionUnmodifiableClassException {
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(processBuilderHooktrue);

// 获取所有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(ClassLoaderloaderStringclassNameClass<?>classBeingRedefinedProtectionDomainprotectionDomainbyte[] 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 

Java安全-RASP基础讲解&代码Demo

最终效果如下

agentmain同理,只是需要main函数去实现注入,这里不过多演示。

0x04 总结

本篇出了最基本的rasp代码,在真实的生产环境中,插件的可延展性、对生产环境造成的cpu占有率影响、是否支持重启等都需要有多方面的考量。后续将根据一些开源产品和好的工具针对此方面作讲解

原文始发于微信公众号(剑客古月的安全屋):Java安全-RASP基础讲解&代码Demo

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月15日10:14:05
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java安全-RASP基础讲解&代码Demohttps://cn-sec.com/archives/4060172.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息