旅行青蛙(旅かえる)逆向筆記

霧花_小路發表於2018-05-05

溫馨提示:閱讀本文你的電腦需要安裝好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
  • 猜測SuperGameMasterCloverPointStock方法是獲得三葉草數量的方法,進入檢視該方法:
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中新增廣告,收集資訊也是不難的,所以大家在下載應用的時候還是應該注意啊!

相關文章