译-Cracking PHP rand()-token 能破解吗?

2016 OCTF初赛 有一道WEB题有意思,这道题的出题思路基于此paper,下面我就翻译一下

                                              Cracking PHP rand()

     在WEb应用中经常需要用到一些随机生成的字符串,例如:session token,CSRF token,例如

当你需要重置密码时,发送到你邮箱的重置链接里就有一个token(防范CSRF攻击),所以这些token

必须保证安全难以被猜到,这些字符串用函数rand()来生成,有时就多次嵌套调用rand()来提高复杂度,

并把输出转换为一个字符串,这样一个随机的token就生成了,我这次就来研究一下到底有没有可能性来

破解rand().

rand() 是怎么工作的?

在PHP里面,通过rand()来生成伪随机数字。那么就需要由srand来设置的“随机数列产生器(seed)”的

初始状态,当调用rand()时,如果你没有调用srand来初始化“seed”,rand()就会用一些难以猜测的数字作

为seed来产生随机数字,因此seed完全决定了rand()会产生什么随机数字。“随机数字生成器”在最初由seed

初始化后,会保持一个状态,然后每次调用rand()就会改变这一状态作为下次调用使用,不同的进程使用不同

的“随机数字生成器”,windows中seed 是32为的,linux中是1024位的。

看看我们的实例程序

这个程序本来用来防止csrf攻击的token,事实上它没有那么安全

public static function gen($len = 5)
{
    $token = '';
    while($len--){
        $choose = rand(0, 2);
        if ($choose === 0)
            $token .= chr(rand(ord('A'), ord('Z')));
        else if($choose === 1)
            $token .= chr(rand(ord('a'), ord('z')));
        else
            $token .= chr(rand(ord('0'), ord('9')));
    }
    return $token;
}

 这个程序先用rand()产生随机数字,来决定使用大写字符还是小写字符还是数字,然后在选择具体的字符。

每次我们打开index.php时,就会产生一个随机的token,我们的工作就是预测这个token,然后用预测到的

token来进行csrf攻击。

Seed Cracking

既然sand()是产生的为随机数列是由seed产生的,那么我们就可以通过遍历初始所有可能的数字作为参数给

srand,如果我们遍历找到了正确的srand参数,那么rand()产生的伪随机数字就可以被预测到了。

特别提醒:

      在linux中上述方法只只用于全新的进程,如果我们需要预测的进程已经执行了多次rand(),那么每次遍历

我们需要执行相同次数的rand()才行。在windows中就不存在这种问题了,所有作为srand输入的“随机数列生

成器”状态都是一样的。

如果你得到一个全新的进程的token,那么下面的程序就能破解它了

for ($i = 0; $i < PHP_INT_MAX; $i++) {
    srand($i);
    if (Token::gen(10) == "2118Jx9w3e") {
        die("Found: $i \n");
    }
}

 如果搜索4294967295种可能的参数给srand,这大约会花费12小时。幸运的是,自从PHP只调用libc时,

我们就可以重新把php 编码抓换成更高效率的c代码来i加快速度。我已经出了两个版本,一个是glibc rand

一个是widows rand这都是以token.php为基础的(从 php ext/standard/rand.c 中借用了大量代码)

。然后就遍历所有可能的seed。这样的话在windows下会话费大约10分钟,linux下大约一个小时就能

搞定。一旦你破解了seed,你通过把自己的“随机数列生成器”设置成和服务器的在用一个状态,那么后

续你们产生的随机数列就是一样的了,这样你就可以预测服务器会给其他用户分配那些token,这样就可以

用来攻击了。

 Linux 下的 破解

windows 下破解seed和破解“随机数列生成器”状态差不多是一回事,可以linux下就不同了。

Glibc rand()通过保存了一系列的数字,且像下面的代码一样来计算下一次调用的状态:

state[i] = state[i-3] + state[i-31]
return state[i] >> 1

 所以每次输出大约是第3和第31次调用后效果的重叠,看看下面的随机字符串

  • 6ZF5kNgonV
  • 9h3byovpGR
  • gGt0A94U92

当下一次调用rand()的时候首先决定是大小写字符还是数字,这是被前面3和31次调用决定的。那

就是gGt0A94U92后面那个9,和9h3byovpGR里面的y。我们我们预测下次rand(0,2)的输出很

大可能可能就是⌊10/10 + 25/26 × 3⌋ = 2 mod 3。那意味着我们会得到一个数字如果我们能预

测这个数字是多少,那么这个数字是有前3次的调用输出(一个数字)和前31次调用输出(一个字母)

决定的。事实上它会是 ⌊2/3 + 1/3 × 10⌋ = 0 mod 10 and ⌊3/3 + 2/3 × 10⌋ = 6 mod 10.

也就是0 和 6之间. 事实上它是 4:

  • 43J2d2ew31

 

你看到了对于linux rand 我们虽然不能准确的快速的破解rand,但是它也并没有我们想象的那么安

全,事实上如果给我们足够多的token我们也许能准确预测,当然我没有实验。

总结

     我们需要创建绝对安全的token,如果我们用rand,在很多使用情境中“随机代码生成器”能被各种

方法破解,token就能被预测,虽然在linux下预测有点麻烦,但严格意义上它确实不安全。但是如果在

windows下,分分钟就能破解你的token。

翻译自:http://www.sjoerdlangkemper.nl/2016/02/11/cracking-php-rand/

 

posted @ 2016-05-16 22:39 QQ是条狗 阅读(...) 评论(...) 编辑 收藏