Skip to content

PHP RASP

江湖风轻 edited this page Oct 9, 2022 · 3 revisions

原理

PHP 解释器提供了挂钩函数与 opcode 的接口,我们需要基于 PHP 提供的头文件,使用 C 系语言构建一个 RASP 拓展。PHP 在加载拓展后,会依次调用模块导出的初始化函数,我们在函数中便可以完成函数与 opcode 的挂钩替换,获取 HTTP 请求参数、函数调用详情以及调用栈,传输到远端进行分析。 对于函数而言,直接搜索函数哈希表然后替换 internal_function.handler 指向自定义的 wrapper,便可以在函数被调用时获得控制权,而 opcode 则只需要简单的调用 zend_set_user_opcode_handler 指向自定义处理函数。 需要注意的是,PHP 的拓展本质是一个动态库,使用导出函数供解释器调用以完成加载。而且针对不同的 PHP 版本,都需要使用对应的头文件分别进行编译,否则无法保证正确加载,Elkeid RASP 每次发布都会附带常见的预编译版本。

多进程架构

PHP 通常不会以 CLI 模式单独运行,大多数会依赖于 PHP-FPMApache。在默认情况下 PHP 以单线程运行,明显这样是无法满足高并发处理需求的,所以 PHP-FPM 这些框架在主进程加载完模块后,会进行多次 fork 使用多进程模型分担流量压力。 在主进程加载 RASP 拓展初始化时,我们挂钩了函数与 opcode,之后主进程 fork 出多个进程,这些进程完全一致且函数与 opcode 都处于被挂钩了的状态,现在的问题是我们在 wrapper 函数中应该将调用数据发往哪里? 为了减少性能影响,Elkeid RASP 系列产品都不会使用脚本引擎在本地进行检测,而是将函数调用数据序列化为 json 通过 unix socket 传输到服务器进行分析。那么为了进行异步的数据收发,通常需要一个单独的 eventloop 线程,但是多线程在进行 fork 时存在安全隐患可能导致死锁,而且单次 fork 只会复制当前调用线程。 为了在多个进程之间共享调用数据和配置,Elkeid RASP 模块在主进程初始化阶段会分配一段共享内存,其中包含一个用来传递调用信息的无锁环形缓冲区,以及一片储存配置的共享区域。分配完成后,调用 fork 复制一个 RASP 独占的新进程通过 unix socket 进行消息通信,从缓冲区消费数据发送到远端,以及从远端收取配置消息写入共享空间。

拓展生命周期

PHP 每个拓展加载后都会经历四个阶段:

  • MINIT,这是模块的启动步骤,对于 RASP 而言通常在此处完成挂钩操作。
  • RINIT,每个请求的到来都会触发,可以获取此次请求的详细信息进行分析。
  • RSHUTDOWN,每个请求处理结束后触发,通常可以忽略该步骤。
  • MSHUTDOWN,模块的卸载步骤,在此处进行资源释放和清理操作。

MINIT

RASP 在此处进行模块初始化和挂钩操作:

