撲克是一種風靡世界的紙牌遊戲,我們不僅可以在家中的餐桌上、賭場上、或者橋牌室中玩撲克,現在還可以在網上玩。我們研究可靠軟體技術的一些人也玩撲克。因為我們現在都會花大量的時間在網上,所以將打撲克和可靠軟體技術研究結合在一起只是時間問題。我們將線上撲克遊戲和軟體安全結合起來研究後,發現一個巨大的安全漏洞,這就是本篇文章所要講的。
人們可以在PlanetPoker這樣的網際網路橋牌室與其他人打德州撲克,這些遊戲是實時的,而且用真錢。由於我們的主要工作是為公司提供安全、可靠且健壯的軟體,所以我們很好奇線上遊戲背後的軟體是什麼樣的。它如何執行?是否公平?我們檢視了PlanetPoker網站上的FAQ頁面,這個頁面包含它們的洗牌演算法(為展現遊戲公平性而公開洗牌演算法,這還是很令人驚訝的),這些足以開始我們的分析了。當我們看到洗牌演算法時,就開始懷疑這其中可能有問題。一個小小的調查研究證明這種直覺是正確的。
遊戲
在德州撲克中,每個玩家發兩張牌(稱作底牌)。最初的發牌後是一輪下注。第一輪下注結束後,接下來所有的發牌都是牌面朝上,所有玩家都可以看到的。莊家在牌桌上發三張牌面朝上的牌(稱為翻牌),然後就是第二輪下注。德州撲克一般是定額下注,就是說每個玩家在每一輪下注都是定額。比如,在3美元到6美元的遊戲中,前兩輪是3美元賭注,而第三輪和第四輪是6美元賭注。第二輪下注後,莊家在牌桌上再發一張牌面朝上的牌(稱為轉牌),然後就是第三輪下注。最後,莊家在牌桌上再發最後一張牌面朝上的牌(稱為河牌),然後就是最後一輪下注。剩餘的每個玩家使用自己手中的兩張底牌和牌桌上的五張公共牌,從這七張牌中選五張,湊成最大的組合。玩家湊成的成手牌的好壞由標準撲克成手牌順序決定。
德州撲克是一種快節奏的,令人興奮的遊戲。這個遊戲很重要的一個組成是虛張聲勢,並且玩家要對其他玩家持有的牌做快速判斷,這些判斷決定誰是最終的勝者。有趣的是,德州撲克還是每年在拉斯維加斯舉辦的世界撲克系列賽中的其中一項。
既然現在每個人和他們的狗都是線上的,而且幾乎所有型別的業務都被呈現在網際網路上,那麼線上賭場和橋牌室的出現就再自然不過了。雖然說要進賭場的話,去印第安保留區和河船就很容易做到,但是更方便的參與遊戲仍是現在的真實需求。如果能在自己家舒舒服服的上網娛樂(更別說可以穿著自己的睡衣),不用忍受二手菸,以及那些令人討厭的玩家,這絕對是很吸引人的。
安全風險無處不在
所有的便利都伴隨著一定的代價。很不幸,玩線上撲克存在真正的風險。賭場本身可能就是一個騙局,其存在只是為了從玩家手上拿錢,它根本沒有打算回報玩家任何勝局。執行線上賭場的伺服器也可能被惡意攻擊者破解,以獲得信用卡號碼,或者嘗試在遊戲中利用一些優勢。因為大多數賭場不對玩家的客戶端程式和託管紙牌遊戲的伺服器之間的網路流量進行認證和加密,可想而知,一個惡意玩家就可能檢查這些網路流量(採用經典的中間人攻擊),以確定對手牌。這些風險都是網路安全專家非常熟悉的。
串通也是一個撲克所獨有的問題(不同於其他遊戲,如21點或擲骰子)。因為撲克玩家互相對抗,他們的對手並不是賭場本身。當一個桌子上的兩個或多個玩家互相串通時,他們作為一個團隊一起玩,往往會使用相同的資金。互相串通的玩家知道他們團隊成員手上的牌(通常是通過細微的訊號),而且他們為使團隊獲得最大的利益而下注,不管是團隊中的誰贏都行。串通在現實的橋牌室中是一個問題,但對線上撲克來說,這個問題更嚴重。線上玩家可以使用即時通訊工具、電話會議聊天工具等,這使得串通問題成為一個嚴重的風險。如果一個線上遊戲的所有玩家都一起合作,來欺騙那些不質疑網路安全的,容易受騙的玩家怎麼辦?你怎麼保證你永遠不會成為這些攻擊的受害者呢?
最後也很重要的一個風險(特別是對本文而言),就是線上撲克軟體本身可能存在缺陷。軟體問題是引起安全風險的一種臭名昭著的形式,而且它常常被過分相信防火牆和加密技術的公司所忽略。軟體應用程式會給一個系統帶來非常多的安全漏洞,我們每天都會花大量的時間來找出並解決這些軟體安全問題,所以我們注意到線上撲克也是遲早的事。本文的其餘部分就專門來討論我們在一個流行的線上撲克遊戲中發現的軟體安全問題。
軟體安全風險
洗虛擬牌
我們關注的第一個軟體缺陷涉及洗虛擬牌。公平洗牌意味著什麼呢?本質上來說,它意味著牌的所有可能組合出現的概率相等,我們稱對這52張牌的每個排序為一次洗牌。
對真實的一副牌,有52!(約2^226)種不重複的洗牌。計算機洗一副虛擬牌時,它從這些可能的組合中選一種。現在有很多洗牌演算法,一些演算法優於其它,一些則是完全錯誤的。
ASF軟體公司開發的演算法被大部分線上撲克遊戲所使用。我們發現他們的洗牌演算法有很多缺陷,根據這些發現,我們聯絡了ASF公司,他們更改了他們的演算法,但是我們還沒有看他們的新演算法。從安全形度確保一切都完全正確並不容易啊(本文的其餘部分將會介紹)。
圖表一:有缺陷的ASF洗牌演算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
procedure TDeck.Shuffle; var ctr: Byte; tmp: Byte; random_number: Byte; begin { Fill the deck with unique cards } for ctr := 1 to 52 do Card[ctr] := ctr; { Generate a new seed based on the system clock } randomize; { Randomly rearrange each card } for ctr := 1 to 52 do begin random_number := random(51)+1; tmp := card[random_number]; card[random_number] := card[ctr]; card[ctr] := tmp; end; CurrentCard := 1; JustShuffled := True; end; |
上面是ASF軟體公司釋出的洗牌演算法,以使人們相信他們的計算機生成的洗牌是完全公平的。不過諷刺的是,這一舉措對我們來說是完全相反的效果。
演算法開始時先初始化一個陣列,其值按順序依次為1到52,代表52張可能的牌。然後程式用系統時間作種子,呼叫Randomize()初始化一個偽隨機數發生器。實際的洗牌是通過依次將陣列中的每個位置與一個隨機選擇的位置交換。這個隨機選擇的位置是通過呼叫偽隨機數發生器選擇的。
問題一:大小差一(Off-By-One)錯誤
精明的程式設計師就會發現,該演算法包含一個大小差一(off-by-one)錯誤。該演算法遍歷初始的那副牌,將其每張牌與其它任意牌交換。然而和大多數Pascal函式不同,Random(n)函式實際上返回一個0到n-1的數字,而不是1到n。演算法利用接下來的一小段程式碼來選擇與當前牌交換的牌:這個公式設定一個值在1到51之間的隨機數。總之,該演算法從不選擇最後一張牌與當前牌交換。當ctr最終指向最後一張牌,也就是第52張牌時,這張牌可以與任何其它牌交換,除了它自身。也就是說,這個洗牌演算法從不允許第52張牌在洗牌結束後依然在第52個位置。這很明顯違反了公平原則,不過很容易修復。
問題二:設計不良的洗牌分佈
進一步考察該洗牌演算法後,我們發現,即使不考慮大小差一(off-by-one)問題,該演算法返回的洗牌結果也不是均勻分佈的。該洗牌的核心基本演算法如圖2所示。
洗牌
進一步考察演算法後發現,即使不考慮大小差一(off-by-one)錯誤,該演算法返回的洗牌結果也不是均勻分佈的。也就是說,一些洗牌結果出現的概率比其它洗牌結果出現的概率大。如果一個玩家知道這個漏洞,就可以在一個牌桌上坐很久,從而利用這種不均勻分佈的優勢。
我們用一個小例子來說明這種問題,這裡我們採用上述洗牌演算法來洗牌,這副牌只有三張(n=3)。
圖2:不要這樣洗牌
1 2 |
for (i is 1 to 3) Swap i with random position between i and 3 |
圖2描述了我們所採用的洗牌演算法,並且描繪了採用該演算法生成所有可能的洗牌結果的樹。如果隨機數源設計良好的話,那麼這棵樹中所有葉子出現的概率相等。
即使只考慮這個小例子,我們就可以發現,該演算法的洗牌結果不是等概率的。231、213、132比312、321、123出現的更頻繁。如果你要對第一張牌下注,並且你知道上述這些洗牌結果的出現概率,你就會知道牌2比其它牌出現的概率大。而當一副牌的牌數增加時,這種概率不等現象會愈發被放大。當用上述演算法洗52張牌時(n=52),洗牌的這種不均勻分佈會造成某些手牌出現概率偏大,從而改變賠率。一些經驗豐富的玩家,他們專門研究賠率,然後就可以利用這種傾斜的手牌概率來贏得賭博。
圖3:可以這樣洗牌
1 2 |
for (i is 1 to 3) Swap i with random position between i and 3 |
圖3提供了一個更好的洗牌演算法。它與上述演算法的關鍵區別在於,遍歷一副牌時,每張牌可能的交換位置減少了。同樣,我們用三張牌的洗牌樹來解釋這個演算法。和ASF提供的演算法不同,該新演算法將每張牌i與[i,n]中的某張牌交換,而不是[1,n]中的某張牌交換,從而將葉子數從3^3=27減少到了3!=6.這很重要,因為n!個不同的葉子意味著,所有可能的洗牌結果,新洗牌演算法都會洗出一次,而且僅僅一次,從而每種洗牌結果出現的概率相等,這才是公平!
在確定性機器上生成隨機數
我們討論的第一組軟體缺陷僅僅改變某些牌出現的概率,一些聰明的賭徒可以利用這種概率傾斜為自己創造優勢,但是這種缺陷並不會完全破環這個系統。相比之下,這部分我們將要討論的第三種缺陷,絕對是可以讓線上撲克玩家完全妥協的“好東西”了。首先我們簡短介紹偽隨機數生成器,為下文奠定基礎。
偽隨機數生成器原理
假設我們要生成1到52之間的一個隨機數,每個數字等概率出現。理想情況下,我們生成0到1之間的一個值,然後將這個值乘以52,其中每個值等概率出現,且不受前值影響。注意0到1之間有無窮多個數,但是計算機不提供無限精度。
為使計算機做到上述演算法所描述的,偽隨機數生成器通常產生一個從0到N之間的整數,然後用那個整數除以N,這樣返回結果就總是0到1之間的數了。之後我們呼叫生成器時,它將第一次呼叫產生的整數結果傳遞給一個函式,這個函式生成一個0到N之間的新整數,然後返回新整數除以N的結果。這意味著,任何偽隨機數生成器返回的唯一值的數目被限定為0到N之間整數的個數。而在大多數常見的隨機數生成器中,N是2^32(約40億),也就是32位數的最大值。換句話說,這種生成器最多能產生40億個可能的值。扳起手指數一數也知道,40億不算多。
開始要給偽隨機數生成器提供一個種子,作為初始的整數,將其傳遞給那個函式。種子是生成隨機數字序列的開端。要注意,偽隨機數生成器的輸出是完全可預測的,它返回的每個值都完全由其先前返回的值決定(最終,由種子決定,即種子是一切的開始)。如果我們知道用於計算任意一個值的那個整數,那麼生成器後續給出的所有值都是可知的。
圖4是寶藍(Borland)編譯器提供的偽隨機數生成器,它就是一個很好的例子。如果我們知道RandSeed的當前值為12345,那麼它產生的下一個整數是1655067934,然後其返回值將是20.由於計算機是完全確定性的機器,所以事情總是如此。
圖4:寶藍的Random()函式實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
long long RandSeed = #### ; unsigned long Random(long max) { long long x ; double i ; unsigned long final ; x = 0xffffffff; x += 1 ; RandSeed *= ((long long)134775813); RandSeed += 1 ; RandSeed = RandSeed % x ; i = ((double)RandSeed) / (double)0xffffffff ; final = (long) (max * i) ; return (unsigned long)final; } |
歷史經驗表明,隨機數生成器的種子通常是基於系統時鐘產生,也就是用系統時間的某些方面作為種子。這意味著,如果你知道生成器是基於哪個時間做種子,你就知道生成器將會生成的所有數值(包括數字出現的順序)。這一切的結果是,偽隨機數完全可預知。毋庸置疑,這一事實對洗牌演算法影響深遠。
在玩撲克時,隨機數生成器是如何被錯誤使用的
ASF軟體使用的洗牌演算法總是開始於一副有序的牌,然後生成一個隨機數序列,用於重排那副牌。回想一下,一副真正的撲克牌有52!(約2^226)種各不相同的洗牌結果,而一個32位隨機數生成器的種子必須是一個32位數,也就是隻有40多億個可能的種子。而每次洗牌前,都會對牌以及生成器種子初始化,所以該演算法只能產生40多億個可能的洗牌結果,而40多億要遠遠小於52!.
更糟的是,圖一所示的演算法採用Pascal函式Randomize()為隨機數生成器選擇種子。Randomize()函式基於午夜開始的毫秒數選擇種子,一天只有86,400,000毫秒。因為這些數字被用作生成器的種子,從而可能的洗牌結果縮減為86,400,000個。八千六百萬要遠遠小於40億,但這還不是最糟的。
破壞系統
瞭解系統時鐘種子後,我們有一個想法,可以把洗牌結果數目減少的更多。通過將我們的程式與生成偽隨機數的伺服器系統時鐘同步,我們可以將可能的組合數降至200,000之後,這個系統就是我們的了,因為搜尋這麼小的洗牌結果集完全不在話下,在PC上就可以實時完成。
RST攻擊本身要求這副牌中的5張牌已知,基於這5張已知牌,我們的程式搜尋那幾十萬個洗牌結果集,然後推匯出完美匹配的一個。在德州撲克這個案例中,我們的程式將作弊玩家的兩張底牌以及前三張翻牌(公共牌)作為輸入。這五張牌在第一輪下注後就全部已知了,有這些資訊就足以讓我們在比賽中實時確定準確的洗牌結果。圖5是我們為攻擊粗粗設計的GUI。左上角的“Site Parameters“框用於同步時鐘,右上角的”Game Parameters“用於輸入5張牌,並初始化搜尋。圖5是所有的牌都被程式確定後的一張截圖。我們現在知道誰拿了什麼牌,以及剩餘的翻牌值,還有誰會提前贏。
圖5:攻擊的圖形使用者介面GUI
一旦知道5張牌,我們的程式就開始不斷的生成洗牌,直到那個洗牌中包含這5張牌,並且順序也一樣。由於Randomize()函式基於伺服器的系統時間,因此在合理精度內猜對開始的種子並不難(猜得越接近,需要搜尋的洗牌結果數就越少)。然而最棒的是這個,一旦找到一個正確的種子,就有可能在幾秒鐘內將我們的攻擊程式與伺服器同步。這種事後同步允許我們的程式不到1秒就確定隨機數生成器使用的種子,以及接下來遊戲將要使用的所有的洗牌。
除了技術細節,我們的攻擊也被很多新聞媒體所報導,這種媒體覆蓋也體現了這個發現人性的一面。登陸我們的網站Web site ,可以看到我們最初的釋出稿 original press release,CNN視訊剪輯,還有紐約時報的一個故事。
洗虛擬牌的正確做法
正如我們所見,洗虛擬牌乍看容易,其實不然。要寫洗牌演算法,最好的方法是,基於紮實的數學基礎開發一種可以安全地產生良好的洗牌的技術。此外,我們認為釋出一個好演算法,並允許被大家審查,是一個很不錯的想法(這與開源狂熱者的觀點不謀而合),關鍵是不能置安全性於模糊狀態。像ASF一樣釋出一個差演算法並不好,但不釋出這樣的差演算法也不好!
密碼學基於堅實的數學基礎開發健壯的演算法,用於保護個人、政府和商業機密,而不是基於模糊的理論。洗牌也一樣,我們可以將加密金鑰的長度與隨機種子的規模做類比,其中,加密金鑰的長度直接關係到很多加密演算法的強度。
開發一個洗牌演算法相當簡單。首先要清楚,演算法不需要能產生52!種洗牌結果,因為玩牌時只會用到很少部分的洗牌結果。然而演算法產生的洗牌結果必須是均勻分佈的,這非常重要。良好的分佈確保在一次洗牌中,每張牌在每個位置出現的概率基本相等。這個分佈性要求相對容易實現和驗證。下面的虛擬碼描述了一個簡單的洗牌演算法,如果配上合適的隨機數生成器,該演算法產生的洗牌結果是均勻分佈的。
1 2 3 4 5 |
START WITH FRESH DECK GET RANDOM SEED FOR CT = 1, WHILE CT <= 52, DO X = RANDOM NUMBER BETWEEN CT AND 52 INCLUSIVE SWAP DECK[CT] WITH DECK[X] |
這個演算法成功的關鍵在於隨機數生成器(RNG)的選擇。RNG直接影響上述演算法能否成功的產生均勻分佈的洗牌,以及這些洗牌能否用於安全的線上牌類遊戲。首先,RNG本身必須產生均勻分佈的隨機數。一些偽隨機數生成器(PRNG)已經被證明具有此數學屬性,比如基於Lehmer演算法的偽隨機數生成器。這些好的PRNG足以用於生成洗牌時的“隨機“數。
正如我們所見,初始種子的選擇是成功與否的關鍵。所有的事情最終都歸結於種子。因此,玩家在玩由PRNG生成的洗牌時,無法確定生成該副洗牌所使用的種子,這一點至關重要。
要確定生成特定洗牌所使用的種子,一種蠻力做法是,系統地遍歷所有可能的種子,生成相應的洗牌序列,並將其與待尋找的洗牌序列對比。為避免這種攻擊,可用的種子數一定要多,使得在特定時間限制內,執行窮舉搜尋不可行。但是要注意,找到一個匹配的洗牌平均只需搜尋一半的種子空間。而對於線上撲克,特定時間限制應該是一場遊戲的時長,這個時長通常以分鐘計。
根據我們的經驗,執行在奔騰400計算機上的簡單程式,可以每分鐘檢查大約200萬個種子。按照這個速度,這個機器對32位種子空間(約2^32個可能的種子)的窮舉搜尋需一天多一點。儘管這個時長必然超過我們規定的那個時間限制,但是如果利用計算機網路執行分散式搜尋,那麼在我們的時間限制內完成搜尋是完全可能的。
我們講蠻力攻擊主要是想強調加密金鑰長度與洗牌使用的種子之間的相似性。暴力破解密碼攻擊要嘗試每個可能的金鑰,以破解加密資訊。同樣,蠻力攻擊洗牌演算法也要檢查所有可能的種子。有關加密金鑰的長度,目前已有一個重大的研究發現。總體而言,該研究是這樣的:
Algorithm |
Weak Key |
Typical Key |
Strong Key |
DES | 40 or 56 | 56 | Triple-DES |
RC4 | 60 | 80 | 128 |
RSA | 512 | 768 or 1024 | 2048 |
ECC | 125 | 170 | 230 |
人們以前認為實時破解56位的資料加密演算法(DES)不可行,但事實並非如此。1997年1月,一個保密的DES金鑰在96天內被找到。之後,又做到41天內破解,然後是56小時,然後是1999年1月,在22小時15分鐘內破解。對短的金鑰長度或者小的種子集來說,這種破解能力的飛躍發展當然不是好兆頭。
人們甚至還發明瞭專門的機器,用於破解加密演算法。1998年,電子前沿基金會EFF就製造了一個專用機,用於破解DES資訊。製造這個機器的目的在於強調DES是多麼不堪一擊(DES是一種流行的、政府認可的演算法,要深入瞭解DES攻擊,請點選 http://www.eff.org/descracker/ )。DES之所以易於被破解,與其金鑰長度直接相關。由此可見,製造專用於破解RNG種子的機器也並非不可能啊。
我們認為32位的種子空間不足以對抗猛烈的蠻力攻擊,但是64位的種子空間應該足以抵抗幾乎所有的蠻力攻擊。因為現在很多計算機都支援64位整數,所以使用64位的種子就很有必要了,而且一個64位數應該足以避免洗牌時遭受蠻力攻擊。
單單用64位還不行。我們決不能斷定攻擊者肯定無法預測或估計PRNG使用的種子。如果他們有方法預測種子,那麼上述蠻力攻擊的計算壓力就變得無關緊要了,因為相比而言,此時破壞整個系統還要容易的多。我們利用的漏洞,不僅僅是ASF的缺陷演算法採用很小的32位的PRNG,還有該方法的種子依賴於一天之中的時間。我們已經證明,這種演算法基本無隨機性可言。
總結分析一下,整個系統的安全依賴於選擇一個不可預測的隨機種子,要實現這樣的選擇,最好是採用基於硬體的技術。基於硬體的方法從物理環境直接拿到不可預測的隨機資料。由於線上撲克等涉及真錢交易的遊戲,都對安全性要求至高,所以有必要進行一些投資,以確保隨機數生成器正確完成。
總而言之,開發一個好的洗牌演算法,並且採用經過驗證的硬體裝置為64位偽隨機數生成器準備種子,有這兩點,足以使洗牌實現公平性以及安全性。實現一個公平的系統並非很難,線上撲克玩家有權提出這樣的要求。