旅行青蛙(旅かえる)逆向笔记

温馨提示:阅读本文你的电脑需要安装好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, "\nを買いますか?\n(所持数 ", SuperGameMaster.FindItemStock(format.itemId), ")" }));
                        }
                        else
                        {
                            storey.confilm.OpenPanel_YesNo(format2.name + "\nを買いますか?");
                        }
                        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中添加广告,收集信息也是不难的,所以大家在下载应用的时候还是应该注意啊!