长亭百川云 - 文章详情

JRASP产品检测原理分析

小陈的Life

51

2024-07-13

戳上面的蓝字关注我吧!


01

架构

0****2

Jrasp-Agent

看到jrasp的架构之后,我的思路就是先从agent为入口进行分析,并带着如下几个疑问:

  • Agent是如何织入插桩代码的

  • Agent是通过何种方式或者规则进行研判并拦截的

  • 其参数和规则是如何动态的更改传递给Agent的

查看Agent的启动脚本,查看agent的启动方式是通过

`function attach_jvm() {`  `# attach target jvm`  `"${RASP_JAVA_HOME}/bin/java" \`    `${RASP_JVM_OPS} \`    `-jar "${RASP_LIB_DIR}/jrasp-core.jar" \`    `"${TARGET_JVM_PID}" \`    `"${RASP_LIB_DIR}/jrasp-launcher.jar" \`    `"raspHome=${RASP_HOME_DIR};serverIp=${TARGET_SERVER_IP};serverPort=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE};enableAuth=${ENABLE_AUTHTH};username=${DEFAULT_USERNAME};password=${DEFAULT_PASSWORD}" ||`    `exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."``   `  `# get network from attach result`  `RASP_SERVER_NETWORK=$(grep "${TARGET_NAMESPACE}" "${RASP_TOKEN_FILE}" | awk -F ";" '{print $4";"$5}')`  `[[ -z ${RASP_SERVER_NETWORK} ]] &&`    `exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail, attach lose response."``   ``}`

程序通过lib/jrasp-core.jar包来启动到目标JVM的PID中,跟进core的源码查看

从pom.xml文件可以得知

<mainClass>com.jrasp.core.CoreLauncher</mainClass>

启动该core jar包的类是CoreLauncher

跟入代码之后发现是动态加载的Agent,关于动态加载Agent的方式我之前在文章《瞒天过海计之Tomcat隐藏内存马》中也有说到过,原文链接:https://tttang.com/archive/1368/

这里加载的AgentJarPath就是jrasp-launcher的jar包路径

找到其中的关键启动代码:

`// 启动加载``public static void premain(String featureString, Instrumentation inst) {`    `LAUNCH_MODE = LAUNCH_MODE_AGENT;  //agent方式`    `install(toFeatureMap(featureString), inst);``}``   ``// 动态加载``public static void agentmain(String featureString, Instrumentation inst) {`    `LAUNCH_MODE = LAUNCH_MODE_ATTACH;  //attach模式`    `install(toFeatureMap(featureString), inst);``}`

toFeatureMap就是解析了shell启动脚本中的参数

跟进install看看agent是如何安装到jvm环境中的

`// 在当前JVM安装rasp``private static synchronized InetSocketAddress install(final Map<String, String> featureMap,`                                                      `final Instrumentation inst) {`    `final String namespace = getNamespace(featureMap);`    `Map<String, String> coreConfigMap = toCoreConfigMap(featureMap);`    `try {`        `final String home = getRaspHome(featureMap);`        `// 将Spy注入到BootstrapClassLoader`        `inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(`            `getRaspSpyJarPath(home)`        `)));`        `// 构造自定义的类加载器,尽量减少Rasp对现有工程的侵蚀`        `final ClassLoader raspClassLoader = loadOrDefineClassLoader(`            `namespace,`            `getRaspCoreJarPath(home)`        `);`        `// CoreConfigure类定义`        `final Class<?> classOfConfigure = raspClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);`        `// 反序列化成CoreConfigure类实例`        `final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", Map.class)`            `.invoke(null, coreConfigMap);`        `// CoreServer类定义`        `final Class<?> classOfProxyServer = raspClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);`        `// 获取CoreServer单例`        `final Object objectOfProxyServer = classOfProxyServer`            `.getMethod("getInstance")`            `.invoke(null);`        `// CoreServer.isBind()`        `final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);`        `// 如果未绑定,则需要绑定一个地址`        `if (!isBind) {`            `try {`                `classOfProxyServer`                    `.getMethod("bind", classOfConfigure, Instrumentation.class)`                    `.invoke(objectOfProxyServer, objectOfCoreConfigure, inst);`            `} catch (Throwable t) {`                `classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);`                `throw t;`            `}``   `        `}`        `// 返回服务器绑定的地址`        `return (InetSocketAddress) classOfProxyServer`            `.getMethod("getLocal")`            `.invoke(objectOfProxyServer);`    `} catch (Throwable cause) {`        `throw new RuntimeException("rasp attach failed.", cause);`    `}``}`

