温馨提示:阅读本文你的电脑需要安装好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
- 猜测
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中添加广告,收集信息也是不难的,所以大家在下载应用的时候还是应该注意啊!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。