前面我们说了一个简单的PHP扩展的编写与实现,本次实现一个具有功能的复杂扩展。
求和函数phpadd
求和函数需要接受参数,先来看下参数的写法。
参数的提示与校验是通过ZEND_BEGIN_ARG_INFO_EX、ZEND_BEGIN_ARG_INFO两个宏定义函数完成的。,其中的ZEND_ARG_INFO设置了参数是否为引用、参数标识以及是否为可选参数等,本示例中,我们接受两个参数,均为整型,写法如下:
ZEND_BEGIN_ARG_INFO_EX(phpext_add_arginfo,0,0,1)
ZEND_ARG_INFO(0,op1)
ZEND_ARG_INFO(0,op2)
ZEND_END_ARG_INFO()
之后开始编写phpadd函数。
PHP_FUNCTION(phpadd){
int argc = ZEND_NUM_ARGS();
long op1,op2;
if(zend_parse_parameters(argc,"ll",&op1,&op2) == FAILURE){
return;
}
RETURN_LONG(op1+op2);
}
其中zend_parse_parameters用来获取函数参数并进行校验,第一个参数是接受的参数,第二个参数表示指定函数的参数类型,后面就是要解析的参数,参数类型对应说明符如下,
有些修饰符需要对应两个参数,比如s 字符串修饰符,需要传入两个值,一个是内容另外一个是字符串长度。
在获取op1、op2完毕之后,调用RETURN_LONG返回函数返回值,至此phpadd函数编写完毕,后面去function中注册即可。
const zend_function_entry phpext_functions[] = {
PHP_FE(confirm_phpext_compiled, NULL) /* For testing, remove later. */
PHP_FE(phpext,NULL)
PHP_FE(phpadd,phpext_add_arginfo)
PHP_FE_END /* Must be the last line in phpext_functions[] */
};
后面正常的编译扩展so,运行效果如下
[root@VM_0_4_centos phpext]# php -r "echo phpadd(3,27);"
30[root@VM_0_4_centos phpext]# php -r "echo phpadd(4,27);"
31
生命周期Hook扩展
上面简单演示了一个功能性扩展函数的实现过程与方法,下面来说一下如何在生命周期上做点文章。
首先之前了解到PHP的生命周期,MINIT周期会在初始化时加载一次,而RINIT针对每次请求都会加载,在我们编写hook函数时需要注意hook的时间点,一般常用的有hook opcode 和hook 内置function两种方式,本次就以hook system为例。
首先使用vld扩展看一下system函数调用的对应的opcode。
发现最后会调用DO_ICALL调用system函数,先来看一下DO_ICALL对应的过程。
ZEND_VM_HOT_HANDLER(129, ZEND_DO_ICALL, ANY, ANY, SPEC(RETVAL))
{
USE_OPLINE
zend_execute_data *call = EX(call);
zend_function *fbc = call->func;
zval *ret;
zval retval;
SAVE_OPLINE();
EX(call) = call->prev_execute_data;
call->prev_execute_data = execute_data;
EG(current_execute_data) = call;
ret = RETURN_VALUE_USED(opline) ? EX_VAR(opline->result.var) : &retval;
ZVAL_NULL(ret);
fbc->internal_function.handler(call, ret);
#if ZEND_DEBUG
ZEND_ASSERT(
EG(exception) || !call->func ||
!(call->func->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) ||
zend_verify_internal_return_type(call->func, ret));
ZEND_ASSERT(!Z_ISREF_P(ret));
#endif
EG(current_execute_data) = execute_data;
zend_vm_stack_free_args(call);
zend_vm_stack_free_call_frame(call);
if (!RETURN_VALUE_USED(opline)) {
zval_ptr_dtor(ret);
}
if (UNEXPECTED(EG(exception) != NULL)) {
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_CONTINUE();
}
可以看到核心的执行点在于
fbc->internal_function.handler(call, ret);
首先会获取zend_function fbc变量,看一下zend_function结构体。
union _zend_function {
zend_uchar type; /* MUST be the first element of this struct! */
uint32_t quick_arg_flags;
struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope;
union _zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_arg_info *arg_info;
} common;
zend_op_array op_array;
zend_internal_function internal_function;
};
其中记录了函数名称、参数以及op_array、internal_function等,后面继续追踪下zend_internal_function执行结构体。
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;
这里的zif_handler相当于函数的指针地址,调用handler好比调用函数。
了解了这个结构体之后,后面就简单了,我们通过zend_set_user_opcode_handler函数设置我们要hook的DO_ICALL,然后判断function_name是否为system,如果是,则报警输出即可。
先写一下hook函数 system_fcall_handler
static int system_fcall_handler(zend_execute_data *execute_data) /* {{{ */ {
const zend_op *opline = execute_data->opline;
zend_execute_data *call = execute_data->call;
zend_function *fbc = call->func;
if (fbc->type == ZEND_INTERNAL_FUNCTION) {
zend_string *fname = fbc->common.function_name;
if(zend_string_equals_literal(fname,"system") ){
printf("system hook!");
}
}
return ZEND_USER_OPCODE_RETURN;
}
先是获取当先执行函数fbc,然后获取fbc的函数名称,通过zend_string_equals_literal判断函数名称是否为system,如果是,输出结果并进行相应的操作,最后返回ZEND_USER_OPCODE_RETURN,表示不执行system。此外ZEND_USER_OPCODE_DISPATCH表示继续执行,两个返回值的区别在于system是否执行。
之后通过zend_set_user_opcode_handler设置handler。
PHP_MINIT_FUNCTION(phpext)
{
// REGISTER_INI_ENTRIES();
zend_set_user_opcode_handler(ZEND_DO_ICALL,(user_opcode_handler_t)system_fcall_handler);
return SUCCESS;
}
编译运行,效果如下:
[root@VM_0_4_centos phpext]# cat test.php
<?php
system("id");
?>[root@VM_0_4_centos phpext]# php test.php
system hook!
下面来看下动态调用system的hook过程类似同上,不过根据opcode显示需要hook DO_FCALL。
root@VM_0_4_centos phpext]# cat test.php
<?php
$a = "sys"."tem";
$a("id");
?>[root@VM_0_4_centos phpext]# php test.php
uid=0(root) gid=0(root) groups=0(root)
system hook!