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

思路

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

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

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

污点传播

这里需要了解的知识点

1
2
3
4
5
6
//操作数类型
#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
2
3
4
5
6
7
8
9
10
11
12
13
(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是如何实现的

1
2
3
4
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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;
}

看一下效果

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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来检测标记已经完成,但是有一部分函数需要来重新实现检测操作,下面来做解释,首先看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
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的时候需要考虑会不会覆盖掉原来的,因此这里定义了一个新的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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处

1
2
3
4
if (stash) {
*stash = func->internal_function.handler;
}
func->internal_function.handler = handler;

这里存储原函数的地址

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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给了学习的机会,在后面一段时间我会去做完我想做的项目,如果有必要,我会把后续的记录整理后发出来,感谢。

参考:

1
2
3
https://segmentfault.com/a/1190000014234234
http://www.voidcn.com/article/p-gdecovzj-bpp.html
https://paper.seebug.org/449/