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

181

这篇文章咕咕了很久,主要是觉得前期可能自己的认识和沉淀还不够,现在感觉自己又有了新的意思,以此作为前2篇的补充和完善。后续再写rasp相关的文章,可能就会偏向trick、技巧等。因为写这三篇文章的目的就是搭起rasp开发的架构。。。

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

Hook Deserial

  • 如果了解一些RASP的攻防手段,可以发现绝大多数的RASP绕过条件都是基于任意代码执行的情况下(反序列化提供),与此同时,java应用中绝大多数的RCE攻击面都是直接或者间接反序列化导致的RCE。
  • 所以Hook反序列化其实是非常重要的点,所以这就意味这我们直接hook住ObjectInputStreamreadObject方法就合理了吗? 显然不是,原因如下:
    • rasp的本质就是给字节码做修改,hook了readObject的逻辑,你又要如何去定义防御算法。对于有上下文的请求就直接拦截吗?显然不是,这极有可能对业务造成干扰。
  • 回顾反序列化常见的补丁做法,就是重写resolveClass方法,所以理论上合理的做法是在这个方法上做文章。如何hook此方法,可以有如下写法:
    public void checkDeserialize() {
        new EventWatchBuilder(moduleEventWatcher)
                .onClass(ObjectInputStream.class)
                .includeBootstrap()
                .onBehavior("resolveClass")
                .onWatch(new AdviceListener() {
                    @Override
                    protected void before(Advice advice) throws Throwable {
                        ObjectStreamClass oc = (ObjectStreamClass) advice.getParameterArray()[0];
                        System.out.println("hook到resolveClass方法:" + oc);
                        if (oc.getName().startsWith("java.net")) { // 添加黑白名单校验逻辑
                            RequestContextHolder.Context context = RequestContextHolder.getContext();
                            if (null != context ) {
                                StackTrace.logAttack(context,"deserialize","high");
                            }
                            ProcessController.throwsImmediately(new RuntimeException("Block By RASP!!!"));
                        }
                        super.before(advice);
                    }
                });
    }

img

  • 如此一来,就相当于我们为应用中的代码加上了resolveClass的补丁了,那么现在决定你rasp安全性的就是你规则的定义了。
  • 值得注意的是,对于rasp来说的,在非定制规则的情况下,是很难说去定义白名单规则的。定义白名单太容易对业务进行冲突了。所以现实中常见的也应该是黑名单的策略。而这份黑名单对于商业的rasp也尤为重要。笔者也无法给出一个完美的黑名单。
  • 对于常见的绕过方式,就ban掉对应的类,例如对应读写mem绕过的方式就可以hook像RandomAcessFile读写文件的类。

Hook Reflect

  • 在谈这一节之前,我想先展示下面这段代码
            Class<?> clazz = Class.forName("java.lang.UNIXProcess");

            //bypass jrasp native hook
            for (Method m : clazz.getDeclaredMethods()) {
                if (m.getName().endsWith("forkAndExec")&&!m.getName().equals("forkAndExec")) {
                    m.setAccessible(true);
                    System.out.println("prefix native method : "+m.getName());
                    Cmd.linuxCmd(new String[]{"/bin/bash","-c","cat /etc/shadow && touch /tmp/shadow2"},m);
                }
            }
  • 没错,这段代码绕过java层面对native方法的hook。。。
  • 回想之前java层面是怎么hook native方法的,就是把forkAndExec变成正常方法,而RASPforkAndExec则是native方法
    • 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);
      }
      
  • 而其绕过思路则是跳过forkAndExec方法,直接反射执行RASPforkAndExec这段native方法,而这native方法恰好是java层面修改不了,也就导致了绕过。
  • 不过这不代表我们就防御不了,对于反序列化的代码执行情况下,我们只需nop掉反射相关的包名即可。但对于jsp等其他一些方式,我们终究还是绕不过对反射相关的hook。
  • 可是业务中存在反射,耦合性太强怎么办,其实这种时候我们只要针对这种攻击情况特殊处理即可,例如使用下面这段防护代码
    • private void checkReflect() {
          new EventWatchBuilder(moduleEventWatcher)
                  .onClass(Method.class)
                  .includeBootstrap()
                  .onBehavior("invoke")
                  .onWatch(new AdviceListener() {
                      @Override
                      protected void before(Advice advice) throws Throwable {
                          Method target = (Method) advice.getTarget();
                          if (target.getName().endsWith("forkAndExec")) {
                              RequestContextHolder.Context context = RequestContextHolder.getContext();
                              StackTrace.logAttack(context, "reflect", "high");
                              ProcessController.throwsImmediately(new RuntimeException("Block By RASP!!! Bad Boy"));
                          }
                          super.before(advice);
                      }
                  });
      }
      
  • 其hook了method的invoke,确定范围是以forkAndExec为后缀方法,这样就不会对业务产生干扰了,同时也可以防御成功。

Web-Console

如果说rasp作为一个商业产品,web控制台是非常有必要的,下面主要会讲讲实现web控制台一些核心功能上实现的主要思路:

Agent管理

前面说到过,本rasp是基于jvm-sandbox框架开发的,所以这个时候框架的一些优势就体现出来了。

jvm-sandbox在使用后,会自动开启一个jetty服务,用于api交互,并且这个端口可以由启动的-P参数指定。

例如以下命令来安装agent./sandbox.sh -p 17531 -P 40001

img

而数据库只要记录图中的SERVER_ADDRSERVER_PORT即可,如果没有用-P参数指定,那么port是随机的,只要grep脚本里回显的port即可。

而有了jetty server可以根据官方的ip合理地管理agent的信息,例如获取module信息

img

其他更丰富的接口可以自行摸索

日志管理

这里介绍一种filebeat的日志运行模式,其实也就是在agent端把日志存储在文件里,然后web-console定时去agent拉取。这是我认为最简单的方式,因为如果要在agent端继承kafka或日志存储的sdk,代价也太大了。

对于agent端,可以简单定义存储规则实现即可,非常简单,想要存日志就调此函数即可。

public static void logAttack(RequestContextHolder.Context context, String attackType, String attackRisk) throws IOException {
    String dateDirName = TimeUtils.getDateDirName();
    String dirName = "/tmp/" + dateDirName;
    FileUtils.createDir(dirName);
    String filePath = dirName + "/" + IDUtils.generateUUID() + ".log";
    FileWriter fileWriter = new FileWriter(filePath);
    System.out.println("开始写入日志");
    try (BufferedWriter writer = new BufferedWriter(fileWriter)) {
        writer.write(TimeUtils.getTimeString());
        writer.newLine();
        writer.write(attackType);
        writer.newLine();
        writer.write(attackRisk);
        writer.newLine();
        writer.write(context.getRequest().getRequestURI());
        writer.newLine();
        writer.write("success Block!");
        writer.newLine();
        String[] stackTraceString = getStackTraceString();
        for (String s : stackTraceString) {
            writer.write(s);
            writer.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

对于console端,在查询时只需要去请求该文件即可,或者也可以设置定时任务。

不过对于agent端和console端不在同一ip上时,可能就需要agent开启一个文件server。

结语

  • 其实rasp就是这么点东西,不过商业化的实现确实有很多去优化的地方,比如agent对系统的性能影响,优化部署简易性等等。
  • 现在有些java语言的rasp用c来写,我觉得也是蛮不错的,可以更方便地hook本地方法,不过笔者喜欢java,就用java了,其实没什么区别(包括性能),都只是在修改字节码罢了。至于其他语言的rasp,我还不会,有机会研究。。。
  • rasp重要的是策略!

参考