代码利用反射,实际调用了jrasp-core中的JettyCoreServer类的bind方法。该方法中初始化了Http服务器和加载了相关模块。

03

模块加载过程

跟进查看模块是如何加载的,可以看到DefaultCoreModuleManager类的reset方法

`@Override``public synchronized CoreModuleManager reset() throws ModuleException {``   `    `logger.info(AGENT_COMMON_LOG_ID, "resetting all loaded modules:{}", loadedModuleBOMap.keySet());``   `    `// 1. 强制卸载所有模块`    `unloadAll();`    `// 2.加载系统模块、必装模块、非必须模块`    `loadModule(systemModuleLibDir, systemModuleLibCopyDir, cfg.getLaunchMode());`    `loadModule(requiredModuleLibDir, requiredModuleLibCopyDir, cfg.getLaunchMode());`    `loadModule(optionalModuleLibDir, optionalModuleLibCopyDir, cfg.getLaunchMode());`    `return this;``}`

跟进loadModule方法,发现其中使用了两个模块回调

`ModuleLibLoader moduleLibLoader = new ModuleLibLoader(from, to, mode);``moduleLibLoader.load(new InnerModuleJarLoadCallback(), new InnerModuleLoadCallback());`

第一个InnerModuleJarLoadCallback是解密Jar包的,不是本次研究重点,重点看InnerModuleLoadCallback的回调函数是如何实现

可以看到这个InnerModuleLoadCallback的回调函数被ModuleJarLoader类的load方法调用了。

进入load方法后,创建了一个ModuleJarClassLoader对象,这是一个继承自URLClassLoader的加载器,后续跟进发现,该加载器对象当做参数被loadingModules方法调用了

`// 加载模块类并判断模块中module上的注解信息``private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,`                               `final ModuleLoadCallback mCb) {`    `final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>(); // 仅用于记录modules个数在打印日志时`    `// todo 怎么加载的??有点类似于服务发现`    `final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);`    `final Iterator<Module> moduleIt = moduleServiceLoader.iterator();`   `......//Many Code`   `}`

看loadingModules方法的前一部分,调用了SPI机制

ServiceLoader.load(Module.class, moduleClassLoader);

而这里的moduleClassLoader就是之前自定义的加载器,加载之前设置好模块目录下的jar文件

因此模块是通过SPI机制注入进去的,加载完之后,就获取了Module的实现类,以及是否实现了相关注解等

这里我拿官方编写的RCE-Hook模块来辅助说明

正好满足继承了Module类,且该类实现了Information注解

之后就调用了刚才创建的回调函数的onLoad方法

`if (null != mCb) {`    `mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);``}`

继续跟进后可以到达DefaultCoreModuleManager的load方法中

