@author:Drag0nf1y
本文首发于奇安信安全社区,现转载到个人博客。
原文链接:
https://forum.butian.net/share/1959
什么是RASP?
Rasp的概念
RASP(Runtime application self-protection)的概念,最早由Gartner在2014年提出,即“运行时自我保护”,指对应用程序的保护不应该单纯依赖于外部的系统,而应该使程序自身带有自我保护的能力。RASP在实现或构建的过程中会被链接到应用程序的运行环境中,并能够控制应用程序代码的执行,实时地检测和阻止攻击行为。
区别于传统的基于网络流量的安全检测设备,诸如WAF或者IDS产品,RASP将防御能力内嵌到应用本身里,在关键的方法调用之前,对执行的方法和参数进行安全校验。因此,相较于传统的基于流量的安全产品,会有更好的检出率和更低对误报率。同时由于在应用内部,接触到的数据对象都是解密完成的,能够直接对于各类加密、混淆绕过的攻击,和有着更好的防护能力。
Rasp的实现
以本文介绍的Java rasp为例,Java是通过Java Agent方式进行实现,具体是使用ASM(或者其他字节码修改框架)技术实现RASP技术。在jdk1.5之后,java提供一个名为Instrumentation的API接口,Instrumentation的最大作用,就是类定义动态改变和操作。开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)启动时,通过 – javaagent参数指定一个包含 Instrumentation 代理的 jar 文件,来启动 Instrumentation 的代理程序。
java.lang.instrument包是Java中来增强JVM上的应用的一种方式,机制是在JVM启动前或启动后attach上去修改方法的字节码。
例子:
以下为一个介绍rasp hook目标类原理的demo,属于本文的前置知识:
首先我们来创建一个agent,然后编写一个premain
方法,该方法会在main函数之前预先执行。这里的参数inst
是java.lang.instrument.Instrumentation
的一个实例,这个接口所对应的包集中了agent所需的所有方法。
package org.javaweb.test;
public class Agent{
public static void premain(String agentOps, Instrumentation inst) {
System.out.println("=======this is agent premain function=======");
}
}
然后我们需要再premain中添加一个自定义的Transformer。
关于Transformer的解释
这个方法可以转换目标类文件,并返回一个新的替换类文件。
添加一个新的Transformer类作为转换器:
package org.javaweb.test;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className.replace("/", "."));
return classfileBuffer;
}
}
(PS:多个转换器同时存在时,转换会由Transformer链来执行,一个tranform类返回的byte[]会作为下一个的classfilebuffer参数的输入)
之后在agent.java中修改premain方法:
System.out.println("=======this is agent premain function=======");
inst.addTransformer(new TestTransformer());
用maven将该方法打成一个jar包之后,我们再创建一个可以被hook的带有main方法的程序:
package org.javaweb.test;
public class TestAgent {
public static void main(String[] args) {
System.out.print("=======This is TestAgent Program!=======");
}
}
maven打包后,我们测试一下这个程序:
java -jar testagent.jar
可以看到输出了很多包的名字,因为我们在新增的TestTransformer中重新生成了。
以上就是通过Agent来修改目标类输出结果的示例。
OpenRasp介绍
简介
OpenRasp是百度开源的一款Rasp产品,社区热度很高,插件开发也十分简单,降低了使用门槛,也吸引了很多企业与研究者开始接触Rasp并将其运用到生产实践中。
OpenRASP利用js来编写规则,通过V8来执行js。这样可以更加方便热部署,以及规则的通用性。同时减少了为不同语言重复制定相同规则的问题。
OpenRasp启动流程
-
首先进入Agent.java的premain方法,这个方法会将agent.jar添加到
BootstrapClassLoader
的ClassPath下,这样 hook 由BootstrapClassLoader
加载的类的时候就能够成功调用到 agent.jar 中的检测入口。否则像java.io.File这样的类将无法加载(Java双亲委派机制,用户自定义的类将会使用SystemClassLoader)。public static void premain(String agentArg, Instrumentation inst) { init(START_MODE_NORMAL, START_ACTION_INSTALL, inst); }
public static synchronized void init(String mode, String action, Instrumentation inst) { try { JarFileHelper.addJarToBootstrap(inst); readVersion(); ModuleLoader.load(mode, action, inst); } catch (Throwable e) { System.err.println("[OpenRASP] Failed to initialize, will continue without security protection."); e.printStackTrace(); } }
-
然后调用ModuleLoader.load函数,ModuleContainer传入的ENGINE_JAR就是rasp-engine.jar:
public static synchronized void load(String mode, String action, Instrumentation inst) throws Throwable { if (Module.START_ACTION_INSTALL.equals(action)) { if (instance == null) { try { instance = new ModuleLoader(mode, inst); } catch (Throwable t) { instance = null; throw t; } } else { System.out.println("[OpenRASP] The OpenRASP has bean initialized and cannot be initialized again"); } } else if (Module.START_ACTION_UNINSTALL.equals(action)) { release(mode); } else { throw new IllegalStateException("[OpenRASP] Can not support the action: " + action); } }
private ModuleLoader(String mode, Instrumentation inst) throws Throwable { if (Module.START_MODE_NORMAL == mode) { setStartupOptionForJboss(); } engineContainer = new ModuleContainer(ENGINE_JAR); //传入的ENGINE_JAR就是rasp-engine.jar engineContainer.start(mode, inst); }
这里会从相关的配置文件中取出moduleEnterClassName值,即为com.baidu.openrasp.EngineBoot,然后去实例化这个类。也就是说这里的module是一个EngineBoot。
-
启动Engine,ModuleContainer加载了rasp-engine.jar之后,实例化并调用其口入口类EngineBoot的start方法,start方法中进行了:加载V8引擎、初始化插件系统、配置核查、初始化字节码转换模块、初始化云管理模块等操作。
public void start(String mode, Instrumentation inst) throws Throwable { module.start(mode, inst);//实际上调用的就是EngineBoot的start方法 }
EngineBoot的start方法:
public void start(String mode, Instrumentation inst) throws Exception { System.out.println("\n\n" + " ____ ____ ___ _____ ____ \n" + " / __ \\____ ___ ____ / __ \\/ | / ___// __ \\\n" + " / / / / __ \\/ _ \\/ __ \\/ /_/ / /| | \\__ \\/ /_/ /\n" + "/ /_/ / /_/ / __/ / / / _, _/ ___ |___/ / ____/ \n" + "\\____/ .___/\\___/_/ /_/_/ |_/_/ |_/____/_/ \n" + " /_/ \n\n"); try { Loader.load();//openrasp_v8_java } catch (Exception e) { System.out.println("[OpenRASP] Failed to load native library, please refer to https://rasp.baidu.com/doc/install/software.html#faq-v8-load for possible solutions."); e.printStackTrace(); return; } if (!loadConfig()) { return; } //缓存rasp的build信息 Agent.readVersion(); BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit); // 初始化插件系统,包括js上下文类初始化和插件文件初始化 if (!JS.Initialize()) { return; } CheckerManager.init(); initTransformer(inst);//初始化字节码转换模块 if (CloudUtils.checkCloudControlEnter()) { CrashReporter.install(Config.getConfig().getCloudAddress() + "/v1/agent/crash/report", Config.getConfig().getCloudAppId(), Config.getConfig().getCloudAppSecret(), CloudCacheModel.getInstance().getRaspId()); } deleteTmpDir(); String message = "[OpenRASP] Engine Initialized [" + Agent.projectVersion + " (build: GitCommit=" + Agent.gitCommit + " date=" + Agent.buildTime + ")]"; System.out.println(message); Logger.getLogger(EngineBoot.class.getName()).info(message); }
-
初始化插件系统
首先是加载V8 JS引擎,OpenRasp的一大特色就是将部分规则通过JS插件的形式来实现编写,这样做有两个优势,一是可以实现跨平台使用,减少了为不同语言重复制定相同规则的问题。另一个就是可以实现规则的热部署,添加或修改规则不需要重新启动服务。
这里设置了v8的logger信息、以及其获取栈内信息的getter方法,获取的信息包括类名、方法名和行号。
InitFileWatcher
启动对js插件的文件监控,从而实现热部署,动态的增删js中的检测规则public synchronized static boolean Initialize() { try { if (!V8.Initialize()) { throw new Exception("[OpenRASP] Failed to initialize V8 worker threads"); } V8.SetLogger(new com.baidu.openrasp.v8.Logger() { @Override public void log(String msg) { pluginLog(msg); } });//设置v8的logger //设置v8获取栈信息的getter方法,这里获得的栈信息,每一条信息包括类名、方法名和行号classname@methodname(linenumber) V8.SetStackGetter(new com.baidu.openrasp.v8.StackGetter() { @Override public byte[] get() { try { ByteArrayOutputStream stack = new ByteArrayOutputStream(); JsonStream.serialize(StackTrace.getParamStackTraceArray(), stack); stack.write(0); return stack.getByteArray(); } catch (Exception e) { return null; } } }); Context.setKeys(); if (!CloudUtils.checkCloudControlEnter()) { UpdatePlugin();//加载js插件到v8引擎中 InitFileWatcher();//启动对js插件的文件监控,从而实现热部署,动态的增删js中的检测规则 } return true; } catch (Exception e) { e.printStackTrace(); LOGGER.error(e); return false; } }
-
然后调用CheckerManager.init():
public synchronized static void init() throws Exception { for (Type type : Type.values()) { checkers.put(type, type.checker);//加载所有类型的检测放入checkers,type.checker就是某种检测对应的类 } }
这里的type参数就是各种JS的规则插件:
-
最后调用
initTransformer(inst)
初始化字节码转换模块,实现插桩,这里分为两种情况:- 对于第一次加载的class进行插桩操作,类加载的时候直接CustomClassTransformer进入agent处理。
- 对于之前已经加载了的类,使用retransform方法遍历所有已经加载的类。
* @param inst 用于管理字节码转换器 */ private void initTransformer(Instrumentation inst) throws UnmodifiableClassException { transformer = new CustomClassTransformer(inst); transformer.retransform(); }
-
跟进
CustomClassTransformer
,该类实现了ClassFileTransformer
接口(JVM TI接口)public class CustomClassTransformer implements ClassFileTransformer { public CustomClassTransformer(Instrumentation inst) { this.inst = inst; inst.addTransformer(this, true); addAnnotationHook(); }
-
跟进
addAnnotationHook
,获取com.baidu.openrasp.hook包下的AbstractClassHook子类,继续调用addHook添加hook点。private void addAnnotationHook() { Set<Class> classesSet = AnnotationScanner.getClassWithAnnotation(SCAN_ANNOTATION_PACKAGE, HookAnnotation.class); for (Class clazz : classesSet) { try { Object object = clazz.newInstance(); if (object instanceof AbstractClassHook) { addHook((AbstractClassHook) object, clazz.getName()); } } catch (Exception e) { LogTool.error(ErrorType.HOOK_ERROR, "add hook failed: " + e.getMessage(), e); } } }
classSet收集所有有HookAnnotation注解的类。
private void addHook(AbstractClassHook hook, String className) { if (hook.isNecessary()) { necessaryHookType.add(hook.getType()); } String[] ignore = Config.getConfig().getIgnoreHooks(); for (String s : ignore) { if (hook.couldIgnore() && (s.equals("all") || s.equals(hook.getType()))) { LOGGER.info("ignore hook type " + hook.getType() + ", class " + className); return; } } hooks.add(hook); }
hooks收集所有不是配置文件中忽略的hook信息。
-
过滤并hook,调用transformer.retransform()
对于已经被加载的类,会经由
retransform
方法到transform
,而对于第一次加载的类,会直接被transform
捕获(这里是重写了ClassFileTransformer)遍历hooks获取所有Hook类,并通过Hook类的isClassMatched方法判断当前类是否Hook类的关注类,如果是,之后的具体操作则交由Hook类的tranformClass方法 。
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException { if (loader != null) { DependencyFinder.addJarPath(domain); } if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) { jspClassLoaderCache.put(className.replace("/", "."), new SoftReference<ClassLoader>(loader)); } for (final AbstractClassHook hook : hooks) { if (hook.isClassMatched(className)) { CtClass ctClass = null; try { ClassPool classPool = new ClassPool(); addLoader(classPool, loader); ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); if (loader == null) { hook.setLoadedByBootstrapLoader(true); } classfileBuffer = hook.transformClass(ctClass); if (classfileBuffer != null) { checkNecessaryHookType(hook.getType()); } } catch (IOException e) { e.printStackTrace(); } finally { if (ctClass != null) { ctClass.detach(); } } } } serverDetector.detectServer(className, loader, domain); return classfileBuffer; }
Hook流程
-
为启动时候进行了插桩操作,当有类被 ClassLoader 加载时候,会把该类的字节码先交给自定义的 Transformer 处理。
-
自定义 Transformer 会判断该类是否为需要 hook 的类,如果是会将该类交给 javassist 字节码处理框架进行处理。
-
javassist 框架会将类的字节码依照事件驱动模型逐步解析每个方法,当触发了我们需要 hook 的方法,就会在方法的开头或者结尾插入进入检测函数的asm字节码。
-
把 hook 好的字节码返回给 transformer 从而载入虚拟机。
具体流程可以看官网址这张图:
Hook 分析案例:
以ProcessBuilderHook 为例:
开始hook插桩
根据com.baidu.openrasp.transformer.CustomClassTransformer#isClassMatched
方法判断是否对目标class进行hook。
public boolean isClassMatched(String className) {
if (ModuleLoader.isModularityJdk()) {
return "java/lang/ProcessImpl".equals(className);
} else {
if (OSUtil.isLinux() || OSUtil.isMacOS()) {
// LOGGER.info("come into linux hook class");
return "java/lang/UNIXProcess".equals(className);
} else if (OSUtil.isWindows()) {
return "java/lang/ProcessImpl".equals(className);
}
return false;
}
}
接着调用的是hook类的transformClass(CtClass ctClass)
->hookMethod(CtClass ctClass)
方法进行了字节码的修改。
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
if (ctClass.getName().contains("ProcessImpl")) {
if (OSUtil.isWindows()) {
String src = getInvokeStaticSrc(ProcessBuilderHook.class, "checkCommand",
"$1,$2", String[].class, String.class);
insertBefore(ctClass, "<init>", null, src);
} else if (ModuleLoader.isModularityJdk()) {
String src = getInvokeStaticSrc(ProcessBuilderHook.class, "checkCommand",
"$1,$2,$4", byte[].class, byte[].class, byte[].class);
insertBefore(ctClass, "<init>", null, src);
}
} else if (ctClass.getName().contains("UNIXProcess")) {
String src = getInvokeStaticSrc(ProcessBuilderHook.class, "checkCommand",
"$1,$2,$4", byte[].class, byte[].class, byte[].class);
insertBefore(ctClass, "<init>", null, src);
}
}
在这里想要将checkCommand函数插入到init函数之前,需要先通过getInvokeStaticSrc
方法获取插桩位置的Java代码,再调用insertBefore方法,使用 Javaassist 进行“插入”的操作。在插入在构造方法之前后,被hook的类在实例化之前会先调用插入的方法。
public static void checkCommand(byte[] command, byte[] args, final byte[] envBlock) {
if (HookHandler.enableCmdHook.get()) {
LinkedList<String> commands = new LinkedList<String>();
if (command != null && command.length > 0) {
commands.add(new String(command, 0, command.length - 1));
}
if (args != null && args.length > 0) {
int position = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] == 0) {
commands.add(new String(Arrays.copyOfRange(args, position, i)));
position = i + 1;
}
}
}
LinkedList<String> envList = new LinkedList<String>();
if (envBlock != null) {
int index = -1;
for (int i = 0; i < envBlock.length; i++) {
if (envBlock[i] == '\0') {
String envItem = new String(envBlock, index + 1, i - index - 1);
if (envItem.length() > 0) {
envList.add(envItem);
}
index = i;
}
}
}
checkCommand(commands, envList);
}
}
跟进checkCommand:
public static void checkCommand(List<String> command, List<String> env) {
if (command != null && !command.isEmpty()) {
HashMap<String, Object> params = null;
try {
params = new HashMap<String, Object>();
params.put("command", StringUtils.join(command, " "));
params.put("env", env);
List<String> stackInfo = StackTrace.getParamStackTraceArray();
params.put("stack", stackInfo);
} catch (Throwable t) {
LogTool.traceHookWarn(t.getMessage(), t);
}
if (params != null) {
HookHandler.doCheckWithoutRequest(CheckParameter.Type.COMMAND, params);
}
}
}
在日志中查看收集到的params内容:
{
"params": {
"stack": [
"java.lang.UNIXProcess.\u003cinit\u003e",
"java.lang.ProcessImpl.start",
"java.lang.ProcessBuilder.start",
"java.lang.Runtime.exec",
"java.lang.Runtime.exec",
"superman.shells.T3OrIIOPShell.getServerLocation",
"superman.shells.T3OrIIOPShell_WLSkel.invoke",
"weblogic.rmi.internal.BasicServerRef.invoke",
"weblogic.rmi.internal.BasicServerRef$1.run",
"weblogic.security.acl.internal.AuthenticatedSubject.doAs",
"weblogic.security.service.SecurityManager.runAs",
"weblogic.rmi.internal.BasicServerRef.handleRequest",
"weblogic.rmi.internal.wls.WLSExecuteRequest.run",
"weblogic.work.ExecuteThread.execute",
"weblogic.work.ExecuteThread.run"
],
"env": [],
"command": "sh -c ls"
}
}
从日志中构建上下文参数信息:
获取到堆栈信息后,会调用HookHandler.doCheckWithoutRequest(CheckParameter.Type.COMMAND, params)
public static void doCheckWithoutRequest(CheckParameter.Type type, Map params) {
boolean enableHookCache = enableCurrThreadHook.get();
try {
enableCurrThreadHook.set(false);
//当服务器的cpu使用率超过90%,禁用全部hook点
if (Config.getConfig().getDisableHooks()) {
return;
}
//当云控注册成功之前,不进入任何hook点
if (Config.getConfig().getCloudSwitch() && Config.getConfig().getHookWhiteAll()) {
return;
}
if (requestCache.get() != null) {
try {
StringBuffer sb = requestCache.get().getRequestURL();
if (sb != null) {
String url = sb.substring(sb.indexOf("://") + 3);
if (HookWhiteModel.isContainURL(type.getCode(), url)) {
return;
}
}
} catch (Exception e) {
LogTool.traceWarn(ErrorType.HOOK_ERROR, "white list check has failed: " + e.getMessage(), e);
}
}
doRealCheckWithoutRequest(type, params);
} catch (Throwable t) {
if (t instanceof SecurityException) {
throw (SecurityException) t;
}
} finally {
enableCurrThreadHook.set(enableHookCache);
}
}
跟进doRealCheckWithoutRequest(type, params)
public static void doRealCheckWithoutRequest(CheckParameter.Type type, Map params) {
/*...*/
try {
LOGGER.info("收集到的checkParameter: " + parameter);
isBlock = CheckerManager.check(type, parameter);
LOGGER.info("是否拦截isBlock: " + isBlock);
}
/*...*/
关注isBlock = CheckerManager.check(type, parameter),这里传进去的parameter是将Type和params进行封住哪个后的json:
{
"type": "COMMAND",
//多了一个type
"params": {
"stack": [
"java.lang.UNIXProcess.\u003cinit\u003e",
"java.lang.ProcessImpl.start",
"java.lang.ProcessBuilder.start",
"java.lang.Runtime.exec",
"java.lang.Runtime.exec",
"superman.shells.T3OrIIOPShell.getServerLocation",
"superman.shells.T3OrIIOPShell_WLSkel.invoke",
"weblogic.rmi.internal.BasicServerRef.invoke",
"weblogic.rmi.internal.BasicServerRef$1.run",
"weblogic.security.acl.internal.AuthenticatedSubject.doAs",
"weblogic.security.service.SecurityManager.runAs",
"weblogic.rmi.internal.BasicServerRef.handleRequest",
"weblogic.rmi.internal.wls.WLSExecuteRequest.run",
"weblogic.work.ExecuteThread.execute",
"weblogic.work.ExecuteThread.run"
],
"env": [],
"command": "sh -c ls"
}
}
跟进check(type, parameter):
public static boolean check(Type type, CheckParameter parameter) {
return checkers.get(type).check(parameter);//调用检测类进行参数检测
}
此处会根据传入的type来选择调用相对应的checkers,这里的checkers就是前面CheckerManager.init()的时候放入的内容。
由于我们这个demo放入的内容是command命令,因此会调用V8AttackChecker的check方法。
之后再对插件层层追踪,跟进V8AttackChecker的checkParam方法
/**
* 执行js插件进行安全检测
* @param checkParameter 检测参数 {@link CheckParameter}
* @return 检测结果
*/
@Override
public List<EventInfo> checkParam(CheckParameter checkParameter) {
return JS.Check(checkParameter);
}
跟进JS.Check(checkParameter),就能看到调用JS插件进行检测的代码了:
OpenRasp绕过
互联网上有很多大佬写过关于Openrasp绕过的文章,但是普遍泛用型较差,或者需要获取shell,利用前提较为困难,这里举一个Kcon黑客大会上分享过的冰蝎4.0的例子:
冰蝎作为一个常用的后门连接工具,在更新4.0之后加入了随机生成类名混淆绕过rasp的功能,测试之后,原本3.0时代类名为rebeyond的类,都被混淆成了随机类名,使得rasp虽然可以检测到命令执行,但是无法判断是冰蝎的后门连接,也较难针对性的进行阻断:
随机生成的类名
冰蝎混淆使用的代码:
net.rebeyond.behinder.utils.Utils.class#getRandomClassName
public static String getRandomClassName(String sourceName) {
String[] domainAs = new String[]{"com", "net", "org", "sun"};
String domainB = getRandomAlpha((new Random()).nextInt(5) + 3).toLowerCase();
String domainC = getRandomAlpha((new Random()).nextInt(5) + 3).toLowerCase();
String domainD = getRandomAlpha((new Random()).nextInt(5) + 3).toLowerCase();
String className = getRandomAlpha((new Random()).nextInt(7) + 4);
className = className.substring(0, 1).toUpperCase() + className.substring(1).toLowerCase();
int domainAIndex = (new Random()).nextInt(4);
String domainA = domainAs[domainAIndex];
int randomSegments = (new Random()).nextInt(3) + 3;
String randomName;
switch(randomSegments) {
case 3:
randomName = domainA + "/" + domainB + "/" + className;
break;
case 4:
randomName = domainA + "/" + domainB + "/" + domainC + "/" + className;
break;
case 5:
randomName = domainA + "/" + domainB + "/" + domainC + "/" + domainD + "/" + className;
break;
default:
randomName = domainA + "/" + domainB + "/" + domainC + "/" + domainD + "/" + className;
}
while(randomName.length() > sourceName.length()) {
}
return randomName;
}
总结
OpenRasp的特点:
相对于传统的ids和waf等基于流量进行威胁检测的产品,Rasp产品的优势与缺陷如下:
优势:
-
准确性高、防绕过能力强
waf往往误报率高,绕过率高,市面上也有很多针对不同waf的绕过方式,而RASP技术防御是根据请求上下文进行拦截的。
-
节省开发成本
Rasp的原理,决定了其节省了大量协议解析、解码解密、防混淆防绕过的工序。节约了这部分工作的开发和,产品使用时分析解密的工作。
-
0day防御与发现
RASP能够洞察应用程序内部的情况,检测到由新攻击引起的行为变化,使它能够对0day攻击、应用自身未知漏洞对目标应用程序的影响作出反应,同时记录完整的利用流程,方便安全人员分析发现0day漏洞。
-
加密流量检测
因为Rasp是部署于业务内部,不关心流量传输和加解密过程,所以对于加密流量中攻击行为的检测要远胜于传统的流量检测产品。
缺陷:
-
部署难度较大、成本较高
Rasp和业务产品的代码结合得十分深入,所以部署面相对狭窄,对于不同用户的hook需求,定制难度较大。而且随着应用服务的语言不同,需要付出额外的开发和维护成本。
理想中的Java RASP实践方式是使用agent进行无侵入部署,但是受限于JVM进程保护机制没有办法对目标类添加新的方法,所以无法反复进行字节码插入。
Open RASP推荐的部署方式都是利用premain模式进行部署,这就造成了必须停止相关业务,加入相应的启动参数,再开启服务。而对甲方来说,重启一次业务完成部署RASP的代价是比较高的。
-
与业务代码结合较深、业务风险较大
因为rasp部署内容深入到业务代码的执行中,出现bug或者其他漏洞风险时,很可能对业务造成极大的影响。如果在RASP所指定的逻辑中出现了严重错误,将直接将错误抛出在业务逻辑中。轻则当前业务中断,重则整个服务中断。例如在RASP的检测逻辑中存在exit()这样的利用,将直接导致程序退出。
-
规则编写要求较高
相对于常规的waf和ids产品,有一个web poc就能快速编写检测规则,迅速上线,而不一定需要深刻理解漏洞的产生原理。
RASP的规则需要经过专业的安全研究人员反复打磨,要求撰写者对漏洞的理解十分深刻,才能找到合适的hook点,之后还要根据业务来定制化,将所有的可能影响业务的可能性都考虑进去,同时尽量减少误报。但是由于攻击者和规则编写者水平的参差不齐,很容易导致规则遗漏,无法拦截相关攻击,或产生大量的攻击误报。也因为规则编写的复杂,产品对于最新的漏洞,可能无法及时覆盖。
-
通用性欠缺
针对不同语言的服务,要使用完全不同的hook方式,几乎等同于两套产品,缺乏泛用型。
参考
浅谈RASP https://www.anquanke.com/post/id/187415#h3-11
浅谈RASP技术攻防之实战[代码实现篇] https://www.03sec.com/Ideas/qian-tanrasp-ji-shu-gong-fang-zhi-shi-zhan-dai-ma.html
OpenRASP系统架构-Java版本 https://rasp.baidu.com/doc/hacking/architect/java.html