我们在写web应用程序时通常对每个类都建立一个 PHP 源文件。为了使用这些源文件,我们就需要在每个脚本开头写大量的的包含语句(include,require)。在 PHP 5 中,不再需要这样了。我们可__autoload()函数和spl_autoload_register函数实现实现自己的加载源文件的机制,它们会在试图使用尚未被定义的类时自动调用。通过调用这些函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。本文的主要目标是讲述如何在扩展中用C语言实现自动加载源文件的机制,但是在这之前我们先熟悉一下在PHP脚本中实现自动加载的方法。

1. 在php脚本中实现自动加载

在 PHP 5 中我们可以定义一个 __autoload() 函数,它会在试图使用尚未被定义的类时自动调用,这样我们就可以定义一些自己的加载规则了。

<?php
function __autoload($class_name) {
    require_once $class_name . '.php';
}

$obj  = new MyClass1();
$obj2 = new MyClass2();
?>

使用spl_autoload_register我们可以一次注册多个加载函数,PHP会在试图使用尚未被定义的类时按注册顺序调用。

<?php
function autoload_services($class_name)
{
    $file = 'services/' . $class_name. '.php';
    if (file_exists($file))
    {
        require_once($file);
    }
}
function autoload_vos($class_name)
{
    $file = 'vos/' . $class_name. '.php';
    if (file_exists($file))
    {
        require_once($file);
    }
}
spl_autoload_register('autoload_services');
spl_autoload_register('autoload_vos');
?>

2. 在php扩展中实现自动加载

最近在写一个php扩展,其中一个功能就是实现类的自动加载,其实也是通过在内核中调用spl_autoload_register函数来实现。使用zend API调用spl_autoload_register函数还是相对简单的,下面我们主要讲一下如何在内核中实现inclue/require/include_once/require_once等指令的功能。其实inclue/require/include_once/require_once等指令主要是读入文件编译并执行,下面的方法就是完成了这些操作,代码中有详细的注释。

/*
*  loader_import首先将PHP源文件编译成op_array,然后依次执行op_array中的opcode
*/
int loader_import(char *path, int len TSRMLS_DC) {
    zend_file_handle file_handle;
    zend_op_array   *op_array;
    char realpath[MAXPATHLEN];

    if (!VCWD_REALPATH(path, realpath)) {
        return 0;
    }

    file_handle.filename = path;
    file_handle.free_filename = 0;
    file_handle.type = ZEND_HANDLE_FILENAME;
    file_handle.opened_path = NULL;
    file_handle.handle.fp = NULL;

    //调用zend API编译源文件
    op_array = zend_compile_file(&file_handle, ZEND_INCLUDE TSRMLS_CC);

    if (op_array && file_handle.handle.stream.handle) {
        int dummy = 1;

        if (!file_handle.opened_path) {
            file_handle.opened_path = path;
        }

        //将源文件注册到执行期间的全局变量(EG)的include_files列表中,这样就标记了源文件已经包含过了
        zend_hash_add(&EG(included_files), file_handle.opened_path, strlen(file_handle.opened_path)+1, (void *)&dummy,
                sizeof(int), NULL);
    }
    zend_destroy_file_handle(&file_handle TSRMLS_CC);

    //开始执行op_array
    if (op_array) {
        zval *result = NULL;
        //保存原来的执行环境,包括active_op_array,opline_ptr等
        zval ** __old_return_value_pp   =  EG(return_value_ptr_ptr);
        zend_op ** __old_opline_ptr     = EG(opline_ptr); 
        zend_op_array * __old_op_array  = EG(active_op_array);
        //保存环境完成后,初始化本次执行环境,替换op_array
        EG(return_value_ptr_ptr) = &result;
        EG(active_op_array)      = op_array;

#if ((PHP_MAJOR_VERSION == 5) && (PHP_MINOR_VERSION > 2)) || (PHP_MAJOR_VERSION > 5)
        if (!EG(active_symbol_table)) {
            zend_rebuild_symbol_table(TSRMLS_C);
        }
#endif
        //调用zend API执行源文件的op_array
        zend_execute(op_array TSRMLS_CC);
        //op_array执行完成后销毁,要不然就要内存泄露了,哈哈
        destroy_op_array(op_array TSRMLS_CC);
        efree(op_array);
        //通过检查执行期间的全局变量(EG)的exception是否被标记来确定是否有异常
        if (!EG(exception)) {
            if (EG(return_value_ptr_ptr) && *EG(return_value_ptr_ptr)) {
                zval_ptr_dtor(EG(return_value_ptr_ptr));
            }
        }
        //ok,执行到这里说明源文件的op_array已经执行完成了,我们要恢复原来的执行环境了
        EG(return_value_ptr_ptr) = __old_return_value_pp;
        EG(opline_ptr)           = __old_opline_ptr; 
        EG(active_op_array)      = __old_op_array; 

        return 1;
    }
    return 0;
}

学习php扩展开发需要你对zend API以及PHP内核比较熟悉,推荐以下参考资料 php扩展入门博客

Hash表对PHP的重要性是每个phper都非常清楚地。但是PHP却有一个与Hash表相关的致命漏洞,我们完全可以通过PHP的Hash冲突漏洞进行DDoS攻击,可以轻易的搞挂一台服务器。那么是什么导致PHP的Hash冲突呢?如何构造Hash冲突?

1. PHP的Hashtab实现方式

PHP中的哈希表实现在源代码的Zend/zend_hash.c中,Zend/zend_hash.h中描述了Hashtab的结构体,从中我们可以大概了解Hashtab的实现机制。

typedef struct _hashtable { 
    uint nTableSize;        // hash Bucket的大小,最小为8,以2x增长。
    uint nTableMask;        // nTableSize-1 , 索引取值的优化
    uint nNumOfElements;    // hash Bucket中当前存在的元素个数,count()函数会直接返回此值 
    ulong nNextFreeElement; // 下一个数字索引的位置
    Bucket *pInternalPointer;   // 当前遍历的指针(foreach比for快的原因之一)
    Bucket *pListHead;          // 存储数组头元素指针
    Bucket *pListTail;          // 存储数组尾元素指针
    Bucket **arBuckets;         // 存储hash数组
    dtor_func_t pDestructor;    // 在删除元素时执行的回调函数,用于资源的释放
    zend_bool persistent;       //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
    unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
    zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

从结构体_hashtable中我们可以看出,PHP内核是使用常用的拉链法实现Hash表的,其中Bucket **arBuckets存储了实际的数据。

php内核hash表数据结构

其实PHP的Hash冲突漏洞就是利用在极端情况下,拉链法的Hashtab会退化为单链表的漏洞。由于Hash函数是公开的,因此经过精心的构造,是可以构造出导致Hash退化为链表的数据的。

php hash表退化为单链表

2. 本地实现PHP的Hash漏洞攻击

在我们使用数组时,PHP内核都是通过把数组内容插入到一个Hash表实现的,但对于关联数组(字符串下标)和索引数组(整数下标)的处理方法是不同的。关联数组使用如下函数将数据添加的Hash表:

ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC){
...
 h = zend_inline_hash_func(arKey, nKeyLength);//根据字符串计算Hash值
 nIndex = h & ht->nTableMask;
...
}

从上述代码中我们可以看出,关联数组需要使用Hash函数根据字符串计算Hash值。而索引数组却直接使用整数下标作为Hash值:

ZEND_API int _zend_hash_index_update_or_next_insert(HashTable *ht, ulong h, void *pData, uint nDataSize, void **pDest, int flag
ZEND_FILE_LINE_DC){
...
nIndex = h & ht->nTableMask; //直接使用下标作为hash值
...
}

由于实现较为简单,大部分攻击是通过构造索引数组来实现的,只要使 公式h & ht->nTableMask得到同样的值即可,其中ht->nTableMaskzend_hash_init函数中赋值:ht->nTableMask = ht->nTableSize - 1

有了以上基础知识,我们就可以构利用Hash冲突漏洞了。先直接上代码:

<?php
 $size = pow(2,16);
 $startTime = microtime(true);
 $array = array();
 for ($key = 0, $maxKey = ($size - 1) * $size; $key <= $maxKey; $key += $size) {
         $array[$key] = 0;
 }
 $endTime = microtime(true);
 echo $endTime - $startTime ,PHP_EOL;

这段代码创建了一个包含216 个元素的索引数组,下标依次为:

0,2^16,2*2^16,......,n*2^16,......,(2^16-1)*2^16

这时ht->nTableMask=2^16-1,转换为二进制就是1111 1111 1111 1111。而以上数组中的任意一个下标h和ht->nTableMask进行与运算都得0,nIndex = h & ht->nTableMask = 0。即所有的下标都得到同样的Hash值,这时Hash表就退化为链表了。事实上,这段代码在我的电脑里运行耗时40多秒。多恐怖啊。

3.常见的Hash冲突攻击形式

POST攻击: PHP会自动把HTTP包中POST的数据解析成数组$_POST,如果我们构造一个无限大的哈希冲突的值,则可以拖垮服务器。

PHP5.3.9以后的版本可以通过php.ini设置GET/POST/COOKIE 的最大输入变量数。默认为1000。但这种方法是无法完全避免攻击的,我们也可以在POST的变量值上做手脚,即反序列化攻击。

反序列化攻击: 如果POST的数据有字段为数组serialize后的值,或数组json_encode后的值,在unserialize或json_decode后,会有可能造成哈希碰撞攻击。 到目前位置PHP官方还没有彻底解决反序列化攻击的问题,请避免在公网上以数组的序列化形式传递数据