`/**`     `* 加载并注册模块`     `* <p>1. 如果模块已经存在则返回已经加载过的模块</p>`     `* <p>2. 如果模块不存在,则进行常规加载</p>`     `* <p>3. 如果模块初始化失败,则抛出异常</p>`     `*`     `* @param uniqueId          模块ID`     `* @param module            模块对象`     `* @param moduleJarFile     模块所在JAR文件`     `* @param moduleClassLoader 负责加载模块的ClassLoader`     `* @throws ModuleException 加载模块失败`     `*/``private synchronized void load(final String uniqueId,`                               `final Module module,`                               `final File moduleJarFile,`                               `final ModuleJarClassLoader moduleClassLoader) throws ModuleException {`    `//判断是否在注册模块列表中`    `if (loadedModuleBOMap.containsKey(uniqueId)) {`        `return;`    `}`    `logger.info(LOADING_MODULE_LOG_ID, "loading module, module={};class={};module-jar={};",`                `uniqueId,`                `module.getClass().getName(),`                `moduleJarFile`               `);``   `    `// 初始化模块信息`    `final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);``   `    `// 注入@Resource资源`    `injectResourceOnLoadIfNecessary(coreModule);``   `    `callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);``   `    `// 设置为已经加载`    `coreModule.markLoaded(true);``   `    `// 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块`    `markActiveOnLoadIfNecessary(coreModule);``   `    `// 注册到模块列表中`    `loadedModuleBOMap.put(uniqueId, coreModule);``   `    `// 通知生命周期,模块加载完成`    `callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);``   ``}`

仔细分析一下这个load方法中的加载过程,最重要的部分就是injectResourceOnLoadIfNecessary方法

`private void injectResourceOnLoadIfNecessary(final CoreModule coreModule) throws ModuleException {`    `try {`        `final Module module = coreModule.getModule();`        `for (final Field resourceField : FieldUtils.getFieldsWithAnnotation(module.getClass(), Resource.class)) {       //获取Module中带有Resouce注解的字段`            `final Class<?> fieldType = resourceField.getType();     //获取字段类型``   `            `// LoadedClassDataSource对象注入`            `if (LoadedClassDataSource.class.isAssignableFrom(fieldType)) {`                `writeField(`                    `resourceField,`                    `module,`                    `classDataSource,`                    `true`                `);`            `}`            `// JsonImpl注入`            `else if (JSONObject.class.isAssignableFrom(fieldType)) {`                `writeField(resourceField, module, new JsonImpl(), true);`            `}`            `// AlgorithmManager 注入`            `else if (AlgorithmManager.class.isAssignableFrom(fieldType)) {`                `writeField(resourceField, module, DefaultAlgorithmManager.instance, true);`            `}`            `...`

这个方法主要动作是注入模块中带@Resource字段的变量

例如之前展示的RCE-Hook图中的几个带@Resource注解,有一个AlgorithmManager的类字段,注入的是DefaultAlgorithmManager.instance实例。

跟进查看DefaultAlgorithmManager类的实现

正好对应rce-hook模块中,算法调用的doCheck方法来验证

至此,Rce-Hook模块就正常通过SPI的方式装载进JVM虚拟机中了

04

检测方法代码的织入过程

前面讲到模块加载过程,模块加载后,会自行调用重写的loadCompleted方法

这里还是拿Rce-hook模块来举例

`@Override``public void loadCompleted() {`    `String clazzName;`    `if (isGreaterThanJava8()) {`        `clazzName = "java.lang.ProcessImpl";`    `} else {`        `clazzName = "java.lang.UNIXProcess";`    `}`    `nativeProcessRceHook(clazzName);``}`

首先简单的判断了一下版本,就调用需要织入的类,到nativeProcessRceHook方法中

`public void nativeProcessRceHook(final String clazz) {`    `new EventWatchBuilder(moduleEventWatcher)`        `.onClass(clazz)`        `.includeBootstrap()`        `.onBehavior("forkAndExec")`        `.onWatch(new AdviceListener() {`            `@Override`            `protected void before(Advice advice) throws Throwable {`                `byte[] prog = (byte[]) advice.getParameterArray()[2];     // 命令`                `byte[] argBlock = (byte[]) advice.getParameterArray()[3]; // 参数`                `String cmdString = getCommandAndArgs(prog, argBlock);`                `HashMap<String, Object> requestInfo = new HashMap<String, Object>(requestInfoThreadLocal.get());`                `algorithmManager.doCheck(ATTACK_TYPE, requestInfo, cmdString);`            `}``   `            `@Override`            `protected void afterReturning(Advice advice) throws Throwable {`                `requestInfoThreadLocal.remove();`            `}`        `});``}`