PHP_MINIT_FUNCTION (php_probe) {
    ZEND_INIT_MODULE_GLOBALS(php_probe, PHP_GINIT(php_probe), PHP_GSHUTDOWN(php_probe))

    if (!gAPIConfig || !gAPITrace)
        return FAILURE;

    if (fork() == 0) {
        INIT_FILE_LOG(zero::INFO, "php-probe");

        char name[16] = {};
        snprintf(name, sizeof(name), "probe(%d)", getppid());

        if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) {
            LOG_ERROR("set death signal failed");
            exit(-1);
        }

        if (pthread_setname_np(pthread_self(), name) != 0) {
            LOG_ERROR("set process name failed");
            exit(-1);
        }

        gSmithProbe->start();

        exit(0);
    }

    for (const auto &api: PHP_API) {
        HashTable *hashTable = CG(function_table);

        if (api.cls) {
#if PHP_MAJOR_VERSION > 5
            auto cls = (zend_class_entry *) zend_hash_str_find_ptr(CG(class_table), api.cls, strlen(api.cls));

            if (!cls) {
                LOG_WARNING("can't found class: %s", api.cls);
                continue;
            }

            hashTable = &cls->function_table;
#else
            zend_class_entry **cls;

            if (zend_hash_find(CG(class_table), api.cls, strlen(api.cls) + 1, (void **)&cls) != SUCCESS) {
                LOG_WARNING("can't found class: %s", api.cls);
                continue;
            }

            hashTable = &(*cls)->function_table;
#endif
        }

#if PHP_MAJOR_VERSION > 5
        auto func = (zend_function *) zend_hash_str_find_ptr(hashTable, api.name, strlen(api.name));

        if (!func) {
            LOG_WARNING("can't found function: %s", api.name);
            continue;
        }
#else
        zend_function *func;

        if (zend_hash_find(hashTable, api.name, strlen(api.name) + 1, (void **)&func) != SUCCESS) {
            LOG_WARNING("can't found function: %s", api.name);
            continue;
        }
#endif

#if PHP_MAJOR_VERSION < 8
        if (func->internal_function.handler == ZEND_FN(display_disabled_function)) {
            LOG_WARNING("disabled function: %s", api.name);
            continue;
        }
#endif

        *api.metadata.origin = func->internal_function.handler;
        func->internal_function.handler = api.metadata.entry;
    }

    for (const auto &opcode: PHP_OPCODE)
        zend_set_user_opcode_handler(opcode.op, opcode.handler);

    return SUCCESS;
}

首先使用 ZEND_INIT_MODULE_GLOBALS 初始化全局变量,用来持续追踪单次请求的详细参数,可以将多次函数调用关联到某个具体的请求。另外需要注意,这里的全局变量并不是语言层面上的,因为 PHP 有多线程版本,在多线程中会为每个线程拷贝一份全新的变量,当然这些都会在头文件提供的宏中进行判断。

ZEND_BEGIN_MODULE_GLOBALS(php_probe)
    Request request{};
ZEND_END_MODULE_GLOBALS(php_probe)

gAPIConfiggAPITrace 便是共享内存中的缓冲区与配置区域,然后 fork 一个进程进行消息通信,最后依次查找函数、挂钩函数以及挂钩 opcode

RINIT

在这个阶段我们可以获取一个请求的详细信息,例如 urlheader 或者 body,储存在全局变量的 request 中:

