本文主要介紹如何對unity3d引擎制作的游戲進行修改。包含了apk文件安裝后在手機中的位置分析、修改游戲時遇見內聯函數之坑時的解決辦法,以及so文件的原理介紹與解析修改。
并將實例教學如何修改unity3d游戲(想學崩壞3修改的同學請注意啦)。
教程是給入門新手看的,請大神繞道勿噴。文章的核心內容在最后利用Il2CppDumper的部分,前面清楚的話可直接繞到最后看。(因為手機截圖下來的圖片分辨率太大。看起來排版會不太舒服,
可以直接到文章最后下載文檔查看,排版會舒服很多)
基礎知識
0x1.apk安裝后在手機中的目錄
apk安裝后會在兩個包下生成相關包:data/data/、data/app/。
這里拿網易云音樂的安裝目錄舉例。Data/App目錄下通常會有三個文件:
1.lib文件夾(包含so庫文件)、
2.oat文件夾(OAT文件是一種android私有ELF文件格式,它不僅包含有從DEX文件翻譯而來的本 地機器指令,還包含有原來的DEX文件內容。
這使得我們無需重新編譯原有的APK就可以讓它正常地在ART 里面運行)、
3.base.apk啟動包。【其中apk啟動包是不允許重命名或刪除的,因為app運行時其實就是鏈接到這個啟動包,然后才能繼續啟動操作。這個啟動包用beyond對比后可以發現,與原安裝包沒有任何不同,
所以就相當與apk的原版安裝包】。
Data/data目錄下一般是存儲lib文件夾(保護so庫文件)以及其他數據文件、緩存等。只需要知道這里的lib實際上與data/app目錄下的lib目錄中內容是一樣的。
游戲在運行的時候,一般都會載入dada/data目錄中的lib與data/app中的lib,通常來說只需要修改data/data中的lib文件夾中的so文件即可達到成功修改的效果。當然也有一小部分游戲根本不讀取data/data目錄
下的lib文件夾,待會會講到。
0x2.Unity3D中的資源路徑
Application.dataPath |
此屬性用于返回程序的數據文件所在文件夾的路徑。例如在Editor中就是Assets了。 |
Application.streamingAssetsPath |
此屬性用于返回流數據的緩存目錄,返回路徑為相對路徑,適合設置一些外部數據文件的路徑。 |
Application.persistentDataPath |
此屬性用于返回一個持久化數據存儲目錄的路徑,可以在此路徑下存儲一些持久化的數據文件。 |
Application.temporaryCachePath |
此屬性用于返回一個臨時數據的緩存目錄。 |
android平臺
Application.dataPath |
/data/app/xxx.xxx.xxx.apk |
Application.streamingAssetsPath |
jar:file:///data/app/xxx.xxx.xxx.apk/!/assets |
Application.persistentDataPath |
/data/data/xxx.xxx.xxx/files |
Application.temporaryCachePath |
/data/data/xxx.xxx.xxx/cache |
IOS平臺
Application.dataPath |
Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxx.app/Data |
Application.streamingAssetsPath |
Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxx.app/Data/Raw |
Application.persistentDataPath |
Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Documents |
Application.temporaryCachePath |
Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Library/Caches |
0x3.C#的inline內聯函數優化
雖然C#不支持inline,但是JIT支持自動inline,即將IL轉成真正機器碼時,會自動將某些函數進行inline展開,只是條件非常苛刻,網上提到JIT自動進行inline展開的一些選擇依據:
1)函數內部有循環語句、catch語句等復雜結構,都不做inline優化。
2)函數體比較長的不做inline優化,只有比較簡單的才可能inline優化。(有人說IL不足32字節才做inline),
2)編譯成機器碼時,inline展開的代碼比函數調用更短的,一定做inline。(注:如果參數多而代碼少,就符合此情況)
這里為什么要講內聯函數呢,加入如果游戲中有一個讀取人物攻擊力的函數,其內部代碼十分簡單,結果被編譯為機器碼的時候變為了內聯函數。那么這個時候要來修改就十分麻煩了,
因為你找到那個讀取人物攻擊力的函數是沒有用的,修改了也是白修改,只能到每一處調用這個函數的地方逐行修改。
好了,說了這么多,下面從開始unity3d游戲開發的的角度逐漸逆向分析。
一、通過unity3d打包生成libil2cpp.so:
1.如何識別u3d游戲?打開解壓包,如果lib文件夾下有libunity.so就證明這是一個unity3d游戲。
2.要修改Unity3d游戲,首先就要對其游戲代碼存放位置有一個基本的了解。Unity3d生成游戲的游戲主邏輯一般放在三個地方:libil2cpp.so、Assembly-CSharp.dll、lua腳本。
【對于libil2cpp.so來說:我們知道,unity3d最大的一個特點是一次制作,多平臺部署,而這一核心功能是靠Mono實現的。但是在2014年年中的時候,Unity3D引出了IL2CPP的概念,IL2CPP,
英文意思即Intermediate Language to cpp,就是把IL中間語言轉換成CPP文件。】
上面所說的這三個地方通常來說是唯一的,即只會出現一種情況。這是由unity3d引擎的生成方式決定的。下面通過開發者的角度對unity3d生成游戲進行實例講解:
新建unity3d工程,工程命名為HelloCPP!:
利用ugui創建兩個text,一個為“CoinUI”顯示“金幣”,一個為“Coin”顯示金幣值,并創建腳本GameManager,綁定在MainCamera中。
腳本GameManager代碼如下:
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine.UI;
- using UnityEngine;
- public class GameManager : MonoBehaviour {
- private GameObject coin;
- void Start () {
- coin = GameObject.Find("Coin");
- }
- private int GetCoin()
- {
- return 50;
-
- }
- public void ChangeCoin()
- {
- float v = GetCoin();
- coin.GetComponent<Text>().text = v.ToString();
- }
- }
- 方法與按鈕事件綁定。那么當點擊按鈕的時候就會更新一次</font>ui。</font></font></font>
復制代碼
代碼中的GetCoin方法放回一個50的數值,當游戲運行起來的時候,腳本會將ui界面中Coin的值改為50,如下:
好了,游戲邏輯已經寫完了,保存場景,直接打包,點擊主菜單file>BuildSetting進入打包界面,選擇轉化為android平臺,并點擊playersetting進入配置界面:
這里我把PackageName設置為com.hellocpp。
然后頁面下拉,找到scriptingbackend:
這里的scriptingBackend就是設置生成游戲的游戲邏輯存放方式,如果選擇默認的Mono2x的話,會在反編譯后的apk的assets\bin\Data\Managed目錄下找到Assembly-CSharp.dll文件,
也就是大多數unity游戲邏輯存放的位置,這種情況下,lib文件夾下是沒有libil2cpp.so文件的。
如果是選擇IL2CPP的話,會在lib文件夾下生成libil2cpp.so文件,并在assets\bin\Data\Managed\Metadata目錄下生成global-metadata.dat配置文件。
對于生成Assembly-CSharp.dll文件的情況來說,用reflector很容易修改,這里略過,直接講解生成libil2cpp.so文件的情況。把生成的apk直接拖入ide中反編譯,進入根目錄后,進入lib文件夾中觀察。
二、對生成的apk進行反編譯分析
直接把apk拖入ide,然后進入lib文件夾查看
生成了兩個文件夾,一個是armeabi-v7a,即arm架構,一個為x86,是因特爾架構。我們這里進入arm文件夾中分析。
【有時會有人問,為什么so修改后模擬器運行閃退,無法正常運行?這種情況多半是因為你只修改了arm文件夾下的so,所以只能在大部分真機中運行,因為真機多半是arm架構的,
而模擬器是因特爾架構的,所以在模擬器上運行會奔潰。】
可以看到,里面一共三個文件,其中libunity與libmain是unity的內部文件,我們不需要去管它,現在只需要知道這里確實生成了libil2cpp.so即可。
好了,現在我們要分析修改這個apk,手機中運行起來我們發現其顯示金幣為50,我們現在來修改其數值。
按照國際慣例,先在ide中搜索字符串“金幣”,發現沒有結果,于是判斷游戲邏輯在so中,我們再搜索loadlibrary,然后發現了里面唯一用到的原生方法是在libmain中,然后估計就有人去分析libmain.so文件了,
但libmain.so里面其實是沒有游戲核心邏輯的,
這只是unity內部的一些庫,真正的游戲核心邏輯是在libil2cpp.so中,這個庫文件實在載入libmain后才被調用的。
所以,碰到unity游戲,一定要先看看lib文件夾下是否有libil2cpp.so,如果有的話,直接分析這個so就行了,從smali分析存粹是浪費體力。
打開IDA,載入so,搜索coin,會發現依然找不到相關函數,推測在jni中動態加載,然而搜索jni也是找不到任何函數。在view-A面板中尋找,發現大多數函數只有一個函數尾,而函數頭似乎被可以“掐”掉了。
三、對Il2CppDumper.exe工具的介紹
出現上述情況的原因與unity引擎中的MetadataCache.cpp相關,打開u3d目錄,可找到MetadataCache.cpp:
意思就是在生成libil2cpp.so時,u3d同時會在目錄assets\bin\Data\Managed\Metadata下生產資源文件global-metadata.dat。游戲中使用的字符串都被保存在了一個叫global-metadata.dat的資源文件里,
只有在動態運行時才會將這些字符串讀入內存。這使得用IDA對游戲進行靜態分析變得更加困難。那么為了解決這個困難,有人造了輪子,即Il2CppDumper.exe。此可讀取global-metadata.dat文件中的信息,并與libil2cpp.so結合起來。
相關源碼可看國外大神的分析:還原使用IL2CPP編譯的unity游戲的symbol(一)
【https://www.nevermoe.com/?p=572】以及(github:https://github.com/nevermoe/unity_metadata_loader)與github:https://github.com/Perfare/Il2CppDumper)
好了,如果你覺得這個看起來過于麻煩的話,可以直接略過,只要學會使用其工具化下來的exe就行了。
這里為了方便下載直接使用,我已經把exe文件生成出來了,會直接打包到百度云。
這個exe文件主要是通過對global-metadata.dat與so文件的結合自動生成相關函數與其對應在ida中的偏移地址。(相關原理其實就是分析global-metadata.dat,這里是自動幫我們省去了這個步驟)。
使用方法:
打開Il2CppDumper.exe,會彈出一個窗口,第一個選擇lib2cpp.so,第二個選擇global-metadata.dat,然后按下鍵盤鍵2,就會自動完成后續的操作了。
生成的文件就是這個dump.cs,我們點進去后直接搜索coin,定位到這里:
下面的數字就是偏移量,復制511f50后進入ida,按g鍵進入到相關地址
發現代碼沒有展開的話,按一下c鍵就可以了。
可以看到,他這里是返回了50。那么,這個時候我們就興奮了,這里就是我們要修改的地方!講道理把這里的0x32修改為0xFF00后,我們在游戲中點擊按鈕,顯現的值就應該變為65280了:
用hex二進制修改器修改后,命名為libil2cpp改.so。
接下來可以直接把so替換掉原so然后打包回編譯,但這種辦法遇到apk有簽名驗證或其他亂七八糟的檢驗時不好操作。這里我們使用另一種部分,即先安裝apk到手機,
然后進去根目錄下去手動把so給替換掉(手機需root)。
把apk與修改后的so一起扔進手機:
安裝apk后,先打開來看看,點擊按鈕后,金幣為50
好了,接下來就是替換so了。在前面的基礎知識中我們講到,apk安裝后,會在data/data與data/app下分別生成自己的包文件。并且兩個文件夾下都有lib,里面封裝了一樣的so庫文件。
那么我們是去替換哪一個呢?答案:兩個都試試。
因為有些app只讀取data/app/com.hellocpp目錄下的lib文件夾信息,不讀取data下的文件夾信息,比如這個apk。你會發現你直接把data/data下的com.hellocpp包給刪掉也是完全可以運行的,但是如果你刪了app目錄下的com.hellocpp/lib,立刻無法運行。
我們把原so重命名為libil2cpp.so原,然后把改后的so命名為libil2cpp.so
大功告成,我們重新打開游戲,然后會發現。
沒有任何變化(心涼)
正常情況這樣修改后就應該會成功了的,但是這里為什么依然沒有任何變化呢。
這里又涉及到前面說的基礎知識,當這種情況發生的時候,很可能就是函數內聯了。
你修改函數本體是沒有任何效果的,因為這個函數被調用它的函數內置了。你必須找到所有調用這個函數的地方,去找到相關點修改。這個就需要去看匯編代碼了。
我們也可以動態調試的時候在getcoin()方法處下一個斷點,然后ida動態調試,會發現按鈕按下時確實沒有斷下來(限于篇幅請讀者自行嘗試)。或者我們直接把那個函數本體給nop掉,會發現程序依舊正常運行,這都說明了函數確實內聯了。
內聯了的函數很難分析,我遇到了就只能跑路,這里只是點出其位置,再深入的分析就要去好好讀代碼了,不多分析(如果有大神會的話麻煩評論區指點指點)
這里我直接找到這個地方,改為mov r0,0
再次替換后運行結果確實變為0了:
實例二
好了,分析完上面這個核心處存在內聯函數的apk,我們下面來一個最常見的apk修改實例。
仍然是上面這個apk的功能,但不同的是為了防止其編譯的時候又被當成內聯函數編譯了,我在方法GetCoin()內增加了一個循環和幾個debug,確保其不被當作內聯。其他功能不變。依舊是在GetCoin()中返回50,然后在ChangeCoin()修改ui界面的數值。
代碼如下:
然后同樣步驟打包生成apk,但把包名改為了com.HellobanInline
生成apk后直接扔進ide中反編譯,然后把global-metadata.dat與libil2cpp.so拿出來,用Il2CppDumper.exe把函數名生成出來:
打開dump.cs,搜索GetCoin()
函數位置在偏移511d48上。
因為方法是返回一個int值的數值,我們直接讓其返回0xff00,也就是65280.
用hex二進制文件修改后把文件命名【libil2cpp改.so】。與apk一起扔到手機中。
Apk安裝完成后,進入data/app中的包com.HellobanInline-1的lib/arm中,把【libil2cpp改.so】復制進來,重命名如下:
【這里對為什么包名后面有一個-1做一下解釋:這是因為復制版本覆蓋。一般來說第一次安裝的話包名后綴-1,第二次覆蓋安裝就會多一個相同包名后綴為-2,再次覆蓋安裝又會變為-1……】
好了,大功告成,這個時候充滿期待的打開apk吧。點擊按鈕后數值已經由50變為了65280!
效果圖:
市面上絕大部分游戲都是直接生成c#方法名后到ida中直接修改就生效了,像實例一的比較少見,但有助與深入理解。
一般生成方法名后,就看修改經驗或游戲開發經驗了,比較火的u3d游戲有很多,比如崩壞3的修改,你可以搜索方法名“GetBaseAttack”,修改為一個超大值,那么你人物就一擊十幾億,防御十幾億,
生命十幾億了。對于崩二的話,其加了愛加密的殼,
并且似乎有檢驗so是否被篡改,若有高人能跳過檢測希望能告訴我一下,十分感謝!然后再其他的游戲修改也都是這套路,多多熟悉就會了。
大概就這些內容吧,自己羅里吧嗦的,感覺篇幅有點累贅了。
[size=10.5000pt]
百度云鏈接:http://pan.baidu.com/s/1i5Hur2H 密碼:viqv 解壓密碼www.wuaipojie.com
world文檔: http://pan.baidu.com/s/1i5EC5pj 密碼:ja84
|