知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

網易易盾發表於2020-12-07

近幾年,移動端遊戲發展迅速。在碎片化的時間爭奪戰中,手遊因其好玩、易玩的優勢,收穫了大量玩家。

在此之上,部分硬核玩家為了追求更好的遊戲體驗,會在PC上使用模擬器來操作手遊,雖然相比於原生的PC端遊戲,模擬器的流暢度還是差強人意。

為了回應這一類玩家,許多遊戲廠商開始嘗試將手遊和PC繫結,實現兩者的互聯互通。如《陰陽師》、《第五人格》、《荒野行動》等手遊都支援在PC上執行,無論是遊戲的操作體驗還是沉浸感均高於手機與模擬器。

不過,外掛也以此為轉折點,不斷繁衍開來。手遊一旦上到PC端,許多弊端就暴露出來,例如應用許可權混亂、取證場景複雜等,這讓遊戲廠商束手無策,也給外掛黨可乘之機。

一般而言,反外掛由兩個部分組成,即靜態程式碼保護與動態執行時對抗。本文將聚焦靜態程式碼保護,從實際的保護效果出發,先闡述通用的PE程式碼保護在易盾端遊反外掛程式碼保護中的應用,後介紹面向遊戲引擎的程式碼保護策略,並在此基礎上,探索一種通用的遊戲邏輯程式碼保護方案。

1.通用程式碼保護

1.1 PE程式碼保護介紹

對於PC端來說,原生程式碼保護指的就是對x86架構的二進位制檔案做的保護,即保護Windows上的PE檔案。

PE程式碼保護已逾20多年的歷史,從21世紀初期,PE程式碼保護已經有了雛形,且誕生了許多直至今日仍具有不小影響力的加固思路與加密演算法,如大名鼎鼎的“UPX”殼,便是利用其最核心的UCL這一效率較高的壓縮演算法來實現的。

根據程式碼保護的實現效果,PE加固用到的技術可以分為以下幾類:

整體加密,最常見的即壓縮/加密殼及其附屬功能,附屬功能包括IAT加密、反除錯、完整性校驗等

混淆,包括花指令、指令變形、程式碼亂序、字串加密等

虛擬機器保護

1)整體加密

整體加密用到了SMC(self-modifying code)的思想,加密原有程式碼,並透過patch PE檔案的entrypoint,再插入殼程式碼,利用殼程式碼新增解壓縮/解密功能的程式碼,即被加密的原有程式碼將在執行時還原;同樣地,上面提到的附屬功能也會被新增在殼程式碼中。

整體加密的效果,更多是體現在對抗靜態分析上,而大部分這類加密殼,在執行時就會被直接釋放到記憶體中,因此對於效能開銷來說,整體加密更多影響的是程式的啟動時間,對執行時效能開銷的影響極小;轉到攻擊者角度,在分析時,無需關注加密殼的加密演算法,用正確的姿勢Dump記憶體,再使用一些技巧修復附屬功能即可,比如恢復可能被加密的匯入表與資料段。

針對這個問題,易盾獨創了一種指令抽取的方法,可以針對需要被保護的關鍵函式,將其核心彙編指令抽取至外部,在執行時,該部分關鍵函式將在外部被執行,攻擊者無法在記憶體中找到連續且完整的程式碼段,大大提高其分析成本。

指令抽取效果如下:

抽取前,可以完整看到函式CFG,可以使用Decompiler看到虛擬碼

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

抽取後,由於核心指令已不在該二進位制檔案中,函式CFG已被破壞,不再能用Decompiler看到虛擬碼,如下圖(圖中的函式符號是加固後手動修改的):

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

2)混淆

與整體加密不同,混淆對程式碼段、資料段都做了直接的變換,因此對靜態分析或者是動態除錯來說,都能起到一定的防禦作用。

根據混淆方式不同,這裡可以分為無原始碼混淆與有原始碼混淆兩種。

無原始碼混淆:易盾在上一小節中提到的指令抽取的基礎上,對抽取的指令透過私有的花指令引擎處理,使在不影響效能的前提下,對原指令進行變形與膨脹,並插入與條件分支相關的花指令,即使攻擊者繞過反除錯,剔除了層層加密,也難以去掉耦合度較高的花指令,進而無法恢復原指令。