方法中新建了一个EventWatch监听器,用于织入到java.lang.ProcessImpl的forkAndExec方法中,至于为什么是forkAndExec方法可以看官方的文档:https://www.jrasp.com/algorithm/rce/rce-basic-principles.html

`@Override``public EventWatcher onWatch(AdviceListener adviceListener) {`    `return build(new AdviceAdapterListener(adviceListener), null, BEFORE, RETURN, THROWS, IMMEDIATELY_RETURN, IMMEDIATELY_THROWS);``}`

看到onWatch方法的内容如上,参数是一个AdviceListener对象,跟进build方法查看是如何解析该对象并织入到目标类方法中的。

`private EventWatcher build(final EventListener listener,`                               `final Progress progress,`                               `final Event.Type... eventTypes) {``   `        `final int watchId = moduleEventWatcher.watch(`                `toEventWatchCondition(),`                `listener,`                `progress,`                `eventTypes`        `);`    `....``}`

调用了moduleEventWatcher.watch方法

查看RaspClassFileTransformer类

`final byte[] toByteCodeArray = new EventEnhancer(this).toByteCodeArray(`    `loader,`    `srcByteCodeArray,`    `behaviorSignCodes,`    `namespace,`    `listenerId,`    `eventTypeArray``);`

而EventWeaver类继承了ClassVisitor,而ASM织入方法的关键函数肯定就是重写的visitMethod方法

这里方法会先判断是否为织入的方法体,之后会有一个替换Native原生方法的过程

`@Override``public void makrNativeMethodEnhance() {`    `if(setNativeMethodPrefix.compareAndSet(false,true)){`        `if(inst.isNativeMethodPrefixSupported()){`            `inst.setNativeMethodPrefix(this,getNativeMethodPrefix());`        `}else{`            `throw new UnsupportedOperationException("Native Method Prefix Unspported");`        `}`    `}``}`

setNativeMethodPrefix方法可以设置native原生方法的前缀,假如有一个名为foo的native方法,则对应的C底层函数名为

native boolean foo(int x);  ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);

执行完inst.setNativeMethodPrefix(transformer,"wrapped_");

native boolean wrapped_foo(int x);  ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);

这里想仔细了解一下是如何工作的,可以移步官方文档:https://www.jrasp.com/guide/technology/native\_method.html

再继续回到EventWeaver类,看看是织入了什么在方法中

之前替换了native的前缀为“JRASP”,因此需要在织入的forkAndExec方法中再调用JRASPforkAndExec方法,类似一种方法替换的方式。

上图是我将织入后的ProcessImpl类dump下来反编译之后的截图,至此就是整个织入的原理和替换的过程

05

攻击事件的检测原理

继续看到ProcessImpl类的forkAndExec第40行,调用了Spy.spyMethodOnBefore方法

`public static Ret spyMethodOnBefore(final Object[] argumentArray,`                                    `final String namespace,`                                    `final int listenerId,`                                    `final int targetClassLoaderObjectID,`                                    `final String javaClassName,`                                    `final String javaMethodName,`                                    `final String javaMethodDesc,`                                    `final Object target) throws Throwable {`    `final Thread thread = Thread.currentThread();`    `if (selfCallBarrier.isEnter(thread)) {`        `return Ret.RET_NONE;`    `}`    `final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);`    `try {`        `final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);`        `if (null == spyHandler) {`            `return Ret.RET_NONE;`        `}`        `return spyHandler.handleOnBefore(`            `listenerId, targetClassLoaderObjectID, argumentArray,`            `javaClassName,`            `javaMethodName,`            `javaMethodDesc,`            `target`        `);`    `} catch (Throwable cause) {`        `handleException(cause);`        `return Ret.RET_NONE;`    `} finally {`        `selfCallBarrier.exit(thread, node);`    `}``}`

跟进handleOnBefore方法

方法中首先获取了对应的事件管理器

`// 获取事件处理器``final EventProcessor processor = mappingOfEventProcessor.get(listenerId);`