PHP_RINIT_FUNCTION (php_probe) {
    zval *server = HTTPGlobals(
            TRACK_VARS_SERVER
#if PHP_MAJOR_VERSION <= 5
            TSRMLS_CC
#endif
    );

    if (!server || Z_TYPE_P(server) != IS_ARRAY)
        return SUCCESS;

    auto fetch = [=](const HashTable *hashTable, const char *key) -> std::string {
        zval *val = hashFind(hashTable, key);

        if (!val)
            return "";

        return {Z_STRVAL_P(val), (std::size_t) Z_STRLEN_P(val)};
    };

    strncpy(PHP_PROBE_G(request).scheme, fetch(Z_ARRVAL_P(server), "REQUEST_SCHEME").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).host, fetch(Z_ARRVAL_P(server), "HTTP_HOST").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).serverName, fetch(Z_ARRVAL_P(server), "SERVER_NAME").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).serverAddress, fetch(Z_ARRVAL_P(server), "SERVER_ADDR").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).uri, fetch(Z_ARRVAL_P(server), "REQUEST_URI").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).query, fetch(Z_ARRVAL_P(server), "QUERY_STRING").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).method, fetch(Z_ARRVAL_P(server), "REQUEST_METHOD").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).remoteAddress, fetch(Z_ARRVAL_P(server), "REMOTE_ADDR").c_str(), SMITH_FIELD_LENGTH - 1);
    strncpy(PHP_PROBE_G(request).documentRoot, fetch(Z_ARRVAL_P(server), "DOCUMENT_ROOT").c_str(), SMITH_FIELD_LENGTH - 1);

    std::optional<short> port = zero::strings::toNumber<short>(fetch(Z_ARRVAL_P(server), "SERVER_PORT"));

    if (port)
        PHP_PROBE_G(request).port = *port;

    for (const auto &e: Z_ARRVAL_P(server)) {
        if (e.type != HASH_KEY_IS_STRING || Z_TYPE_P(e.value) != IS_STRING)
            continue;

        std::string override;
        std::string key = std::get<std::string>(e.key);

        if (key == "HTTP_CONTENT_TYPE" || key == "CONTENT_TYPE") {
            override = "content-type";
        } else if (key == "HTTP_CONTENT_LENGTH" || key == "CONTENT_LENGTH") {
            override = "content-length";
        } else if (zero::strings::startsWith(key, "HTTP_")) {
            override = zero::strings::tolower(key.substr(5));
            std::replace(override.begin(), override.end(), '_', '-');
        } else {
            continue;
        }

        strncpy(PHP_PROBE_G(request).headers[PHP_PROBE_G(request).header_count][0], override.c_str(), SMITH_FIELD_LENGTH - 1);
        strncpy(PHP_PROBE_G(request).headers[PHP_PROBE_G(request).header_count][1], Z_STRVAL_P(e.value), SMITH_FIELD_LENGTH - 1);

        if (++PHP_PROBE_G(request).header_count >= SMITH_HEADER_COUNT)
            break;
    }

    if (strcasecmp(PHP_PROBE_G(request).method, "post") != 0 && strcasecmp(PHP_PROBE_G(request).method, "put") != 0)
        return SUCCESS;

    auto begin = PHP_PROBE_G(request).headers;
    auto end = begin + PHP_PROBE_G(request).header_count;

    if (std::find_if(begin, end, [](const auto &header) {
        if (strcmp(header[0], "content-type") != 0)
            return false;

        return strncmp(header[1], "multipart/form-data", 19) == 0;
    }) != end) {
        strncpy(
                PHP_PROBE_G(request).body,
                toString(
                        HTTPGlobals(
                                TRACK_VARS_POST
#if PHP_MAJOR_VERSION <= 5
                                TSRMLS_CC
#endif
                        )
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                ).c_str(),
                SMITH_FIELD_LENGTH - 1
        );

        zval *files = HTTPGlobals(
                TRACK_VARS_FILES
#if PHP_MAJOR_VERSION <= 5
                TSRMLS_CC
#endif
        );

        if (!files || Z_TYPE_P(files) != IS_ARRAY)
            return SUCCESS;

        for (const auto &e: Z_ARRVAL_P(files)) {
            if (Z_TYPE_P(e.value) != IS_ARRAY)
                continue;

            strncpy(PHP_PROBE_G(request).files[PHP_PROBE_G(request).file_count].name, fetch(Z_ARRVAL_P(e.value), "name").c_str(), SMITH_FIELD_LENGTH - 1);
            strncpy(PHP_PROBE_G(request).files[PHP_PROBE_G(request).file_count].type, fetch(Z_ARRVAL_P(e.value), "type").c_str(), SMITH_FIELD_LENGTH - 1);
            strncpy(PHP_PROBE_G(request).files[PHP_PROBE_G(request).file_count].tmp_name, fetch(Z_ARRVAL_P(e.value), "tmp_name").c_str(), SMITH_FIELD_LENGTH - 1);

            if (++PHP_PROBE_G(request).file_count >= SMITH_FILE_COUNT)
                break;
        }

        return SUCCESS;
    }

    php_stream *stream = php_stream_open_wrapper("php://input", "rb", REPORT_ERRORS, nullptr);

    if (!stream)
        return SUCCESS;

    char buffer[1024] = {};

    int n = php_stream_read(stream, buffer, sizeof(buffer));

    if (n < 0) {
        php_stream_close(stream);
        return SUCCESS;
    }

    for (int i = 0, j = 0; i < n && j < SMITH_FIELD_LENGTH - 1; i++) {
        if (!isprint(buffer[i]))
            continue;

        PHP_PROBE_G(request).body[j++] = buffer[i];
    }

    php_stream_close(stream);

    return SUCCESS;
}

RSHUTDOWN

这个阶段时请求已经处理完成,我们需要将 request 清空:

