對於任何一款要長期線上運營的游戲,防破解防外掛是必不可少的。本文總結了手游常用的防破解防外掛技術方案,這些方案都經過了筆者所在團隊和線上項目的長期考驗。很多方案來自於弱聯網手游項目,但大部分思路也同樣適用於強聯網游戲。以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:
apktool d -r abc.apk
得到同名文件夾。注意用命令行解包,若把apk的后綴改為zip解壓縮,得到的文件夾中會缺少apktool.yml文件,到后面重新打包時會報錯:
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文件。
(2)運行命令:
apktool b abc
得到abc.apk文件,位於目錄abc/dist/。
(3)運行命令簽名打包:
jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore mykey.keystore -signedjar abc_signed.apk abc/dist/abc.apk mykey
得到新包abc_signed.apk。
網上有些教程里會加上-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 正常情況下,玩家的本地時間會一直往前走。如果檢測到本地時間有后退的情況,就可以判定為作弊。
判定為作弊后,如何懲罰玩家,就取決於業務需求了。
有一種時間外掛叫加速齒輪,可以加速本地時間的流逝。這個也可以通過聯網時本地時間和聯網時間的差值來判定,如果該差值呈現一個穩定線性遞增的模式,就可以判定為使用了時間加速功能。
下一篇:
手游防破解防外掛技術方案(二)服務端篇
參考