本文主要介紹如何對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中的資源路徑
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代碼如下:
代碼中的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! 效果圖:
|
圖片1.png (78.43 KB, 下載次數: 10)