Web中的随机数安全总结

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/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容