以jvm-sandbox为基础 浅谈RASP实现要点(上)

292

本文大都为笔者学习rasp的见解,如有错误,请大佬及时指正。

GitHub - luelueking/jvm-sandbox-rasp: 一个基于jvm-sandbox高度定制化rasp

rasp-demo

  • 先来看看怎么用jvm-sandbox来制作一个rasp
  • 新建一个maven工程,并引入依赖
    • <parent>
          <groupId>com.alibaba.jvm.sandbox</groupId>
          <artifactId>sandbox-module-starter</artifactId>
          <version>1.4.0</version>
      </parent>
      
  • 编写一个类
    • package com.lue.rasp;
      
      import com.alibaba.jvm.sandbox.api.Information;
      import com.alibaba.jvm.sandbox.api.Module;
      import com.alibaba.jvm.sandbox.api.ModuleLifecycle;
      import com.alibaba.jvm.sandbox.api.ProcessController;
      import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
      import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;
      import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;
      import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;
      import org.kohsuke.MetaInfServices;
      
      import javax.annotation.Resource;
      
      @MetaInfServices(Module.class)
      @Information(id = "rasp-rce-hook" , author = "1ue" , version = "0.0.9")
      public class RceHook implements Module, ModuleLifecycle{
      
          // 引入事件Watcher
          @Resource
          private ModuleEventWatcher moduleEventWatcher;
      
      
          public void checkRceCommand() {
              new EventWatchBuilder(moduleEventWatcher)
                      .onClass(ProcessBuilder.class) // 所hook的类
                      .includeBootstrap() // hook由bootstrap加载的需要加上
                      .onBehavior("start") // 所hook方法
                      .onWatch(new AdviceListener() {
                          @Override
                          protected void before(Advice advice) throws Throwable {
                              System.out.println("hook到ProcessBuilder的start方法");
                              // TODO 添加上下文
                              // 直接拦截 抛出一个异常
                              ProcessController.throwsImmediately(new RuntimeException("Block By RASP!!!"));
                              super.before(advice);
                          }
                      });
          }
      
          @Override
          public void onLoad() throws Throwable {
      
          }
      
          @Override
          public void onUnload() throws Throwable {
              System.out.println("rasp的RCE-HOOK卸载!!!");
          }
      
          @Override
          public void onActive() throws Throwable {
              System.out.println("rasp的RCE-HOOK激活!!!");
          }
      
          @Override
          public void onFrozen() throws Throwable {
      
          }
      
          @Override
          public void loadCompleted() {
              checkRceCommand();
              System.out.println("安装rasp的RCE-HOOK完毕!!!");
          }
      
      }
      
  • 然后mvn clean install之后把target下的Rce-Hook-1.0-SNAPSHOT.jar放入到sandbox安装目下的module文件。
  • 使用sandbox.sh来attach你想要保护的java进程。
  • 简单测试一下,完事!
  • 解释说明,jvm-sandbox中有module这个概念,上面这段代码可以理解为对我们自定义了一个module,用来对ProcessBuilder的start方法进行了增强。
  • 当然jvm-sandbox的底层也是利用agent、asm等技术对字节码进行了增强,下面会解释为何使用它来尝试制作rasp。

Load Class

针对这样的缺点,我们除了尽量避免在Engine中使用到会造成冲突的jar以外(这种太难保证了),还有一种方法就是对其进行加载器隔离,也即是通过一个应用类加载器去加载Engine,而不是使用扩展类加载器进行加载。

  • 那么就来看看jvm-sandbox(一款JVM层AOP框架)是怎么做的。(下面为jvm-sanbox加载器架构图)

