-
Notifications
You must be signed in to change notification settings - Fork 427
PHP RASP
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-FPM
或 Apache
。在默认情况下 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,模块的卸载步骤,在此处进行资源释放和清理操作。
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)
gAPIConfig
与 gAPITrace
便是共享内存中的缓冲区与配置区域,然后 fork
一个进程进行消息通信,最后依次查找函数、挂钩函数以及挂钩 opcode
。
在这个阶段我们可以获取一个请求的详细信息,例如 url
、header
或者 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;
}
这个阶段时请求已经处理完成,我们需要将 request
清空:
PHP_RSHUTDOWN_FUNCTION (php_probe) {
PHP_PROBE_G(request) = {};
return SUCCESS;
}
在模块卸载的阶段,我们还原挂钩的函数以及 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, ¶m);
这是一个类似于 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
限制了参数个数上限为 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 |
assert
在PHP7
中已经不再属于函数,pcntl_exec
相当于exec
系统调用,在PHP-FPM
中被禁止使用。