并针对Before事件创建了BeforeEvent对象

`final BeforeEvent event = process.getEventFactory().makeBeforeEvent(`                `processId,`                `invokeId,`                `javaClassLoader,`                `javaClassName,`                `javaMethodName,`                `javaMethodDesc,`                `target,`                `argumentArray``);`

并传递在handleEvent方法后,调用了对应Listener的onEvent方法

跟进onEvent方法中,可以到下图处理listener的位置

此时的堆栈关系图如下所示

而处理的正好是RceHook模块的内部类

`@Override``protected void before(Advice advice) throws Throwable {`    `byte[] prog = (byte[]) advice.getParameterArray()[2];     // 命令`    `byte[] argBlock = (byte[]) advice.getParameterArray()[3]; // 参数`    `String cmdString = getCommandAndArgs(prog, argBlock);`    `HashMap<String, Object> requestInfo = new HashMap<String, Object>(requestInfoThreadLocal.get());`    `algorithmManager.doCheck(ATTACK_TYPE, requestInfo, cmdString);``}`

解析advice对象中传入的命令参数,并添加到algorithmManager类的doCheck方法中检测。

而这个algorithmManager对象,之前在将@Resource注解的时候,会在SPI注入的时候自动装载了,但由于DefaultAlgorithmManager类的字段都有static修饰词,因此是个单例模式。只需要找到在哪里注册过algorithmManager的算法即可。

注册的过程在com.jrasp.module.rcenative.algorithm.RceAlgorithm类中

`@Override``public String getName() {`    `return "rce-check";``}``   ``@Override``public String getType() {`    `return "rce";    //定义模块的类型``}``   ``@Override``public String getDescribe() {`    `return "命令执行检测算法";``}``   ``@Override``public void loadCompleted() {`    `// 默认初始化`    `RceReflectCheck rceReflectCheck = new RceReflectCheck();// 算法1:基于栈的检测`    `RceCommonCheck rceCommonCheck = new RceCommonCheck(null);// 算法2:常用渗透命令`    `RceDnsCheck rceDnsCheck = new RceDnsCheck();// 算法3: DNSlog检测`    `RceOtherCheck rceOtherCheck = new RceOtherCheck();// 算法4: 记录所有的命令执行`    `list.add(rceReflectCheck);`    `list.add(rceCommonCheck);`    `list.add(rceDnsCheck);`    `list.add(rceOtherCheck);`    `algorithmManager.register(this);``}``   ``@Override``public void onUnload() throws Throwable {`    `// 算法卸载`    `algorithmManager.destroy(this);`    `for (int i = 0; i < list.size(); i++) {`        `list.get(i).close();`    `}`    `// 算法模块清空`    `list = null;``}`

所以前面调用doCheck方法,会直接进入该算法的check方法中处理

查看com.jrasp.module.rcenative.algorithm.RceAlgorithm类的check方法,其中返回的状态可以通过jrasp/cfg/config.json中的rce_reflct_check_action等参数的参数值。

其检测思路就是调用RCE检测算法模块中的runCheckUnit方法

