在線撲克如何作弊:一次軟件安全研究

撲克是一種風靡世界的紙牌遊戲,我們不僅可以在家中的餐桌上、賭場上、或者橋牌室中玩撲克,現在還可以在網上玩。我們研究可靠軟件技術的一些人也玩撲克。因爲我們現在都會花大量的時間在網上,所以將打撲克和可靠軟件技術研究結合在一起只是時間問題。我們將在線撲克遊戲和軟件安全結合起來研究後,發現一個巨大的安全漏洞,這就是本篇文章所要講的。

人們可以在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位僞隨機數生成器準備種子,有這兩點,足以使洗牌實現公平性以及安全性。實現一個公平的系統並非很難,在線撲克玩家有權提出這樣的要求。

原文鏈接: cigital   翻譯: 伯樂在線 hf_cherish
譯文鏈接: http://blog.jobbole.com/70736/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章