溫馨提示:閱讀本文你的電腦需要安裝好apktool、signapk、.NET Reflector、dnSpy。他們都可以在github或吾愛雲盤上獲取。
一、APK結構
- 旅行青蛙是個Unity的遊戲。簡單說下Unity:Unity是一個用於製作3D遊戲的C#框架,可以跨平臺。也就是說旅行青蛙的核心遊戲邏輯在Android和iOS上面是一樣的程式碼。顯然Android更容易讓我們分析,本文先從APK的結構開始。
- 使用apktool反編譯APK,發現Unity遊戲的smali程式碼並沒有太多的資訊,基本都是呼叫Google的Ad介面之類的,或者是Google Play的應用內購買,就不需要太關心了。
- lib資料夾中主要都是Unity、Mono等的支援動態庫so檔案,也不是我們關心的物件。
- 經查閱資料可以得知,Unity遊戲的主要邏輯程式碼存放於
assets/bin/Data/Managed
下的Assembly-CSharp.dll
動態庫檔案中,C#的dll檔案不難分析,我們使用.NET Reflector和dnSpy進行分析和修改。
二、Assembly-CSharp.dll修改
- 使用.NET Reflector開啟Assembly-CSharp.dll檔案,觀察整個dll的結構。發現幾乎所有邏輯程式碼都位於“-”下面。
- 我們執行遊戲,在商店點選購買昂貴的商品,或者在抽獎區抽獎,遊戲會提示“みつ葉が足りません”和“ふくびき券が足りません”和。
- 雖然不懂日語,但是大概知道是提醒你不夠的意思,因為電腦沒有日文輸入法,所以在.NET Reflector中嘗試搜尋漢字“足”,看看有什麼結果。
- 結果找到了兩個方法中提及了“足”字,分別是
SetInfoPanelData
方法和PushRollButton
方法。首先檢視SetInfoPanelData
方法,發現是進行商品購買的邏輯程式碼,程式碼如下:
public void SetInfoPanelData(int shopIndex, Vector3 pos)
{
if (shopIndex == -1)
{
this.unsetCursor();
this.InfoPanel.GetComponent<InfoPanel>().SetInfoPanel(-1);
}
else if (Mathf.Abs(this.flickMove) <= (this.S_FlickChecker.flickMin / 3f))
{
if (this.selectShopIndex != shopIndex)
{
this.InfoPanel.GetComponent<InfoPanel>().SetInfoPanel(shopIndex);
this.selectShopIndex = shopIndex;
this.setCursor(pos);
SuperGameMaster.audioMgr.PlaySE(Define.SEDict["SE_Cursor"]);
}
else
{
ShopDataFormat format = SuperGameMaster.sDataBase.get_ShopDB(shopIndex);
ItemDataFormat format2 = SuperGameMaster.sDataBase.get_ItemDB_forId(format.itemId);
if (format2 != null)
{
if (!format2.spend && (SuperGameMaster.FindItemStock(format2.id) != 0))
{
SuperGameMaster.audioMgr.PlaySE(Define.SEDict["SE_Cancel"]);
}
else if (SuperGameMaster.CloverPointStock() >= format2.price)
{
if (SuperGameMaster.FindItemStock(format.itemId) < 0x63)
{
<SetInfoPanelData>c__AnonStorey1 storey = new <SetInfoPanelData>c__AnonStorey1 {
$this = this
};
base.GetComponent<FlickCheaker>().stopFlick(true);
storey.confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
if (format2.type == Type.LunchBox)
{
storey.confilm.OpenPanel_YesNo(string.Concat(new object[] { format2.name, "
を買いますか?
(所持數 ", SuperGameMaster.FindItemStock(format.itemId), ")" }));
}
else
{
storey.confilm.OpenPanel_YesNo(format2.name + "
を買いますか?");
}
storey.confilm.ResetOnClick_Yes();
storey.confilm.SetOnClick_Yes(new UnityAction(storey, (IntPtr) this.<>m__0));
storey.confilm.SetOnClick_Yes(new UnityAction(storey, (IntPtr) this.<>m__1));
storey.confilm.SetOnClick_Yes(new UnityAction(storey, (IntPtr) this.<>m__2));
storey.confilm.ResetOnClick_No();
storey.confilm.SetOnClick_No(new UnityAction(storey, (IntPtr) this.<>m__3));
storey.confilm.SetOnClick_No(new UnityAction(storey, (IntPtr) this.<>m__4));
}
else
{
<SetInfoPanelData>c__AnonStorey2 storey2 = new <SetInfoPanelData>c__AnonStorey2 {
$this = this
};
base.GetComponent<FlickCheaker>().stopFlick(true);
storey2.confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
storey2.confilm.OpenPanel("もちものがいっぱいです");
storey2.confilm.ResetOnClick_Screen();
storey2.confilm.SetOnClick_Screen(new UnityAction(storey2, (IntPtr) this.<>m__0));
storey2.confilm.SetOnClick_Screen(new UnityAction(storey2, (IntPtr) this.<>m__1));
}
}
else
{
<SetInfoPanelData>c__AnonStorey3 storey3 = new <SetInfoPanelData>c__AnonStorey3 {
$this = this
};
base.GetComponent<FlickCheaker>().stopFlick(true);
storey3.confilm = this.ConfilmUI.GetComponent<ConfilmPanel>();
storey3.confilm.OpenPanel("みつ葉が足りません");
storey3.confilm.ResetOnClick_Screen();
storey3.confilm.SetOnClick_Screen(new UnityAction(storey3, (IntPtr) this.<>m__0));
storey3.confilm.SetOnClick_Screen(new UnityAction(storey3, (IntPtr) this.<>m__1));
}
}
}
}
}
- 定位到關鍵程式碼:
SuperGameMaster.CloverPointStock() >= format2.price
- 猜測
SuperGameMaster
的CloverPointStock
方法是獲得三葉草數量的方法,進入檢視該方法:
public static int CloverPointStock()
{
return SuperGameMaster.saveData.CloverPoint;
}
- 顯然直接修改該函式就可以實現固定數量的三葉草,使用64位的dnSpy修改程式碼,定位到該方法,右擊滑鼠單擊“編輯IL指令”,刪去前兩句指令中的一句,再修改第一句指令為ldc.i4 9876,儲存後函式變為:
public static int CloverPointStock()
{
return 9876;
}
- 按照同樣的方法分析
PushRollButton
方法,得到程式碼:
public void PushRollButton()
{
if (SuperGameMaster.TicketStock() < 5)
{
<PushRollButton>c__AnonStorey0 storey = new <PushRollButton>c__AnonStorey0 {
confilm = this.ConfilmUI.GetComponent<ConfilmPanel>()
};
storey.confilm.OpenPanel("ふくびき券が足りません");
storey.confilm.ResetOnClick_Screen();
storey.confilm.SetOnClick_Screen(new UnityAction(storey, (IntPtr) this.<>m__0));
}
else
{
SuperGameMaster.GetTicket(-5);
SuperGameMaster.set_FlagAdd(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();
}
}
- 定位到關鍵程式碼:
if (SuperGameMaster.TicketStock() < 5)
- 以及
SuperGameMaster.GetTicket(-5);
SuperGameMaster.set_FlagAdd(Type.ROLL_NUM, 1);
- 修改任意一處都可以,顯然修改
TicketStock
方法的返回值更省事,使用dnSpy按同樣的方法修改程式碼,原來方法為:
public static int TicketStock()
{
return SuperGameMaster.saveData.ticket;
}
- 修改為:
public static int TicketStock()
{
return 5;
}
三、APK重打包和簽名
- 經過以上的修改,可以實現無限抽獎券和無限三葉草,將APK重新打包即可。
- 將修改後的dll檔案儲存,替換原本的Assembly-CSharp.dll,然後使用apktool重新打包,再進行簽名,就可以使用了。
四、總結和未完待續
- 有時間的話會繼續分析這個程式碼。除此之外也發現,Unity遊戲如果不進行任何保護的話,是很容易被篡改的,網上有很多流傳的「漢化版」以及「破解版」基本都是這樣的原理。小路不會在APK中新增其他東西,但是網路上其他人就不一定了。在這種APK中新增廣告,收集資訊也是不難的,所以大家在下載應用的時候還是應該注意啊!