對於任何一款要長期線上運營的遊戲,防破解防外掛是必不可少的。本文總結了手遊經常使用的防破解防外掛技術方案,這些方案都通過了筆者所在團隊和線上項目的長期考驗。不少方案來自於弱聯網手遊項目,但大部分思路也一樣適用於強聯網遊戲。以Unity爲例,但思路也適用於非Unity項目。筆者儘量作到總結全面,但願能幫助你們造成一個總體的防護思路。
強聯網遊戲的特色是不少邏輯在服務端計算,重要數據由服務端控制,客戶端多數時候着重於表現。而弱聯網遊戲由於要求玩家能在不聯網或網絡環境不好的狀況也能正常玩,因此客戶端可能包含了不少重要的遊戲邏輯和數據,服務端則提供一些額外的業務邏輯,好比做弊校驗,數據同步,排行榜,各類聯網活動等。若是咱們信賴客戶端的邏輯和數據,那麼一旦客戶端被破解,整個遊戲就會被操控,輕者損失了部分玩家,重者會污染遊戲的整個生態環境。最麻煩的是,破解者只要有代碼,本質上被破解就只是個成本和時間的問題。可是,咱們仍有各類方式來抵禦常見的破解和外掛。對於那些根本上很難防住的破解方式,咱們至少能大大增長其破解成本。
本文從兩方面來總結:客戶端和服務端。這篇先講客戶端,分爲幾個章節:
- 加固
- 內存加密
- 代碼混淆
- 破解apk
- 資源加密
- 玩家存檔加密
- 時間防做弊
加固
加固是對代碼作各類形式的變換,好比加密,混淆,隱藏等,以提升代碼逆向的難度。這是全部遊戲都通用的一個技術,有很多公司提供了成熟的解決方案,好比網易,騰訊,樂變。已有的加固技術包括:
1 加殼
目的是防止二次打包。對加殼後的apk包重簽名,進遊戲時會閃退。
加殼分兩種方式:
(1)dex加固:比較成熟,不少廠商採用的解決方案,好比樂變。
(2)so加固:比較新,網易易盾用的此方案,native層加密,更安全可靠。
2 反調試
目的是防止IDA動態調試。
這部分沒什麼須要過多考慮的,建議直接從這些成熟的解決方案中挑選一個應用於項目。
內存加密
網上有一些內存修改器能夠搜索和修改內存數據,從而實現各類誇張的效果,好比金幣無限,血量無限,攻擊力無限等。經常使用的工具備八門神器,葫蘆俠,燒餅修改器。他們的使用原理都是相似的,好比,若要修改玩家當前的金幣數,先用工具在內存中搜索當前的金幣數值,會搜出來不少內存地址。而後消耗一些金幣,在以前的內存地址中再搜索當前的金幣數,獲得較少的匹配地址。重複該步驟,直到只剩一個地址匹配,就是存放金幣的內存地址。最後,經過工具更改該地址存儲的數值,就能把金幣數改爲一個很大的數值。
要防止這種工具的破解,就須要對內存數據作加密,讓工具搜索不到該數據所在的內存地址。最簡單的方案是:
1 準備一個key值,不要用字符串明文,得是運行期動態生成的。
2 存數據時,先把數據和一個key作異或操做,再存到內存。
3 讀數據時,把從內存讀出的數據和一樣的key作異或,返回給上層。
該方案簡單高效,能防住大部份內存修改器,但有一些搜索功能比較強大的工具,好比燒餅修改器有模糊搜索功能,仍能搜索到通過加密的數據。因而咱們須要一個更強大的方案。
因爲這些內存修改器都是在搜索到的內存地址集合裏再次搜索篩查,因此只要不停地變換數據存儲的地址,就能從根本上防住這種修改器。具體作法是:
對於任何一個須要加密的數據類型:
1 分配N個同類型元素的數組,N至少爲3。
2 每次存儲數據時,數組index加1,若超出數組長度則index歸零,而後將數據和一個key作異或,獲得加密數據,將其存儲到該index指向的數組槽。記錄下當前的index和key。
3 讀取數據時,根據存儲的index,讀取數組槽中的數據,和key作異或,將結果返回。
實測下來,通過這樣的處理後,燒餅修改器也徹底沒法搜索到其內存地址,因此能有效防住這種類型的工具。該方案據說在騰訊內部項目裏使用了,筆者本身在Unity裏實現了一套加密數據類型,可直接拿來在項目中使用,放在Github上
[1]:
該代碼實現的要點:
1 用泛型儘可能精簡了代碼。
2 實現了類型轉換的操做符,這樣能最大程度簡化已有項目的重構,好比若要將基礎數據類型更改成加密數據類型,只須要更改變量聲明處的類型,好比將int改成EncryptInt,其餘的上層代碼不須要作任何改動,自定義的類型轉換操做符會幫助編譯器處理剩下的工做。
須要注意的是,實際項目中應全面地對任何遊戲界面可見的關鍵性數據作加密,好比金幣,血量,攻擊力等。並且,全部會和關鍵性數據作運算的相關數據,也得用加密類型。好比,有一個遊戲內彈框界面,上面可讓玩家自由選擇要購買的道具數量及對應的金幣花費,那麼此處的金幣花費的變量也應作加密。不然,玩家經過屢次更改道具數量,就能用工具很容易地搜索出金幣花費對應的地址,而後將其修改成0或者負數,再進行購買,就能達到買道具不花錢或者買完金幣增長的效果。防破解這種事,百密一疏就會致使嚴重的問題,因此在防護上要儘可能考慮全面。
代碼混淆
網上有各類工具能對Unity遊戲的dll文件作反編譯,或者對so文件作反彙編。Dll反編譯後,全部代碼就很是可讀,毫無安全性。因此咱們須要把代碼中的各類元素,好比類名,函數名,變量名,改爲無心義或很難看懂的名字,使得破解者即便反編譯了代碼也很難讀懂,從而加大破解難度。經常使用的Unity代碼混淆工具備Obfuscator,Obfuscar,CodeGuard等,這些工具大部分都是在.Net IL層修改字節碼,不影響正常開發流程。另外,還有不少針對iOS和安卓原生層的工具。
以Obfuscator插件爲例,有一個名爲ObfuscatorOptions的配置文件,其中不少設置會影響混淆的強度。值得注意的設置有:
1 Name mapping history
勾選,混淆時會生成符號映射文件,記錄混淆先後的名字映射關係。
2 Rename
選擇哪些被混淆。對於上層接入了lua的項目,就只勾選private和protected的函數和變量,不對public成員作混淆。由於public函數可能被lua層調用,若是作混淆,那麼lua代碼也要相應作修改,沒法方便地維護。
函數名被混淆後,會帶來一些不便:
(1)崩潰統計後臺顯示的是混淆後的名字,若是是private或protected函數,就須要查符號映射表獲得混淆前的名字。
(2)若接入了xlua代碼熱修復,那麼熱修復private或protected函數時,也須要查符合映射表,調用xlua_hotfix時得傳入混淆後的函數名。
3 Fake code
勾選後會增長垃圾代碼,經過改變一些fake相關的參數能夠調整混淆的強度。須要注意fake code加得越多會致使代碼尺寸越大,一是會增長包體,二是在IL2CPP模式下,iOS包體代碼尺寸可能會超過蘋果規定的限制,從而致使審覈上傳時被拒。
4 Unity methods
該列表中的函數不會被混淆,可根據項目自身需求刪減。除了這個列表,對於本身寫的lua層回調函數,使用了反射調用的函數,和Inspector裏綁定的事件函數,還能夠在函數聲明前加[SkipRename]屬性來避免被混淆。
代碼混淆的做用除了增長破解難度之外,還能用於應付蘋果審覈。蘋果對馬甲包的審覈很嚴格,若是你的app和其餘app在代碼和資源上類似度很高,就會有審覈被拒的風險。代碼混淆工具就能夠用來人爲製造二進制包的差別化。可是,因爲流行的混淆工具都是在IL層把各類名字改成隨機的相似亂碼的名字,二進制的特徵和正常app是不一樣的,可能會在蘋果機審階段被查出來,致使被拒。不少開發者就由於過分使用了混淆工具,收到了蘋果爸爸相似這種回信:
We discovered that your app contains obfuscated code, selector mangling, or features meant to subvert the App Review process by changing this app's concept after approval to the App Store. The next submission of this app may require a longer review time, and this app will not be eligible for an expedited review until this issue is resolved.
因此,爲了不沒必要要的審覈風險,建議你們不要過分依賴這些混淆工具,能夠本身寫一些腳本,在源代碼層或IL層處理字符串替換。
破解apk
破解apk包的危害很大。破解者能夠把包破解後,傳到網上供人下載。對於Unity apk包,網上已經有比較統一的破解流程,這裏作一個簡單的總結。下面的方法能處理未作加固加殼處理的,若作了加固加殼,就會使得一些文件結構被修改,方法就不必定奏效了。
Unity有兩種腳本後端模式:mono和il2cpp。mono比較老,如今大部分遊戲使用了il2cpp。Apk解包後,經過裏面的文件信息能判斷是哪種模式:
1 若是assets/bin/Data/Managed/下有一堆dll文件,其中有Assembly-CSharp.dll,則是mono
2 若是assets/bin/Data/Managed/下有三個文件夾:etc/,Metadata/,Resources/,則是il2cpp
無論是mono或il2cpp,破解流程都大體以下:
1 解包
可用apktool運行命令解包abc.apk:
獲得同名文件夾。注意用命令行解包,若把apk的後綴改成zip解壓縮,獲得的文件夾中會缺乏apktool.yml文件,到後面從新打包時會報錯:javascript
brut.directory.PathNotExist: apktool.yml
2 修改代碼
解包後根據文件信息判斷是mono仍是il2cpp。
對於mono包:
(1)Windows機器上安裝.Net Reflector和Reflexil插件,用它打開assets/bin/Data/Managed/Assembly-CSharp.dll。
(2)查看反編譯的dll代碼,嘗試去找須要破解的邏輯,直接修改IL代碼,或寫源代碼而後用Reflexil編譯成IL。
(3)將修改後的代碼導出爲新的Assembly-CSharp.dll,覆蓋前面解包目錄下的同名文件。
對於il2cpp包:
(1)用il2cppDumper工具
[2],根據這兩個文件:
- lib/armeabi-v7a/libil2cpp.so:包含全部可執行彙編代碼
- assets/bin/Data/Managed/Metadata/global-metadata.dat:包含符號表信息
運行il2cppDumper,會生成兩個文件:
- dump.cs:包含全部函數及地址信息
- script.py或ida.py(由il2cppDumper版本決定):做爲IDA的腳本後面使用
(2)查看dump.cs,嘗試去找本身感興趣的函數信息。
(3)用IDA打開libil2cpp.so,先運行script.py或ida.py添加各類符號的可讀信息,如果ida.py,還須要選擇script.json。這時各類類和函數都具備了可讀的字符串名字。找到須要破解的邏輯地址,修改彙編代碼。
(4)將修改後的代碼導出爲新的libil2cpp.so,覆蓋解包目錄下的同名文件。
3 重簽名打包
(1)運行命令:
keytool -genkey -keystore mykey.keystore -keyalg RSA -validity 10000 -alias mykey
獲得mykey.keystore文件。html
(2)運行命令:
獲得abc.apk文件,位於目錄abc/dist/。java
(3)運行命令簽名打包:
jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore mykey.keystore -signedjar abc_signed.apk abc/dist/abc.apk mykey
獲得新包abc_signed.apk。git
網上有些教程裏會加上-tsa參數,測試下來會致使報錯:
jarsigner error: java.lang.NullPointerException
上述破解方式的關鍵仍是在於讀懂反編譯或反彙編的代碼,找到關鍵邏輯代碼作修改。破解者可能會搜索user,level,coin這種常見的關鍵字,進而很容易就找到關鍵邏輯。因此,咱們能夠儘可能混淆這些關鍵類名,函數名,變量名等,改爲一些難讀懂甚至具備誤導性的名字,就能增長破解的難度。可是,如前面所說,這些都只是增長了破解難度,只要有代碼,破解就只是時間和成本問題。
針對這種破解方式,有些安全方案對這些靜態文件作了保護。mono模式下,對Assembly-CSharp.dll作加密,改變了PE文件格式,使得反編譯工具沒法識別。il2cpp模式下,可對so文件作加密,或對global-metadata.dat符號文件作保護,使得工具沒法還原出符號信息,也增長了破解難度。
資源加密
普通的未加密的ipa和apk包,咱們能夠用工具解包,很容易獲得資源的明文形式。對於Unity包,能夠用資源查看工具(好比AssetStudio)解出Resources目錄下的資源和各類AssetBundle資源。因此咱們須要對資源作加密,以保證至少沒法用工具簡單地解包。
通常Unity項目的不少資源都打成了AssetBundle,因此須要對AssetBundle作加密。很容易想到的方式是:
1 構建打AssetBundle包時,對資源作對稱加密
2 運行期加載時,先把AssetBundle加載到內存,用key解密,獲得解密後的AssetBundle內存
3 調用AssetBundle.LoadFromMemory(Async)接口從內存中加載資源,初始化對象
這一切看起來很清晰完美。但不幸的是,用AssetBundle.LoadFromMemory(Async)加載資源,會致使內存使用量暴增。一份資源經過該接口加載,會在內存裏出現三份拷貝,除了資源自己在系統層或GPU層有一份,還會在Native層和託管層裏各有一份。若是是LZMA格式,會先解壓縮再存儲,內存消耗比資源原始資源尺寸更大。因此,官方其實不推薦使用該接口
[3]。
那麼,還有更簡單的方式嗎?也有,UWA提供了一個加密方式
[4],經過給AssetBundle文件內容加一個偏移,就能作到沒法用資源查看工具直接讀取其內容。該方案的優勢是簡單高效,不耗額外內存,但缺點也很明顯,它的防禦強度很弱。
除了AssetBundle,ScriptableObject資源也沒有簡便的加密方式。因此,Unity在設計上就沒有很好地支持資源加密,多是由於國外沒有咱們國內市場的一些困擾。Unity中國團隊針對咱們的國情,出了個Unity加強版,接口上直接支持了AssetBundle的加密,使用起來很簡單
[5]。是否合適好用就由你們各自判斷了。
除了Unity格式的資源,對於通用格式的資源,好比csv,json,xml,lua文件等,可能也包含很是重要的信息,而且文件尺寸一般不大。就能夠用前面提到的方式,打包時作對稱加密,運行期先讀到內存作解密,而後加載初始化。
須要注意的是,無論加密什麼格式的資源,加密的密鑰務必要隱藏好,至少不要用明文字符串,應在運行期用算法動態生成,而後儘量讓這個函數不容易被發現和讀懂。每發佈一次版本,均可以更換一次密鑰,使得破解者用老版本的密鑰沒法破解新版本的資源。
另外,網上有VirBox Protector這種加固工具,也包含了資源加密的功能。
玩家存檔加密
重要的數據都須要加密。和資源同樣,玩家存檔本質也是一種重要的數據,會序列化成文件,因此加密思路和資源加密相似。不一樣的是存檔數據由玩家玩的時候動態生成,並且可能在不一樣代碼版本間流通,須要考慮兼容性。對於強聯網遊戲,玩家存檔數據中重要的部分都存儲在服務端,只要設計得當,客戶端不管如何怎麼修改數據,都不會致使嚴重的後果。但對於弱聯網遊戲,玩家在沒聯網的狀況也能玩,就不得不以客戶端的數據爲主導,防破解的難度很大。
存檔可存放在自定義的文件中,這種狀況下加密方式能夠和資源加密同樣。對於Unity包,本地存檔常放在PlayerPrefs中,本質上是鍵值對,咱們沒法對PlayerPrefs整個文件操做,就能夠對鍵和值分別作加密,或只對值作加密。和資源加密同樣,注意保護好加密密鑰。若是要更換密鑰,須要處理數據的先後兼容問題。除了文件加密外,玩家存檔在內存中的數據應作內存加密。
一種破解方式是,玩家把本身的存檔文件傳到網上,其餘玩家下載下來複制到本地,實現存檔轉移。好比有些遊戲淘寶上就有賣家將高進度或破解後的我的存檔出售。爲了防護這種狀況,可讓一個玩家的存檔包含了本身的標識符信息,使得在另外一個玩家的設備上沒法打開。一個簡單的方案是,存檔的加密密鑰有玩家UDID或設備ID參與,好比用原始密鑰和UDID作異或拼接等操做,或者原始密鑰和UDID的MD5作異或操做。
時間防做弊
不少遊戲功能依賴於系統時間,好比體力恢復,建築升級,各類CD時間。對於強聯網遊戲,全部時間都由服務端控制,比較好處理。弱聯網遊戲則相對比較麻煩。若是徹底信任本地時間,那麼玩家可經過修改本地系統時間來達到不少目的。因此,總體思路是,聯網的時候徹底信任網絡時間。沒聯網的時候,就用系統本地時間。等到聯網後再對時間作校訂,以及作做弊斷定。
網絡時間可經過NTP協議或本身的服務端獲取。NTP其實不太可靠,有時會連不上,建議使用本身的服務端。注意因爲網絡傳輸的延時及不穩定性,獲取到的網絡時間會在真實時間值附近波動,因此在做弊斷定時,應留有足夠的閾值。
iOS或安卓原生層都有接口可獲取設備開機到如今的流逝時間,好比在安卓上,接口是SystemClock.elapsedRealtime()。該數值不會受到玩家修改本地時間而影響,因此是一個更值得信賴的數值。但該接口的問題是設備重啓後,這個數值會從新從零開始計算。
藉助這個設備啓動流逝時間的機制,可設計一個聯網時徹底可靠的時間獲取邏輯,不受玩家調整本地時間的影響。方案以下:
1 遊戲啓動後開啓協程獲取網絡時間,若沒網絡或沒獲取到就隔一段時間再觸發,直到獲取成功。
2 獲取到網絡時間時,記錄獲取到的網絡時間爲N1,記錄此刻設備重啓後流逝的時間D1。
3 之後任意時刻要獲取當前的時間,就先獲取此時設備重啓後流逝的時間D2,計算當前時間爲:
Tn = N1 + (D2 - D1)
N1,D1,D2都是徹底可信賴的,因此任意時刻的Tn也是準確的。
因爲訪問原生層接口可能會有必定性能消耗,若是時間獲取調用頻率很高,就能夠優化爲每幀只訪問一次原生層接口,緩存該值,該幀的後續操做都訪問緩存的值,直到下一幀再調用原生層接口。
沒聯網的時候,就使用系統本地時間。再次聯網時,對時間作校訂,以及做弊斷定。要斷定玩家是否修改了系統本地時間來做弊,有以下方式:
1 正常狀況下,玩家的本地時間和聯網時間可能有必定差值。但只要玩家不調本地時間,該差值應幾乎在某一固定值附近波動。若是檢測到該差值有很大變化,就能夠斷定爲做弊。
2 正常狀況下,玩家的本地時間會一直往前走。若是檢測到本地時間有後退的狀況,就能夠斷定爲做弊。
斷定爲做弊後,如何懲罰玩家,就取決於業務需求了。
有一種時間外掛叫加速齒輪,能夠加速本地時間的流逝。這個也能夠經過聯網時本地時間和聯網時間的差值來斷定,若是該差值呈現一個穩定線性遞增的模式,就能夠斷定爲使用了時間加速功能。
參考