PHP_RSHUTDOWN_FUNCTION (php_probe) {
    PHP_PROBE_G(request) = {};
    return SUCCESS;
}

MSHUTDOWN

在模块卸载的阶段,我们还原挂钩的函数以及 opcode,释放已分配的资源:

PHP_MSHUTDOWN_FUNCTION (php_probe) {
    for (const auto &api: PHP_API) {
        HashTable *hashTable = CG(function_table);

        if (api.cls) {
#if PHP_MAJOR_VERSION > 5
            auto cls = (zend_class_entry *) zend_hash_str_find_ptr(CG(class_table), api.cls, strlen(api.cls));

            if (!cls) {
                LOG_WARNING("can't found class: %s", api.cls);
                continue;
            }

            hashTable = &cls->function_table;
#else
            zend_class_entry **cls;

            if (zend_hash_find(CG(class_table), api.cls, strlen(api.cls) + 1, (void **)&cls) != SUCCESS) {
                LOG_WARNING("can't found class: %s", api.cls);
                continue;
            }

            hashTable = &(*cls)->function_table;
#endif
        }

#if PHP_MAJOR_VERSION > 5
        auto func = (zend_function *) zend_hash_str_find_ptr(hashTable, api.name, strlen(api.name));

        if (!func) {
            LOG_WARNING("can't found function: %s", api.name);
            continue;
        }
#else
        zend_function *func;

        if (zend_hash_find(hashTable, api.name, strlen(api.name) + 1, (void **)&func) != SUCCESS) {
            LOG_WARNING("can't found function: %s", api.name);
            continue;
        }
#endif

#if PHP_MAJOR_VERSION < 8
        if (func->internal_function.handler == ZEND_FN(display_disabled_function)) {
            LOG_WARNING("disabled function: %s", api.name);
            continue;
        }
#endif

        if (!*api.metadata.origin) {
            LOG_WARNING("null origin handler");
            continue;
        }

        func->internal_function.handler = *api.metadata.origin;
    }

    for (const auto &opcode: PHP_OPCODE)
        zend_set_user_opcode_handler(opcode.op, nullptr);

#ifdef ZTS
    ts_free_id(php_probe_globals_id);
#else
    PHP_GSHUTDOWN(php_probe)(&php_probe_globals);
#endif

    return SUCCESS;
}

变量

PHP 中所有的变量都储存在一个 zval 结构体中,实际上就是一个巨大的 union,根据类型获取相对应的值。为了更好的传递函数调用信息,我们需要将每个参数转为可读字符串类型,所以我们手写一个转换函数:

std::string toString(
        zval *val
#if PHP_MAJOR_VERSION <= 5
        TSRMLS_DC
#endif
) {
    if (!val)
        return "";

    switch (Z_TYPE_P(val)) {
        case IS_NULL:
            return "null";

#if PHP_MAJOR_VERSION > 5
        case IS_FALSE:
            return "false";

        case IS_TRUE:
            return "true";
#else
        case IS_BOOL:
            return Z_BVAL_P(val) ? "true" : "false";
#endif

        case IS_LONG:
            return std::to_string(Z_LVAL_P(val));

        case IS_DOUBLE:
            return std::to_string(Z_DVAL_P(val));

        case IS_STRING:
            return {Z_STRVAL_P(val), (std::size_t) Z_STRLEN_P(val)};

        case IS_ARRAY: {
            auto quoted = [](const std::string &str) -> std::string {
                std::stringstream ss;
                ss << std::quoted(str);

                return ss.str();
            };

            bool uneven = false;
            std::map<std::string, std::string> kv;

            for (const auto &e: Z_ARRVAL_P(val)) {
                switch (e.type) {
                    case HASH_KEY_IS_LONG:
                        kv.insert({
                            std::to_string(std::get<unsigned long>(e.key)),
                            toString(
                                    e.value
#if PHP_MAJOR_VERSION <= 5
                                    TSRMLS_CC
#endif
                            )
                        });

                        break;

                    case HASH_KEY_IS_STRING:
                        uneven = true;

                        kv.insert({
                            quoted(std::get<std::string>(e.key)),
                            toString(
                                    e.value
#if PHP_MAJOR_VERSION <= 5
                                    TSRMLS_CC
#endif
                            )
                        });

                        break;

                    default:
                        break;
                }
            }

            std::list<std::string> items;

            if (!uneven) {
                std::transform(
                        kv.begin(),
                        kv.end(),
                        std::back_inserter(items),
                        [](const auto &it) {
                            return it.second;
                        }
                );

                return zero::strings::join(items, " ");
            }

            std::transform(
                    kv.begin(),
                    kv.end(),
                    std::back_inserter(items),
                    [&](const auto &it) {
                        return it.first + ": " + quoted(it.second);
                    }
            );

            return zero::strings::format("{%s}", zero::strings::join(items, ", ").c_str());
        }

        case IS_RESOURCE: {
            const char *type = zend_rsrc_list_get_rsrc_type(
#if PHP_MAJOR_VERSION > 5
                    Z_RES_P(val)
#else
                    Z_LVAL_P(val) TSRMLS_CC
#endif
            );

            if (!type)
                break;

            if (strcmp(type, "curl") == 0)
                return curlInfo(
                        val
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                );

            return "resource";
        }

        default:
            break;
    }

    return "unknown";
}

