taint是大牛风雪之隅写的一个php扩展,用来检测GET、POST、COOKIE等中可能有的漏洞。其中用了很多php扩展开发必须的知识,简单记录代码逻辑如下。

首先,看到代码最末端的PHP_MINIT_FUNCTION,该方法在每个php进程初始化时执行,如果需要的话,则加载taint扩展模块,包括注册新函数,以及override旧函数,这里主要是一些与字符串有关的函数。前者使用zend_set_user_opcode_handler方法,设置echo、include等的钩子函数。

同样需要注意的是PHP_RINIT_FUNCTION,该方法在每个web请求开始时执行。所以,如果没有sapi支持的话,就不必要检查了。对于POST、GET、COOKIE数组中的每个值,标记它为possible tainted。这样,如果后续有使用到这些值,即op的两端有这些值的话,result也就是possible tainted了。

RINIT里使用php_taint_mark_strings方法标识tainted字符串,在其末尾追加一个unsigned字符0x6A8FCE84作为标识符。在该函数开始,会先检查是否有嵌套:

|     if (++ht->nApplyCount > 1) {
|         ht->nApplyCount–;
|         return;
|     }

不过我不是很明白GET、POST、COOKIE在什么情况下会有嵌套存在。

打标记,由于这里仅改变了Z_STRVAL_PP(ppzval)的内存大小和内容,但是并没有改变Z_STRLEN_PP(ppzval)。所以之后常规方法是用ppzval的时候,不会读取到打上的标记值:

| Z_STRVAL_PP(ppzval) = erealloc(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval) + 1 + PHP_TAINT_MAGIC_LENGTH);
| PHP_TAINT_MARK(*ppzval, PHP_TAINT_MAGIC_POSSIBLE);

以echo的钩子函数为例,说明单操作符。
static int php_taint_echo_handler(ZEND_OPCODE_HANDLER_ARGS) /* {{{ */ {
    zend_op *opline = execute_data->opline;
        zval *op1 = NULL;
        zend_free_op free_op1;

        switch(TAINT_OP1_TYPE(opline)) {
                case IS_TMP_VAR:
                        op1 = php_taint_get_zval_ptr_tmp(TAINT_OP1_NODE_PTR(opline), execute_data->Ts, &free_op1 TSRMLS_CC);
                        break;
                case IS_VAR:
                        op1 = TAINT_T(TAINT_OP1_VAR(opline)).var.ptr;
                        break;
                case IS_CV: {
                                zval **t = TAINT_CV_OF(TAINT_OP1_VAR(opline));
                                if (t && *t) {
                                        op1 = *t;
                                } else if (EG(active_symbol_table)) {
                                        zend_compiled_variable *cv = &TAINT_CV_DEF_OF(TAINT_OP1_VAR(opline));
                                        if (zend_hash_quick_find(EG(active_symbol_table), cv->name, cv->name_len + 1, cv->hash_value, (void **)&t) == SUCCESS) {
                                                op1 = *t;
                                        }
                                }
                    }
                        break;
        }

        if (op1 && IS_STRING == Z_TYPE_P(op1) && PHP_TAINT_POSSIBLE(op1)) {
                if (ZEND_ECHO == opline->opcode) {
                        php_taint_error("function.echo" TSRMLS_CC, "Attempt to echo a string that might be tainted");
                } else {
                        php_taint_error("function.echo" TSRMLS_CC, "Attempt to print a string that might be tainted");
                }
        }

        return ZEND_USER_OPCODE_DISPATCH;
} /* }}} */

这里就是检查echo的内容,是否在之前被打伤tainted标记。这里区分不同类型的变量,获取其值。摘录网上的说明如下:

IS_CV:这种类型的操作数比较重要,此类型是在PHP后来的版本中(大概5.1)中才出现,CV的意思是compiled variable,即编译后的变量,变量都是保存在一个符号表中,这个符号表是一个哈希表,试想如果每次读写变量的时候都需要到哈希表中去检索,势必会对效率有一定的影响,因此在执行上下文环境中,会将一些编译期间生成的变量缓存起来,此过程以后再详细介绍。此类型操作数一般以!开头表示,比如变量$a=123;$b=”hello”这段代码,$a和$b对应的操作数可能就是!0和!1, 0和1相当于一个索引号,通过索引号从缓存中取得相应的值。

IS_TMP_VAR:表示临时变量,临时变量一般在前面加~来表示,这是一些OP执行过程中需要用到的中间变量,例如初始化一个数组的时候,就需要一个临时变量来暂时存储数组zval,然后将数组赋值给变量。

IS_VAR: 一般意义上的变量,以$开发表示。

IS_TMP_VAR, 顾名思义,这个是一个临时变量,保存一些op_array的结果,以便接下来的op_array使用,这种的操作数的u保存着一个指向变量表的一个句柄(整数),这种操作数一般用~开头,比如~0,表示变量表的0号未知的临时变量

IS_VAR 这种就是我们一般意义上的变量了,他们以$开头表示

IS_CV 表示ZE2.1/PHP5.1以后的编译器使用的一种cache机制,这种变量保存着被它引用的变量的地址,当一个变量第一次被引用的时候,就会被CV起来,以后对这个变量的引用就不需要再次去查找active符号表了,CV变量以!开头表示。

这样,逻辑就很清晰了!最后,还有一个小疑问,mark taint时分配的那些unsigned字节,会在什么时候释放呢?由于这些空间是用erealloc分配的,初步猜测是在这个php进程结束时被释放。

2 Comments

  1. 雪候鸟 says:

    呵呵, 看了你的文章, 发现了一个我代码中的失误:
    if (ZEND_ECHO == opline->opcode) {
    php_taint_error(“function.echo” TSRMLS_CC, “Attempt to echo a string that might be tainted”);
    } else {
    php_taint_error(“function.echo” TSRMLS_CC, “Attempt to print a string that might be tainted”);
    }
    第二个, 应该是function.print.

    呵呵, thanks

  2. admin says:

    哇!!谢谢您写出这些优秀的代码,并且开源出来给大家学习!!

Leave a Reply