轉(zhuǎn)自長(zhǎng)亭知乎專欄漠另,實(shí)習(xí)時(shí)小姐姐的約稿格嗅,已經(jīng)不在那邊了所以版權(quán)不歸我哈
筆者一直自認(rèn)玩過(guò)不少游戲署照,無(wú)奈水平太菜,日常送人頭吗浩。痛定思痛建芙,決定沖(xie)冠(xiu)一(gai)怒(qi),經(jīng)過(guò)幾次失敗的嘗試之后懂扼,終于搞定了幾款時(shí)下熱門的Unity游戲禁荸。出于各種原因,本文以一款不具名的國(guó)外游戲作為實(shí)例阀湿,分享筆者研究過(guò)程中的一些心得赶熟,與各位分享。
0X00 打包黨的鶸改法
首先采用最簡(jiǎn)單的打包黨策略陷嘴,演示一下如何快速修改一款單機(jī)游戲的金幣/寶石等資源映砖。這部分快速帶過(guò),主要負(fù)責(zé)熟悉 Unity 游戲結(jié)構(gòu)灾挨,時(shí)至今日已經(jīng)不算一種技術(shù)了邑退。
首先將游戲安裝 APK 解包竹宋,這里使用 apktool 或者直接看作 zip 解包是沒(méi)有區(qū)別的,因?yàn)橛螒驀?yán)重依賴框架地技,Java 層和 Manifest 等文件價(jià)值不大蜈七。作為 Unity 游戲的一個(gè)特征點(diǎn),可以很明顯的發(fā)現(xiàn)這樣一個(gè)文件夾莫矗。
Assembly-CSharp.dll 等幾個(gè)文件是 Unity 游戲最鮮明的特點(diǎn)飒硅。通過(guò) file 或者 binwalk 查看可以發(fā)現(xiàn)它們是 C# 字節(jié)碼格式( IL ),這種格式如果不進(jìn)行加密作谚,可以輕松的還原 C#指令三娩。
接下來(lái)在手機(jī)中安裝一次游戲,看一下大致的游戲邏輯妹懒,確定需要修改什么雀监。
看起來(lái)右上角的寶石不錯(cuò)。彬伦。。
一般來(lái)說(shuō)在游戲里邊寶石都是稀缺資源伊诵,這里以其作為目標(biāo)单绑。為了快速定位,嘗試使用 diamond 曹宴,gem 等字符串在 cs源碼中進(jìn)行全局搜索搂橙,很快就能定位到關(guān)鍵位置。
private void SetupFirebaseDefault()
{
this.Defaults.Add("EnergyStart", 20);
this.Defaults.Add("InitialEnergy", 75);
this.Defaults.Add("EnergyFillerSecs", 420);
this.Defaults.Add("InitialGold", 200);
this.Defaults.Add("InitialGems", 50);
this.Defaults.Add("XpRequirement", 75);
this.Defaults.Add("XpReqIncremental", 75);
this.Defaults.Add("XpAttackBonus", 5);
this.Defaults.Add("BubblesRequired", 6);
this.Defaults.Add("StoreBubbleCostsAddOn", 2);
this.Defaults.Add("ReviveCost", 100);
this.Defaults.Add("ShopTokenCost", 100);
this.Defaults.Add("ShopTokenCostIncremental", 75);
this.Defaults.Add("ShopTokenMax", 10);
this.Defaults.Add("ShopFigMax", 3);
this.Defaults.Add("ShopKeyMax", 5);
this.Defaults.Add("EnergyPricesInGemsBig", 200);
this.Defaults.Add("EnergyPricesInGemsSmall", 50);
this.Defaults.Add("EnergyPackageBig", 60);
this.Defaults.Add("EnergyPackageSmallMin", 5);
this.Defaults.Add("RepeatedFigurineTokenConversion", 7);
this.Defaults.Add("RefreshLootCost", 20);
...
}
InitialGems 字段明顯是初始寶石數(shù)的意思笛坦,OK就它了区转。C# 字節(jié)碼的修改有很多種方式,比較方便的工具是 Reflector 版扩,這里因?yàn)闆](méi)有這個(gè)工具废离,使用 ILDASM 反編譯,隨后修改礁芦, ILASM 重編譯回去的方法蜻韭。這兩個(gè)工具都是微軟官方提供的,當(dāng)然可以百度搜到柿扣。 ILDASM 具有圖形化界面肖方,直接從其中 dump 出來(lái)即可,隨后修改 dump 出的 IL 文件如下:
接下來(lái)使用 ILASM 命令 ilasm.exe name.il /DLL 可以將 IL 文件回編譯成 DLL 未状,將其替換 APK 包中的對(duì)應(yīng) DLL 俯画,簽名,安裝之后可以發(fā)現(xiàn)修改生效了司草。
這就是 Unity 游戲打包黨快速修改的過(guò)程艰垂,看起來(lái)很簡(jiǎn)單泡仗,但是卻存在不少問(wèn)題:
- 這個(gè)游戲沒(méi)有做任何保護(hù),一旦存在保護(hù)材泄,比如IL字節(jié)碼加密沮焕,就需要去跟IL字節(jié)碼加載的邏輯,伺機(jī)恢復(fù)明文IL字節(jié)碼拉宗。
- 加殼問(wèn)題峦树,好在加殼是針對(duì)Java層的,考查了國(guó)內(nèi)幾款主流游戲之后發(fā)現(xiàn)基本沒(méi)有加殼旦事,因?yàn)闅げ⒉荒鼙WCELF文件的安全魁巩,ELF文件很可能使用其他安全策略。
- 重打包問(wèn)題姐浮,國(guó)內(nèi)游戲是不可能讓你修改數(shù)據(jù)重打包的谷遂,特別是聯(lián)網(wǎng)游戲,會(huì)有多處完整性校驗(yàn)卖鲤,因此修改工作必須在運(yùn)行過(guò)程中進(jìn)行肾扰。
綜合以上,我們雖然完成了對(duì)一款毫無(wú)安全保護(hù)的 Unity 游戲的修改蛋逾,但是為了進(jìn)一步研究適用于更復(fù)雜條件下的修改策略集晚,還需要進(jìn)一步研究心得方案。
0X01 注入與hook
考慮到國(guó)內(nèi)主流游戲的安全機(jī)制区匣,必須使用運(yùn)行時(shí)修改的方式偷拔。比較理想的方式是先注入 zygote 進(jìn)程。zygote 進(jìn)程是 Dalvik 虛擬機(jī)的孵化器進(jìn)程亏钩。眾所周知莲绰,常規(guī)的 Android APP 是運(yùn)行在 Dalvik 虛擬機(jī)(或者其繼承者 ART )中的,虛擬機(jī)需要加載很多運(yùn)行所需的庫(kù)(如 libdvm.so)姑丑,并且初始化虛擬機(jī)對(duì)象蛤签。這個(gè)過(guò)程費(fèi)時(shí)費(fèi)力,為了保證應(yīng)用的啟動(dòng)速度栅哀,zygote被設(shè)計(jì)為虛擬機(jī)進(jìn)程的父進(jìn)程顷啼。當(dāng)應(yīng)用啟動(dòng)時(shí),直接從 zygote 上 fork() 出來(lái)昌屉,繼承其虛擬內(nèi)存空間钙蒙。因此,注入到 zygote 進(jìn)程的好處是先于應(yīng)用代碼執(zhí)行间驮,可以有效避免注入過(guò)程被應(yīng)用的 anti-ptrace 機(jī)制檢測(cè)到躬厌。
Android 平臺(tái)上的注入已經(jīng)是相對(duì)成熟的一套代碼,最初是由看雪版主古河大大發(fā)布,隨后出現(xiàn)了很多的更新扛施、優(yōu)化版本鸿捧。其基本思路是利用 Linux 平臺(tái)上的跨進(jìn)程控制機(jī)制 ptrace ,通過(guò)對(duì) ptrace 的封裝實(shí)現(xiàn)目標(biāo)進(jìn)程的讀疙渣、寫匙奴,寄存器獲取、保存妄荔、恢復(fù)泼菌,頁(yè)狀態(tài)變更、寫入一段施工程序啦租、遠(yuǎn)程調(diào)用施工程序哗伯,負(fù)責(zé)將待注入模塊加載到目標(biāo)內(nèi)存中。這些內(nèi)容前人之述備矣篷角,這里貼幾個(gè)相關(guān)鏈接焊刹,不在做具體展開(kāi)。
[原創(chuàng)]發(fā)個(gè)Android平臺(tái)上的注入代碼-『Android安全』-看雪安全論壇 libinject
android hook 框架 libinject2 簡(jiǎn)介恳蹲、編譯虐块、運(yùn)行 libinject2
Android進(jìn)程的so注入--Poison(穩(wěn)定注入版) - 水汐。2014 的專欄 - CSDN博客 Poison注入框架
在完成注入之后嘉蕾,我們的功能代碼即可在目標(biāo)進(jìn)程中執(zhí)行贺奠,接下來(lái)需要在目標(biāo)進(jìn)程中執(zhí)行 hook 過(guò)程。通過(guò) hook 技術(shù)荆针,可以截?cái)嘁粋€(gè)函數(shù)的執(zhí)行流程并插入自定義的代碼敞嗡。針對(duì) zygote 注入颁糟,這個(gè)問(wèn)題稍微復(fù)雜航背。因?yàn)樵谖覀冏⑷?zygote 的時(shí)機(jī),游戲進(jìn)程還沒(méi)有啟動(dòng)棱貌,因此無(wú)法直接 hook 到目標(biāo)函數(shù)玖媚。后面將會(huì)介紹到,我們的目標(biāo)函數(shù)是 native 層的 c 函數(shù)婚脱。因?yàn)?zygote 進(jìn)程最后會(huì) fork 成游戲進(jìn)程今魔,為了感知游戲進(jìn)程中目標(biāo)函數(shù)的加載,可以監(jiān)控該函數(shù)所在的庫(kù)的加載障贸,那么就需要用到 linker 中的 dlopen 函數(shù)错森。
void *dlopen(const char *filename, int flags);
通過(guò) hook dlopen 函數(shù)并檢查 filename 參數(shù)確定目標(biāo)庫(kù)的加載,隨后再一次進(jìn)行實(shí)際的功能性 hook 篮洁,hook 目標(biāo)函數(shù)達(dá)到修改目的涩维。也就是說(shuō),通過(guò) zygote hook 的方式 hook 一個(gè)目標(biāo)函數(shù)袁波,需要進(jìn)行兩次 hook 瓦阐,第一次是 hook linker 中的 dlopen 以確定目標(biāo)模塊的基址蜗侈,第二次是在該模塊中 hook 目標(biāo)函數(shù)。這里有一個(gè)小問(wèn)題是由于 dlopen 函數(shù)在每一個(gè) zygote 的子進(jìn)程中都會(huì)被 hook 睡蟋,導(dǎo)致系統(tǒng)性能下降踏幻,一個(gè)解決方案是定期查看** /proc/pid/cmdline** 如果自身不是目標(biāo)進(jìn)程那么就解除 hook 。
針對(duì) Unity 游戲的 hook 思路大體如上戳杀,本節(jié)的最后再講講關(guān)于使用的 hook 框架该面。Hook 操作的原理可以理解為強(qiáng)行修改程序的代碼段,通過(guò)修改目標(biāo)地址上的字節(jié)碼為 B 豺瘤,JMP 等指令將指令流跳轉(zhuǎn)到 hook 者控制的位置執(zhí)行另一段指令吆倦。當(dāng)然實(shí)際實(shí)現(xiàn)中復(fù)雜性遠(yuǎn)遠(yuǎn)大于這句描述,因?yàn)橹噶顖?zhí)行完畢之后通常需要返回到 hook 前的位置坐求,如何保證 hook 點(diǎn)處指令蚕泽、寄存器值等各種信息完好,是需要很大工作量的桥嗤。Java 層的 hook 框架可以使用 XScript 须妻、frida 、cydia substrate 等等泛领,native 層筆者嘗試過(guò)的有效工具有 cydia substrate和android-inline-hook ( ele7enxxh/Android-Inline-Hook )荒吏。ARM 平臺(tái)上的 hook 工具開(kāi)發(fā)有幾個(gè)坑點(diǎn),一個(gè)原因是由于 ARM 有大量位置相關(guān)代碼渊鞋,如果 hook 點(diǎn)在這種指令上绰更,那么想要在異地恢復(fù)這條指令相當(dāng)困難;另一個(gè)原因是 ARM 上存在 Thumb 指令集锡宋,需要考慮判斷當(dāng)前指令集并執(zhí)行不同的操作儡湾。
有了注入和 hook 兩種工具,就可以完成對(duì)目標(biāo)函數(shù)的運(yùn)行時(shí)修改执俩。下一節(jié)探討針對(duì) Unity 游戲徐钠,具體修改哪些函數(shù)可以完成對(duì)游戲邏輯的控制。
0X02 Mono加載C#字節(jié)碼過(guò)程分析
可能很多人都像我一樣好奇過(guò)役首,Android 是一個(gè)類 Java 虛擬機(jī)部署在 Linux 平臺(tái)上尝丐,怎么就跑起來(lái)了微軟的 C# ?其實(shí) C# 已經(jīng)被 ECMA 組織標(biāo)準(zhǔn)化(雖然這組織和微軟淵源頗深)衡奥,并且標(biāo)準(zhǔn)基礎(chǔ)上出現(xiàn)了一套運(yùn)行時(shí)( Common Language Runtime , CLR )爹袁。這套運(yùn)行時(shí)的具體實(shí)現(xiàn)是一個(gè)叫做 mono 的開(kāi)源項(xiàng)目。
本節(jié)介紹 mono 執(zhí)行 C# 字節(jié)碼的過(guò)程矮固。Android 上的 Unity 正是通過(guò) mono 的 Just-in-time Compile 機(jī)制完成了從 C# 語(yǔ)言世界到 ARM 機(jī)器碼世界的轉(zhuǎn)化失息。接下來(lái)對(duì) Mono 項(xiàng)目的源碼中對(duì) DLL 處理的邏輯做一個(gè)分析。
首先,mono 加載 DLL 文件之后根时,會(huì)進(jìn)行預(yù)編譯瘦赫,首先調(diào)用 /mono/mini/mini.c 中的 mono_precompile_assemblies 函數(shù),該函數(shù)對(duì)所有需要加載的 assembly 文件逐個(gè)調(diào)用 mono_precompile_assembly 蛤迎。
void mono_precompile_assemblies ()
{
GHashTable *assemblies = g_hash_table_new (NULL, NULL);
mono_assembly_foreach ((GFunc)mono_precompile_assembly, assemblies);
g_hash_table_destroy (assemblies);
}
static void
mono_precompile_assembly (MonoAssembly *ass, void *user_data)
{
...
for (i = 0; i < mono_image_get_table_rows (image, MONO_TABLE_METHOD); ++i) {
method = mono_get_method (image, MONO_TOKEN_METHOD_DEF | (i + 1), NULL);
mono_compile_method (method);
if (strcmp (method->name, "Finalize") == 0) {
invoke = mono_marshal_get_runtime_invoke (method, FALSE);
mono_compile_method (invoke);
}
...
}
這里摘取了 mono_precompile_assembly 函數(shù)的關(guān)鍵步驟确虱。該函數(shù)中針對(duì)當(dāng)前需要處理的的 assembly ,對(duì)其中每一個(gè)函數(shù)調(diào)用 mono_compile_method 進(jìn)行編譯替裆,同時(shí)編譯 invoke 校辩。這個(gè) invoke 是對(duì)應(yīng)函數(shù)的一個(gè)包裝器,當(dāng) mono最終調(diào)用函數(shù)時(shí)辆童,會(huì)通過(guò)包裝器調(diào)用而不是直接調(diào)用宜咒。因此在函數(shù) compile 完成之后,會(huì)生成并編譯 invoke 函數(shù)把鉴。
接下來(lái)分析的關(guān)鍵是 mono_compile_method 函數(shù)故黑,真正的編譯過(guò)程發(fā)生在這個(gè)函數(shù)中。該函數(shù)不是唯一的庭砍,因?yàn)?mono 同時(shí)支持 AOT( ahead of time )編譯场晶,未來(lái)也可能添加其他功能。因此這個(gè)函數(shù)這里為一個(gè)函數(shù)指針怠缸,在 JIT 編譯環(huán)境下執(zhí)行的是 mono_jit_compile_method 函數(shù)诗轻。
gpointer
mono_jit_compile_method (MonoMethod *method)
{
MonoException *ex = NULL;
gpointer code;
code = mono_jit_compile_method_with_opt (method, mono_get_optimizations_for_method (method, default_opt), &ex);
if (!code) {
g_assert (ex);
mono_raise_exception (ex);
}
return code;
}
這個(gè)函數(shù)調(diào)用了 mono_jit_compile_method_with_opt 函數(shù)做具體操作,注意這里返回的是 gpointer 指針揭北,其實(shí)這個(gè)指針指向的就是 DLL 腳本最終編譯成匯編所在的地址扳炬,后續(xù)如果我們需要修改生成的匯編代碼,修改這個(gè)指針即可搔体。接下來(lái)我們稍微深入跟進(jìn)一些恨樟。
static gpointer
mono_jit_compile_method_with_opt (MonoMethod *method, guint32 opt, MonoException **ex)
{
...
target_domain = mono_get_root_domain ();
info = lookup_method (target_domain, method); //先查表判斷是否已經(jīng)編譯
if (info) {
/* We can't use a domain specific method in another domain */
if (! ((domain != target_domain) && !info->domain_neutral)) {
MonoVTable *vtable;
MonoException *tmpEx;
mono_jit_stats.methods_lookups++;
vtable = mono_class_vtable (domain, method->klass);
g_assert (vtable);
tmpEx = mono_runtime_class_init_full (vtable, ex == NULL);
if (tmpEx) {
*ex = tmpEx;
return NULL;
}
return mono_create_ftnptr (target_domain, info->code_start);
}
}
code = mono_jit_compile_method_inner (method, target_domain, opt, ex);//實(shí)際編譯點(diǎn)
···
p = mono_create_ftnptr (target_domain, code);
···
return p;
}
這里隱去了編譯 invoke 函數(shù)的代碼和一些細(xì)枝末節(jié)的 check 〖挡瘢可以看到厌杜, mono_jit_compile_method_with_opt 函數(shù)的主要流程是首先查表看當(dāng)前要編譯的函數(shù)是否已經(jīng)編譯奉呛,如果已經(jīng)編譯计螺,則直接返回編譯好的結(jié)果;否則瞧壮,調(diào)用 mono_jit_compile_method_inner 函數(shù)實(shí)際編譯并注冊(cè)到 target_domain 中登馒,隨后通過(guò) mono_create_ftnptr 函數(shù)獲取函數(shù)指針。因?yàn)檫@部分代碼是復(fù)用的咆槽,除了首次加載 DLL 之外的一些情景也會(huì)調(diào)用該函數(shù)陈轿,其中存在一些函數(shù)已經(jīng)編譯的情況。 mono_jit_compile_method_inner 函數(shù)以下是一些與機(jī)器相關(guān)的具體機(jī)器碼生成過(guò)程,對(duì)虛擬機(jī)感興趣的朋友可以進(jìn)一步學(xué)習(xí)麦射,這里就不繼續(xù)深究了蛾娶,簡(jiǎn)單把整個(gè)調(diào)用過(guò)程整理一下:
graph TD;
start-->mono_precompile_assemblies
mono_precompile_assemblies-->|foreach|mono_precompile_assembly
mono_precompile_assembly-->|函數(shù)體|mono_jit_compile_method
mono_precompile_assembly-->|invoke|mono_marshal_get_runtime_invoke
mono_marshal_get_runtime_invoke-->mono_jit_compile_method
mono_jit_compile_method-->mono_jit_compile_method_with_opt
mono_jit_compile_method_with_opt-->|已經(jīng)編譯過(guò)|mono_create_ftnptr
mono_jit_compile_method_with_opt-->|沒(méi)有編譯過(guò)|mono_jit_compile_method_inner
mono_jit_compile_method_inner-->mini_method_compile
mini_method_compile-->mono_codegen
mono_codegen-->mono_create_ftnptr
mono_create_ftnptr-->finish
至此我們的分析完成了,盡管虛擬機(jī)可以使用花樣繁多的語(yǔ)言開(kāi)發(fā)潜秋,但是最終在執(zhí)行前一需要恢復(fù)成本地機(jī)器碼去執(zhí)行蛔琅。這就給了我們下 hook 的機(jī)會(huì),下一節(jié)介紹通過(guò)修改 mono 編譯出來(lái)的匯編函數(shù)邏輯峻呛,完成對(duì)游戲流程的動(dòng)態(tài)修改罗售。
0X03 通過(guò)修改虛擬機(jī)生成的匯編指令修改游戲邏輯
接下來(lái)我們嘗試?yán)们懊鎯晒?jié)介紹的知識(shí),修改游戲的執(zhí)行邏輯.
private void MainButtonClicked()
{
...
case UI_ConfirmationPopup.ScreenType.BuyCoins:
{
int num = Tuning.ShopCoinPackagesPrices[this.coinIndex];
int num2 = Tuning.ShopCoinPackages[this.coinIndex];
if (UserProfile.Gems >= num)
{
UserProfile.Gold += num2;
UserProfile.Gems -= num;
this.purchasedAmount = num2;
this.screenType = UI_ConfirmationPopup.ScreenType.CoinsPurchased;
Events.Instance.UI_MARKET_PURCHASED();
GeneralManager.Analytics.ReportGoldPurchased(this.coinIndex, num);
this.CallItQuits();
}
else
{
this.DisableAssets(true);
this.LaunchOutOfGems();
}
break;
}
...
}
我們選擇 MainButtonClicked 這個(gè)函數(shù)作為目標(biāo)钩述,其中的購(gòu)買金幣分支會(huì)檢測(cè)當(dāng)前鉆石數(shù)量寨躁,如果數(shù)量夠則進(jìn)行購(gòu)買,否則不進(jìn)行購(gòu)買牙勘。在 mono_jit_compile_method_with_opt 函數(shù)上下鉤子职恳,檢查第一個(gè)參數(shù) method 的 name 字段是否包含“ MainButtonClicked ”,在包含這個(gè)字段時(shí)方面,將 gpointer 指向的函數(shù) dump 出來(lái)话肖。
if(!strstr(name, "MainButtonClicked")) return target(arg1, arg2, arg3);
LOGE("find MainButtonClicked");
void* funcptr = target(arg1, arg2, arg3);
LOGE("function MainButtonClicked base is: %0lx", funcptr);
int fd = open("/data/local/tmp/dump", O_WRONLY | O_CREAT);
if(fd == -1){
LOGE("open error: %s", strerror(errno));
exit(-1);
}
if(write(fd, funcptr, 0x1000 * 0x1000) == -1){
LOGE("write error: %s", strerror(errno));
exit(-1);
}
如上述代碼所示,這次 hook 在 mono_jit_compile_method_with_opt 函數(shù)每次編譯 C# 函數(shù)點(diǎn)進(jìn)行判斷葡幸,當(dāng)被編譯的函數(shù)是我們的目標(biāo) MainButtonClicked 時(shí)最筒,對(duì)內(nèi)存進(jìn)行 dump ,將編譯成機(jī)器碼的 MainButtonClicked 輸出出來(lái)蔚叨,接下來(lái)床蜘,使用 IDA 對(duì)該函數(shù)進(jìn)行分析。
在加載該函數(shù)時(shí)需要注意蔑水,由于 dump 出來(lái)的是部分內(nèi)存邢锯,不像標(biāo)準(zhǔn)的 elf 文件一樣有各種配置能夠加載,識(shí)別為 binary file 搀别,需要手動(dòng)指定處理器架構(gòu)丹擎,這里是 ARM 。另外需要指定硬盤文件偏移和程序在內(nèi)存中偏移的映射關(guān)系歇父,注意上邊代碼中第四行輸出了程序在內(nèi)存中的地址蒂培,IDA 能夠利用 file_offset+memory_base 計(jì)算出相當(dāng)一部分的跳轉(zhuǎn)指令的跳轉(zhuǎn)地址(當(dāng)然,由于我們只 dump 了很小一部分內(nèi)存榜苫,仍然有很多依賴相對(duì)偏移尋址的跳轉(zhuǎn)目標(biāo)無(wú)法恢復(fù)护戳,但對(duì)程序結(jié)構(gòu)的分析無(wú)太大影響)。
通過(guò) IDA 加載后垂睬,可以看出函數(shù)明顯是一個(gè) switch-case 結(jié)構(gòu):
這個(gè)結(jié)構(gòu)與 MainButtonClicked 函數(shù)原始形式一致媳荒,通過(guò)分析二者關(guān)系可以定位到金幣購(gòu)買時(shí)點(diǎn)擊確定按鍵對(duì)應(yīng)到的 case :
case UI_ConfirmationPopup.ScreenType.BuyCoins:
{
int num = Tuning.ShopCoinPackagesPrices[this.coinIndex];
int num2 = Tuning.ShopCoinPackages[this.coinIndex];
if (UserProfile.Gems >= num)
{
UserProfile.Gold += num2;
UserProfile.Gems -= num;
this.purchasedAmount = num2;
this.screenType = UI_ConfirmationPopup.ScreenType.CoinsPurchased;
Events.Instance.UI_MARKET_PURCHASED();
GeneralManager.Analytics.ReportGoldPurchased(this.coinIndex, num);
this.CallItQuits();
}
else
{
this.DisableAssets(true);
this.LaunchOutOfGems();
}
break;
}
途中的兩個(gè)分支就是 C# 中的 if-else 抗悍。由于高級(jí)語(yǔ)言數(shù)據(jù)結(jié)構(gòu)比較復(fù)雜,反應(yīng)在機(jī)器碼層面取數(shù)據(jù)涉及問(wèn)題較多钳枕,因此修改取數(shù)據(jù)比較困難缴渊。但是可以看到,上面的 block 中 R5 是最后取出的當(dāng)前剩余寶石鱼炒,當(dāng)與其進(jìn)行比較之后疟暖,如果寶石充足,則會(huì)跳轉(zhuǎn)到紅色分支開(kāi)始購(gòu)買田柔,增加金幣扣除寶石俐巴。因此應(yīng)當(dāng)修改的邏輯是圖中 1 處,通過(guò) nop(mov r0, r0)掉跳轉(zhuǎn)強(qiáng)制執(zhí)行購(gòu)買流程硬爆。為了在修改金幣的同時(shí)不減少寶石欣舵,將2處寶石運(yùn)算改為 add r0 , r0 , r5 。
確定了修改點(diǎn)之后將上述兩條命令匯編缀磕,使用之前的 hook 稍作修改缘圈,當(dāng)執(zhí)行到 MainButtonClicked 編譯時(shí)修改程序機(jī)器碼( mono 已經(jīng)很貼心的 mprotect 過(guò)了),完成對(duì)游戲的修改袜蚕,接下來(lái)嘗試購(gòu)買寶石糟把,哇,奇跡發(fā)生了牲剃。
0X04 后記
本文從一個(gè)簡(jiǎn)單小游戲的破解出發(fā)遣疯,介紹了 Unity3D 引擎使用 mono 進(jìn)行 C# JIT 編譯的思路,并設(shè)計(jì)了實(shí)驗(yàn)性質(zhì)的 hook 方案凿傅。其實(shí)針對(duì)這款簡(jiǎn)單的小游戲缠犀,更簡(jiǎn)單的破解方式還有很多種,牛刀殺雞是為了以后更容易殺牛聪舒。因?yàn)樵趯?shí)際的環(huán)境中辨液,分析游戲面臨著過(guò)反調(diào)試、脫殼箱残、對(duì)抗去符號(hào)滔迈、DLL 解密等多重挑戰(zhàn)。限于篇幅不可能對(duì)這些技術(shù)一一介紹被辑,感興趣的朋友可以自行百度/谷歌燎悍。另外,由于使用了 AOT 機(jī)制敷待,文章中介紹的 hook 思路可能會(huì)更適用于 iOS 间涵,條件所限沒(méi)有嘗試仁热。攻擊是為了更好的防御榜揖,使用的實(shí)例勾哩,介紹的工具都為了更好的說(shuō)明技術(shù)本身,請(qǐng)不要用違法的目的举哟。
參考文獻(xiàn)
[1]: Mono為何能跨平臺(tái)思劳?聊聊CIL(MSIL) - 慕容小匹夫 - 博客園 "Mono為何能跨平臺(tái)?聊聊CIL(MSIL)"
[2]: Welcome to Ecma International "EMCA官網(wǎng)"