值得注意的是,语言中常见的数组和字典在 PHP 中都是 array 类型,它实际上就是 kv 类型的哈希表,针对广义上的数组储存会将 key 设为连续的 index 数字,所以在转为字符串时可以将这些 key 忽略。

调用参数

函数挂钩后被调用时,我们临时获取到执行权,这是我们需要提取出调用参数以及调用栈,传递给远端分析,我们先来看函数原型:

void entry(zend_execute_data *execute_data, zval *return_value);

很显然我们需要从 execute_data 提取出参数,PHP 提供的一种兼容性较好的方式是调用 zend_parse_parameters

long l;
char *s;
int s_len;
zval *param;

zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lsz", &l, &s, &s_len, &param);

这是一个类似于 scanf 的公开接口,我们可以指定输入参数的具体类型,将其分解到对应的 C 语言类型中,或者使用 'z' 直接获取原始 zval。但是一个明显的缺点是,如果有两个入参不同的函数,我们便需要编写两个 wrapper 函数,并使用不同的 zend_parse_parameters 参数进行爬取。 简化一下,我们可以单纯地使用 'z' 提出出所有参数的原始 zval,以使用上一步的转换函数转为字符串,但是还存在的问题是函数的参数个数会不一样,而且 PHP 允许忽略可选参数,例如使用 'z|z' 爬取一个必要参数和一个可选参数。 为了更好地定义 wrapper 以及挂钩函数,我编写了一套模板用来提取参数:

template<int ClassID, int MethodID, bool CanBlock, bool Ret, int Required, int Optional = 0>
class APIEntry {
public:
    static constexpr auto getTypeSpec() {
        constexpr size_t length = Required + (Optional > 0 ? Optional + 1 : 0);
        std::array<char, length + 1> buffer = {};

        for (size_t i = 0; i < length; i++) {
            if (i == Required) {
                buffer[i] = '|';
                continue;
            }

            buffer[i] = 'z';
        }

        return buffer;
    }

    static void entry(INTERNAL_FUNCTION_PARAMETERS) {
        entry(std::make_index_sequence<Required + Optional>{}, INTERNAL_FUNCTION_PARAM_PASSTHRU);
    }

    template<size_t ...Index>
    static void entry(std::index_sequence<Index...>, INTERNAL_FUNCTION_PARAMETERS) {
        zval *args[sizeof...(Index)] = {};
        int argc = std::min(Required + Optional, (int)ZEND_NUM_ARGS());

#if PHP_MAJOR_VERSION > 5
        constexpr
#endif
        auto spec = getTypeSpec();

        if (zend_parse_parameters(
#if PHP_MAJOR_VERSION > 5
                argc,
#else
                argc TSRMLS_CC,
#endif
                spec.data(),
                &args[Index]...) != SUCCESS) {
            origin(INTERNAL_FUNCTION_PARAM_PASSTHRU);
            return;
        }

        Trace trace = {
                ClassID,
                MethodID
        };

        while (trace.count < std::min(argc, SMITH_ARG_COUNT)) {
            zval *arg = args[trace.count];

            if (!arg)
                continue;

            strncpy(
                    trace.args[trace.count++],
                    toString(
                            arg
#if PHP_MAJOR_VERSION <= 5
                            TSRMLS_CC
#endif
                    ).c_str(),
                    SMITH_ARG_LENGTH - 1
            );
        }

        std::vector<std::string> stackTrace = traceback(
#if PHP_MAJOR_VERSION <= 5
                TSRMLS_C
#endif
        );

        for (int i = 0; i < stackTrace.size() && i < SMITH_TRACE_COUNT; i++) {
            strncpy(trace.stackTrace[i], stackTrace[i].c_str(), SMITH_TRACE_LENGTH - 1);
        }

        trace.request = PHP_PROBE_G(request);

        if constexpr (CanBlock) {
            if (gAPIConfig->block(trace)) {
                trace.blocked = true;

                gAPITrace->enqueue(trace);

                zend_throw_exception(
                        nullptr,
                        "API blocked by RASP",
                        0
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                );

                return;
            }
        }

        if constexpr (!Ret) {
            if (!gAPIConfig->surplus(ClassID, MethodID))
                return;

            gAPITrace->enqueue(trace);
            origin(INTERNAL_FUNCTION_PARAM_PASSTHRU);

            return;
        }

        origin(INTERNAL_FUNCTION_PARAM_PASSTHRU);

        strncpy(
                trace.ret,
                toString(
                        return_value
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                ).c_str(),
                SMITH_ARG_LENGTH - 1
        );

        if (!gAPIConfig->surplus(ClassID, MethodID))
            return;

        gAPITrace->enqueue(trace);
    }

public:
    static constexpr APIMetadata metadata() {
        return {entry, &origin};
    }

public:
    static handler origin;
};

例如对于有可选参数的 readfile 函数,我们可以简单的定义 wrapper

APIEntry<1, 1, false, false, 1, 1>::metadata()

Required 表明必需的参数个数,Optional 表示可选的参数个数,当然对于 readfile 而言,可选参数实际为 2 个,但是最后一个我们用不上所以忽略。

Opcode 参数

opcode 处理函数中获取参数有所不同,并且 opcode 限制了参数个数上限为 2。在 PHP7 之后,我们可以简单的使用 zend_get_zval_ptr 获取参数的原始 zval

int entry(
        zend_execute_data *execute_data
#if PHP_MAJOR_VERSION <= 5
        TSRMLS_DC
#endif
) {
    zval *op1 = zend_get_zval_ptr(execute_data->opline, execute_data->opline->op1_type, &execute_data->opline->op1, execute_data);
    zval *op2 = zend_get_zval_ptr(execute_data->opline, execute_data->opline->op2_type, &execute_data->opline->op2, execute_data);

    return ZEND_USER_OPCODE_DISPATCH;
}

zend_get_zval_ptr 只会简单地拷贝一份 zval,并不会修改引用计数,但是在 PHP5 中这是危险的。由于设计问题,在 PHP5 中调用 zend_get_zval_ptr 会认为你当前持有该变量,获取参数后会进行释放,所以接口内部会修改引用计数,导致返回 ZEND_USER_OPCODE_DISPATCH 调用原始处理函数时,无法获取到正确的入参。针对这个问题,我们需要使用大量的宏判断以适配各版本。同样为了更好地批量定义 wrapper,我编写了一套可配置模板:

