架构说明

背景介绍

RASP 是一种新型应用安全防护技术。这种技术直接将防护引擎嵌入到应用内部,能够感知应用上下文。传统的防护设备,WAF、IDS等等,均是对HTTP请求进行分析和处理,并结合请求特征库进行匹配,能做的事情比较有限。

举个例子,当发生SQL注入攻击时,WAF和IDS只能看到HTTP请求。而RASP技术不但能看到完整的SQL语句,还可以和当前的HTTP请求进行关联,并结合语义引擎、用户输入识别等能力,实现对SQL注入的检测。

如果你还想了解更多,可以参考下面的内容

官方链接

绿盟科技博客

Seebug

Java 版本

java 版本使用 javaagent 机制来实现。在服务器启动时,可动态的修改Java字节码,对敏感操作的函数进行挂钩,比如:

  • 数据库操作
  • 文件读取、写入操作
  • 命令执行
  • ...

当服务器发生攻击,就会触发这些Hook点,此时RASP agent就可以获取到函数的参数,比如要读取的文件名、要执行的命令等等。

启动流程

  1. 启动时首先会进入 javaagent 的 premain 函数,该函数会在 main 函数之前预先执行,javaagent参考文档
  2. 当去 hook 像 java.io.File 这样由 BootstrapClassLoader 加载的类的时候,无法从该类调用非 BootstrapClassLoader 加载的类中的接口,所以 agent.jar 会先将自己添加到 BootstrapClassLoader 的ClassPath下,这样 hook 由 BootstrapClassLoader 加载的类的时候就能够成功调用到 agent.jar 中的检测入口
  3. 释放 log4j 日志配置文件,如果存在则跳过
  4. 根据 openrasp.yml 文件初始化相应配置项
  5. 初始化 JS 插件模块
    • JS 上下文类初始化
    • 插件文件初始化
  6. 初始化字节码转换模块
    • 给 load class 操作进行插桩操作,当类加载的时候会先进入 agent 进行处理
    • 对于在初始化前已加载的类执行 retransform 处理,e.g FileInputStream
  7. 输出启动成功日志,开启全局 Hook 开关(启动阶段为关闭状态)
    • 若启动过程中发生错误,记录错误日志
  8. 给 openrasp.yml 配置文件和 js 插件目录以及 assets 目录增加文件监控,以便文件内容更改的时候不需要重启就能够实时生效

Hook Class 流程

  1. 因为启动时候进行了插桩操作,当有类被 ClassLoader 加载时候,所以会把该类的字节码先交给自定义的 Transformer 处理
  2. 自定义 Transformer 会判断该类是否为需要 hook 的类,如果是会将该类交给 javassist 字节码处理框架进行处理,javassist
  3. javassist 框架会将类的字节码依照事件驱动模型逐步解析每个方法,当触发了我们需要 hook 的方法,我们会在方法的开头或者结尾插入进入检测函数的字节码
  4. 把 hook 好的字节码返回给 transformer 从而载入虚拟机

启动时架构如下图所示:

images/startup.png

请求处理流程

我们以 tomcat + JDBC + MySQL 为例,简单说明下请求处理的流程

  1. 服务器收到一个请求,从而进入了服务器的请求 hook 点(该 hook 点每个服务器不一样,具体参照源码),该 hook 点标注当前线程为请求线程,开启当前线程的检测开关并把请求对象和响应对象进行缓存,以便后面使用
  2. 服务器发起SQL查询
  3. 进入 SQLStatementHook 点,我们挂钩了 execute、executeUpdate、executeQuery 等方法,从该方法进入检测流程如下:
    • 判断当前线程是否为请求线程(第一步标记的),如果是继续下面检测
    • 采集 connection_id(这个字段仅JDBC支持)SQL 语句以及数据库类型 等信息
    • 构建参数信息,调用本地插件和 JS 插件进行安全检测,JS 插件由 Rhino 引擎执行,Rhino 引擎执行是 mozilla 为 java 提供的 JavaScript引擎,该引擎会将 JS 代码编译为 java 的 class 字节码在 JVM 中运行,Rhino 引擎文档
    • 根据插件的执行结果决定是拦截请求、放行还是仅打印日志
  4. 进入 SQLResultSetHook 点,我们挂钩了 resultSet.next 方法
    • 调用本地插件检查是否发生拖库行为,默认策略为一次查询结果超过500条就报警
  5. 若决定拦截攻击
    • 输出报警日志到 logs/alarm.log
    • 如果header还没有发出,默认使用 302 跳转到拦截页面
    • 如果body还没有发出,则重置未发送的body
    • 输出自定义拦截页面跳转js脚本
      • </script><script>location.href='.../?request_id=xxx'</script>