加花前:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

加花後:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

有原始碼混淆:易盾使用自研的安全編譯器作為解決方案,支援控制流混淆、指令變形、字串混淆、防反編譯等功能。同樣處理“無原始碼混淆”中被保護的函式,開啟控制流混淆與指令變形後,效果如下:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

3)虛擬機器保護

其中強度最高的自然是虛擬機器保護,因其引入了一種私有的CPU指令集,將真實的原生彙編指令轉化為私有的指令執行在虛擬出來的執行時之上;但與強度相對的,由於虛擬執行時本身也依賴真實的執行時環境,會帶來極大的效能開銷,而遊戲與一般的商用軟體不容,需要進行嚴格的最佳化,且遊戲邏輯相關的程式碼往往會被頻繁呼叫,在這種情景下,虛擬機器保護並不會是一個好的選擇。

1.2 遊戲邏輯外掛原理

接下來我們換一個角度,從攻擊者的視角出發,要實現一個遊戲外掛(特指修改遊戲資料,改變遊戲設計邏輯的行為,暫不討論模擬點選型別的外掛),要關注的點一般有這兩個:

與遊戲邏輯相關的關鍵數值,如攻擊力、血量、倒數計時等

關鍵函式,如傷害判定、戰鬥結算、技能釋放等

對於關鍵數值,攻擊者往往會選擇使用一些記憶體修改工具,定位到目標地址進行修改。

最常見的工具有Cheat Engine,該工具可以實現掃描目標程式記憶體,並新增監視,最終使得攻擊者找到關鍵數值的地址實現篡改。

一個簡單的例子:使用CE修改某單機遊戲中的金幣與鑽石,可透過反覆出售道具並回購,定位金幣與鑽石在記憶體中的地址並修改,如下圖:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

那麼傳統程式碼保護可以防禦這一類外掛嗎?無論是整體加密、混淆甚至是強度看起來最高的虛擬機器,似乎都不能滿足。

對於關鍵函式的定位,這裡又分兩種情況:

第一種情況是遊戲邏輯相關的程式碼就是原生的機器碼,如Unity3D IL2CPP及UE4。攻擊者往往會選擇靜態分析結合動態除錯的形式來分析遊戲邏輯,此時傳統程式碼保護可以起到比較好的防禦效果,大大提高攻擊者分析遊戲邏輯的時間成本,而且對於大部分攻擊者而言,這個時間成本是不可估量的。

第二種情況是部分遊戲邏輯相關的程式碼是透過遊戲引擎來載入的,如Unity3D Mono。這類遊戲的引擎往往包含一個執行時直譯器,來解釋執行存放於外部的遊戲邏輯,此時遊戲邏輯所在檔案自然就不符合傳統的PE程式碼保護方案了。

由上面兩節的介紹不難看出,傳統的程式碼保護方案在對抗遊戲邏輯篡改類的外掛時,只適用於部分場景。

而對於符合要求的這類遊戲來說,需要考慮的也不僅僅是程式碼保護的強度,還有遊戲的流暢度、穩定性。大量的混淆、虛擬化,勢必會對這兩點造成影響。

2.遊戲引擎保護

由於傳統程式碼保護方案並不能覆蓋到所有遊戲程式碼保護的場景,因此我們需要使用一些特定的保護方案,在兼顧效能的同時,起到保護遊戲邏輯難以被分析的目的。這裡以Unity3D引擎為例,介紹易盾端遊程式碼保護對於這一遊戲引擎的保護方案。

2.1 Unity3D端遊的程式碼保護

1)Mono DLL整體加密

未被保護的Mono DLL符合常規的PE檔案格式,以“MZ”開頭,可以被一些工具直接解析。

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

且可以透過反編譯工具直接看到邏輯:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

針對這個問題,易盾結合傳統PE程式碼保護中SMC的思想,設計了一種整體加密的保護方案。加密後的DLL不再符合PE檔案格式:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

且使用反編譯工具無法開啟:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

2)Mono DLL方法級加密

