当前位置: 首页 > 工具软件 > PHP Taint > 使用案例 >

php扩展初探(三): taint解析

方琦
2023-12-01

前言

终于可以抽点时间说下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");
      }
    }
 类似资料: