php哈希沖突攻擊解析

一段攻擊代碼

<?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 '插入 ', $size, ' 個惡意的元素需要 ', $endTime - $startTime, ' 秒', "\n";
 
$startTime = microtime(true);
$array = array();
for ($key = 0, $maxKey = $size - 1; $key <= $maxKey; ++$key) {
    $array[$key] = 0;
}
$endTime = microtime(true);
echo '插入 ', $size, ' 個普通元素需要 ', $endTime - $startTime, ' 秒', "\n";

插入結(jié)果

php5(5.2)

插入 65536 個惡意的元素需要 79.778307914734 秒
插入 65536 個普通元素需要 0.028948068618774 秒

php7

插入 65536 個惡意的元素需要 9.2177460193634 秒
插入 65536 個普通元素需要 0.004227876663208 秒

php 數(shù)組的實現(xiàn)

php 中的數(shù)組是 php 中非常好用的一個數(shù)據(jù)結(jié)構(gòu),有了這個數(shù)據(jù)結(jié)構(gòu)的加持 php 的開發(fā)效率才能如此之高。
但是我們知道世界上并沒有完美的事物樟凄,php 的數(shù)組雖然給我們帶來的簡單易用的一些特性,但是也會給我們帶來一些隱患堕阔。
哈希表是我們非常常見的一個數(shù)據(jù)結(jié)構(gòu),php 的數(shù)組就是通過哈希表來實現(xiàn)的。
php 的哈希表解決沖突采用的是拉鏈法,沖突的元素通過加入相應(yīng)hash槽的鏈表來解決碰纬。

php 經(jīng)歷了很多的版本,我們常用熟悉的版本有5.3匹厘、5.67.0 這幾個版本脐区。
其中 php5 版本的 hashtable 的實現(xiàn)與 php7 的有所不同愈诚。

php5

//hashTable的數(shù)據(jù)結(jié)構(gòu)
typedef struct _hashtable {
    uint nTableSize;// hashtable 的大小
    uint nTableMask;//這個和 ntableSize 是對應(yīng)的關(guān)系,為 nTableSize 的負(fù)數(shù)
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;   /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets; // 指向第一個桶鏈表
    dtor_func_t pDestructor; // 元素刪除的函數(shù)
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;
//保存數(shù)據(jù)的單鏈表結(jié)構(gòu)
typedef struct bucket {
   ulong h;                  /* Used for numeric indexing */
   uint nKeyLength;        //key長度
   void *pData;        //指向bucket中保存的數(shù)據(jù)的指針
   void *pDataPtr;    //指針數(shù)據(jù)
   struct bucket *pListNext;    //指向hashtable桶列中下一個元素
   struct bucket *pListLast;    //指向hashtable桶列前一個元素
   struct bucket *pNext;        //指向具有同一個hash index的桶列的后一個元素
   struct bucket *pLast;        //指向具有同一個hash index的桶列的前一個元素
   const char *arKey;        //必須是最后一個成員牛隅,key的名稱
} Bucket;

php7

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    consistency)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask; //散列函數(shù)的映射數(shù)
    Bucket           *arData;     //指向當(dāng)前桶的第一個數(shù)據(jù)
    uint32_t          nNumUsed;   //已用的Bucket的數(shù)量(包含的是那些被刪除的或者是無效的bucket)
    uint32_t          nNumOfElements;//有效的bucket的數(shù)量
    uint32_t          nTableSize;//可以容納的bucket的數(shù)量
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;//用來自動確定php數(shù)組的索引
    dtor_func_t       pDestructor;//自動清理無用bucket的函數(shù)
};

php7 的 Bucket 實現(xiàn)就簡單的多


typedef struct _Bucket {
    zval              val;//元素的數(shù)據(jù)
    zend_ulong        h;                /* hash value (or numeric index)   */
    zend_string      *key;              /* string key or NULL for numerics */
} Bucket;

這個是 php7 hashtable的數(shù)據(jù)分布
數(shù)據(jù)分布是這樣的 映射表 + bucket(順序插入)


/*
 * HashTable Data Layout
 * =====================
 *
 *                 +=============================+
 *                 | HT_HASH(ht, ht->nTableMask) |
 *                 | ...                         |
 *                 | HT_HASH(ht, -1)             |
 *                 +-----------------------------+
 * ht->arData ---> | Bucket[0]                   |
 *                 | ...                         |
 *                 | Bucket[ht->nTableSize-1]    |
 *                 +=============================+
 */
 
