终于可以抽点时间说下taint了。
taint的安装就不提了,普通的扩展安装即可。
taint主要由三部分构成,污点标记、污点传播、污点沉降。
taint在php7中的标记是在_zend_string中的内存回收u.v.flags字段中,如下:
typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; }; typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, /* used for strings & objects */// 标记在这里 uint16_t gc_info) /* keeps GC root number (or 0) and color */ } v; uint32_t type_info; } u; } zend_refcounted_h;
标记是通过宏实现
# define TAINT_MARK(str) (GC_FLAGS((str)) |= IS_STR_TAINT_POSSIBLE) # define TAINT_POSSIBLE(str) (GC_FLAGS((str)) & IS_STR_TAINT_POSSIBLE) # define TAINT_CLEAN(str) (GC_FLAGS((str)) &= ~IS_STR_TAINT_POSSIBLE)
其中GC_FLAGS是获取内存回收flag字段
#define GC_FLAGS(p) (p)->gc.u.v.flags
其次,在初始化连接是,taint会把所有的http请求都打上标记。
PHP_RINIT_FUNCTION(taint) { if (SG(sapi_started) || !TAINT_G(enable)) { return SUCCESS; } if (Z_TYPE(PG(http_globals)[TRACK_VARS_POST]) == IS_ARRAY) { php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_POST])); } if (Z_TYPE(PG(http_globals)[TRACK_VARS_GET]) == IS_ARRAY) { php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_GET])); } if (Z_TYPE(PG(http_globals)[TRACK_VARS_COOKIE]) == IS_ARRAY) { php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_COOKIE])); } return SUCCESS; }
php_taint_mark_strings主要是遍历array并进行污点标记。
taint污点传播主要由两部分构成,第一部分是劫持handler,第二部分是覆盖php内核中相关的字符串函数。
内置函数覆盖示例:
// 覆盖strval函数 php_taint_override_func(f_strval, PHP_FN(taint_strval), &TAINT_O_FUNC(strval));
strval是获取变量的值,hook实现过程为:
/* {{{ proto string strval(mixed $value) */ PHP_FUNCTION(taint_strval) { zval *num; int tainted = 0; if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &num) == FAILURE) { return; } if (Z_TYPE_P(num) == IS_STRING && TAINT_POSSIBLE(Z_STR_P(num))) { tainted = 1; } TAINT_O_FUNC(strval)(INTERNAL_FUNCTION_PARAM_PASSTHRU); if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STR_P(return_value) != Z_STR_P(num) && Z_STRLEN_P(return_value)) { TAINT_MARK(Z_STR_P(return_value)); } }
过程中先判断参数是否存在污点,如果存在污点,则通过TAINT_O_FUNC(strval)(INTERNAL_FUNCTION_PARAM_PASSTHRU);
调用内置函数执行,执行完成之后把返回值进行污点标记。
内置handler覆盖示例:
// 通过zend_set_user_opcode_handler替换handler zend_set_user_opcode_handler(override_opcode_handlers[idx].opcode, (user_opcode_handler_t)override_opcode_handlers[idx].handler); // 替换ZEND_CONCAT为php_taint_concat_handler函数 static const taint_custom_handler override_opcode_handlers[] = { ... { ZEND_CONCAT, php_taint_concat_handler }, ... };
hook的函数内容如下:
static int php_taint_concat_handler(zend_execute_data *execute_data) /* {{{ */ { const zend_op *opline = execute_data->opline; zval *op1, *op2, *result; taint_free_op free_op1, free_op2; int tainted = 0; // 获取操作数op1 op2、result指针 op1 = php_taint_get_zval_ptr(execute_data, opline->op1_type, opline->op1, &free_op1, BP_VAR_R, 1); op2 = php_taint_get_zval_ptr(execute_data, opline->op2_type, opline->op2, &free_op2, BP_VAR_R, 1); result = EX_VAR(opline->result.var); // 判断参数是否存在污点标记 if ((op1 && IS_STRING == Z_TYPE_P(op1) && TAINT_POSSIBLE(Z_STR_P(op1))) || (op2 && IS_STRING == Z_TYPE_P(op2) && TAINT_POSSIBLE(Z_STR_P(op2)))) { tainted = 1; } // 调用concat_function进行concat concat_function(result, op1, op2); // 结果打污点 if (tainted && IS_STRING == Z_TYPE_P(result) && Z_STRLEN_P(result)) { TAINT_MARK(Z_STR_P(result)); } // 回收 if ((TAINT_OP1_TYPE(opline) & (IS_VAR|IS_TMP_VAR)) && free_op1) { zval_ptr_dtor_nogc(free_op1); } if ((TAINT_OP2_TYPE(opline) & (IS_VAR|IS_TMP_VAR)) && free_op2) { zval_ptr_dtor_nogc(free_op2); } CALL_ORIGIN_HANDLER(); execute_data->opline++; return ZEND_USER_OPCODE_CONTINUE; } /* }}} */
后面便是检测危险函数中是否存在污点了,常见的危险源如
{ ZEND_INCLUDE_OR_EVAL, php_taint_include_or_eval_handler }, { ZEND_DO_FCALL, php_taint_fcall_handler }, { ZEND_DO_ICALL, php_taint_fcall_handler }, { ZEND_DO_FCALL_BY_NAME, php_taint_fcall_handler }
eval监控的代码如下:
static int php_taint_include_or_eval_handler(zend_execute_data *execute_data) /* {{{ */ { const zend_op *opline = execute_data->opline; taint_free_op free_op1; zval *op1; // 获取操作数op1 op1 = php_taint_get_zval_ptr(execute_data, opline->op1_type, opline->op1, &free_op1, BP_VAR_R, 0); // 判断是否被标记,如果标记则输出。 if ((op1 && IS_STRING == Z_TYPE_P(op1) && TAINT_POSSIBLE(Z_STR_P(op1)))) switch (opline->extended_value) { case ZEND_INCLUDE_ONCE: php_taint_error("include_once", "File path contains data that might be tainted"); break; case ZEND_REQUIRE_ONCE: php_taint_error("require_once", "File path contains data that might be tainted"); break; case ZEND_INCLUDE: php_taint_error("include", "File path contains data that might be tainted"); break; case ZEND_REQUIRE: php_taint_error("require", "File path contains data that might be tainted"); break; case ZEND_EVAL: php_taint_error("eval", "Code contains data that might be tainted"); break; } // 回调原始handler CALL_ORIGIN_HANDLER(); // 解绑继续执行 return ZEND_USER_OPCODE_DISPATCH; }
此外常用的system命令执行检测逻辑如下:
// 获取函数名称 zend_string *fname = fbc->common.function_name; // 比较函数名 if (zend_string_equals_literal(fname, "passthru") || zend_string_equals_literal(fname, "system") || zend_string_equals_literal(fname, "exec") || zend_string_equals_literal(fname, "shell_exec") || zend_string_equals_literal(fname, "proc_open") || zend_string_equals_literal(fname, "popen")) { zval *cmd = ZEND_CALL_ARG(ex, arg_count); // 判断cmd是string并且存在标记,告警。 if (IS_STRING == Z_TYPE_P(cmd) && TAINT_POSSIBLE(Z_STR_P(cmd))) { php_taint_error(ZSTR_VAL(fname), "CMD statement contains data that might be tainted"); } }