• 系统架构 - PHP 版本
    • 简要说明
    • 启动流程
      • 日志模块
      • V8模块
      • HOOK模块
      • 文件监控模块
      • 白名单实现
      • 容器支持
    • 请求处理流程

    系统架构 - PHP 版本

    简要说明

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

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

    ref: Extending and Embedding PHP

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

    启动流程

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

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

    日志模块启动流程如下:

    • 初始化日志模块所需全局变量
    • 申请共享内存(针对特定SAPI),用于部分日志的进/线程间同步
    • 获取本机网卡以及主机信息,用于基线日志记录
    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,启动流程如下

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

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

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

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

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

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

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

    请求处理流程

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

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