OpenRASP 中JS引擎执行架构如下图所示:

images/js.png

基线检测

我们以 tomcat 启动为例,说明基线检测流程

  1. 进入了基线检测关键函数(tomcat 启动函数)
  2. 进入本地基线检测插件(注:基线检测不进入js插件检测),检测当前环境的关键参数(http-only是否开启,是否为root启动等)
  3. 根据检测结果决定是否拦截,不拦截的情况下只打印日志到 logs/policy_alarm.log

检测架构如下图所示:

images/check.png

PHP 版本

关于PHP扩展开发,可参考 Extending and Embedding PHP

以 cli SAPI 为例,其单个请求生命周期如下图所示:

ref: Extending and Embedding PHP

OpenRASP 核心原理为:在 MINIT 阶段,替换全局compiler_globalsfunction_tableclass_table中特定 PHP_FUNCTION 对应的函数指针(封装原有handler,增加前置、后置处理),由此实现对敏感函数的挂钩。通过敏感函数参数结合请求信息判断是否存在攻击行为,进而采取拦截或者放行操作。

启动流程

OpenRASP 采用模块化的结构,按照初始化顺序,启动流程如下:

  1. 初始化OpenRASP所需全局变量
  2. 注册INI配置条目,通过ini配置文件初始化全局配置
  3. 日志模块,记录报警、插件、基线等日志,支持FILE/TCP/UDP
  4. V8模块,JS运行环境,负责插件加载与结合运行时上下文的检测能力
  5. HOOK模块,敏感PHP_FUNCTION挂钩执行检测及检测结果处理
  6. INJECT模块,针对特定URL,修改响应内容,注入HTML
  7. 安全基线检查模块,检查敏感ini配置项
  8. 文件监控模块,监控插件目录,实现运行时检测逻辑修改

下面针对几个主要的模块进行针对性说明:

日志模块

日志模块启动流程如下:

  1. 初始化日志模块所需全局变量
  2. 申请共享内存(针对特定SAPI),用于部分日志的进/线程间同步
  3. 获取本机网卡以及主机信息,用于基线日志记录
V8模块

我们将 V8 嵌入到 OpenRASP 中作为 JavaScript 插件的执行引擎

  • MINIT 阶段载入所有插件,生成一份 V8 Startup Snapshot
  • 请求处理线程第一次触发检测时,使用 V8 Startup Snapshot 还原此线程独享的 V8 Isolate
  • 每个请求线程在对应的 V8 Isolate 环境上执行检测逻辑
  • 执行检测逻辑前向 V8 Platform 添加超时监控后台任务,超时后中断检测
  • GSHUTDOWN 阶段销毁线程对应 V8 Isolate
HOOK模块

HOOK流程包含两类:compiler_globals的handler替换和用户自定义opcode_handler,启动流程如下

  1. 在全局compiler_globals对应的hashtable(function_table和class_table)中查找非禁用函数对应zend_function
  2. 封装原有handler,根据需求增加前置、后置处理
  3. 针对指定opcode(如ZEND_INCLUDE_OR_EVAL)通过zend_set_user_opcode_handler自定义处理逻辑
文件监控模块

我们使用经过我们优化增强的 libfswatch 实现了跨平台的文件监控

  • MINIT 阶段初始化 fswatch 实例,并开启后台线程进行目标文件目录监控
  • 目标文件目录发生变化时,根据类型,向支持重载的 SAPI 主线程发送重载信号
  • MSHUTDOWN 阶段停止目标文件目录监控,销毁 fswatch 实例和后台线程
白名单实现

