最近TapTap上有一款Roguelike的射击游戏Soul Knight火了,下载玩了一会儿,果然很好玩。但是只有骑士一个角色,其他角色都需要付费解锁,角色的养成也需要大量的宝石,对我这样的手残党来说真是很尴尬。但是Soul Knight是单机游戏,存档甚至购买记录都保存在本地,于是乎,偷个懒看看能不能破解,好玩了再入手正式版支持一下开发者:)。
首先我去TapTap下载的最新的版本,解包后发现已经il2cpp过了,Managed文件夹下传统的Dll被编译到Lib下的libil2cpp.so了,这下子不能再修改Assembly-CSharp.dll了。之前没试过il2cpp的游戏修改,但是还是决定进行一系列的尝试,参考了IL2Cpp Dumper的教程Dump出了所有的类接口,下面是一个角色控制器:
// Namespace:
public class C01Controller // TypeDefIndex: 3435
{
// Fields
private RGHand hand2; // 0x84
private float in_skill_time; // 0x88
// Methods
public void .ctor(); // 4c5200 - 18233
private void Awake(); // 4c52b0 - 18234
private void Start(); // 4c5398 - 18235
private void Update(); // 4c53e8 - 18236
private void FixedUpdate(); // 4c5534 - 18237
public virtual void AutoLock(); // 4c5578 - 18238
public virtual void SwitchWeapon(); // 4c5604 - 18239
public virtual void RoleSkill(); // 4c5658 - 18240
public virtual void RoleSkillEnd(); // 4c5a18 - 18241
public virtual void RoleAtk(bool value); // 4c5bbc - 18242
virtual void DeadChild(); // 4c5e80 - 18243
public virtual void KillSomeOne(); // 4c5eb8 - 18244
private void Hand2Atk(); // 4c5f00 - 18245
private void Hand2AtkStop(); // 4c5f30 - 18246
public virtual void Dizzy(float the_time); // 4c5f60 - 18247
}
每行代码后的注释是其在so里的偏移,把libil2cpp.so放到IDA中去分析,找到对应的ARM 汇编片段如下(Awake函数):
STMFD SP!, {R4-R6,R10,R11,LR}
ADD R11, SP, #0x10
MOV R4, R0
LDR R0, =(_GLOBAL_OFFSET_TABLE_ - 0x4C52CC)
LDR R5, =0x44AE4
ADD R0, PC, R0 ; _GLOBAL_OFFSET_TABLE_
ADD R0, R5, R0
LDRB R0, [R0,#(byte_D8E17D - 0xD8E0B0)]
CMP R0, #0
BNE loc_4C52FC
LDR R0, =(_GLOBAL_OFFSET_TABLE_ - 0x4C52E8)
LDR R1, =0x13FB8
ADD R6, PC, R0 ; _GLOBAL_OFFSET_TABLE_
LDR R0, [R1,R6]
LDR R0, [R0]
BL sub_AAB6B8
ADD R0, R5, R6
MOV R1, #1
到这里我已经一头雾水了——为了修改一个小东西还需要学整个ARM的指令集!不过要修改的东西应该是存档,既然存档保存在本地,应该有硬编码的存档路径字符串,于是乎又参考 这篇文章提取出了所有的字符串(包括方法名,类名,属性名),然而没法提取出代码内部的字符串,所以这条路暂时放弃了,等以后深入研究C++之后再来尝试。
去网上搜索了一下,发现已经有人做出了破解版本,下载解包后发现是旧版的游戏,没有采用il2cpp的方式进行编译,游戏运行启动时会弹出一个Toast广告,进入游戏后发现宝石特别多,其他逻辑都一样,猜测应该是修改了存档。既然启动时进行了弹窗,说明其修改了Android启动时的流程,这一点和Assembly-CSharp.dll无关,于是开始考虑安卓部分的反编译,找到了APKDB这个神器,解包classes.dex发现这段弹窗代码如下:
public class com
{
public static void huluxia(Context paramContext)
{
String str1 = "" + "海量劲爆游戏请访问 ruansky.com" + "";
String str2 = "" + "" + "";
String str3 = "" + "" + "";
String str4 = "" + "" + "";
Toast.makeText(paramContext, Html.fromHtml(str1 + str2 + str3 + str4), 1).show();
new StringBuilder(String.valueOf("海量劲爆游戏请访问 ruansky.com")).append("").append("").append("").toString();
}
}
继续跟进调用者,最终入口处代码如下:
protected void onCreate(Bundle paramBundle)
{
SavesRestoring.DoSmth(this);
com.huluxia(this);
com.huluxia(this);
super.onCreate(paramBundle);
// ...
}
这里的SaveRestoring.DoSmth(this)大概就是修改存档的地方,原来apk内有一个data.save的文件,这个文件其实是一个zip包,里面包含存档与设备信息等等,该函数在启动时会解压缩这个压缩文件覆盖掉PlayerPrefs生成的存档,达到修改的目的。存档文件是一个xml文件,内容如下:
对比Assembly-CSharp.dll里的代码很容易就能确定Key-Value块代表的内容。经过一番修改这样就能解锁所有人物以及无限宝石之类Balabala....
但游戏里只有两条命,在不修改的人物属性的前提下想无限复活怎么办?只有修改Assembly-CSharp.dll里的逻辑了,下载ILSpy及插件Reflexil,找到RGController里的Reborn:
// RGController
public void Reborn()
{
GameObject gameObject = UnityEngine.Object.Instantiate(Resources.Load("Effect/effect_reborn")) as GameObject;
gameObject.transform.SetParent(base.transform, false);
RGGameManager.GetInstance().ShowTextInfo(base.transform.position, ">_<", 3.5f, 1f);
this.move_dir = Vector2.zero;
this.awake = true;
this.role_attribute.hp = this.role_attribute.max_hp;
this.role_attribute.armor = this.role_attribute.max_armor;
this.role_attribute.energy = this.role_attribute.max_energy;
this.UpdateStateBar();
RGGameProcess.GetInstance().has_reborn = true;
base.transform.GetComponent().enabled = true;
this.anim.SetTrigger("reborn");
this.StartHitTrigger(2f);
}
将这里的
RGGameProcess.GetInstance().has_reborn = true;
修改为
RGGameProcess.GetInstance().has_reborn = false;
就可以了。修改后替换Dll,重新打包签名即可。
总结一点,il2cpp编译的Unity游戏修改需要的门槛比较高,常规的dll则修改起来比较容易,对那些dll加密可以参考这篇文章直接Dump内存中解密后的dll。