JRASP产品检测原理分析
2022-9-1 16:56:2 Author: mp.weixin.qq.com(查看原文) 阅读量:9 收藏

戳上面的蓝字关注我吧!


01
架构

02
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安装raspprivate 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方法

@Overridepublic 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模块来举例

@Overridepublic 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

@Overridepublic 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原生方法的过程

@Overridepublic 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模块的内部类

@Overrideprotected 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类中

@Overridepublic String getName() {    return "rce-check";}
@Overridepublic String getType() { return "rce"; //定义模块的类型}
@Overridepublic String getDescribe() { return "命令执行检测算法";}
@Overridepublic 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);}
@Overridepublic 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方法

@Overridepublic 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加载模块到检测的全过程

06
事件上报过程

事件是通过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.logfilebeat.config.modules:  path: ${path.config}/modules.d/*.yml  reload.enabled: falsesetup.template.settings:  index.number_of_shards: 1output.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



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