美文网首页七星网络安全
Web中的随机数安全总结

Web中的随机数安全总结

作者: rivir | 来源:发表于2017-12-02 13:15 被阅读181次

    mt_srand() 和 mt_rand()

    mt_srand()
    : 为mt_rand()函数播种的函数

    php manual 的解释是:

    mt_srand : 播下一个更好的随机数发生器种子,用 seed 来给随机数发生器播种。 没有设定 seed 参数时,会被设为随时数。

    Note: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。

    mt_rand()
    : 生成随机数的函数

    php manual的解释为:生成更好的随机数,很多老的 libc 的随机数发生器具有一些不确定和未知的特性而且很慢。PHP 的 rand() 函数默认使用 libc 随机数发生器。mt_rand() 函数是非正式用来替换它的。该函数用了 » Mersenne Twister 中已知的特性作为随机数发生器,它可以产生随机数值的平均速度比 libc 提供的 rand() 快四倍。

    如果没有提供可选参数 min 和 max,mt_rand() 返回 0 到 mt_getrandmax() 之间的伪随机数。

    mt_rand()函数的两种适用场景

    • 指定范围参数,比如mt_rand(1,1000)
    • 不指定范围,让系统自动生成

    如果我们自己指定范围的话,如果过小则很容易被爆破出来的,因此大多实际应用中都是不指定范围, mt_rand()函数默认范围是0到 mt_getrandmax()之间的伪随机数

    我们来看下mt_getrandmax()函数的最大值是多少

    php > echo mt_getrandmax();
    2147483647
    php > echo 2**31-1;
    2147483647
    

    发现mt_getrandmax()的最大值是2**31-1的大小,也就是说随机数的范围在0x00000000~0xffffffff 之间, 在这个范围内我们是可以爆破的,我们可以爆破在0x00000000~0xffffffff 之间的种子值,匹配生成的随机数是否和我们爆破的随机数相等, 爆破的工具已经有大牛用c写了php_mt_seed的一个工具

    http://www.openwall.com/php_mt_seed/

    php_mt_seed

    我们来看下该工具爆破0x00000000~0xffffffff爆破的过程,爆破完也就几分钟的时间

    比如一个很简单的程序:

    <?php
    
    mt_srand(20);
    echo mt_rand();
    
    >>>
    873212871
    

    我们用php_mt_seed去爆破我们的种子,如果种子值取得小,几乎是秒破的

    lj@lj /d/T/C/M/C/p/php_mt_seed-4.0> ./php_mt_seed 873212871
    Pattern: EXACT
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0x30000000 - 0x33ffffff, speed 5033.2 Mseeds/s 
    seed = 0x32524e6c = 844254828 (PHP 3.0.7 to 5.2.0)
    seed = 0x32524e6d = 844254829 (PHP 3.0.7 to 5.2.0)
    Found 2, trying 0xfc000000 - 0xffffffff, speed 5219.6 Mseeds/s 
    Version: 5.2.1+
    Found 2, trying 0x00000000 - 0x01ffffff, speed 0.0 Mseeds/s 
    seed = 0x00000014 = 20 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 3, trying 0x04000000 - 0x05ffffff, speed 37.3 Mseeds/s ^C
    

    php_mt_rand 工具只能用于爆破mt_rand()函数产生的随机数的种子值, 无论是否显式调用mt_srand()函数播种,但不能用于mt_rand(1,1000)这种指定范围的和rand函数的爆破

    常见的三种用mt_srand() 播种的情况

    • 固定种子: 比如mt_srand(1000)

    这种情况如果是调用mt_rand()函数用php_mt_seed工具几乎秒破

    • 动态种子
    1. mt_srand(mt_rand(0,1000));
    
    // 如果动态种子的值不是很大,我们可以可以去写一个脚去生成所有种子值生成的随机数序列,l类似彩虹表, 然后一一对比即可
    
    2. mt_srand(time());
    // 这种动态种子其实和比静态种子还危险,因为time()函数生成的种子是已知的,每个人生成的time()的值都是一样的
    
    
    • 程序自动播种
      这种情况也可以分为两种情况
    1. 用mt_srand()函数,种子值随机
    2. 程序隐式调用mt_srand()函数,不再需要用户来调用mt_srand()函数

    这种情况就比较复杂,也比较符合实际情况

    , 但问题来了,到底系统自动完成播种是什么时候,因为是隐式调用的,如果是每次调用mt_rand()函数都会自动播种,那么破解seed也就没有意义了,这样就会变成真随机数了

    我们来找对应php源码来分析下:

    PHPAPI void php_mt_srand(uint32_t seed)
    {
        /* Seed the generator with a simple uint32 */
        php_mt_initialize(seed, BG(state));
        php_mt_reload();
    
        /* Seed only once */
        BG(mt_rand_is_seeded) = 1;
    }
    /* }}} */
    
    /* {{{ php_mt_rand
     */
    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)++;
        s1 ^= (s1 >> 11);
        s1 ^= (s1 <<  7) & 0x9d2c5680U;
        s1 ^= (s1 << 15) & 0xefc60000U;
        return ( s1 ^ (s1 >> 18) );
    }
    

    php_mt_srand 是播种函数,根据注释我们我们知道该程序的大概功能是先初始化一个seed, 然后调用php_mt_reload 生成N个的随机数,并赋值标志位:mt_rand_is_seeded为1, 表示已经播种的意思

    php_mt_rand是生成随机数函数, 我们看到这么一段

    if (UNEXPECTED(!BG(mt_rand_is_seeded))) {
        php_mt_srand(GENERATE_SEED());
    }
    
    if (BG(left) == 0) {
        php_mt_reload();
    }
    --BG(left);
    

    如果没有播种,就调用php_mt_srand函数播种,那么下一次调用mt_rand()函数时就会跳过这步

    因此,其实可以知道,mt_rand()函数并不是每一次调用都会都会随机播种,那么什么时候会重新播种呢? 即什么时候mt_rand_is_seeded 标志位会被初始化为0呢? 这步赋值0的操作在源码basic_functions.c中有定义, 这里不细致贴了, 即在每个新进程开始的时候会初始化一次mt_rand_is_seeded

    rand()

    rand() 函数在产生随机数的时候没有调用 srand(),则产生的随机数是有规律可询的.

    产生的随机数可以用下面这个公式预测 : state[i] = state[i-3] + state[i-31] (一般预测值可能比实际值要差1)

    <?php
    $randstr = array();
    for ($i = 0; $i <= 50; $i++) {
        $randstr[$i] = rand(0, 30);
        if ($i >= 31) {
            echo "第" . $i . "个随机数:";
            echo "$randstr[$i]=(" . $randstr[$i - 31] . "+" . $randstr[$i - 3] . ") mod 32 +1\n";
        } else {
            echo "第" . $i . "个随机数:" . $randstr[$i] . "\n";
        }
    }
    
    >>>
    
    第0个随机数:2
    第1个随机数:0
    第2个随机数:1
    第3个随机数:26
    第4个随机数:24
    第5个随机数:10
    第6个随机数:17
    第7个随机数:27
    第8个随机数:23
    第9个随机数:2
    第10个随机数:6
    第11个随机数:18
    第12个随机数:25
    第13个随机数:17
    第14个随机数:14
    第15个随机数:2
    第16个随机数:14
    第17个随机数:26
    第18个随机数:20
    第19个随机数:14
    第20个随机数:17
    第21个随机数:6
    第22个随机数:15
    第23个随机数:0
    第24个随机数:23
    第25个随机数:1
    第26个随机数:17
    第27个随机数:2
    第28个随机数:17
    第29个随机数:25
    第30个随机数:27
    第31个随机数:19=(2+17) mod 32 +1
    第32个随机数:25=(0+25) mod 32 +1
    第33个随机数:29=(1+27) mod 32 +1
    第34个随机数:15=(26+19) mod 32 +1
    第35个随机数:19=(24+25) mod 32 +1
    第36个随机数:8=(10+29) mod 32 +1
    第37个随机数:1=(17+15) mod 32 +1
    第38个随机数:15=(27+19) mod 32 +1
    第39个随机数:0=(23+8) mod 32 +1
    第40个随机数:4=(2+1) mod 32 +1
    第41个随机数:21=(6+15) mod 32 +1
    第42个随机数:19=(18+0) mod 32 +1
    第43个随机数:29=(25+4) mod 32 +1
    第44个随机数:7=(17+21) mod 32 +1
    第45个随机数:3=(14+19) mod 32 +1
    第46个随机数:0=(2+29) mod 32 +1
    第47个随机数:22=(14+7) mod 32 +1
    第48个随机数:29=(26+3) mod 32 +1
    第49个随机数:21=(20+0) mod 32 +1
    第50个随机数:6=(14+22) mod 32 +1
    
    

    可以看到只需要产生前31个随机数,后面的32-50个随机数我们都可以用前面的随机数去预测后面的随机数值

    我们来看几道题, 一个是EIS 上的一道随机数的题,源码为:

    <?php
    include "flag.php";
    session_start();
    if (isset($_GET['code']) && intval($_GET['code']) === $_SESSION['code']) {
        die($flag);
    } else {echo "wrong answer!";}
    srand(rand(0, MAX_NUM));
    for ($i = 0; $i < 3; $i++) {
        echo "<h3>randnum$i:" . rand(0, MAX_NUM) . "</h3><br>";
    }
    echo 'sessionid: ' . session_id();
    var_dump($_SESSION);
    $_SESSION['code'] = rand(0, MAX_NUM);
    var_dump($_SESSION);
    ?>
    <form action="" method="get">
    the next random num is:<input type="text" name="code"/>
    <input type="submit"/>
    </form>
    
    

    srand()的种子值是动态, 而MAX_NUM 的值也是未知,不太好确定种子的范围和rand随机数的范围, 通过观察发现随机数值基本都是3位数和2位数的,没有超过4位数的, 看到有大佬们直接猜测MAX_NUM位1000, 然后去爆破即可

    <?php
    
    for ($i = 0; $i < 1001; $i++) {
        srand($i);
        echo 'srand:' . $i . ':' . rand(1, 1000) . ' ' . rand(1, 1000) . ' ' . rand(1, 1000) . ' ' . rand(1, 1000);
        echo "\n";
    }
    

    这样写一个php脚本然后生成一个类似彩虹表的东西, 每次生成前三个随机数,对照以下彩虹表就可以预测出来第四个随机数了, 一般误差在+1左右,

    这种做法虽然有一点猜测的做法,但也是合理的, 真正MAX_NUM 确实是1000, 如果出题人改成999,那么用于生成的字典会大大增多,如果是爆破900-1100的MAX_NUM值,大概需要200*1000=20000这样大小的字典

    脚本如下:

    <?php
    
    $max_num = 1000;
    for ($k = 900; $k <= 1100; $k++) {
        $max_num = $k;
        for ($i = 0; $i <= 1000; $i++) {
            srand($i);
            echo 'srand:' . $i . ':' . rand(1, $max_num) . ' ' . rand(1, $max_num) . ' ' . rand(1, $max_num) . ' ' . rand(1, $max_num);
            echo "\n";
        }
    }
    
    

    第二种做法是写一个py脚本直接去爆破随机数,因为随机数的范围都是小于1000的,因此这些直接爆破0,1000即可

    import requests
    
    url = 'http://0.0.0.0:91/index.php'
    s = requests.session()
    
    # headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0'}
    # html = s.get(url,headers=headers)
    
    for i in range(1000):
        #s = requests.session()
    
        url2 = url+'?code='+str(i)
        res = s.get(url2)
        print res.content
        if 'flag' in res.content:
            print res.content 
            break
    
    

    大概每个session 可以爆出2,3次就无法再次爆破了,也是有一点随机性在里面的

    ps: 其实session()这个函数有点像随机数播种, 程序每次运行一次session函数,都会分配一个固定的sessionid, 上面这个程序把session放在前面,那么循环部分的sessionid都是一样的,和我们浏览器访问并没有很大区别, 但如果是把session()函数放到循环体里面,那么每次访问的sessionid的值都会变化,相当于1000个人同时访问一次站点, 前面相当于一个人访问了1000次站点

    下面来看一到湖湘杯的题目:

    <?php
    error_reporting(0);
    $flag = "*********************";
    echo "please input a rand_num !";
    function create_password($pw_length = 10) {
        $randpwd = "";
        for ($i = 0; $i < $pw_length; $i++) {
            $randpwd .= chr(mt_rand(100, 200));
        }
        return $randpwd;
    }
    
    session_start();
    var_dump($_SESSION);
    
    mt_srand(time());
    
    $pwd = create_password();
    var_dump(($_SESSION['userLogin'] == $_GET['login']));
    
    echo $pwd . '||';
    
    if ($pwd == $_GET['pwd']) {
        echo "first";
        if ($_SESSION['userLogin'] == $_GET['login']) {
            echo "Nice , you get the flag it is " . $flag;
        }
    
    } else {
        echo "Wrong!";
    }
    
    $_SESSION['userLogin'] = create_password(32) . rand();
    
    ?>
    
    

    mt_srand()函数用time()做种子值, 相当于已知的, 我们可以本地用time()这个种子值去预测pwd的值, 这第一层判断很容易绕过, 第二层的判断就有点迷了

    发现这个第二层的判断为if ($_SESSION['userLogin'] == $_GET['login']), 只是简单的判断了下是否相等,而没有判断$_GET['login'] 这个值是否为空, 因为程序如果第一次加载,那么此时$_SESSION还没有赋值,$_SESSION['login'] 的内容自然是空, NULL===NULL, 很容易就绕过了第二层, 因此这题第二层判断形如虚设:

    <?php
    
    function create_password($pw_length = 10) {
        $randpwd = "";
        for ($i = 0; $i < $pw_length; $i++) {
            $randpwd .= chr(mt_rand(100, 200));
        }
        return $randpwd;
    }
    
    mt_srand(time());
    $pass = create_password();
    echo $pass . "\n";
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, 'http://114.215.138.89:10080/?pwd=' . $pass);
    $output = curl_exec($curl);
    print_r($output);
    curl_close($curl);
    

    如果你的时间和服务器上面的时间不同步,即time()的值不相同话,需要去偏移一个大概范围去爆破

    如果这题是改成如下

    <?php
    error_reporting(0);
    $flag = "*********************";
    echo "please input a rand_num !";
    function create_password($pw_length = 10) {
        $randpwd = "";
        for ($i = 0; $i < $pw_length; $i++) {
            $randpwd .= chr(mt_rand(100, 200));
        }
        return $randpwd;
    }
    
    session_start();
    var_dump($_SESSION);
    
    mt_srand(time());
    
    $pwd = create_password();
    var_dump(($_SESSION['userLogin'] == $_GET['login']));
    
    echo $pwd . '||';
    
    if ($pwd == $_GET['pwd']) {
        echo "first";
        if (isset($_GET['login']) && $_SESSION['userLogin'] == $_GET['login']) {
            echo "Nice , you get the flag it is " . $flag;
        }
    
    } else {
        echo "Wrong!";
    }
    
    $_SESSION['userLogin'] = create_password(32) . rand();
    
    ?>
    

    那么又该如何来解呢? 我才这个题目的愿意应该也是想这样考的, 这样的话难度大大提高

    rand()函数在没有调用srand()的时候产生的随机数值是可以预测的, 需要通过这个缺陷去得到$_SESSION['userLogin']的值, 具体实现之后有时间会在写一篇文章来分析

    参考大佬们的博客:

    http://mp.weixin.qq.com/s/3TgBKXHw3MC61qIYELanJg

    http://wonderkun.cc/index.html/index.php/2017/03/16/php%E7%9A%84%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7%E5%88%86%E6%9E%90/

    相关文章

      网友评论

        本文标题:Web中的随机数安全总结

        本文链接:https://www.haomeiwen.com/subject/wydnbxtx.html