经过对比,我们最终选择了 Double Array Trie 算法来匹配白名单,具体实现如下:

  1. 白名单存储。白名单放在共享内存里,当云端下发新的配置,通过读写锁更新
  2. 白名单匹配。使用 Double Array Trie 算法在白名单里寻找匹配的项目,并生成检测类型的 bitmask。当进入检测点,根据 bitmask 来决定是否直接放行。
  3. 内存消耗。每个检测类型最多允许10条白名单,URL长度最大200。最坏情况下,PHP 版本内存 400 KB。
容器支持

为了适配百度内部的ORP平台,我们特意实现了扩展进程管理模型,以避免再安装一个独立的agent。

在扩展初始化阶段,我们会 fork 出三个进程,分别用于异步日志发送、远程管理、进程守护等功能;在 PHP-FPM 或者 apache 退出或者重启时,我们会杀死这些进程。具体请参考我们的代码实现。

请求处理流程

PHP(mysqli) + MySQL为例,简要说明请求处理的流程,即 RINIT - RSHUTDOWN 阶段:

  1. 为新请求计算唯一的request-id,设置response header
  2. 初始化不同logger,收集日志相关请求信息
  3. 连接数据库,触发 mysqli_connect HOOK点:enforce_policy为1时,若用高权限用户连接数据库,记录基线日志,中断当前请求;enforce_policy为0时,仅当成功连接数据库后检查是否为高权限用户,若是记录日基线志,并将连接信息存入共享内存防止其他进/线程重复报警
  4. 数据库语句检测,即mysqli_query HOOK点pre检测,收集查询参数调用V8模块执行检测,具体流程如下:
    1. (待添加)
  5. 慢查询检测。即mysqli_query HOOK点post检测,通过 call_user_function 检测查询结果数目,超过 openrasp.slowquery_min_rows 配置项则报警
  6. RSHUTDOWN阶段:释放请求相关资源,根据 openrasp.inject_urlprefix 配置判断是否注入用户自定义HTML

云控后台

业务模型

云控后台为了将 RASP Agent 分组,引入 APP 的概念,每个 APP 代表一个业务线,每个 APP 有独立的 Secret 用于提供认证,每个 APP 可以管理多个 RASP Agent 的配置和插件。

技术架构

目前,Agent 管理后台采用的是 Go 语言编写,数据库采用的是 MongoDB + Elasticsearch,通讯采用 HTTP/HTTPS + JSON 定时通讯的方式。 整体的架构图如下所示:

ref: rasp-cloud image

如上图所示:

  1. 虚线下方为安装了 RASP 的服务器,上方为云控后台,云控后台分为 Agent ServerPanel ServerAgent Server 负责和 RASP Agent 通信,Panel Server 负责用户与前端的交互。Agent Server 可以部署多个实例,Panel Server只可以部署一个。
  2. Secrete 认证:RaspAgent和云控后台之间的认证方式采用 appID + secrete私钥 的形式进行认证。
  3. 主机注册:在启动阶段,若 RASP Agent 开启远程管理,每隔 5 分钟尝试向 Agent Server 注册一次,直到成功,注册信息将会存入 MongoDB,用于前端展示。
  4. 定时心跳:每隔 3 分钟 RASP Agent与云端通信一次,心跳时间可配置。发送心跳时,同时提交本地配置的版本号。若有更新版本的配置或者插件,云端会通过心跳返回新的配置和插件。
  5. 报警日志上传:图中绿色箭头为报警日志的上传流向,Agent Server 采集日志有两种可选模式,第一种模式直接将 RASP Agent 上传的日志存入 ES,这种方式在日志量较大的情况下会有日志丢失的情况,第二种是将日志写入文件,然后由 logstash 采集并传入 ES,这种方式较为复杂,但日志不易丢失,不同语言的 RASP 内部日志上传方式如下:
    • PHP 版本每隔 10s 检查是否有新的日志。若上传成功,会记录文件读取偏移量,并将状态文件落地。
    • Java 版本使用 Log4j 内置的缓冲区,默认大小是 128 条。若未发送的日志量超过缓冲区大小,最早的日志会被抛弃。
  6. 插件与配置升级:用户通过 Panel Server 下发配置和插件到 MongoDB,然后由 RASP Agent 通过与 Agent Server 心跳取走新的配置和插件。