本文首发先知社区,文章链接:https://xz.aliyun.com/t/4214
前段时间想写一个静态代码审计工具,需要对php扩展熟悉一些,那么自己从零开始接触这一块,如果有错误的地方,麻烦师傅们指正。
另外呢网上虽然有一些文章,但是感觉都不是特别细,对于刚入门的我来说有些难以理解,因此详细的记录下自己的学习过程。
我在mac环境上折腾了两天gdb,还是没折腾好,无奈选择docker,这里推荐一个
1 | https://github.com/hxer/php-debug/blob/master/Dockerfile |
这个dockerfile的vld和php版本不匹配,需要更换下低版本的vld。
启动命令
1 | docker run -i -d --security-opt seccomp=unconfined -v /Users/p0desta/Desktop/code:/home php5-debug |
编写最简单的php扩展
在ext目录下执行命令
1
./ext_skel --extname=p0desta
然后进入到扩展目录下,编辑config.m4文件
1
2
316 dnl PHP_ARG_ENABLE(foobar, whether to enable foobar support,
17 dnl Make sure that the comment is aligned:
18 dnl [ --enable-foobar Enable foobar support])删除第16-18行的注释
然后去php_p0desta.h文件,添加函数声明
1
2PHP_FUNCTION(confirm_foobar_compiled);
PHP_FUNCTION(p0desta);然后到p0desta.c中
1
2
3
4
5const zend_function_entry p0desta_functions[] = {
PHP_FE(p0desta, NULL)
PHP_FE(confirm_p0desta_compiled, NULL) /* For testing, remove later. */
PHP_FE_END /* Must be the last line in p0desta_functions[] */
};添加如下
PHP_FE(p0desta, NULL)
然后到最底下编写函数
1
2
3
4PHP_FUNCTION(p0desta)
{
php_printf("hello world");
}然后在当前目录下执行命令
1
2
3phpize
./configure --enable-p0desta --enable-debug
make
然后会在modules文件夹下生存so
文件,在php.ini中添加拓展
1 | extension=p0desta.so |
然后就可以调用自写的函数。
php代码的大致执行流程
开始 -> Scanning,将php代码转换为语言片段(Tokens) -> Parsing,将tokens转化为简单而有意义的表达式 -> Compilation,将表达式编译成opcode -> Execution,顺次执行opcodes,从而实现php脚本的功能。
hook最简单的opcode
关于一些宏的解释参考:https://github.com/pangudashu/php7-internal/blob/master/7/hook.md
这里我使用zend_set_user_opcode_handler
函数来hook echo
函数
1 | zend_set_user_opcode_handler(ZEND_ECHO, ppecho); |
主要原理是将对应的Zend op的handler函数替换成我们自己定义的来实现HOOK
首先我在扩展.h中定义如下
1 |
|
扩展.c中
1 | PHP_MINIT_FUNCTION(p_echo) |
如果打算放行继续执行的话return ZEND_USER_OPCODE_DISPATCH
,如果不继续执行的话return ZEND_USER_OPCODE_RETURN
编译完之后看一下效果
Webshell简单防御初探
关于一些PHP内核中的定义详情请参考https://www.kancloud.cn/kancloud/php-internals/42755
这里我们暂时需要了解的有
全局变量
1
2
3
4EG()、这个宏可以用来访问符号表,函数,资源信息和常量
CG() 用来访问核心全局变量
PG() PHP全局变量。我们知道php.ini会映射一个或者多个PHP全局结构。举几个使用这个宏的例子:PG(register_globals), PG(safe_mode), PG(memory_limit)
FG() 文件全局变量。大多数文件I/O或相关的全局变量的数据流都塞进标准扩展出口结构。函数类型
Zend引擎将函数分为以下几个类型1
2
3
4
5#define ZEND_INTERNAL_FUNCTION 1
#define ZEND_USER_FUNCTION 2
#define ZEND_OVERLOADED_FUNCTION 3
#define ZEND_EVAL_CODE 4
#define ZEND_OVERLOADED_FUNCTION_TEMPORARY 5ZEND_USER_FUNCTION (用户函数:用户定义的函数)
1
2
3
4
5
6
function test(){
}
ZEND_INTERNAL_FUNCTION (内部函数:由扩展、PHP内核、Zend引擎提供的内部函数)
变量函数
1
2$func = 'print_r';
$func('i am print_r function.');匿名函数
php7的_zend_execute_data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct _zend_execute_data {
const zend_op *opline; /* executed opline */
zend_execute_data *call; /* current call */
zval *return_value;
zend_function *func; /* executed function */
zval This; /* this + call_info + num_args */
zend_execute_data *prev_execute_data;
zend_array *symbol_table;
void **run_time_cache; /* cache op_array->run_time_cache */
zval *literals; /* cache op_array->literals */
};
我们看一下如下代码的opcode
1 |
|
我们hook掉INCLUDE_OR_EVAL
修改php_hook_eval.h
增加
1 | PHP_FUNCTION(confirm_foobar_compiled); |
修改hook_eval.c
增加
1 | static int HOOK_INCLUDE_OR_EVAL(ZEND_OPCODE_HANDLER_ARGS) |
直接在execute_data
中往下找调用的函数system
这个也就是操作数
1 | string型变量比较特殊,因为内核在保存String型变量时,不仅保存了字符串的值,还保存了它的长度,所以它有对应的两种宏组合STRVAL和STRLEN,即:Z_STRVAL、Z_STRVAL_P、Z_STRVAL_PP与Z_STRLEN、Z_STRLEN_P、Z_STRLEN_PP。 |
编写HOOK_INCLUDE_OR_EVAL
如下
1 | static int HOOK_INCLUDE_OR_EVAL(ZEND_OPCODE_HANDLER_ARGS) |
看下执行流程
当然,只hook掉ZEND_INCLUDE_OR_EVAL
是很难防御的,比如说
1 | <?php |
这种就必须再去hook DO_FCALL
为了不影响业务并且去做更好的防御,还需要更深入的研究。
参考:
1 | http://drops.xmd5.com/static/drops/web-7333.html |
这篇我讲继续学习污点标记以及标记打在何处,学习过程我会通过阅读http://pecl.php.net/package/taint
的源码来详述实现原理和一些细节。
下一篇讲会对污点跟踪进行分析。
污点标记
这里我们认为所有传入的数据都是不可信的,也就是说所有通过请求发送过来的数据都需要打上标记,被打上标记的数据是会传播的,比如说当进行字符串的拼接等操作在结束后要对新的数据从新标记,因为这个新的字符串仍然是不可信数据,但是经过一些处理函数,比如说addslashes
这类函数,就可以将标记清除掉。
标记点
首先我们需要知道怎么打标记,将标记打在何处
首先php7和php5的变量结构体是不一样的,因为结构体的不同,标记打在何处也就产生了区别
php7
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
32typedef 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;
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;在taint中,对于php7来说污染标记的原理是利用
zend_uchar flags
变量回收结构中未被使用的标记为去做污染标记,如果随着版本的升级,这个位被使用后,那么就会产生冲突。php5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
zend_ast *ast;
} zvalue_value;
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};可以看到这个版本的字段并不多,没有方便我们做标记的位置。
看下taint中是如何实现的吧。
1 | Z_STRVAL_PP(ppzval) = erealloc(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval) + 1 + PHP_TAINT_MAGIC_LENGTH); |
看的宏的定义
1 | #define PHP_TAINT_MAGIC_NONE 0x00000000 |
可能这样看不是很直观,直接看图
既然这样,那么当想要消除标记的时候直接再将
1 | #define PHP_TAINT_MAGIC_NONE 0x00000000 |
打上即可。
http请求
上面我们认为所有的请求都是不可信的,再没有经过安全函数时都要打上标记,接下来看下获取http请求参数以及给参数打上标记。
获取http请求参数,看鸟哥的文章http://www.laruence.com/2008/04/04/17.html
1 | #define TRACK_VARS_POST 0 |
鸟哥问中提到根据测试的结果,可以认定PG(http_globals)[TRACK_VARS_GET]是一个hash table;
我们先利用一下代码获取一下请求参数看一下,这里为了简单分析,直接修改上篇文章HOOK_INCLUDE_OR_EVAL来分析
1 | HashTable *ht; |
可以看到是可以直接从这个hashtable里面获取到我们的参数的
可以利用相关的宏方便获取的,在zend_hash.h里面可以找到相关的宏
将hashtable中的数据全都遍历出来
1 | static int HOOK_INCLUDE_OR_EVAL(ZEND_OPCODE_HANDLER_ARGS) |
这几个函数的作用其实命名已经很明确了,但是还是想看一下,拿zend_hash_get_current_key
来说
我们打个断点break zend_hash_get_current_key_ex
我们来看一下
正如上面所说,跟命名是一样的,str_index
将返回我们想要得到的key
将其打印出来
打标记
我们重新创建一个扩展,完成基本定义
1 |
|
我们在请求初始化时,也就是PHP_RINIT_FUNCTION
里面进行调用
1 | PHP_RINIT_FUNCTION(ptaint) |
然后递归对数组进行标记
1 | static void php_taint_mark_arr(zval *symbol_table TSRMLS_DC) |
看下效果
参考:
1 | http://www.laruence.com/2009/04/28/719.html |
上篇写的污点标记,这篇我会分析一下污点传播以及检测攻击点。
思路
这里我暂且认为只要经过类似mysql_real_escape_string
、addslashes
、htmlentities
这类函数,我们都将标记清除,但是如果经过类似base64_decode
、strtolower
或者字符串拼接这类经过传递仍然可能存在危害的函数,我们要进行标记传递。
这里有个问题,就是如果开始的时候进行了全局转义,就一定没有了危险嘛,如果某次请求又经过了类似 stripslashes
这样的函数使引号逃逸出来呢,这里我觉得可以不进行污点清除,将其置为中间态,经过stripslashes
的时候再恢复污点状态,这样可以减少一部分漏报。
然后思路是在一开始所有的请求变量都打上标记,在一些危险函数,如eval
、include
、file_put_contents
、unlink
这类函数时进行检测标记,如果仍然存在标记,我们认为它存在攻击点,因此做出警告。
污点传播
这里需要了解的知识点
1 | //操作数类型 |
以及opline里获取到参数,大致思路是,根据HOOK的OP指令的不同,获取op1或者op2,然后根据op1_type或者op2_type分情况抽取参数值:
1 | (1) IS_TMP_VAR |
但是这里也有说的不对的地方,可能是版本的原因,比如说opline->var.ptr
,我们直接这样是获取不到的,但是我们可以参考tmp的实现方式。
具体请看zend_execute.c
我们来看下get_zval_ptr_tmp
是如何实现的
1 | 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) |
但是这个接口我们并不能直接调用,所以必须重新实现一下
1 |
|
看一下效果
可以看到这样实现是可以的,那么我们完善代码
1 | static zval *ptaint_get_zval_ptr_tmp(zend_uint var, const zend_execute_data *execute_data, zend_free_op *should_free TSRMLS_DC) |
至此,hook opcode来检测标记已经完成,但是有一部分函数需要来重新实现检测操作,下面来做解释,首先看一下
1 | typedef struct _zend_internal_function { |
Hook内部函数其实和hook opcode的思路大体一致,通过修改handler的指向,指向我们实现的函数,在完成相应操作后继续调用原来的函数实现hook。
这里参考taint的实现,修改handler
1 | static void ptaint_override_func(char *name, uint len, php_func handler, php_func *stash TSRMLS_DC) /* {{{ */ { |
看下效果,handler的地址成功被修改
但是如此的话是有问题的,在进行修改handler的时候需要考虑会不会覆盖掉原来的,因此这里定义了一个新的结构体
1 | static struct ptaint_overridden_fucs /* {{{ */ { |
在修改handler处
1 | if (stash) { |
这里存储原函数的地址
然后将原来的handler修改为新函数,然后在新函数中利用上面的指针可以重新调用原来的处理函数
1 | PHP_FUNCTION(ptaint_strtoupper) |
然后在这重新调用原来函数执行,如果原来的字符串有标记的话将返回值也打上标记进行标记传递。
同样的原理,如果多个参数的情况,可以根据情况进行污点的检测,当然,如果想要做的更细的话,那就需要华更多的心思了。
文章到这里就结束了,感谢鸟哥的taint给了学习的机会,在后面一段时间我会去做完我想做的项目,如果有必要,我会把后续的记录整理后发出来,感谢。
参考:
1 | https://segmentfault.com/a/1190000014234234 |