與上文傳統程式碼保護中的指令抽取類似,將Mono方法中關鍵的IL指令抽離到外部,在執行前才解密,使得攻擊者無法在記憶體中找到包含完整邏輯的DLL。下面是加密效果。

方法加密前,程式碼邏輯清晰可見:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

方法加密後,指令已被抽離,邏輯不再可見:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

3)Mono DLL格式私有化

用一種私有化的檔案格式儲存加固前DLL中的關鍵加密資訊,即使是遊戲執行過程中,DLL也不會恢復。

4)IL2CPP global-metadata 加密

大部分場景中,IL2CPP遊戲程式碼分析的第一步,就是用一個開源工具IL2CppDumper來解析遊戲程式碼的符號資訊。而這些符號資訊的來源就是global-metadata.dat這個檔案。

保護原理即透過一種自定義的加密演算法來保護落地的global-metadata.dat檔案,使得符號解析失效。

加密前,工具執行正常:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

加密後,工具解析失敗:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

5)IL2CPP 指令抽取

IL2CPP的遊戲邏輯在GameAssembly.dll中,透過加固,將遊戲核心程式碼抽離至外部,配合私有的亂序變形引擎,攻擊者將無法在記憶體中找到完整的GameAssembly.dll。

2.2一種通用的遊戲引擎保護方案

正如上一節中提到的,要保護某一種遊戲引擎的程式碼邏輯,勢必要對整個引擎有一定的瞭解,再針對其中的程式碼邏輯的執行時特性,結合加密、混淆等技術實現一種或多種特化的保護方案。

對遊戲開發者來說,在接入遊戲程式碼保護方案後,若需要升級甚至更換引擎,相應的成本勢必會增加;

對安全開發團隊來說,遊戲引擎的更新,也需要耗費大量成本去升級保護方案。那麼有沒有一種相對通用且效能較好安全性較高的保護方案呢?也即需要關注三點:

通用性,與遊戲引擎種類相對應,遊戲開發的語言也是五花八門,要做到針對不同語言或者編譯產物都能實現保護功能。

效能,保護後的遊戲程式碼在執行時的執行效率應當與保護前接近。

安全性,需要支援各類混淆,支援虛擬機器保護。

先看安全性,眾所周知,設計虛擬機器保護方案的直譯器時,有三個角度,分別是針對原始碼、中間語言和彙編指令。其中,中間語言和彙編指令的直譯器是被應用最廣的,而原始碼級虛擬機器由於要考慮到編譯環境,且安全性較另外兩者稍低,一般只在指令碼語言(如JavaScript、Lua和Python等)中被使用。

再看通用性,因為部分指令碼語言並不會編譯成原生的彙編指令,所以這種通用保護方案在設計上,就無法採用針對彙編指令進行加固的形式;而對於中間語言來說,雖然兼顧了通用性,但要在現有的中間語言基礎上為多種遊戲開發語言適配編譯器前端,或重新設計一種支援多語言的中間語言,工作量無疑是巨大的。最後還剩下一個選擇,這裡採用定製AST引擎來解析原始碼。

最後回到效能上,對於遊戲程式碼的保護來說,大部分情況下不會大範圍地開啟程式碼虛擬化,更多時候是選擇混淆加上少量核心程式碼虛擬化的形式,此時只需一個輕量的虛擬機器,穿插一些混淆技術,就可以起到很好的保護效果,也由於虛擬機器設計上的輕量化,效能和穩定性都可以在掌控之中。如下圖:

知物由學 | 端遊程式碼保護:從原生程式碼到遊戲引擎

3.端遊程式碼保護總結

程式碼保護是端遊反外掛戰鬥中處於最前線的第一道牆,程式碼保護方案的優秀與否,與實際的遊戲體驗與安全性緊密相關。

不過,這些尚不能覆蓋到反外掛的所有方面。可能細心的讀者也發現了,上文中在介紹關鍵數值型別的外掛時,還留有一個坑,本文針對程式碼的保護方案似乎都不能很好地覆蓋到這一型別的外掛。那麼如何解決呢?敬請關注後續內容,帶你走進執行時反外掛的前世今生。


相關文章