前言
这节讲一下PHP 的hook的两种方式,opcode handler hook和method hook,没有然后。
handler
PHP提供了内置opcode handler替换函数zend_user_opcode_handlers。
ZEND_API int zend_set_user_opcode_handler(zend_uchar opcode, user_opcode_handler_t handler)
{
if (opcode != ZEND_USER_OPCODE) {
if (handler == NULL) {
/* restore the original handler */
zend_user_opcodes[opcode] = opcode;
} else {
zend_user_opcodes[opcode] = ZEND_USER_OPCODE;
}
zend_user_opcode_handlers[opcode] = handler;
return SUCCESS;
}
return FAILURE;
}
原理很简单,内置维护了一个zend_user_opcode_handlers表,直接替换表中user_opcode_handler_t对象即可。
其实user_opcode_handler_t就是一个函数指针。
typedef int (*user_opcode_handler_t) (zend_execute_data *execute_data);
替换完函数地址之后,当zend虚拟机执行到改opcode时,会查询该表找到对应的替换之后的handler函数地址并执行。
替换示例:
static int func(zend_execute_data *execute_data){
return ZEND_USER_OPCODE_DISPATCH;
}
zend_set_user_opcode_handler(ZEND_DO_FCALL,func);
这里的返回值需要说明下:
#define ZEND_USER_OPCODE_CONTINUE 0 /* execute next opcode */
#define ZEND_USER_OPCODE_RETURN 1 /* exit from executor (return from function) */
#define ZEND_USER_OPCODE_DISPATCH 2 /* call original opcode handler */
#define ZEND_USER_OPCODE_ENTER 3 /* enter into new op_array without recursion */
#define ZEND_USER_OPCODE_LEAVE 4 /* return to calling op_array within the same executor */
#define ZEND_USER_OPCODE_DISPATCH_TO 0x100 /* call original handler of returned opcode */
不同的返回值会走不同的处理逻辑。
因此如果是ZEND_USER_OPCODE_DISPATCH,整个hook的过程可以理解为先执行新的替换之后的handler,之后交给原始handler继续执行。
事实上,经过分析Zend虚拟机执行过程之后,我们知道,其实都是翻译成一个个opcode进行执行,然后寻找这个opcode中的handler执行,再来看一下内置的handler hook函数。
ZEND_API int zend_set_user_opcode_handler(zend_uchar opcode, user_opcode_handler_t handler)
{
if (opcode != ZEND_USER_OPCODE) {
if (handler == NULL) {
/* restore the original handler */
zend_user_opcodes[opcode] = opcode;
} else {
// 原始opcode一并替换为ZEND_USER_OPCODE
zend_user_opcodes[opcode] = ZEND_USER_OPCODE;
}
// 替换该opcode对应的handler函数地址。
zend_user_opcode_handlers[opcode] = handler;
return SUCCESS;
}
return FAILURE;
}
因此之后所有替换之后的handler本质上都会走到ZEND_USER_OPCODE这个分支里面,继续追踪下ZEND_USER_OPCODE分支。
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_USER_OPCODE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
int ret;
SAVE_OPLINE();
// 获取替换之后的handler返回值
ret = zend_user_opcode_handlers[opline->opcode](execute_data);
opline = EX(opline);
// 返回值判断,并继续走相应的流程。
switch (ret) {
case ZEND_USER_OPCODE_CONTINUE:
ZEND_VM_CONTINUE();
case ZEND_USER_OPCODE_RETURN:
if (UNEXPECTED((EX_CALL_INFO() & ZEND_CALL_GENERATOR) != 0)) {
zend_generator *generator = zend_get_running_generator(EXECUTE_DATA_C);
zend_generator_close(generator, 1);
ZEND_VM_RETURN();
} else {
ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
case ZEND_USER_OPCODE_ENTER:
ZEND_VM_ENTER();
case ZEND_USER_OPCODE_LEAVE:
ZEND_VM_LEAVE();
case ZEND_USER_OPCODE_DISPATCH:
ZEND_VM_DISPATCH(opline->opcode, opline);
default:
ZEND_VM_DISPATCH((zend_uchar)(ret & 0xff), opline);
}
}
关注下DISPATCH时的执行流程,
#define ZEND_VM_DISPATCH(opcode, opline) ZEND_VM_TAIL_CALL(((opcode_handler_t)zend_vm_get_opcode_handler_func(opcode, opline))(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
method
PHP维护了一个全局函数表,其中包含了所有的内置函数,可以通过CG(function_table)的函数获取。因此内置函数hook方法: 在全局函数表中寻找函数zend_function结构体,之后替换handler为自己的即可,taint的实现过程如下:
typedef void (*php_func)(INTERNAL_FUNCTION_PARAMETERS);
static void php_taint_override_func(const char *name, php_func handler, php_func *stash) /* {{{ */ {
zend_function *func;
if ((func = zend_hash_str_find_ptr(CG(function_table), name, strlen(name))) != NULL) {
// 原始函数指针备份
if (stash) {
*stash = func->internal_function.handler;
}
// 替换为新的函数
func->internal_function.handler = handler;
}
}
内部函数的基本结构体为:
typedef struct _zend_internal_function {
/* Common elements */
zend_uchar type;
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string* function_name;
zend_class_entry *scope;
zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_internal_arg_info *arg_info;
/* END of common elements */
zif_handler handler;
struct _zend_module_entry *module;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;
因此函数hook也是在执行之间进行替换,之后可以先获取备份的原始函数指针,之后指针调用之前的函数。
void (*zif_handler)(INTERNAL_FUNCTION_PARAM_PASSTHRU)