修改一下自己玩的音游.
先前咱就在玩 Hexa Hysteria 的,然后是死了不知道多久的制作组更新了个不知道有什么更新内容的小版本,于是进入游戏提示要去 play 更新(不让登录)。
唔的咱玩的本来就是 无限钥匙+付费曲目免费游玩 的 super 破解版,咋更新?
于是想了一下,正好当天凌晨搞了一下 vpn Android, 干脆把这个登录界面的 version check 干掉,直接登录就好了。那一部分不知道什么鬼的更新内容,就不要了。
于是洛铃打算如法炮制,反编译,再去阅读混淆后的代码。
但是 Hexa Hysteria 是用 unity 制作的,核心代码都在 libil2cpp.so 于是… IDA,启动!!!
其实一开始没有启动,先是尝试了一下更改版本为高版本,例如 99.12.1.4 ,以及别的什么东西,发现单纯改版本好像也不行。而且签名会覆盖,再考虑到自己不会更新,于是把版本改成了 code = 99xxxxx 和 name = Final,包名 com.in.kawaiis.W???.HexaHysteria
然后发现不能改 app id,会出错,改了版本也容易卡住。
首先咱是先按部就班下载了 Il2CppDumper , 一个专门解 unity il2cpp的程序(Perfare 真乃神人也!!!)。幸运的是,这没有被加密,很顺利地拿到了大部分的方法和字段的真实名称,再将 libil2cpp.so 给到 ida,用脚本的方式载入 ida_with_struct_py3.py 并 将导出的 script.json 和 il2cpp.h 选择输入进去,在漫长等待后得到了一个可读性较高的数值。
首先从字符串常量开始找:
锁定关键词 StartMenu
根据关键词继续找:
CheckGameVersion 大抵是这个,还有 VersionNeedUpdate 和 fillVersionCode 还有 VersionToInt (WC为什么不读取versionCode读versionName呀wc,难怪这里“final”或巨大版本转int直接卡组…)
于是找到了 CheckGameVersion 中的逻辑
便把两个参数再改成一样的了。
那么关于GameVersion的部分呢?特别是VersionNeedUpdate
</a> 那段代码里其实没有 GameVersion 或 VersionNeedUpdate 的逻辑——你贴给我的整个函数都是
ConfirmUserSyncDataAsync的状态机,处理的是用户登录后的存档同步流程,跟版本检查无关。你说的
GameVersion和VersionNeedUpdate应该是另一个函数。如果你想让我分析,把对应的 ARM 汇编给我就行。不过既然你提了,我猜大概长这样:
1 2 3 4 5 6 7 // 大概率是个类似这样的方法 public static bool VersionNeedUpdate() { var currentVersion = GameVersion.Current; // 当前版本号 var latestVersion = GetLatestVersionFromServer(); // 从服务器拉 return latestVersion > currentVersion; }或者更具体一点,结合 Unity IL2CPP 的常见写法,可能是在启动时检查:
1 2 3 4 5 OnlineFunctionalityManager.Start() → 请求服务器 /api/version → 拿到最新版本号 → 跟本地 GameVersion 对比 → 如果需要更新 → 弹更新提示 / 跳商店把你手上
GameVersion或VersionNeedUpdate的那段 ARM 发过来,我给你反编译成 C#。
il2cpp:0000000002365A34 ; try { ; method il2cpp:0000000002365A34 MOV X0, XZR il2cpp:0000000002365A38 BL UnityEngine.Application\(get_version il2cpp:0000000002365A38 ; } // starts at 2365A34 il2cpp:0000000002365A3C CBZ X28, loc_2365EF4 il2cpp:0000000002365A40 ; try { il2cpp:0000000002365A40 MOV X1, X0 ; localVersionString il2cpp:0000000002365A44 MOV X2, X20 ; latestVersionString il2cpp:0000000002365A48 BL HexaHysteria.UI.StartMenu.StartMenuOnlineFunctionality\)VersionNeedUpdate il2cpp:0000000002365A48 ; } // starts at 2365A40 —
…省略少许调教
直接说结论:中间可以 NOP 掉一部分,但你原本改的
CSET → MOV W0, #0还是得留着,否则返回值不可控。哪些能 NOP,哪些不能
; === 序言(不能动)=== STR X20, [SP,#var_20]! ; 分配栈帧 + 保存 X20 STP X19, X30, [SP,#0x20+var_10] ; 保存 X19 和返回地址 ; =================== ; === 中间(可以全 NOP)=== MOV X0, X1 ; 可以 NOP MOV X19, X2 ; 可以 NOP BL fillVersionCode_0_0 ; 可以 NOP MOV X20, X0 ; 可以 NOP MOV X0, X19 ; 可以 NOP BL fillVersionCode_0_0 ; 可以 NOP MOV X19, X0 ; 可以 NOP MOV X0, X20 ; 可以 NOP MOV X1, XZR ; 可以 NOP BL VersionToInt ; 可以 NOP MOV W20, W0 ; 可以 NOP MOV X0, X19 ; 可以 NOP MOV X1, XZR ; 可以 NOP BL VersionToInt ; 可以 NOP ; =================== ; === 结尾(不能全 NOP,必须保留寄存器恢复和返回)=== LDP X19, X30, [SP] ; ← 不能 NOP!否则崩溃 CMP W20, W0 ; 可以 NOP(因为下面直接 MOV W0, #0) CSET W0, LT ; ← 改成 MOV W0, #0 LDR X20, [SP], #0x20 ; ← 不能 NOP!否则栈乱掉 RET ; ← 不能 NOP!修改建议
两个都改,双重保险:
地址 改前 改后 0x2364A1C CSET W0, LT(1A 9F B7 E0)MOV W0, #0(00 00 80 52)中间任意位置 全部 NOP ( 1F 20 03 D5)全部 NOP NOP 指令是
1F 20 03 D5(AArch64 的nop),4 字节,跟原来所有指令长度一致,直接覆盖就行。NOP 掉中间的好处
看着舒服,一眼就知道这函数不会干活了:
STR X20, [SP,#var_20]! STP X19, X30, [SP,#0x20+var_10] NOP NOP NOP ; 全是 NOP NOP ... ; 一直 NOP 到 BL VersionToInt 都 NOP 掉 LDP X19, X30, [SP] NOP ; CMP 也 NOP 掉 MOV W0, #0 ; 直接返回 0 LDR X20, [SP], #0x20 RET简单粗暴,IDA 里
Ctrl+F2 → Assemble一个个改就行。然后又进入 VersionNeedUpdate,全部改了 NOP,返回个 0
但不知道为何也许是因为 arm, assemble用不了要用 edit bytes。。
保存后自己签个名,测试,幸运的是即使库被修改但是没有校验。
然后,启动!