文章首发于先知社区:RASP简单实现
前言
前边的Javassit、Javaagent其实都是为了RASP做铺垫,由于接触较少,这里也只是浅浅的做个测试,了解下RASP原理。
RASP
RASP(Runtime application self-protection,应用程序运行时防护),其与WAF等传统安全防护措施的主要区别于其防护层级更加底层——在功能调用前或调用时能获取访问到当前方法的参数等信息,根据这些信息来判定是否安全。
Rasp 与 Waf 区别
优点
- 误报率低:WAF 放置在 Web 应用程序外层,依赖于分析网络流量,拦截所有它认为可疑的输入而并不分析这些输入是如何被应用程序处理的。RASP 通过对应用程序上下文,会精确分析用户输入在应用程序里的行为,根据分析结果区分合法行为还是攻击行为,然后对攻击行为进行响应和处理。 RASP 不依赖于网络流量分析,大大减少了误报。
- 保护全面性:WAF 在分析与过滤用户输入并检测有害行为方面比较有效,但是对应用程序的输出检查则毫无办法。RASP 不但能监控用户输入,也能监控应用程序组件的输出,这就使 RASP 具备了全面防护的能力。RASP 解决方案能够定位 WAF 通常无法检测到的严重问题——未处理的异常、会话劫持、权限提升和敏感数据披露等等。
缺点
- 性能损耗:RASP 实时拦截、深入检测用户数据流,这是对精确度和误判率都有很大的帮助,但是对用户性能有一些影响,这些性能消耗也必然影响到用户的体验,这也是影响企业客户部署 RASP 的很大一方面原因。现在 RASP 的提供商在优化方面做了很大努力,大部分 RASP 对性能影响在 5% 左右。
- 部署成本:RASP 是针对应用程序的,每个应用程序都必须有独立的探针,不能像防火墙一样只在入口放置一个设备就可以了,并且需要根据应用开发的技术不同使用不同的 RASP。比如 PHP 应用与 Java 应用需要不同的 RASP 产品,增加了部署成本。
双亲委派
简单解释下就是,如下三个类,在JVM加载某个类时,会先从BootstrapClassLoader进行加载,如果它没有则会从ExtClassLoader,还没有则到AppClassLoader,最终到自定义的类加载器
- 启动类加载器(BootstrapClassLoader),由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
而默认情况下premain,agentmain都是由AppClassLoader加载的,用一个实例看一下
Main.java
内容随便,这个只是后边会用到
public class Main {
public static void main(String[] args) throws IOException {
ProcessBuilder command = new ProcessBuilder().command("cmd", "/c", "chdir");
Process process = command.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println(bufferedReader.readLine());
}
}
ClassLoaderDemo
public class ClassLoaderDemo {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
System.out.println(ClassLoader.getSystemClassLoader().toString());
}
}
分别打成jar包后执行:
java -javaagent:agent.jar=Sentiment -jar Main.jar
可以看到当前使用的类加载器是AppClassLoader
而这就会引起一个问题
问题
这里直接引用的参考文章里师傅写的内容,但是好像有些不准确的地方,因此简单了解下就好
agentmain和main都是同一个appClassLoader加载的,并且我们写好的各种类都是AppClassLoader加载的,那BootstrapClassLoader和extClassLoader加载的类调用我们写好的代理方法,这些类加载器向上委派寻找类时,扩展类加载器和引导类加载器都没有加过,直接违背双亲委派原则!举个例子,因为我们可以在transform函数里面获取到类字节码,并加以修改,如果我们在系统类方法前面插了代理方法,由于这些系统类是被Bootstrap ClassLoader加载的,当BootstrapClassLoader检查这些代理方法是否被加载时,直接就报错了,因为代理类是appClassLoader加载的
要解决这个问题,我们就应该想办法把代理类通过BootstrapClassLoader进行加载,从百度的OpenRASP可以学到解决方案:
// localJarPath为代理jar包的绝对路径
inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))
通过appendToBootstrapClassLoaderSearch
方法,可以把一个jar包放到Bootstrap ClassLoader的搜索路径,也就是说,当Bootstrap ClassLoader检查自身加载过的类,发现没有找到目标类时,会在指定的jar文件中搜索,从而避免前面提到的违背双亲委派问题。
RaspDemo
这里编写一个Hook ProcessBuilder执行cmd的简单例子,同时也遇到了上述提到的双亲委派问题,之后会提到。
Main.java
主程序不变
public class Main {
public static void main(String[] args) throws IOException {
ProcessBuilder command = new ProcessBuilder().command("cmd", "/c", "chdir");
Process process = command.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println(bufferedReader.readLine());
}
}
PreMainDemo
public class PreMainDemo {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals("java.lang.ProcessBuilder") && inst.isModifiableClass(aClass)){
inst.addTransformer(new TransformerDemo(),true);
inst.retransformClasses(aClass);
}
}
}
}
TransformerDemo
public class TransformerDemo implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
byte[] bytes = null;
if (className.equals("java/lang/ProcessBuilder")) {
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = null;
try {
ctClass = cp.get("java.lang.ProcessBuilder");
CtMethod[] methods = ctClass.getMethods();
String source = "if ($0.command.get(0).equals(\"cmd\")){\n" +
" System.out.println(\"Dangerous....\");\n" +
" System.out.println($0);\n" +
" return null;\n" +
"}";
for (CtMethod method : methods) {
if (method.getName().equals("start")) {
method.insertBefore(source);
break;
}
}
bytes = ctClass.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
}
return bytes;
}
}
双亲委派问题
上述PreMainDemo中,通过if判断,来找到JVM加载的ProcessBuilder类,进而触发transform对该类进行修改
if (aClass.getName().equals("java.lang.ProcessBuilder") && inst.isModifiableClass(aClass)){
但是当执行之后发现,JVM并没有加载ProcessBuilder类,那就无法通过if判断,触发transform
猜想
上述问题我上网找了一些资料,但各抒己见,所以说说我的看法。(仅个人观点,望师傅们指正!!!)
上边提到premain函数默认使用AppClassLoader进行加载的,并且我们在代码中也没有加载ProcessBuilder类,因此默认不会加载ProcessBuilder类。因为该类在rt.jar包中,而该包是由BootstrapClassLoader
进行加载的
解决
既然没有加载ProcessBuilder类,那就可以在遍历JVM加载类之前,用ProcessBuilder processBuilder = new ProcessBuilder();
对其进行加载即可。
public class PreMainDemo {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException, ClassNotFoundException, InstantiationException, IllegalAccessException {
ProcessBuilder processBuilder = new ProcessBuilder();
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals("java.lang.ProcessBuilder") && inst.isModifiableClass(aClass)){
inst.addTransformer(new TransformerDemo(),true);
inst.retransformClasses(aClass);
}
}
}
}
这里其实就可以在复制一遍Main中的代码,因为这样的话即可看出在触发Transofrom前后执行cmd
的结果
最终代码
public class PreMainDemo {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException, ClassNotFoundException, InstantiationException, IllegalAccessException {
ProcessBuilder command = new ProcessBuilder().command("cmd", "/c", "chdir");
Process process = command.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println(bufferedReader.readLine());
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals("java.lang.ProcessBuilder") && inst.isModifiableClass(aClass)){
inst.addTransformer(new TransformerDemo(),true);
inst.retransformClasses(aClass);
}
}
}
}
接着将Main.java和 打包
Main.jar
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Main-Class>RASP.Main</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
agent.jar
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>RASP.PreMainDemo</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>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
执行agent
java -javaagent:AgentMemory-1.0-SNAPSHOT-jar-with-dependencies.jar=Sentiment -jar AgentMemory-1.0-SNAPSHOT.jar
可以看到一开始执行了chdir输出了D:\java\AgentMemory\target
,因为此时还没触发transform,但之后触发后,输出了Dangerous,并返回了null,所以这里在InputStream inputStream = process.getInputStream();
爆了空指针异常,成功h
后记
本篇主要是为了简单了解一下RASP,其中可能有很多错误,尤其是在双亲委派的部分,望师傅们指正。
参考
从零开始的Java RASP实现(二) - bitterz - 博客园 (cnblogs.com)