bucket1.png

內(nèi)部沖突的解決

那么 php 的內(nèi)部沖突 php 是怎么解決的那炕柔?
首先涉及到的一個常量是 php hashtable 中 nTableMask。
我們先來看php數(shù)組是如何初始化的

 ZEND_API void ZEND_FASTCALL _zend_hash_init(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
{
    GC_REFCOUNT(ht) = 1;
    GC_TYPE_INFO(ht) = IS_ARRAY;
    ht->u.flags = (persistent ? HASH_FLAG_PERSISTENT : 0) | HASH_FLAG_APPLY_PROTECTION | HASH_FLAG_STATIC_KEYS;
    ht->nTableMask = HT_MIN_MASK; //這里對 nTableMask 進(jìn)行了定義
    HT_SET_DATA_ADDR(ht, &uninitialized_bucket);
    ht->nNumUsed = 0;
    ht->nNumOfElements = 0;
    ht->nInternalPointer = HT_INVALID_IDX;
    ht->nNextFreeElement = 0;
    ht->pDestructor = pDestructor;
    ht->nTableSize = zend_hash_check_size(nSize);
}

下面是上面關(guān)鍵常量的定義

#define HT_MIN_MASK ((uint32_t) -2) // 11111111111111111111111111111000
#define HT_MIN_SIZE 8 //初始化最大nTableSize

uint32_t 是無符號的int類型媒佣,吧 -2 轉(zhuǎn)換成無符號就是將-2原碼區(qū)反加一

php 添加數(shù)據(jù)到hashTable的代碼
主要關(guān)注的變量 nIndex h u2.next

  • nIndex:哈希槽
  • h:當(dāng)前的數(shù)組的key
  • u2.next:沖突鏈表前一個bucket的位置記錄(Bucket是順序插入的匕累,php7性能提高就是因為做了hashTable的數(shù)據(jù)結(jié)構(gòu)重構(gòu))
 ...
 add_to_hash:
    idx = ht->nNumUsed++;
    ht->nNumOfElements++;
    if (ht->nInternalPointer == HT_INVALID_IDX) {
        ht->nInternalPointer = idx;
    }
    zend_hash_iterators_update(ht, HT_INVALID_IDX, idx);
    p = ht->arData + idx;
    p->key = key;
    if (!ZSTR_IS_INTERNED(key)) {
        zend_string_addref(key);
        ht->u.flags &= ~HASH_FLAG_STATIC_KEYS;
        zend_string_hash_val(key);
    }
    p->h = h = ZSTR_H(key);
    ZVAL_COPY_VALUE(&p->val, pData);
    nIndex = h | ht->nTableMask;  //這里就是計算的方法  nTableMask初始值11111111111111111111111111111000
    Z_NEXT(p->val) = HT_HASH(ht, nIndex); // 這里將上一個bucket的u2.next的值放到下一個Bucket的u2.next
    HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(idx);

    return &p->val;
...

我們知道了hash的計算方式了,就可以分析上面的攻擊代碼了默伍。
我們?nèi)〕鰜韼讉€數(shù)據(jù)進(jìn)行調(diào)試測試欢嘿,新建一個 php 文件使用php -f 運行文件

$arr1 = [
    0 => '!',//4294967288
    65536 => '@',//4294967288
    131072 => '#',
    196608 => '$',
    262144 => '%',
];

下面的nIndex的變化

65536.png
196608.png
1311072.png
262144.png

我們可以看到nIndex幾次都沒有變化,這說明我們的Bucket都是放到同一個hash槽中,我們在通過p *(Bucket*)ht.arData@5也糊,
查看bucket數(shù)據(jù)中u2.next指向炼蹦。

{

{val = {value = {lval = ....}, u2 = {next = 4294967295, ....}}, h = 0, key = 0x0},

{val = {value = {lval = ....}, u2 = {next = 0, ....}}, h = 65536, key = 0x0},

{val = {value = {lval = ....}, u2 = {next = 1, ....}}, h = 131072, key = 0x0},

{val = {value = {lval = ....}, u2 = {next = 2, ....}}, h = 196608, key = 0x0},

{val = {value = {lval = ....}, u2 = {next = 3, ....}}, h = 262144, key = 0x0}

}

為什么第一個是4294967295 因為這個是第一個引用的hash槽的位置

butcket2.png

為何會有攻擊效果