img

  • JVM Sandbox中有两个自定义的ClassLoader:SandBoxClassLoader加载沙箱模块功能,ModuleJarClassLoader加载用户定义模块功能
  • 它们通过重写java.lang.ClassLoaderloadClass(String name, boolean resolve)方法,从而与应用类加载器完全隔离。
  • 同时也打破了双亲委派约定,不会引起应用的类污染、冲突。
  • 那么这里就有一个重要的知识点,究竟是怎么打破双亲委派的?答案是SPI****机制。jvm-sandbox中使用在BootstrapClassLoader中埋藏的Spy类完成目标类和沙箱内核的通讯。
  • 而spy中有多个SpyHandler,这个SpyHandler是个接口!
    • public interface SpyHandler { // 。。。
      
  • 因此启动类加载器就要委托子类来加载SpyHandler对应的实现类来实现,从而破坏了双亲委派。
  • 因此作为框架使用者,我可以放心的在我的pom中加入依赖😊。

Hook Native

  • 众所周知,demo中所展示的hook是很容易被绕过的。那么就来hook下native方法吧。看看代码实现

    • package com.lue.rasp;
      
      @MetaInfServices(Module.class)
      @Information(id = "rasp-rce-native-hook" , author = "1ue" , version = "0.0.1")
      public class NativeRceHook implements Module, ModuleLifecycle {
      
          @Resource
          private ModuleEventWatcher moduleEventWatcher;
      
      
          public void checkRceCommand() {
              new EventWatchBuilder(moduleEventWatcher)
                      .onClass("java.lang.UNIXProcess")
                      .includeBootstrap()
                      .onBehavior("forkAndExec")
                      .onWatch(new AdviceListener() {
                          @Override
                          protected void before(Advice advice) throws Throwable {
                              System.out.println("hook到native的forkAndExec方法");
                              // TODO 添加上下文
                              // 直接拦截
                              ProcessController.throwsImmediately(new RuntimeException("Block By RASP!!!"));
                              super.before(advice);
                          }
                      });
          }
      
      // 。。。
      }
      
  • 好像并没有什么不同🤔,原来是jvm-sandbox已经支持了对native方法进行了增强呀!🐮

  • 但是native方法是是Java层面向系统层面调用的入口,它是怎么办到的呢?

  • 先来看一下jvm-sandbox打包时对MANIFEST.MF的定义

    • <manifestEntries>
          <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
          <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
          <Can-Redefine-Classes>true</Can-Redefine-Classes>
          <Can-Retransform-Classes>true</Can-Retransform-Classes>
          <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
      </manifestEntries>
      
  • 可以看到其中有一个Can-Set-Native-Method-Prefix属性设置为了true,这个意思是开启native函数的prefix功能。

  • 为什么要开启,这就涉及到了jvm的动态解析

    • 如果给jvm增加一个ClassTransformer并设置native prefix,jvm将使用动态解析方式。

    • 假设我们有这样一个 native 方法,标准解析下对应的native 方法实现

    • native boolean foo(int x);  ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);
      
    • 给jvm增加一个ClassTransformer并设置native prefix 为wrapped_,这个类在转换之后。

    • 方法的解析规则将变成:

    • native boolean wrapped_foo(int x);  ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);
      
  • 那么我们对forkAndExec进行hook

    • 原始字节码:

      • java.lang.UNIXProcess#forkAndExec
      private final native int forkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10);
      
    • 修改后的字节码:

      • java.lang.UNIXProcess#forkAndExec
      private int forkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10) throws IOException {
          return this.RASPforkAndExec(var1, var2, var3, var4, var5, var6, var7, var8, var9, var10);
      }
      
      • java.lang.UNIXProcess#RASPforkAndExec
      private final native int RASPforkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10);
      
  • 相当于利用jvm动态解析的特性让其在解析native方法的时候解析到了被我们设置了prefix的方法,那么我们便可以在java层面对其进行增强。

  • 而jvm-sandbox中也正是这么做的

    • // DefaultModuleEventWatcher.java
      if(isNativeSupported) {
          inst.setNativeMethodPrefix(sandClassFileTransformer, sandClassFileTransformer.getNativePrefix());
          logger.debug("watch={} in module={} enable native method supported, prefix={}",
                  watchId,
                  uniqueId,
                  sandClassFileTransformer.getNativePrefix());
      }
      
  • 那么具体是如何增强的,可以仔细阅读源码中的这段代码

    • // EventWeaver.java
      /*
       * native 方法插桩策略:
       * 1.原始的native变为非native方法,并增加AOP式方法体
       * 2.在AOP中增加调用wrapper后的native方法
       * 3.增加proxy的native方法
       */
      private MethodVisitor rewriteNativeMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
      
          //去掉native
          int newAccess = access & ~ACC_NATIVE;
      
          final MethodVisitor mv = super.visitMethod(newAccess, name, desc, signature, exceptions);
          return new ReWriteAdapter(api, new JSRInlinerAdapter(mv, newAccess, name, desc, signature, exceptions), newAccess, name, desc) {
      
              private final Label beginLabel = new Label();
              private final Label endLabel = new Label();
              private final Label startCatchBlock = new Label();
              private final Label endCatchBlock = new Label();
              private int newLocal = -1;
      
              // 加载ClassLoader
              private void loadClassLoader() {
                  push(targetClassLoaderObjectID);
              }
      
              /**
               * 流程控制
               */
      
              @Override
              public void visitEnd() {
                  if (!name.startsWith(nativePrefix)) {
                      getCodeLock().lock(() -> {
                          mark(beginLabel);
                          loadArgArray();
                          dup();
                          push(namespace);
                          push(listenerId);
                          loadClassLoader();
                          push(targetJavaClassName);
                          push(name);
                          push(desc);
                          loadThisOrPushNullIfIsStatic();
                          invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnBefore);
                          swap();
                          storeArgArray();
                          pop();
                          processControl(desc, false);
                          final String proxyMethodName = nativePrefix + name;
                          final ProxyMethod proxyMethod = new ProxyMethod(access, proxyMethodName, desc);
                          final String owner = toInternalClassName(targetJavaClassName);
                          if (!isStaticMethod()) {
                              loadThis();
                          }
                          loadArgs();
                          if (isStaticMethod()) {
                              mv.visitMethodInsn(Opcodes.INVOKESTATIC, owner, proxyMethod.getName(), proxyMethod.getDescriptor(), false);
                          } else {
                              //wrapper的方法永远都是private
                              mv.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, proxyMethod.getName(), proxyMethod.getDescriptor(), false);
                          }
                          proxyNativeAsmMethods.add(proxyMethod);
                          loadReturn(Type.getReturnType(desc));
                          push(namespace);
                          push(listenerId);
                          invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnReturn);
                          processControl(desc, true);
                          returnValue();
                          mark(endLabel);
                          mv.visitLabel(startCatchBlock);
                          visitTryCatchBlock(beginLabel, endLabel, startCatchBlock, ASM_TYPE_THROWABLE.getInternalName());
                          newLocal = newLocal(ASM_TYPE_THROWABLE);
                          storeLocal(newLocal);
                          loadLocal(newLocal);
                          push(namespace);
                          push(listenerId);
                          invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnThrows);
                          processControl(desc, false);
                          loadLocal(newLocal);
                          throwException();
                          mv.visitLabel(endCatchBlock);
                      });
                  }
                  super.visitLocalVariable("t", ASM_TYPE_THROWABLE.getDescriptor(), null, startCatchBlock, endCatchBlock, newLocal);
                  super.visitEnd();
              }
          };
      }
      

