戳上面的蓝字关注我吧!
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
[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