初探php扩展层面(三)
p0desta WEB安全 6429浏览 · 2019-03-12 01:25

上篇写的污点标记,这篇我会分析一下污点传播以及检测攻击点。

思路

这里我暂且认为只要经过类似mysql_real_escape_stringaddslasheshtmlentities这类函数,我们都将标记清除,但是如果经过类似base64_decodestrtolower或者字符串拼接这类经过传递仍然可能存在危害的函数,我们要进行标记传递。

这里有个问题,就是如果开始的时候进行了全局转义,就一定没有了危险嘛,如果某次请求又经过了类似 stripslashes这样的函数使引号逃逸出来呢,这里我觉得可以不进行污点清除,将其置为中间态,经过stripslashes的时候再恢复污点状态,这样可以减少一部分漏报。

然后思路是在一开始所有的请求变量都打上标记,在一些危险函数,如evalincludefile_put_contentsunlink这类函数时进行检测标记,如果仍然存在标记,我们认为它存在攻击点,因此做出警告。

污点传播

这里需要了解的知识点

//操作数类型
#define IS_CONST    (1<<0)  //1:字面量,编译时就可确定且不会改变的值,比如:$a = "hello~",其中字符串"hello~"就是常量
#define IS_TMP_VAR  (1<<1)  //2:临时变量,比如:$a = "hello~" . time(),其中"hello~" . time()的值类型就是IS_TMP_VAR
#define IS_VAR      (1<<2)  //4:PHP变量是没有显式的在PHP脚本中定义的,不是直接在代码通过$var_name定义的。这个类型最常见的例子是PHP函数的返回值
#define IS_UNUSED   (1<<3)  //8:表示操作数没有用
#define IS_CV       (1<<4)  //16:PHP脚本变量,即脚本里通过$var_name定义的变量,这些变量是编译阶段确定的

以及opline里获取到参数,大致思路是,根据HOOK的OP指令的不同,获取op1或者op2,然后根据op1_type或者op2_type分情况抽取参数值:

(1)    IS_TMP_VAR
如果op的类型为临时变量,则调用get_zval_ptr_tmp获取参数值。
(2)    IS_VAR
如果是变量类型,则直接从opline->var.ptr里获取
(3)    IS_CV
如果是编译变量参考ZEND_ECHO_SPEC_CV_HANDLER中的处理方式,是直接从EG(active_symbol_table)中寻找。
(4)IS_CONST
如果op类型是常量,则直接获取opline->op1.zv即可。
上述方法都是从PHP源码中选取的,比如一个ZEND_ECHO指令的Handler会有多个,分别处理不同类型的op,这里有:
ZEND_ECHO_SPEC_VAR_HANDLER
ZEND_ECHO_SPEC_TMP_HANDLER
ZEND_ECHO_SPEC_CV_HANDLER
ZEND_ECHO_SPEC_CONST_HANDLER

但是这里也有说的不对的地方,可能是版本的原因,比如说opline->var.ptr,我们直接这样是获取不到的,但是我们可以参考tmp的实现方式。

具体请看zend_execute.c

我们来看下get_zval_ptr_tmp是如何实现的

static zend_always_inline zval *_get_zval_ptr_tmp(zend_uint var, const zend_execute_data *execute_data, zend_free_op *should_free TSRMLS_DC)
{
    return should_free->var = &EX_T(var).tmp_var;
}

但是这个接口我们并不能直接调用,所以必须重新实现一下

#define PTAINT_T(offset) (*EX_TMP_VAR(execute_data, offset))

static zval *ptaint_get_zval_ptr_tmp(zend_uint var, const zend_execute_data *execute_data, zend_free_op *should_free TSRMLS_DC)
{
    return should_free->var = &PTAINT_T(var).tmp_var;
}

static int hook_include_or_eval(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = execute_data->opline;
    zval *op1 = NULL;
    zend_free_op free_op1;
    switch (PTAINT_OP1_TYPE(opline))
    {
        case IS_TMP_VAR:
            op1 = ptaint_get_zval_ptr_tmp(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);
            break;
        default:
            break;
    }
    return ZEND_USER_OPCODE_DISPATCH; 
}

看一下效果

可以看到这样实现是可以的,那么我们完善代码

static zval *ptaint_get_zval_ptr_tmp(zend_uint var, const zend_execute_data *execute_data, zend_free_op *should_free TSRMLS_DC)
{
    return should_free->var = &PTAINT_T(var).tmp_var;
}
static zval *ptaint_get_zval_ptr_var(zend_uint var, const zend_execute_data *execute_data, zend_free_op *should_free TSRMLS_DC)
{
    zval *ptr = PTAINT_T(var).var.ptr;
    return should_free->var = ptr;
}
static zval **ptaint_get_zval_cv_lookup(zval ***ptr, zend_uint var, int type TSRMLS_DC)
{
    zend_compiled_variable *cv = &CV_DEF_OF(var);

    if (!EG(active_symbol_table) ||
        zend_hash_quick_find(EG(active_symbol_table), cv->name, cv->name_len+1, cv->hash_value, (void **)ptr)==FAILURE) {
        switch (type) {
            case BP_VAR_R:
            case BP_VAR_UNSET:
                zend_error(E_NOTICE, "Undefined variable: %s", cv->name);
                /* break missing intentionally */
            case BP_VAR_IS:
                return &EG(uninitialized_zval_ptr);
                break;
            case BP_VAR_RW:
                zend_error(E_NOTICE, "Undefined variable: %s", cv->name);
                /* break missing intentionally */
            case BP_VAR_W:
                Z_ADDREF(EG(uninitialized_zval));
                if (!EG(active_symbol_table)) {
                    *ptr = (zval**)EX_CV_NUM(EG(current_execute_data), EG(active_op_array)->last_var + var);
                    **ptr = &EG(uninitialized_zval);
                } else {
                    zend_hash_quick_update(EG(active_symbol_table), cv->name, cv->name_len+1, cv->hash_value, &EG(uninitialized_zval_ptr), sizeof(zval *), (void **)ptr);
                }
                break;
        }
    }
    return *ptr;
}
static zval *ptaint_get_zval_ptr_cv(zend_uint var, int type TSRMLS_DC)
{
    zval ***ptr = EX_CV_NUM(EG(current_execute_data), var);

    if (UNEXPECTED(*ptr == NULL)) {
        return *ptaint_get_zval_cv_lookup(ptr, var, type TSRMLS_CC);
    }
    return **ptr;
}