template<int ClassID, int MethodID, bool Extended = false>
class OpcodeEntry {
public:
    static int entry(
            zend_execute_data *execute_data
#if PHP_MAJOR_VERSION <= 5
            TSRMLS_DC
#endif
    ) {
#if PHP_VERSION_ID >= 80000
        zval *op1 = zend_get_zval_ptr(execute_data->opline, execute_data->opline->op1_type, &execute_data->opline->op1, execute_data);
        zval *op2 = zend_get_zval_ptr(execute_data->opline, execute_data->opline->op2_type, &execute_data->opline->op2, execute_data);
#elif PHP_VERSION_ID >= 70300
        zend_free_op should_free;
        zval *op1 = zend_get_zval_ptr(execute_data->opline, execute_data->opline->op1_type, &execute_data->opline->op1, execute_data, &should_free, BP_VAR_IS);
        zval *op2 = zend_get_zval_ptr(execute_data->opline, execute_data->opline->op2_type, &execute_data->opline->op2, execute_data, &should_free, BP_VAR_IS);
#elif PHP_VERSION_ID >= 70000
        zend_free_op should_free;
        zval *op1 = zend_get_zval_ptr(execute_data->opline->op1_type, &execute_data->opline->op1, execute_data, &should_free, BP_VAR_IS);
        zval *op2 = zend_get_zval_ptr(execute_data->opline->op2_type, &execute_data->opline->op2, execute_data, &should_free, BP_VAR_IS);
#elif PHP_VERSION_ID >= 50500
        auto extract = [&](zend_uchar type, znode_op *node) {
            switch (type) {
                case IS_TMP_VAR:
                    return &EX_TMP_VAR(execute_data, node->var)->tmp_var;

                case IS_VAR:
                    return EX_TMP_VAR(execute_data, node->var)->var.ptr;

                default:
                    break;
            }

            zend_free_op should_free;

            return zend_get_zval_ptr(type, node, execute_data, &should_free, BP_VAR_IS TSRMLS_CC);
        };

        zval *op1 = extract(execute_data->opline->op1_type, &execute_data->opline->op1);
        zval *op2 = extract(execute_data->opline->op2_type, &execute_data->opline->op2);
#elif PHP_VERSION_ID >= 50400
        auto extract = [&](zend_uchar type, znode_op *node) {
            switch (type) {
                case IS_TMP_VAR:
                    return &((temp_variable *)((char *)execute_data->Ts + node->var))->tmp_var;

                case IS_VAR:
                    return ((temp_variable *)((char *)execute_data->Ts + node->var))->var.ptr;

                default:
                    break;
            }

            zend_free_op should_free;

            return zend_get_zval_ptr(type, node, execute_data->Ts, &should_free, BP_VAR_IS TSRMLS_CC);
        };

        zval *op1 = extract(execute_data->opline->op1_type, &execute_data->opline->op1);
        zval *op2 = extract(execute_data->opline->op2_type, &execute_data->opline->op2);
#else
        auto extract = [&](int type, znode *node) {
            switch (type) {
                case IS_TMP_VAR:
                    return &((temp_variable *)((char *)execute_data->Ts + node->u.var))->tmp_var;

                case IS_VAR:
                    return ((temp_variable *)((char *)execute_data->Ts + node->u.var))->var.ptr;

                default:
                    break;
            }

            zend_free_op should_free;

            return zend_get_zval_ptr(node, execute_data->Ts, &should_free, BP_VAR_IS TSRMLS_CC);
        };

        zval *op1 = extract(execute_data->opline->op1.op_type, &execute_data->opline->op1);
        zval *op2 = extract(execute_data->opline->op2.op_type, &execute_data->opline->op2);
#endif

        Trace trace = {
                ClassID,
                MethodID
        };

        strncpy(
                trace.args[trace.count++],
                toString(
                        op1
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                ).c_str(),
                SMITH_ARG_LENGTH - 1
        );

        strncpy(
                trace.args[trace.count++],
                toString(
                        op2
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                ).c_str(),
                SMITH_ARG_LENGTH - 1
        );

        if constexpr (Extended) {
#if PHP_VERSION_ID >= 50400
            strncpy(trace.args[trace.count++], std::to_string(execute_data->opline->extended_value).c_str(), SMITH_ARG_LENGTH - 1);
#else
            strncpy(trace.args[trace.count++], std::to_string(Z_LVAL(execute_data->opline->op2.u.constant)).c_str(), SMITH_ARG_LENGTH - 1);
#endif
        }

        std::vector<std::string> stackTrace = traceback(
#if PHP_MAJOR_VERSION <= 5
                TSRMLS_C
#endif
        );

        for (int i = 0; i < stackTrace.size() && i < SMITH_TRACE_COUNT; i++) {
            strncpy(trace.stackTrace[i], stackTrace[i].c_str(), SMITH_TRACE_LENGTH - 1);
        }

        trace.request = PHP_PROBE_G(request);

        if (!gAPIConfig->surplus(ClassID, MethodID))
            return ZEND_USER_OPCODE_DISPATCH;

        gAPITrace->enqueue(trace);

        return ZEND_USER_OPCODE_DISPATCH;
    }
};

其中比较特殊的是 Extended,这是因为 PHP 中存在一个特殊的 opcode,也就是 ZEND_INCLUDE_OR_EVAL,它可以看做多个子 opcode 的集合。为了区分具体的操作是 eval 或是 include,我们需要额外的获取一个标志位,供服务端进行区分判断。

阻断

Elkeid RASP 系列产品都支持阻断功能,下发的规则为正则格式,当某次函数调用的入参匹配到规则时,便会抛出异常:

......
        if constexpr (CanBlock) {
            if (gAPIConfig->block(trace)) {
                trace.blocked = true;

                gAPITrace->enqueue(trace);

                zend_throw_exception(
                        nullptr,
                        "API blocked by RASP",
                        0
#if PHP_MAJOR_VERSION <= 5
                        TSRMLS_CC
#endif
                );

                return;
            }
        }
......

在获取到入参后,查询共享内存中的规则,如果匹配则调用 zend_throw_exception 抛出异常。

性能

资源占用:

  • 单个 PHP 进程的内存占用增长约 7M。
  • 单个 PHP 进程的 CPU 增长小于 %2。
  • 对于一个 PHP 进程组,额外创建一个通信进程。

耗时统计:

api average(ns) tp90(ns) tp95(ns) tp99(ns)
passthru 15421.034781554868 13197.200000000004 15493.39999999999 237840.09999999954
system 9150.411468751894 16550.6 19003.0 37398.719999999885
exec 8766.203311700127 16398.6 18939.799999999996 34817.0999999997
shell_exec 9421.62037992353 17205.0 19705.0 43732.91999999992
proc_open 8735.06981968308 17033 19621.0 34133.599999999955
popen 6197.004372381126 6662.4 8780.499999999995 17723.260000000006
file 6078.228969122295 14572.9 16554.799999999996 24646.309999999954
readfile 3194.912538746733 3672.600000000002 4886.5999999999985 9566.199999999993
file_get_contents 5957.900887753861 7671.0 8890.25 16371.149999999947
file_put_contents 3224.3691446648013 2868.0 3262.1499999999996 8150.669999999993
copy 2873.517952775073 2972.0 3451.0 9257.470000000032
rename 3356.0933333333332 4983.6 6922.5999999999985 12561.160000000003
unlink 2542.6100176710743 2908 3829.5 9480.699999999988
dir 2603.6219809709687 2741.5 3217.5 8634.199999999983
opendir 3853.008359265361 5571.0 6505.5999999999985 10775.199999999964
scandir 3196.0514355528408 4123.200000000001 4949.549999999999 8905.97999999997
fopen 3161.4416473176097 3168.5 3542.75 8217.750000000016
move_uploaded_file 2862.6720479422734 2884.0 3171.7999999999993 8084.879999999997
splfileobject::construct 4243.37449516583 5447.799999999999 6497.949999999999 11698.980000000003
socket_connect 4211.529264111669 4307.0 5049.44999999999 11996.7
gethostbyname 5103.737816465396 6755.800000000001 8366.199999999999 13759.359999999962
dns_get_record 6716.215250491159 9247.0 10155.65 21651.48999999994
putenv 11979.022368659695 11790 13426 67583.00000000041
curl_exec 10650.08878674247 12138.7 14603.049999999996 47374.10000000011
ZEND_INCLUDE_OR_EVAL 6083.132776567417 5832.0 6501.399999999994 15980.07000000004

assertPHP7 中已经不再属于函数,pcntl_exec 相当于 exec 系统调用,在 PHP-FPM 中被禁止使用。

Clone this wiki locally