如果所有的元素都進(jìn)了同一個hash槽,那么我們的Hashtable查詢和插入的時間復(fù)雜度就會從 O(1) => O(n)
當(dāng)然 php7 有所改善狸剃,如果在php5中的插入方式會慢很多掐隐。

hash沖突

有效預(yù)防

  1. 在 php5.3 以上的版本中,post 參數(shù)的數(shù)量存在最大的限制 max_input_vars => 1000

  2. 在web服務(wù)器層面進(jìn)行處理,如通過限制請求 body 大小和參數(shù)的數(shù)量等虑省,這個也是目前使用最多的解決方案

其實以上的兩種解決方案并不能解決問題匿刮,因為只是單純的在參數(shù)的數(shù)量上進(jìn)行限制了熟丸,但是入股請求的數(shù)據(jù)中包含 json 數(shù)據(jù),但其中的數(shù)據(jù)就是碰撞的 array狞山。理論上,只要 php 代碼某處構(gòu)造 array 的數(shù)據(jù)依賴于外部輸入勘纯,則都可能造成這個問題,因此徹底的解決方案是更改 Zend 底層的 HashTable 實現(xiàn)

  1. 限制每個桶鏈表的最長長度

  2. 使用其他數(shù)據(jù)結(jié)構(gòu)如紅黑樹取代鏈表組織碰撞哈希(并不解決哈希碰撞堤结,只是減輕攻擊影響竞穷,將N個數(shù)據(jù)的操作時間從 O(N^2) 降至 O(NlogN) ,代價是普通情況下接近 O(1) 的操作均變?yōu)?O(logN)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子厉萝,更是在濱河造成了極大的恐慌章母,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡金蜀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來二庵,“玉大人眨猎,你說我怎么就攤上這事寺渗⌒攀猓” “怎么了涡拘?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵棘利,是天一觀的道長。 經(jīng)常有香客問我,道長或渤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任舀武,我火速辦了婚禮,結(jié)果婚禮上跛梗,老公的妹妹穿的比我還像新娘寻馏。我一直安慰自己,他們只是感情好核偿,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布诚欠。 她就那樣靜靜地躺著,像睡著了一般漾岳。 火紅的嫁衣襯著肌膚如雪轰绵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天尼荆,我揣著相機(jī)與錄音左腔,去河邊找鬼。 笑死捅儒,一個胖子當(dāng)著我的面吹牛液样,可吹牛的內(nèi)容都是我干的振亮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鞭莽,長吁一口氣:“原來是場噩夢啊……” “哼双炕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起撮抓,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤妇斤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后丹拯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體站超,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年乖酬,在試婚紗的時候發(fā)現(xiàn)自己被綠了死相。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡咬像,死狀恐怖算撮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情县昂,我是刑警寧澤肮柜,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站倒彰,受9級特大地震影響审洞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜待讳,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一芒澜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧创淡,春花似錦痴晦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汁针,卻和暖如春术辐,著一層夾襖步出監(jiān)牢的瞬間砚尽,已是汗流浹背施无。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留必孤,地道東北人猾骡。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓瑞躺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兴想。 傳聞我的和親對象是個殘疾皇子幢哨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 按圖索驥。PHP中使用最為頻繁的數(shù)據(jù)類型非字符串和數(shù)組莫屬嫂便,PHP比較容易上手也得益于非常靈活的數(shù)組類型捞镰。 在開始...
    拉風(fēng)的老衲閱讀 735評論 0 3
  • 一. PHP數(shù)組特點介紹 php數(shù)組可謂是php的核心,其key=>value的存儲結(jié)構(gòu)毙替,讓我們處理數(shù)據(jù)可以...
    OamMot閱讀 5,063評論 1 20
  • 朋友圈分類 1.家人與親戚 2.同事 3.孩子老師岸售、小朋友同學(xué)家長 4.同學(xué) 5同頻的學(xué)習(xí)伙伴
    夸寶兒閱讀 142評論 0 0
  • 古鎮(zhèn)上有兩種聲音,一樣的寂寥:白天是算命鑼厂画,夜里是梆子……覃某每次想起那些美妙的古鎮(zhèn)凸丸,青青石板路,悠悠古城垣袱院。輕撫...
    旅行作家好嘢閱讀 3,245評論 39 92
  • -1- 美玲在廚房里忙得熱火朝天忽洛,鍋里燉得牛肉香氣四溢腻惠,炸好得金黃色的丸子整齊地擺在帶花邊的瓷盤子里;她的額頭滲出...
    瑞雪嚨翔閱讀 697評論 8 8