static int hook_include_or_eval(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = execute_data->opline;
    zval *op1 = NULL;
    zend_free_op free_op1;
    switch (PTAINT_OP1_TYPE(opline))
    {
        case IS_TMP_VAR:
            op1 = ptaint_get_zval_ptr_tmp(PTAINT_OP1_GET_VAR(opline), execute_data, &free_op1 TSRMLS_CC);
            break;
        case IS_VAR:
            op1 = ptaint_get_zval_ptr_var(PTAINT_OP1_GET_VAR(opline), execute_data, &free_op1 TSRMLS_CC);
            break;
        case IS_CONST:
            op1 = PTAINT_OP1_GET_ZV(opline);
            break;
        case IS_CV:
            op1 = ptaint_get_zval_ptr_cv(PTAINT_OP1_GET_VAR(opline), 0);

    }
    if(op1 && Z_TYPE_P(op1) == IS_STRING && PHP_TAINT_POSSIBLE(op1))
    {
        if (opline->extended_value == ZEND_EVAL)
        {
                zend_error(E_WARNING, "(eval): Variables are not safely processed into the function");
        }else{
                zend_error(E_WARNING, "(include or require): Variables are not safely processed into the function");
        }
    }
    return ZEND_USER_OPCODE_DISPATCH; 
}

至此,hook opcode来检测标记已经完成,但是有一部分函数需要来重新实现检测操作,下面来做解释,首先看一下

typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    const char * function_name;
    zend_class_entry *scope;
    zend_uint fn_flags;
    union _zend_function *prototype;
    zend_uint num_args;
    zend_uint required_num_args;
    zend_arg_info *arg_info;
    /* END of common elements */

    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    struct _zend_module_entry *module;
} zend_internal_function;

Hook内部函数其实和hook opcode的思路大体一致,通过修改handler的指向,指向我们实现的函数,在完成相应操作后继续调用原来的函数实现hook。

这里参考taint的实现,修改handler

static void ptaint_override_func(char *name, uint len, php_func handler, php_func *stash TSRMLS_DC) /* {{{ */ {
    zend_function *func;
    if (zend_hash_find(CG(function_table), name, len, (void **)&func) == SUCCESS) {
        if (stash) {
            *stash = func->internal_function.handler;
        }
        func->internal_function.handler = handler;
    }
}

看下效果,handler的地址成功被修改

但是如此的话是有问题的,在进行修改handler的时候需要考虑会不会覆盖掉原来的,因此这里定义了一个新的结构体

static struct ptaint_overridden_fucs /* {{{ */ {
    php_func strval;
    php_func sprintf;
    php_func vsprintf;
    php_func explode;
    php_func implode;
    php_func trim;
    php_func rtrim;
    php_func ltrim;
    php_func strstr;
    php_func str_pad;
    php_func str_replace;
    php_func substr;
    php_func strtolower;
    php_func strtoupper;
} ptaint_origin_funcs;

在修改handler处

if (stash) {
            *stash = func->internal_function.handler;
        }
        func->internal_function.handler = handler;

这里存储原函数的地址

然后将原来的handler修改为新函数,然后在新函数中利用上面的指针可以重新调用原来的处理函数

PHP_FUNCTION(ptaint_strtoupper)
{
    zval *str;
    int tainted = 0;
    php_func strtoupper;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &str) == FAILURE) {
        return;
    }

    if (IS_STRING == Z_TYPE_P(str) && PHP_TAINT_POSSIBLE(str)) {
        tainted = 1;
    }

    PTAINT_O_FUNC(strtoupper)(INTERNAL_FUNCTION_PARAM_PASSTHRU);

    if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STRLEN_P(return_value)) {
        Z_STRVAL_P(return_value) = erealloc(Z_STRVAL_P(return_value), Z_STRLEN_P(return_value) + 1 + PHP_TAINT_MAGIC_LENGTH);
        PHP_TAINT_MARK(return_value, PHP_TAINT_MAGIC_POSSIBLE);
    }
}

然后在这重新调用原来函数执行,如果原来的字符串有标记的话将返回值也打上标记进行标记传递。

同样的原理,如果多个参数的情况,可以根据情况进行污点的检测,当然,如果想要做的更细的话,那就需要华更多的心思了。

文章到这里就结束了,感谢鸟哥的taint给了学习的机会,在后面一段时间我会去做完我想做的项目,如果有必要,我会把后续的记录整理后发出来,感谢。

参考:

https://segmentfault.com/a/1190000014234234
http://www.voidcn.com/article/p-gdecovzj-bpp.html
https://paper.seebug.org/449/
0 条评论
某人
表情
可输入 255
目录