`@Override``public CheckResult runCheckUnit(String[] stack, Object... parameters) {`    `if (action != -1) {`        `boolean userCode = false, reachedInvoke = false;`        `String message = "";`        `int i = 0;`        `if (stack.length > 3 && stack[0].startsWith("sun.reflect.GeneratedMethodAccessor")`            `&& "sun.reflect.GeneratedMethodAccessorImpl.invoke".equals(stack[1])`            `&& "java.lang.reflect.Method.invoke".equals(stack[2])`           `) {`            `i = 3;`        `}`        `for (; i < stack.length; i++) {`            `String method = stack[i];`            `// 命令执行----->用户代码----->反射调用`            `if (!reachedInvoke) {`                `if ("java.lang.reflect.Method.invoke".equals(method)) {`                    `reachedInvoke = true;`                `}`                `// 用户代码,即非 JDK、com.jrasp 相关的函数`                `if (!method.startsWith("java.")`                    `&& !method.startsWith("sun.")`                    `&& !method.startsWith("com.sun.")`                    `&& !method.startsWith("com.jrasp.")) {`                    `userCode = true;`                `}`            `}``   `            `if (method.startsWith("ysoserial.Pwner")) {`                `message = "Using YsoSerial tool";`                `break;`            `}`            `if (method.startsWith("net.rebeyond.behinder")) {`                `message = "Using BeHinder defineClass webshell";`                `break;`            `}`            `if (method.startsWith("com.fasterxml.jackson.databind.")) {`                `message = "Using Jackson deserialze method";`                `break;`            `}`            `// 对于如下类型的反射调用:`            `// 1. 仅当命令直接来自反射调用才拦截`            `// 2. 如果某个类是反射生成,这个类再主动执行命令,则忽略`            `if (!userCode) {`                `if ("ognl.OgnlRuntime.invokeMethod".equals(method)) {`                    `message = "Using OGNL library";`                    `break;`                `} else if ("java.lang.reflect.Method.invoke".equals(method)) {`                    `message = "Unknown vulnerability detected";`                `}`            `}``   `            `// 本算法的核心检测逻辑`            `if (attackStackSet.contains(method)) {`                `message = method;`            `}`        `}``   `        `if (!"".equals(message)) {`            `CheckResult result = new CheckResult(action);`            `// 确定是攻击`            `result.setConfidence(100);`            `result.setMessage(message);`            `result.setAlgorithm(checkName);`            `// 阻断`            `result.setCanBlock(true);`            `return result;`        `}`    `}`    `return null;``}`

前面做一些常规检测,之后在判断堆栈的方法是否在attackStackSet存在

而attackStackSet就是添加的黑名单Set列表

以上就是整个agent加载模块到检测的全过程

0****6

事件上报过程

事件是通过filebeat监控日志文件输出到Kafka,可以看官方给出的安装脚本:http://www.jrasp.com/guide/install/filebeat.html

filebeat.yaml文件的内容如下

`filebeat.inputs:``- type: log`  `fields:`        `kafka_topic: "jrasp-daemon"`  `paths:`    `- /usr/local/jrasp/logs/jrasp-daemon.log``- type: log`  `fields:`        `kafka_topic: "jrasp-agent"`  `paths:`    `- /usr/local/jrasp/logs/jrasp-agent.log``- type: log`  `fields:`        `kafka_topic: "jrasp-module"`  `paths:`    `- /usr/local/jrasp/logs/jrasp-module.log``filebeat.config.modules:`  `path: ${path.config}/modules.d/*.yml`  `reload.enabled: false``setup.template.settings:`  `index.number_of_shards: 1``output.kafka:`  `enabled: true`  `hosts: ["kafka1:9092","kafka2:9092","kafka3:9092"]`  `topic: '%{[fields.kafka_topic]}'``processors:`  `- add_host_metadata:`      `when.not.contains.tags: forwarded`  `- add_cloud_metadata: ~`  `- add_docker_metadata: ~`  `- add_kubernetes_metadata: ~``   ``processors:`  `- decode_json_fields:`      `fields: ['message']`      `target: ''`      `overwrite_keys: true`  `- drop_fields:`      `fields: ["host","agent","log","input","ecs","@timestamp"]``   ``logging.level: info`

可以从yaml配置文件中看出三个不同的jrasp日志文件会通过filebeat上报输出给Kafak,此时再从后台订阅消息并处理事件即可查看到漏洞信息。

最后,非常感谢jrasp的作者Patton师傅的答疑,在遇到环境问题和代码问题的时候所给予的帮助!

07

Reference

[1].https://www.jrasp.com

[2].https://blog.csdn.net/zxlyx/article/details/124120795

[3].https://www.wangan.com/docs/90

[4].https://www.elastic.co/guide/en/beats/filebeat/current/command-line-options.html

[5].https://www.cnblogs.com/lsdb/p/7762871.html


相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2