译-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/