Contextual Analysis

  • 目前很多商业化的rasp应该具有上下文分析的功能,那么如何获取上下文也是rasp实现的一个要点。
  • 这里我的做法是维护一个全局的weakMap,并hook住http相关的方法来往Map中添加Context信息。于此同时,其他hook方法在判断是可以从中获取Context信息。
package com.lue.rasp;

@MetaInfServices(Module.class)
@Information(id = "rasp-http-hook" , author = "1ue" , version = "0.0.3")
public class HttpHook implements Module, ModuleLifecycle {

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    public void hookRequest() {
        // 添加请求上下文
        new EventWatchBuilder(moduleEventWatcher)
                .onClass("javax.servlet.http.HttpServlet")
                .includeSubClasses()
                .onBehavior("service")
                .withParameterTypes(
                        "javax.servlet.http.HttpServletRequest",
                        "javax.servlet.http.HttpServletResponse"
                ).onWatch(new AdviceListener() {
                    @Override
                    protected void before(Advice advice) throws Throwable {
                        System.out.println("hook到HttpServlet的service方法");
                        // jvm-sandbox 是在独立的 ClassLoader 中运行的,因此需要做一层代理
                        HttpServletRequest request = InterfaceProxyUtils.puppet(HttpServletRequest.class, advice.getParameterArray()[0]);
                        HttpServletResponse response = InterfaceProxyUtils.puppet(HttpServletResponse.class, advice.getParameterArray()[1]);
//                        System.out.println(request);
//                        System.out.println(response);
                        RequestContextHolder.set(new RequestContextHolder.Context(request, response));
                        super.before(advice);
                    }
                });

        // TODO 移除请求上下文
    }

// ...
}
package com.lue.rasp;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 通过 ThreadLocal 保存当前的 HttpServletRequest 和 HttpServletResponse 对象
 */
public class RequestContextHolder {
    private static final ThreadLocal<Context> REQUEST_THREAD_LOCAL = new ThreadLocal<Context>();

    public static Context getContext() {
        return REQUEST_THREAD_LOCAL.get();
    }

    public static void remove() {
        REQUEST_THREAD_LOCAL.remove();
    }

    public static void set(Context context) {
        REQUEST_THREAD_LOCAL.set(context);
    }

    /**
     * 请求上下文,包含 request 和 response
     */
    public static class Context {
        private final HttpServletRequest request;
        private final HttpServletResponse response;

        public Context(HttpServletRequest request, HttpServletResponse response) {
            this.request = request;
            this.response = response;
        }

        public HttpServletRequest getRequest() {
            return request;
        }

        public HttpServletResponse getResponse() {
            return response;
        }
    }
}
  • 注意点:由于jvm-sandbox是在独立的ClassLoader中运行的,因此需要做一层代理。同时jvm-sandbox各个module之间相互隔离,所以不能把RequestContextHolder与hook放在2个不同的jar中。
  • 当然目前的实现还有一些缺陷,比如没有实现移除Context(下次再讲)以及使用了ThreadLocal可能存在线程注入(有更好的实现方式)。先占个坑。

参考