十月一的假期間,在知乎上看到一個問題《網頁遊戲都有哪些安全問題?》, 我是一個網頁遊戲開發者,對這個問題很是感興趣,印象比較深入。當時是在遊玩,也沒時間細看這個問題。後來,在微博上,有一位朋友的轉發,又讓我看到這個 問題,冥冥中,有種想回答的衝動。上週六時,研發部門內部週會時,聽到其餘項目組的一個整型溢出問題,致使刷錢的bug,又讓我想起這個問題,更加堅決我 要回答這個問題的決心,總結一下這項目中,全部經歷過的webgame安全問題的經驗,以加固當前項目安全壁壘,避免損失。亦可分享給其餘作 webgame研發的朋友,作交流探討。php
知乎中的原問題是『網頁遊戲都有哪些安全問題?』,我以爲不妥,我給改爲了『網頁遊戲都有哪些安全問題?如何作得更安全?』,同時,問題也從『你們 來研究探討一下,網頁遊戲攻防技術。一定,這個話題很敏感。目前,網頁遊戲已經不少了,會不會被黑產盯上?網頁遊戲會不會被黑,數據庫會不會被拖庫』改爲 了『你們來研究探討一下,網頁遊戲攻防技術。一定,這個話題很敏感。目前,網頁遊戲已經不少了,會不會被黑產盯上?網頁遊戲會不會被入侵?入侵方式有哪 些?如何作好網頁遊戲的入侵防護?挽救措施有哪些?如何才能最小化減小廠商損失?入侵方式有哪些?如何作好網頁遊戲的入侵防護?挽救措施有哪些?如何才能 最小化減小廠商損失?』,更改的理由是『本文原提問者開篇提到「你們來研究探討一下,網頁遊戲攻防技術。」,那麼應該不光提到如何入侵,更應該提到如何防護,應該細心描述漏洞造成原理,規避方式,以提升研發者技能水平;應該詳細講解安全事件發生後,如何最小化減小廠商損失,減小用戶損失,保護遊戲平衡。』,幸運的是,這個修改,被知乎經過了。對此,表示感謝。html
在後來閱讀這篇提問以及回答時,已經有幾位網友回答了,多數是站在安全工做者角度上回答了這個問題。在這篇日誌裏,我將以webgame研發者角度,切合遊戲業務模塊邏輯,從業務需求,數據庫設計,程序編寫,操做方式上來說解漏洞造成原理,規避方案,也歡迎你們討論。前端
登陸認證
近幾年,網頁遊戲幾乎都是以聯運方式運營,意味着遊戲服務器自己不保存用戶密碼,用戶登陸在平臺,經過平臺跟遊戲服務器的接口對接登陸。接口作加密認證。 故webgame的賬號密碼安全問題,這裏不提了。但登陸認證的hash字符串安全,也仍是要注意的。好比登陸hash字符串的生效時間,hash字符串 的加密參數來源,好比包括用戶名、登陸IP,瀏覽器user-agent等數據,以防止改hash被泄漏了,也是很難經過服務器的驗證。java
遊戲充值
webgame的遊戲充值流程,跟普通網頁充值流程一致,沒有特殊的地方,其不一樣點就是跟其餘衆多平臺作聯合運營時,勢必要每一個公司作接口對接,且接口規 範各式各樣,且遊戲廠商沒有話語權,必須按照他們的接口規範來,這實在棘手。騰訊的充值接口的驗證方式,安全性作的較爲突出,大約代碼:mysql
02 |
$signKey = array ( 'openid' , 'appid' , 'ts' , 'payitem' , 'token' , 'billno' , 'version' , 'zoneid' , 'providetype' , 'amt' , 'payamt_coins' , 'pubacct_payamt_coins' ); |
05 |
foreach ( $signKey as $key ) { |
06 |
if (isset( $data [ $key ])) |
08 |
$sign [ $key ] = $data [ $key ]; |
11 |
######開始生成簽名############ |
13 |
$url = rawurlencode( $url ); |
18 |
foreach ( $sign as $key => $val ) |
20 |
$arrQuery [] = $key . '=' . str_replace ( '-' , '%2D' , $val ); |
22 |
$query_string = join( '&' , $arrQuery ); |
24 |
$src = 'GET&' . $url . '&' .rawurlencode( $query_string ); |
26 |
$key = $this ->config->get( 'qq_appkey' ). '&' ; |
28 |
$sig = base64_encode (hash_hmac( "sha1" , $src , strtr ( $key , '-_' , '+/' ), true)); |
29 |
if ( $sig != $data [ 'sig' ] ) { |
31 |
$return [ 'msg' ] = '請求參數錯誤:(sig)' ; |
32 |
$this ->output->set(json_encode( $return )); |
在此基礎上,還能夠作的嚴謹點:程序員
- 增長隨機參數名、參數值。隨機參數名、參數值由聯運方隨機生成,按照參數名的字符串所屬ASCII碼順序排序,參數名、參數值均參與sign的計算,增長暴力破解密鑰(app key)難度。
- 增長回調驗證訂單號,金額信息。遊戲充值服務器接收到充值請求時,反向到該平臺回調接口,確認此筆訂單有效性,以防止加密密鑰泄漏的問題。
遠程文件引入
在網頁遊戲的研發中,多數都是使用框架來作,即便用REQUEST來的參數,做爲請求文件名的一部分,來使用,那麼很容易造成遠程文件引入的漏洞。在咱們以前的遊戲中,曾出現過一例這樣的漏洞問題。web
4 |
if ( ! file_exists (APPROOT. 'controllers/' .load( 'Router' )->getDirectory().load( 'Router' )->getClass().EXT)) |
6 |
load( 'Errors' )->show404( 'Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.' ); |
8 |
include (APPROOT. 'controllers/' .load( 'Router' )->getDirectory().load( 'Router' )->getClass().EXT); |
9 |
load( 'Benchmark' )->mark( 'load_basic_class_time_end' ); |
webgame中的遠程文件引入redis
從代碼以及案例圖中,能夠看到對於REQUEST的參數沒有過濾處理,直接做爲文件名來include引入的,故致使這種問題,相似上頁圖中QQ羣 網站的漏洞。若PHP version < 5.3.4 ,還會發生Null(%00) 截斷的問題,帶來更大的安全問題。在咱們新的項目中,咱們更改了實現方式,咱們遊戲全部接口都會走gateway,gateway裏,對控制器名作類名規 範的檢測處理,再在指定幾個目錄下作autoload加載文件,且還會對REQUEST的類名、方法用ReflectionClass反射類的處理,檢測 到類、方法、參數是否合法。一來避免『遠程文件引入』漏洞問題,二來便於先後端聯調時,拋出更詳細的異常,方便調試。下面爲參考代碼:算法
01 |
require_once CONFIG_PATH . "/auto.php" ; |
02 |
spl_autoload_register( "__autoload" ); |
08 |
$view ->error(MLanguages::COM__INVALID_REQUST); |
09 |
$msg = new Afx_Amf_plugins_AcknowledgeMessage( $val ->data[0]-> $messageIdField ); |
10 |
$msg ->setBody( $view ->get()); |
11 |
$message ->data = $msg ; |
14 |
$a = new Yaf_Request_Simple(); |
15 |
$a ->setControllerName( $method [0]); |
16 |
$a ->setActionName( $method [1]); |
17 |
$objC = new ReflectionClass( $method [0]. "Controller" ); |
18 |
$arrParamenter = $objC ->getMethod( $method [1]. "Action" )->getParameters(); |
19 |
$arrRequest = isset( $val ->data[0]->body[0]) ? ( array ) $val ->data[0]->body[0] : array (); |
21 |
foreach ( $arrParamenter as $objParam ) |
23 |
$parm = $objParam ->getName(); |
24 |
$bIsOption = $objParam ->isOptional(); |
25 |
if (isset( $arrRequest [ $parm ])) |
27 |
$a ->setParam( $parm , $arrRequest [ $parm ]); |
29 |
elseif ( $objParam ->isOptional()) |
40 |
$rp = $app ->getDispatcher()->dispatch( $a ); |
41 |
$msg = new Afx_Amf_plugins_AcknowledgeMessage( $val ->data[0]-> $messageIdField ); |
42 |
$msg ->setBody( $view ->get()); |
43 |
$message ->data = $msg ; |
SQL 注入
SQL注入原理、方式,跟普通web應用同樣,沒什麼特別的,在使用REQUEST來的參數時,過濾處理便可。可能在消息格式,以及注入操做簡便上,會矇蔽研發人員的眼睛,被忽略掉了。好比咱們項目的AMF消息格式,在前端界面沒出來以前,咱們後端程序員通常使用Pinta來模擬操做,調試程序。前端界面出來以後,會使用Charles proxy來捕捉http請求。在這些過程當中,請求接口、參數的構造,沒有普通web那麼簡單。研發人員也容易忽略對請求參數的過濾,故很容易造成這種問題。造成原理見:《WEB開發安全與運維安全淺見》,防護方式作過濾處理,或SQL預編譯。
sql
AMF消息格式的WEBGAME中的SQL注入
AMF消息格式的WEBGAME中的SQL注入
爲了提升遊戲服務器的吞吐能力,網頁遊戲的架構也是一直在演變的。在以前以Mysql做爲數據存儲的webgame架構中,其餘節點都是能夠水平擴 展,或者說依賴簡單粗暴的增長服務器來解決,單單做爲惟一數據存儲中心,不能這麼作。爲此,不少webgame的數據存儲改用Nosql來代替,甚至 java、C/C++的遊戲數據,直接在內存中操做,遊戲關服時,才寫入到DB中。故SQL注入的問題,也會愈來愈少。
通信協議與消息格式
網頁遊戲雖然名字叫網頁遊戲,但通信協議並不是全是http,也有不少使用socket,以及http+socket並用的作法。咱們是http協 議+amf消息格式,以及socket並用來實現。在http與https的取捨上,咱們考慮到ssl的啓用後,大量的ssl解密加密運算,勢必會增長服 務器大量的CPU計算壓力。而傳輸的內容,多數是遊戲業務的操做,響應,是能接受被監聽嗅探的行爲的(認證信息除外)。站在安全角度,這不能理解。但站在 產品角度,考慮一下 投入產出,而後選擇http通信,也是能夠理解的。socket在咱們遊戲中,除了在聊天應用上使用外,在一些組隊、幫派戰之類須要多個玩家之間同步數據 信息時,咱們也會使用socket來推送數據。在使用socket做爲全部業務傳輸的協議時,協議格式通常都是開源協議,好比msgpack、protobuf之類,或者自定義的協議。使用自定義協議時,務必檢測整個消息包的每個參數,類型範圍,避免個別超大數值、邊界數值出現,致使主程序內存越界,以致於服務宕機,沒法正常服務的狀況發生。
金幣複製-整型溢出
上週週六開週會時,聽到其餘項目組的一個關於整型溢出致使產生刷金幣的問題。在這裏,我抽象該案例,分享一下。商城出售開啓揹包格子的所需道具『梧桐 木』。在遊戲中,用戶包裹格子數量通常都會做爲一個收費點,一款遊戲的格子大約爲每行7格子,一共8行這樣。好比前面3行是默認開放的,第4行是收費的, 並且第一個格子所需品梧桐木的價格1個銀子,第二個梧桐木是2個銀子,第三個是4個銀子。依次類推,意味着這些梧桐木的價格總和其實就是一個第一項爲1, 公比爲2,項爲35的等比數列。 當用戶選擇購買梧桐木數量大於31時,好比32-36中這些數字時,這些等比數列的和就是大於2147483647。(只是舉例,實際上不會以這樣的價格 出售物品)
在java中,4字節的存放int型變量的範圍是-2147483648至2147483647。在java、c的有符號int型中存儲時,數的最高位描述符號位,4字節共32位,除去最高位的符號位,剩下31位,每一個位上能表示2個數字,4字節的有符號的整數表示範圍爲:負整數2^31個,範圍爲『-1至-2147483648』;正整數2^31個,範圍爲『2147483647至1』。 好比下圖(注意十進制數字跟二進制表示的變化順序):
當開啓格子數字爲大於31時,好比32,那麼所需費用就是2147483647個銀兩,再買點其餘物品,湊成超過2147483647的數字,好比又買了3個銀子的其餘道具,總共花費2147483650個銀子,在4字節的有符號int中表示出來的結果,變成符號位爲1,即負整數。數值位爲0000000 00000000 00000000 00000010,也就是10000000 00000000 00000000 00000010,對應十進制的-2147483646。 程序邏輯上,再判斷現有銀兩是否足夠支付此筆花費時,是經過的。當使用當前餘額減去這筆花費時,將變成減去一個負數,那麼實際上就是加上一個正整數。變成 了本身銀兩帳戶餘額的增加。而餘額字段類型是long,則正確的存儲了這些餘額,溢出漏洞被利用。在C中,使用無符號的數值類型,便可完成數值類型溢出刷 錢的行爲,但在java中,好像沒有無符號的類型。這也能夠先肯定全部參與計算的數值必須爲正整數做爲必要條件(遊戲業務特性,遊戲內全部數字,確定全爲 正整數,甚至都不包括零),先作大小判斷,再作正正相加,不能得負;負負相加,不能得正。來判斷是否發生了溢出問題。在PHP中,不用擔憂溢出問題。
金幣複製-併發請求
Rpg類型的網頁遊戲中,多數都有道具出售的功能,直接賣到商店,以及道具材料從商店買入功能。當玩家同時針對買入、賣出兩個操做,瞬間大量併發請求時,在服務器的處理邏輯通常有分別的兩個進程處理,共享數據分別數據庫中的對應帳戶餘額表,以下圖:
webgame買入、賣出併發請求處理
03 |
$iBalance = $obj ->getBalance( 'user1' ); |
06 |
if (! $obj ->setBalance( 'user1' , $iBalance + 100)) |
11 |
if (! $obj ->delItems( $items )) |
19 |
$iBalance = $obj ->getBalance( 'user1' ); |
21 |
if (! $obj ->setBalance( 'user1' , $iBalance - 50)) |
26 |
if (! $obj ->addItems( $items )) |
賣出請求的處理進程爲1,買入請求的處理進程爲2。在進程1還沒將結果寫入到DB時,進程2也從DB讀取到餘額爲50。這是,兩個進程拿到的餘額信 息都是50。進程1按照邏輯代碼,計算出剩餘餘額是150;進程2計算出的剩餘餘額是0。最後,無論那個進程最後寫入餘額,都是錯誤的結果。(注:這裏的 代碼邏輯操做,跟mysql事務無任何關係,事務只能保證單個進程的事務範圍內多條語句都正確執行,或回滾。好比能保證扣錢成功,且物品刪除掉的兩個語句 都正確執行。能保證其中之一的語句執行失敗時,都正確回滾。)
其實,在事物開啓時候,SELECT語句是否能夠取到最新的數據,或者是否須要等待鎖釋放,取決於MYSQL的事務隔離級別。在MYSQL的事務隔離級別中,有一下幾種隔離級別:
- READ-UNCOMMITTED(讀取未提交內容)級別
- READ-COMMITTED(讀取提交內容
- REPEATABLE-READ(可重讀)
- SERIERLIZED(可串行化)
對於READ-UNCOMMITTED,能夠讀取其餘事務中未提交的數據,並且聽說性能還高不到哪裏去,幾乎沒有在實際應用中使用;對於READ- COMMITTED,在同一事務中,會由於其餘事務隨時可能有新的commit,致使同一select可能返回不一樣結果。這個也不適合遊戲業務;再說第四 個SERIERLIZED,只要事務開啓,全部其餘查詢,均排隊等待該事務提交以後,對於上面提到的賣出買入狀況,第二個事務的SELECT操做,不會立 刻返回,會處於鎖等待狀態,一直到前一個事務結束。這個隔離級別,雖然能避免上面的問題,但性能較差,通常不會去使用。而REPEATABLE-READ 隔離級別,也是mysql默認的隔離級別,從功能上,比較符合遊戲業務須要,也應該是廣大webgame架構中mysql的默認隔離級別。
對於這個問題,你可能很快就給出解決辦法,把UPDATE語句改成UPDATE `role_gold` SET gold = gold + 100 WHERE role_id = 1或者UPDATE `role_gold` SET gold = 150 WHERE role_id = 1 AND gold = 100來解決,但這種多個事務同時操做修改多個表的多條記錄時,還容易引起死鎖問題,好比《webgame中Mysql Deadlock ERROR 1213 (40001)錯誤的排查歷程》。並且,當條件爲跨表內數據是否存在,或者另外條件不在MYSQL中,而在其餘網絡接口的響應中時,如何作呢?
金幣複製--邏輯漏洞
引用DNF的漏洞新聞 《利用網遊漏洞狂刷遊戲幣賺錢 玩家自曝3天賺17萬》
玩家曝出刷幣漏洞 一個遊戲道具可刷400人民幣
該漏洞究竟是什麼?原來遊戲中「雲冪袖珍罐」這個道具,能夠開出2件同樣的遊戲裝備,還有極少概率開出遊戲幣,開出的裝備不值錢,但若是開出金幣了,則分 爲5000萬、8000萬以及1億遊戲幣。而1億遊戲幣,按正常市場行情,可在交易網上賣400多元人民幣。據玩家稱,在遊戲中,角色的裝備是須要用包裹 來存放的,不過目前角色的包裹最多隻有48格,也就是隻能存放最多48件裝備。漏洞就是利用包裹的有限空間,存放47件裝備(存放滿了又沒法開罐子),只 留下一格空位,而在開「雲冪袖珍罐」出裝備時,就會因包裹空間不足,而致使開罐失敗,而罐子還存在。玩家繼續開罐,直到出現金幣,但金幣不會佔據包裹的空 間,所以開罐成功,而後罐子消失。發現這個漏洞後,部分玩家狂刷遊戲幣,而後立刻在第三方交易平臺出售遊戲幣,兌換成現金。
這種問題,都是研發人員邏輯不嚴謹致使,這種問題,也較難發現。規避方式能夠依賴下面提到的『運營數據監控』。
道具複製--揹包整理
跟上面的賣出、買入同樣,同時穿裝備、整理包裹。在設計時,可能會將身上裝備設計在裝備表中;將不在身上的裝備,設計到揹包表中。當同時進行穿裝備 跟整理包裹的請求併發時,也會發生跟上面賣出,買入的狀況,線程1讀取DB,發現包裹裏有這裝備,而後準備刪除揹包表的這條記錄,當準備寫入到裝備表時, 另一個整理包裹請求的線程來了,讀取了整個揹包表,進行道具的合併、排序。這時,以前的線程將這個裝備寫入到裝備表,並刪除了揹包表裏的數據,並提交事 務。這個穿裝備的全部操做都是合理、正常,且正確執行的。但另一個整理揹包的線程讀取了以前的揹包表裏的數據,包括那件被穿上的裝備。 在遊戲中,整理揹包須要對可堆疊道具作堆疊操做的,意味着須要合併多個道具,刪除部分道具。這意味着這裏的操做,當前cgi線程的內存中的數據,將都會以 覆蓋的形式,寫入到DB中,那麼意味着,以前被穿到身上的那件裝備,也會從新被寫入到揹包中。那就變成兩張表裏出現了兩個相同惟一ID的相同屬性的道具。 玩家就能夠把揹包中的這個道具出售給其餘玩家。
在java或者C之類程序中,數據放內存中的遊戲,也會存在這個問題,除非作讀鎖,但讀鎖會帶來鎖等待,鎖等待會致使線程被佔用,阻塞後面請求的處 理,堆積大量請求。致使系統負載升高,服務器繁忙,以致於沒法響應。好了,大約理解道具複製的造成緣由了嗎?這個問題,咱們從根本緣由想一想,問題到底出現 在哪裏?如何規避呢?細心的同窗不難發現,對於穿裝備的操做結果,會對下一個請求產生影響的,當前操做未獲得服務端響應以前,服務端是不能處理下一個響應 的。對此,咱們作了響應處理鎖--『用戶併發請求鎖』。
用戶併發請求鎖的實現,php中session以文件形式存儲時,php會對session文件加鎖,不釋放(若是不特地執行 session_write_close),知道當前響應完成。另一個線程才能夠正常讀取,這簡介的造成了單個用戶的併發請求鎖,可是,後面的進程一直 處於等待狀態,也會佔用一個php-fpm進程,阻塞其餘用戶的正常請求對php線程的使用。爲此,咱們使用NOSQL的K-V形式結構,以 user_name爲key的形式,實現用戶併發請求鎖,第一個請求,生成這個k-v數據,後一個請求發現有這個key了,那麼馬上拋出異常,結束響 應,FLASH根據異常內容,提醒用戶不要進行惡意操做。即不會發生併發請求,又不會阻塞請求處理。同時,在請求結束的析構函數裏,對這個鎖進行刪除操 做,不影響下一個正常請求。若由於程序異常,發生語法錯誤,致使析構函數無法執行,沒有刪除用戶鎖時,能夠在生成鎖的時候,設置過時時間,好比5秒,甚至 2秒,利用nosql的過時機制,實現用戶解鎖,避免用戶長時間沒法正常遊戲。
類CC攻擊-多用戶共享資源鎖的timebomb
咱們如今研發的項目,是以NOSQL Redis做爲DB,來存儲數據的,redis並無成熟的事務處理機制,watch甚至算不上關係型數據庫中的事務處理。對此,更須要對錶進行加鎖解 鎖。java之類語言的項目,不少都是直接操做內存的,更是須要資源鎖,來解決併發問題,解決多個請求操做同一份數據的問題。公司有另一個項目,出現過 一次由於鎖的顆粒度較大,帶來的鎖等待timebomb的問題,也致使了線程繁阻塞忙,請求堆積,系統負載上升,致使宕機的問題。這個項目的鎖是針對全部 用戶的鎖,每一個用戶的請求發來時,當前線程會對全部用戶的數據加鎖,直到響應完成,才釋放掉。這麼作,是爲了解決因當前操做,會影響到其餘用戶數據,好比 多人PK,多個玩家之間的交互。
當其餘請求一併發來時,那麼資源會馬上被鎖住,直到上一個請求結束,才釋放鎖,那麼其餘線程都處於等待狀態。用戶基數小時,是看不出來鎖帶來的影響的,內 存操做都比較快。當用戶基數大時,或者說請求數增大時,後面的請求的等待時間會愈來愈長,超過webserver的等待時間,直接返回timeout,不 能正常提供服務。
這種問題的發生,是由於鎖的顆粒太大了,不該該將全部用戶都鎖住,最好細化到當前請求所影響到的單個用戶,只鎖住單個用戶的數據。這樣,才減小timebomb的發生。
其餘
知乎裏的朋友提到,不少webgame 的前端作了判斷,然後端沒作判斷的問題,這種問題,實屬不應存在。在咱們的項目中,後端作的驗證判斷,遠比前端多的多。有時候,爲了界面上的動畫表現,前 端flash通常會在用戶操做以後,馬上渲染,而後,再根據後端響應,決定是否繼續作界面元素改動。好比脫裝備,玩家操做時,會先渲染裝備從角色面板,跳 到揹包裏的動畫,而後,再根據後端響應結果,決定是否回滾動畫。這樣,避免顯得操做後,必定時間的反映遲鈍假象,以提升用戶體驗。固然,後端是必定會作判 斷的,判斷角色揹包是否有空格之類。如今的webgame研發,通常都不會存在前端判斷,然後端不判斷的作法了。若是有,也應該是個別遺漏狀況。
好比去年的time33算法的hash dos的問題,使用json消息格式的webgame必定要注意,php只是在接收請求時,作了最大數量的限制。但在json解碼以後的數據中,是沒有處理的。這裏千萬別忘記了。
運營數據異常監控
再完善的防護措施,都仍會有安全漏洞。適當的監控措施,也必定要有,監控等級、金幣、遊戲幣、經驗、珍貴物品的變化等等,一旦發現,馬上報警,在漏洞未擴散以前,第一時間去修復漏洞,以減小損失,維護遊戲平衡。
日誌系統
日誌系統必定不能漏掉,全部操做,必須寫入日誌,當安全事件發生後,能夠做爲各類數據回滾,交易糾紛處理的可靠數據。也是做爲數據監控的最準確的數據來源。
若是你用了我畫的小清新般的插圖,請記得爲圖片寫上署名來源,畫圖是最花費我時間的一件事。
原文地址:http://www.cnxct.com/experience-with-webgame-of-security-and-defense/