最近旅行的青蛙甚火,便來試一試Android和iOS上面Unity3D遊戲C#腳本和C/C++函數的分析與修改,由於Android大部分是mono模式如果C#腳本沒有加密的話是直接可以拿過來分析修改的,但是iOS大都使用IL2CPP模式把C#腳本轉成C/C++代碼了,直接分析的話要難很多,所以本文先從Android開始分析然後結合Android再分析iOS。
首先將Android apk安裝包解壓,查看assets/bin/Data/Managed文件夾下面的dll文件,其中Assembly-CSharp.dll便是遊戲中的C#腳本,使用Reflector或者dnSpy進行反編譯都可以,如何出現反編譯出錯的情況,那可能就是腳本被加密了,一般在嘗試在libmono.so中的 mono_image_open_from_data_with_name 中獲取解密後的dll腳本,而當前分析的遊戲C#腳本並沒有加密。在dnSpy中的反編譯結果如下:
左側就可以看到對應的類,點擊類就可以看到反編譯出來的代碼,下面先跟遊戲裡面的一些特徵來分析反編譯出來的腳本。
首先使用 adb install tabikaeru.apk 命令安裝apk文件,然後打開遊戲可以看到如下界面:
首先來看看怎麼修改顯示的文字以達到漢化的效果,使用dnSpy點擊編輯->搜索程序集搜索右邊選擇數字/字符串然後搜索名前:
點擊CallTutorial可以找到剛剛在屏幕上面顯示的文字:
滑鼠點擊右鍵選擇編輯IL指令,修改剛剛看到的文字為漢字:
然後點擊應用即可看到修改後的效果:
點擊文件->全部保存替換 assets/bin/Data/Managed 文件夾下面對應的文件即可,然後將文件打包成apk使用jarsinger、signapk.jar或Android助手重籤名安裝到手機就能看到修改後的效果了。
要修改三葉草數可以從購買的時候入手,比如提示三葉草不足的時候:
直接搜索足找到目標代碼:
根據找到的如下代碼來分析:
if (SuperGameMaster.CloverPointStock() >= itemDataFormat.price)
{
if (SuperGameMaster.FindItemStock(shopDataFormat.itemId) < 99)
{
base.GetComponent<FlickCheaker>().stopFlick(true);
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
if (itemDataFormat.type == Item.Type.LunchBox)
{
confilm.OpenPanel_YesNo(string.Concat(new object[]
{
itemDataFormat.name,
"\nを買いますか?\n(所持數\u3000",
SuperGameMaster.FindItemStock(shopDataFormat.itemId),
")"
}));
}
else
{
confilm.OpenPanel_YesNo(itemDataFormat.name + "\nを買いますか?");
}
confilm.ResetOnClick_Yes();
confilm.SetOnClick_Yes(delegate
{
confilm.ClosePanel();
});
confilm.SetOnClick_Yes(delegate
{
this.GetComponent<FlickCheaker>().stopFlick(false);
});
confilm.SetOnClick_Yes(delegate
{
this.BuyItem();
});
confilm.ResetOnClick_No();
confilm.SetOnClick_No(delegate
{
confilm.ClosePanel();
});
confilm.SetOnClick_No(delegate
{
this.GetComponent<FlickCheaker>().stopFlick(false);
});
}
else
{
base.GetComponent<FlickCheaker>().stopFlick(true);
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
confilm.OpenPanel("もちものがいっぱいです");
confilm.ResetOnClick_Screen();
confilm.SetOnClick_Screen(delegate
{
confilm.ClosePanel();
});
confilm.SetOnClick_Screen(delegate
{
this.GetComponent<FlickCheaker>().stopFlick(false);
});
}
當三葉草的存儲小於當前商品的價格的時候就會彈出這個提示,點擊SuperGameMaster.CloverPointStock()得到如下代碼:
public static int CloverPointStock()
{
return SuperGameMaster.saveData.CloverPoint;
}
這裡返回的是一個從saveData.CloverPoint中獲取的整數,可以嘗試將其修改成返回一個特定的數字每次查詢三葉草的數目都是這麼多,右鍵點擊編輯IL指令,然後點擊重置並右鍵刪除第一條指令,因為只是返回一個數字的話兩條指令就夠了:
然後修改第二條指令為一個數字,具體的指令代表的類型可以查看IL指令說明:
點擊確定後就能看到修改代碼的效果:
public static int CloverPointStock()
{
return 9999;
}
重新籤名安裝之後每次購買商品之後三葉草的數目也不會減少,一直是9999。
修改抽獎券也是同樣的套路,搜索足找到目標代碼,其實剛剛搜索的時候就已經看到了。
if (SuperGameMaster.TicketStock() < 5)
{
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
confilm.OpenPanel("ふくびき券が足りません");
confilm.ResetOnClick_Screen();
confilm.SetOnClick_Screen(delegate
{
confilm.ClosePanel();
});
return;
}
點擊 SuperGameMaster.TicketStock() 也是差不多的代碼同樣修改返回數字即可:
public static int TicketStock()
{
return SuperGameMaster.saveData.ticket;
}
public static int TicketStock()
{
return 9999;
}
每次抽獎都是抽到白球就不是很開心了,怎麼能夠提供抽獎的概率呢?抽到其它顏色的球的時候會提示:
搜索白玉找到如下代碼:
public static readonly Dictionary<Rank, string> PrizeBallName = new Dictionary<Rank, string>
{
{
Rank.White,
"白玉"
},
{
Rank.Blue,
"青玉"
},
{
Rank.Green,
"緑玉"
},
{
Rank.Red,
"赤玉"
},
{
Rank.Gold,
"黃玉"
}
};
右鍵點擊PrizeBallName選擇分析找到在那裡被使用的:
雙擊PrizeBallName得到如下代碼:
public void PushRollButton()
{
if (SuperGameMaster.TicketStock() < 5)
{
ConfilmPanel confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
confilm.OpenPanel("ふくびき券が足りません");
confilm.ResetOnClick_Screen();
confilm.SetOnClick_Screen(delegate
{
confilm.ClosePanel();
});
return;
}
SuperGameMaster.GetTicket(-5);
SuperGameMaster.set_FlagAdd(Flag.Type.ROLL_NUM, 1);
base.GetComponentInParent<UIMaster>().freezeObject(true);
base.GetComponentInParent<UIMaster>().blockUI(true, new Color(0f, 0f, 0f, 0.3f));
this.LotteryCheck();
this.ResultButton.GetComponent<RollResultButton>().CngImage((int)this.result);
this.ResultButton.GetComponent<RollResultButton>().CngResultText(Define.PrizeBallName[this.result] + "がでました");
this.LotteryWheelPanel.GetComponent<LotteryWheelPanel>().OpenPanel(this.result);
SuperGameMaster.SetTmpRaffleResult((int)this.result);
SuperGameMaster.SaveData();
SuperGameMaster.audioMgr.PlaySE(Define.SEDict["SE_Raffle"]);
this.BackFunc();
}
從代碼來看就是剛剛抽獎結果提示的地方,來分析下這個結果是怎麼生成的,可以看到 Define.PrizeBallName[this.result] 這個result決定了抽到的是什麼顏色的球,那麼看看這個result是在哪裡生成的。在當前文件中看到了兩處賦值:
public void LotteryCheck()
{
int num = Random.Range(0, Define.PrizeBalls[Rank.RankMax]);
this.result = Rank.White;
int i = 0;
int num2 = 0;
while (i < 5)
{
num2 += Define.PrizeBalls[(Rank)i];
if (num < num2)
{
this.result = (Rank)i;
break;
}
i++;
}
}
ublic void SetTmpResult()
{
this.result = (Rank)SuperGameMaster.GetTmpRaffleResult();
this.BackFunc();
}
下面是在UIMaster_raffle.UI_Start中調用的,判斷之前的獎品有沒有領取,LotteryCheck這個剛好是在顯示結果之前調用的,猜測就是這裡控制的不同顏色的球的出現概率,分析下這段代碼:
public void LotteryCheck()
{
int num = Random.Range(0, Define.PrizeBalls[Rank.RankMax]); //Rank.RankMax是5,Define.PrizeBalls[Rank.RankMax]是100,從0-100隨機生成一個數
this.result = Rank.White; //默認都是白色的球
int i = 0;
int num2 = 0;
while (i < 5)
{
num2 += Define.PrizeBalls[(Rank)i]; //根據PrizeBalls的值小於60是白色,大於等於60並且少於87是藍色,大於等於87小於96是綠色,大於等於96小於99是紅色,大於等於99是金色。
if (num < num2)
{
this.result = (Rank)i;
break;
}
i++;
}
}
從上面代碼分析PrizeBalls中就定義了不同顏色的球搖出概率:
public static readonly Dictionary<Rank, int> PrizeBalls = new Dictionary<Rank, int>
{
{
Rank.White,
60
},
{
Rank.Blue,
27
},
{
Rank.Green,
9
},
{
Rank.Red,
3
},
{
Rank.Gold,
1
},
{
Rank.RankMax,
100
}
};
所以直接修改這裡的數字就控制搖出的概率了,比如不出白球,其它球的概率都一樣:
public static readonly Dictionary<Rank, int> PrizeBalls = new Dictionary<Rank, int>
{
{
Rank.White,
0 //不存在白球
},
{
Rank.Blue,
25
},
{
Rank.Green,
25
},
{
Rank.Red,
25
},
{
Rank.Gold,
25
},
{
Rank.RankMax,
100
}
};
農場裡面大部分都是三葉草、四葉草的概率是很小的,那麼怎麼修改這個呢?在界面上面好像不太好找關聯,先試試搜索三葉草的英語clover,點擊checkCloverCreate看起來就是控制生成的函數:
public void checkCloverCreate()
{
this.cloverList = SuperGameMaster.GetCloverList();
bool flag = false;
if (this.cloverList.Count == 0)
{
flag = true;
//這句話翻譯的意思就是:有了四葉草的初期化標誌。四葉草生成
//也就是flag為true的時候會生成四葉草
Debug.Log("[CloverFarm] クローバーの初期化フラグが立ちました。四葉を生成します");
}
if (this.cloverList.Count < this.cloverMax)
{
Debug.Log(string.Concat(new object[]
{
"[CloverFarm] クローバーの數を調整します:",
this.cloverList.Count,
" > ",
this.cloverMax
}));
}
while (this.cloverList.Count < this.cloverMax)
{
CloverDataFormat cloverDataFormat = new CloverDataFormat();
cloverDataFormat.lastHarvest = new DateTime(1970, 1, 1);
cloverDataFormat.timeSpanSec = -this.cloverList.Count - 1;
cloverDataFormat.newFlag = true;
this.cloverList.Add(cloverDataFormat);
}
if (this.cloverList.Count > this.cloverMax)
{
Debug.Log(string.Concat(new object[]
{
"[CloverFarm] クローバーの數を調整します:",
this.cloverList.Count,
" > ",
this.cloverMax
}));
this.cloverList.RemoveRange(this.cloverMax - 1, this.cloverList.Count - this.cloverMax);
}
List<GameObject> list = new List<GameObject>();
for (int i = 0; i < this.cloverList.Count; i++)
{
if (!this.cloverList[i].newFlag && this.cloverList[i].timeSpanSec <= 0)
{
list.Add(this.LoadCloverObject(i, this.cloverList[i]));
}
}
int num = 0;
for (int j = 0; j < this.cloverList.Count; j++)
{
if (this.cloverList[j].newFlag)
{
//這裡根據flag調用不同的函數,
if (!flag)
{
//生成三葉草
list.Add(this.NewCloverObject(j, this.cloverList[j], list));
}
else
{
//生成四葉草,不同的是第四個參數為true
list.Add(this.NewCloverObject(j, this.cloverList[j], list, true));
flag = false;
}
this.cloverList[j].x = list[list.Count - 1].transform.localPosition.x;
this.cloverList[j].y = list[list.Count - 1].transform.localPosition.y;
Clover component = list[list.Count - 1].GetComponent<Clover>();
this.cloverList[j].element = component.element;
this.cloverList[j].spriteNum = component.spriteNum;
this.cloverList[j].point = component.point;
this.cloverList[j].newFlag = false;
num++;
}
}
foreach (GameObject gameObject in list)
{
int num2 = this.cloverOrderInLayer;
foreach (GameObject gameObject2 in list)
{
if (gameObject.transform.position.y < gameObject2.transform.position.y)
{
num2++;
}
}
gameObject.GetComponent<SpriteRenderer>().sortingOrder = num2;
}
Debug.Log(string.Concat(new object[]
{
"[CloverFarm] クローバー生成完了:",
list.Count,
"\u3000/ (新規:",
num,
")"
}));
}
首先看this.NewCloverObject(j, this.cloverList[j], list)這個內部調用就是四個參數的,只不過第四個參數是false:
public GameObject NewCloverObject(int index, CloverDataFormat cloverData, List<GameObject> cloversObj)
{
return this.NewCloverObject(index, cloverData, cloversObj, false);
}
也就是第四個參數為true就是四葉草,點進去看看:
public GameObject NewCloverObject(int index, CloverDataFormat cloverData, List<GameObject> cloversObj, bool fourLeafFlag)
{
Vector2 size = base.GetComponent<BoxCollider2D>().size;
PolygonCollider2D component = base.GetComponent<PolygonCollider2D>();
Vector2 vector;
vector..ctor(base.GetComponent<BoxCollider2D>().offset.x - size.x / 2f, base.GetComponent<BoxCollider2D>().offset.y - size.y / 2f);
int num = 0;
bool flag;
Vector3 vector2;
do
{
flag = false;
vector2 = new Vector2(vector.x + Random.Range(0f, size.x), vector.y + Random.Range(0f, size.y));
if (!component.OverlapPoint(vector2 + base.transform.position))
{
flag = true;
}
else
{
for (int i = 0; i < cloversObj.Count; i++)
{
Vector2 size2 = cloversObj[i].GetComponent<BoxCollider2D>().size;
if (Mathf.Abs(vector2.x - cloversObj[i].transform.localPosition.x) < size2.x / 2f && Mathf.Abs(vector2.y - cloversObj[i].transform.localPosition.y) < size2.y / 4f)
{
flag = true;
}
}
num++;
if (num >= 100)
{
break;
}
}
}
while (flag);
GameObject gameObject = Object.Instantiate<GameObject>(this.basePrefab, Vector3.zero, Quaternion.identity);
CloverDataFormat cloverDataFormat = new CloverDataFormat();
cloverDataFormat.point = 1;
cloverDataFormat.element = 0;
//如果第四個參數是false,這裡就是四葉草生成的概率,是fourLeaf_percent是1那這個概率就是1/100
if (Random.Range(0f, 10000f) < this.fourLeaf_percent * 100f)
{
cloverDataFormat.element = 1;
}
//如果傳了這個參數為true直接生成四葉草
if (fourLeafFlag)
{
cloverDataFormat.element = 1;
}
int element = cloverDataFormat.element;
if (element != 0)
{
if (element == 1)
{
cloverDataFormat.spriteNum = Random.Range(0, this.fourCloverSprite.Length);
gameObject.GetComponent<SpriteRenderer>().sprite = this.fourCloverSprite[cloverDataFormat.spriteNum];
}
}
else
{
cloverDataFormat.spriteNum = Random.Range(0, this.cloverSprite.Length);
gameObject.GetComponent<SpriteRenderer>().sprite = this.cloverSprite[cloverDataFormat.spriteNum];
}
gameObject.GetComponent<Clover>().SetCloverData(index, cloverDataFormat);
gameObject.transform.parent = base.transform;
gameObject.transform.localScale = Vector3.one;
gameObject.transform.localPosition = vector2;
return gameObject;
}
那麼這裡既可以修改四葉草生成的概率也可以修改 fourLeafFlag 這個為true,第一種方式和上面一樣,看看第二種。右鍵fourLeafFlag編輯IL指令:
這裡首先取fourLeafFlag判斷然後跳轉,所以修改為true即可。修改 ldarg.s fourLeafFlag為ldc.i4 1,修改後的代碼如下:
if (true)
{
cloverDataFormat.element = 1;
}
Android的修改的部分就到這裡了,其它的大家可以自己去嘗試,總結下分析Unity3D遊戲過程,當然這篇文章講的是最簡單的情況,還有使用保護將dll腳本加密的,就需要hook函數或者從內存查找dump,還有通過C#腳本調用lua腳本來實現的,邏輯在lua腳本裡面,lua腳本又加密了的情況等等。
那麼沒保護的情況的話,一般可以從界面顯示搜索來分析,然後根據一些特定單詞去搜索查找關鍵代碼部分。這篇主要是Android上面使用mono模式的情況,如果是使用IL2CPP的話比如iOS上面,C#腳本都轉成了cpp文件c代碼的形式的話分析起來就會麻煩很多,下面來看看iOS上面修改hook代碼。
上面主要也是為下面做鋪墊吧,因為在iOS現在都是IL2CPP模式,C#腳本已經被轉成了C/C++代碼。所以要單獨分析iOS的話難度會大很多,如果從Android的C#腳本入手的話,因為iOS和Android腳本都是一樣的話,可以從Android分析的函數名來對應iOS的c函數然後進行hook修改。
首先從越獄設備上面提取旅行青蛙的ipa包,使用frida-ios-dump一鍵提取即可。由於是日文名字,先通過./dump.py -l把名字列出來,然後複製名字或者通過bundle id去dump就可以了。
由於使用IL2CPP選項編譯unity遊戲,會生成cpp的代碼,直接使用IDA看是看不到函數和函數名的,而且遊戲中使用的字符串都被保存在global-metadata.dat的資源文件裡。
首先要通過提取global-metadata.dat文件裡面的字符串對對應的c函數進行符號還原。具體也有現成的文章:還原使用IL2CPP編譯的unity遊戲的symbol,github上面也有線程的項目也做這件事情Il2CppDumper。
下載release的工具,運行Il2CppDumper.exe並依次選擇il2cpp的可執行文件和global-metadata.dat文件,然後選擇Auto(Plus)模式,將生成dump.cs文件和script.py腳本。使用IDA打開可執行文件然後使用script.py腳本即可還原符號。
Making method name...
Make method name done
Setting String...
Set string done
Making function...
Make function done, please wait for IDA to complete the analysis
Script finish !
還原之後就可以根據之前分析到的函數名來hook對應的代碼了,首先是三葉草的數目通過SuperGameMaster.CloverPointStock()獲取的,在IDA搜索CloverPointStock如下:
接著就可以直接hook這個函數了,由於要inline hook目前是在越獄機器上面,後面會講到非越獄機器hook的方案。使用MonkeyDev新建一個Logos Tweak項目,清空.xm的內容並寫入如下內容:
#import <substrate.h>
#import <dlfcn.h>
#import <mach-o/dyld.h>
int (*old_clover_point_stock)(void);
int new_clover_point_stock(void)
{
return 9999;
}
%ctor
{
@autoreleasepool
{
unsigned long clover_point_stock = _dyld_get_image_vmaddr_slide(0) + 0x100093A2C;
MSHookFunction((void *)clover_point_stock, (void *)&new_clover_point_stock, (void **)&old_clover_point_stock);
}
}
然後在build settings裡面設置埠和設備密碼然後command + b安裝就能看到效果了,其它函數的hook也是一樣的:
#import <substrate.h>
#import <dlfcn.h>
#import <mach-o/dyld.h>
int (*old_clover_point_stock)(void);
int new_clover_point_stock(void)
{
return 9999;
}
int (*old_ticket_stock)(void);
int new_ticket_stock(void)
{
return 9999;
}
void (*old_lotterycheck)(uint64_t obj);
void new_lotterycheck(uint64_t obj)
{
*(int*)(obj + 80) = rand() % 4 + 1;
}
uint64_t (*old_new_clover_object)(uint64_t obj, int index, uint64_t cloverData, uint64_t cloversObj, int fourLeafFlag);
uint64_t new_new_clover_object(uint64_t obj, int index, uint64_t cloverData, uint64_t cloversObj, int fourLeafFlag)
{
return old_new_clover_object(obj,index,cloverData,cloversObj,1);
}
%ctor
{
@autoreleasepool
{
unsigned long clover_point_stock = _dyld_get_image_vmaddr_slide(0) + 0x100093A2C;
MSHookFunction((void *)clover_point_stock, (void *)&new_clover_point_stock, (void **)&old_clover_point_stock);
unsigned long ticket_stock = _dyld_get_image_vmaddr_slide(0) + 0x100093AA4;
MSHookFunction((void *)ticket_stock, (void *)&new_ticket_stock, (void **)&old_ticket_stock);
unsigned long lotterycheck = _dyld_get_image_vmaddr_slide(0) + 0x100086CF4;
MSHookFunction((void *)lotterycheck, (void *)&new_lotterycheck, (void **)&old_lotterycheck);
unsigned long new_clover_object = _dyld_get_image_vmaddr_slide(0) + 0x100037100;
MSHookFunction((void *)new_clover_object, (void *)&new_new_clover_object, (void **)&old_new_clover_object);
}
}
這裡有一個函數RaffelPanel$$LotteryCheck要修改裡面的result的值,就要根據彙編或者偽代碼來看result的賦值是在什麼位置了,該函數通過F5獲得的偽代碼如下:
__int64 __fastcall RaffelPanel__LotteryCheck(__int64 a1)
{
__int64 v1; // x19
__int64 v2; // x0
__int64 v3; // x0
int v4; // w20
int v5; // w23
signed int v6; // w24
__int64 v7; // x0
__int64 result; // x0
v1 = a1;
if ( !(byte_10137EDBB & 1) )
{
sub_100DEAD34(6810LL);
byte_10137EDBB = 1;
}
v2 = qword_101439198;
if ( *(_BYTE *)(qword_101439198 + 266) & 1 && !*(_DWORD *)(qword_101439198 + 188) )
{
sub_100DFF71C();
v2 = qword_101439198;
}
if ( !*(_QWORD *)(*(_QWORD *)(v2 + 160) + 192LL) )
LABEL_17:
sub_100DE28B4();
v3 = sub_1000FB954();
v4 = Random__Range_71094(0LL, 0LL, v3, 0LL);
v5 = 0;
*(_DWORD *)(v1 + 80) = 0; this.result = Rank.White; //默認都是白色的球
v6 = -1;
while ( 1 )
{
v7 = qword_101439198;
if ( *(_BYTE *)(qword_101439198 + 266) & 1 )
{
if ( !*(_DWORD *)(qword_101439198 + 188) )
{
sub_100DFF71C();
v7 = qword_101439198;
}
}
if ( !*(_QWORD *)(*(_QWORD *)(v7 + 160) + 192LL) )
goto LABEL_17;
result = sub_1000FB954();
v5 += result;
if ( v4 < v5 ) //if (num < num2)
break;
if ( ++v6 >= 4 )
return result;
}
*(_DWORD *)(v1 + 80) = v6 + 1; //this.result = (Rank)i;
return result;
}
這裡的*(_DWORD *)(v1 + 80)的位置其實就是this.result所以直接修改80偏移位置的值就可以了。
總結來說的話,要分析iOS裡面轉換後的腳本C代碼還是不容易的,如果能夠根據Android C#腳本分析的結果然後對iOS的符號進行恢復一下的話,就可以直接根據Android分析到的函數直接來Hook iOS對應的函數來修改參數或者值了。不過這裡還是在越獄設備上面進行的hook,然後可能還會寫個非越獄設備同樣進行靜態的hook操作。
本文由看雪翻譯小組 AloneMonkey 原創
轉載請註明來自看雪社區
熱門閱讀
點擊閱讀原文/read,
更多乾貨等著你~