熊大快跑4.5.0

这个熊大快跑apk文件是一个使用il2cpp打包的unity游戏。
如何判断一个安卓unity游戏是mono打包还是il2cpp打包呢?
将apk改成zip解压缩
如果看到下面这些,那就是IL2CPP
1 2 3
| lib/arm64-v8a/libil2cpp.so lib/armeabi-v7a/libil2cpp.so assets/bin/Data/Managed/Metadata/global-metadata.dat
|
如果看到下面这些,那就是Mono
1 2
| assets/bin/Data/Managed/Assembly-CSharp.dll assets/bin/Data/Managed/UnityEngine*.dll
|
并且 没有 libil2cpp.so、没有 global-metadata.dat,那基本就是 Mono。
Mono 包下,游戏逻辑通常直接在:assets/bin/Data/Managed/Assembly-CSharp.dll里。
对于IL2CPP打包的apk文件,该如何逆向呢?
先大概了解unity IL2CPP打包的流程
1 2 3 4
| C# 代码 -> IL -> IL2CPP 转成 C++ -> 编译成 libil2cpp.so
|
方法名,类名,字段名主要靠 global-metadata.dat
真实函数逻辑 在 libil2cpp.so
使用工具 Il2CppDumper
其实一般都是有防护的,就是比如对于gm.dat会进行加密等
这个就是对gm.dat文件进行加密了,第一次使用工具dump失败。
现在ai很强大了,可以使用ai去辅助我们分析和定位它的加密逻辑。
加载函数调用链:
1 2 3 4 5
| libil2cpp.so -> il2cpp_init / 相关初始化入口 -> vm::Runtime::Init -> vm::MetadataCache::Initialize -> vm::MetadataLoader::LoadMetadataFile
|
使用ida去载入libil2cpp.so文件。
等待加载好后直接搜索字符串 global-metadata.dat,发现这个可以搜索到,此刻还是挺开心的,开发者没有混淆这个字符串。
通过这个字符串可以定位到MetadataCache::Initialize()函数,也就是sub_D186D0

这个函数的作用是调用sub_D42514(“global-metadata.dat”)去加载metadata,成功后读取 metadata 头里的各个 offset/count,分配若干运行时表,然后把一批“索引型指针”修正成真正的内存地址。
所以要继续跟进sub_D42514函数,这个函数就是LoadMetadataFile函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
| __int64 __fastcall sub_D42514(const char *s) { void *n8_9; char *v3; size_t n8_1; char *v5; unsigned __int64 n8_7; char *v7; unsigned __int64 n8_4; char *v9; void *n8_5; unsigned __int64 n8_2; char *v12; __int64 v13; __int64 v14; __int64 v15; __int64 v16; __int64 v17; __int64 v18; __int64 v19; _QWORD v21[2]; void *v22[2]; void *v23; const char *Metadata; __int64 n8; void *v26; char *v27; unsigned __int64 n8_8; void *v29; int v30; void *v31[2]; void *v32; __int64 v33; unsigned __int64 n8_6; void *v35; char *v36; unsigned __int64 n8_3;
sub_D570BC(v31); Metadata = "Metadata"; n8 = 8; n8_9 = (void *)((unsigned __int64)LOBYTE(v31[0]) >> 1); if ( ((__int64)v31[0] & 1) != 0 ) v3 = (char *)v32; else v3 = (char *)v31 + 1; if ( ((__int64)v31[0] & 1) != 0 ) n8_9 = v31[1]; v27 = v3; n8_8 = (unsigned __int64)n8_9; sub_CE95F0(&v33, &v27, &Metadata); if ( ((__int64)v31[0] & 1) != 0 ) operator delete(v32); n8_1 = strlen(s); Metadata = s; n8 = n8_1; if ( (v33 & 1) != 0 ) v5 = (char *)v35; else v5 = (char *)&v33 + 1; if ( (v33 & 1) != 0 ) n8_7 = n8_6; else n8_7 = (unsigned __int64)(unsigned __int8)v33 >> 1; v27 = v5; n8_8 = n8_7; sub_CE95F0(v31, &v27, &Metadata); v30 = 0; if ( (v33 & 1) != 0 ) v7 = (char *)v35; else v7 = (char *)&v33 + 1; if ( (v33 & 1) != 0 ) n8_4 = n8_6; else n8_4 = (unsigned __int64)(unsigned __int8)v33 >> 1; v36 = v7; n8_3 = n8_4; sub_CE67B4(v22, &v36); if ( ((__int64)v22[0] & 1) != 0 ) v9 = (char *)v23; else v9 = (char *)v22 + 1; if ( ((__int64)v22[0] & 1) != 0 ) n8_5 = v22[1]; else n8_5 = (void *)((unsigned __int64)LOBYTE(v22[0]) >> 1); v36 = v9; n8_3 = (unsigned __int64)n8_5; sub_CE67B4(&Metadata, &v36); v21[0] = "global-metadata2.dat"; v21[1] = 20; n8_2 = (unsigned __int64)(unsigned __int8)Metadata >> 1; if ( ((unsigned __int8)Metadata & 1) != 0 ) v12 = (char *)v26; else v12 = (char *)&Metadata + 1; if ( ((unsigned __int8)Metadata & 1) != 0 ) n8_2 = n8; v36 = v12; n8_3 = n8_2; sub_CE95F0(&v27, &v36, v21); if ( ((unsigned __int8)Metadata & 1) != 0 ) operator delete(v26); if ( ((__int64)v22[0] & 1) != 0 ) operator delete(v23); v13 = sub_CF87CC(&v27, 3, 1, 1, 0, &v30); if ( v30 ) { v13 = sub_CF87CC(v31, 3, 1, 1, 0, &v30); if ( v30 ) { sub_CF9FF8("ERROR: Could2 not open %s"); LABEL_41: v19 = 0; goto LABEL_42; } } v15 = v13; v16 = sub_CFA184(v13, v14); v17 = sub_CF8AE0(v15, &v30); if ( v30 ) goto LABEL_41; v18 = v17; sub_CF8A0C(v15, &v30); if ( v30 ) { sub_CFA194(v16); goto LABEL_41; } v19 = sub_D424A4(v16, v18); sub_CFA194(v16); LABEL_42: if ( ((unsigned __int8)v27 & 1) != 0 ) operator delete(v29); if ( ((__int64)v31[0] & 1) != 0 ) operator delete(v32); if ( (v33 & 1) != 0 ) operator delete(v35); return v19; }
|
这个函数先取程序数据目录…/Data,拼接”Metadata”,再拼接传入的文件名s,然后又额外构造了一个路径,目标文件名是 global-metadata2.dat,尝试打开文件,如果打开失败,就回退到global-metadata.dat,成功后拿文件句柄去做读取,调用sub_D424A4(v16, v18)生成最终的返回值。
说明sub_D424A4这个函数就是对global-metadata.dat解密的,跟进看一下

按照这个逻辑,读取的位置是4,7,10,13,16……
也就是从第4字节开始,每隔3字节取出1个,然后对每个取出的字节做取反。
解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def dec(data:bytes)->bytes: out = bytearray() if len(data) >= 9: count = (len(data) - 7) // 3
for i in range(count): i = 4 + i*3 x = (~(data[i])) & 0xff out.append(x) return bytes(out)
def main(): input_file = r"E:\re\actual combat\熊大快跑\assets\bin\Data\Managed\Metadata\global-metadata.dat" output_file = r"E:\re\actual combat\熊大快跑\assets\bin\Data\Managed\Metadata\global-metadata1.dat"
with open(input_file,"rb") as f: data = f.read() result = dec(data) with open(output_file,"wb") as f: f.write(result) print("完成")
if __name__ == "__main__": main()
|
解出来的global-metadata1.dat放到010里面看看,前四字节是AF 1B B1 FA,那就没问题了

然后使用工具去dump。

然后就是ida加载libil2cpp.so文件,去依次导入script.json, il2cpp.h文件,就能进行静态分析了。
看到文件夹里面有dump.cs文件,dump.cs是工具根据global-metadata.dat和libil2cpp.so恢复出来的一份类,字段,方法等清单,方便查找定位。
| 包含的内容 |
不包含的内容 |
| 类名:比如GameManager |
函数逻辑:大括号里面的具体执行代码,显示为{ } |
| 字段:变量名和在内存里的偏移 |
局部变量:函数内部临时定义的变量名 |
| 方法名:比如Start() |
完整控制流:比如for如何循环等 |
| RVA,VA,Offset |
具体的算法实现 |
RVA是相对于模块基址的偏移,VA是真正运行时的虚拟地址(函数真正的内存地址),Offset是文件内的偏移。
VA = ImageBase + RVA
真实地址 = libli2cpp.so在内存中的基址+RVA
安卓系统每次加载so文件的起始地址是不一样的,但是RVA(也就是偏移)是不变的。
这个文件的作用:可以查找到关键函数,去ida里面静态分析。
还可以根据dump.cs里面的类名和方法名,还有地址,去hook。
在这个文件里面,分析的思路就是先搜索关键词,比如要确定想要修改金币和钻石,那就搜索coins,diamond,count等关键词。
会出现多个结果,这就要需要区分它是显示层还是真实数据层,比如:textDiamondNum,coinNum 这种Text字段大概率是显示层。
真正要看的是有get/set属性的,有Save/Init的类,有 UseItem/AddItem 这种业务函数的类。
定位到Item类,这个是管理道具的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| [Serializable] public class Item { public int itemId; public string name; public string spriteName; public int price; public string description; public string commond; public string tag; private string CountStr; public int quality; private string LevelStr; private const string KEY_COUNT = "itemNum"; private const string KEY_LEVEL = "itemLevel";
public int Count { get; set; } public int Level { get; set; }
public void .ctor() { }
public void .ctor(int itemId) { }
public virtual void Init() { }
[Obsolete("该方法仅用于在木块化代码时候修改数据使用", False)] public virtual void InitOld() { }
public int get_Count() { }
public void set_Count(int value) { }
public int get_Level() { }
public void set_Level(int value) { }
public void Save() { } }
|
private string CountStr 和 public int Count { get; set; }
对外公开使用的属性是 int 类型的 Count ,但是在底层内存里面,实际存储的变量是一个私有的字符串 CountStr。
get_Count() :读方法,比如应用商店这个界面想显示你有多少的钱,就会去读取 Item.Count ,但是实际上底层调用的是 get_Count()
它就是把实际存储的CountStr经过解密算法转成数字交出去。
set_Count():写方法, 当在游戏中吃到了金币,或者花了钱,游戏需要更新余额时,底层调用的是 set_Count。传入的 value 就是新的余额,这个方法会把新余额加密并存入 CountStr。
在 Unity il2cpp 的底层 C++ 结构中,所有类的实例(对象)都继承自 Il2CppObject。这个基类自带一个 16 字节(0x10)的对象头。
子类的第一个自定义字段 itemId 刚好存放在内存偏移 0x10 的位置。
itemId 搜索一下,会发现,这就是用于存放这个游戏里面物品的 id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| public enum ItemIDCustom { public int value__; public const ItemIDCustom None = 0; public const ItemIDCustom Diamond = 1; public const ItemIDCustom Coin = 2; public const ItemIDCustom Shield = 3; public const ItemIDCustom DoubleCoin = 4; public const ItemIDCustom Pet_Skill = 11; public const ItemIDCustom Magnet = 20; public const ItemIDCustom RideShard = 21; public const ItemIDCustom PetXiaobai = 30; public const ItemIDCustom Pet1 = 31; public const ItemIDCustom Pet2 = 32; public const ItemIDCustom Pet3 = 33; public const ItemIDCustom PetMax = 49; public const ItemIDCustom Suipian1 = 51; public const ItemIDCustom Suipian2 = 52; public const ItemIDCustom Suipian3 = 53; public const ItemIDCustom Suipian4 = 54; public const ItemIDCustom Suipian5 = 55; public const ItemIDCustom Suipian6 = 56; public const ItemIDCustom Suipian7 = 57; public const ItemIDCustom Suipian8 = 58; public const ItemIDCustom Suipian9 = 59; public const ItemIDCustom Suipian10 = 60; public const ItemIDCustom PetNet = 70; public const ItemIDCustom Fish1 = 71; public const ItemIDCustom Fish2 = 72; public const ItemIDCustom Fish3 = 73; public const ItemIDCustom Fish4 = 74; public const ItemIDCustom Fish5 = 75; public const ItemIDCustom Fish6 = 76; public const ItemIDCustom Role1 = 100; public const ItemIDCustom Role2 = 101; public const ItemIDCustom Role3 = 102; public const ItemIDCustom Role4 = 103; public const ItemIDCustom Role5 = 104; public const ItemIDCustom Role6 = 105; public const ItemIDCustom RolePackage = 122; public const ItemIDCustom PetPackage = 123; public const ItemIDCustom RiderPackage = 130; public const ItemIDCustom WeaponPackage = 131; public const ItemIDCustom TimeLimitedPackage = 132; public const ItemIDCustom Skill = 150; public const ItemIDCustom ToyCard1 = 201; public const ItemIDCustom ToyCard2 = 202; public const ItemIDCustom ToyCard3 = 203; public const ItemIDCustom ToyCard4 = 204; public const ItemIDCustom Physical = 240; public const ItemIDCustom Resurrect = 800; public const ItemIDCustom Lottery = 801; public const ItemIDCustom Free_Game = 802; public const ItemIDCustom VIP = 803; public const ItemIDCustom Level_Star = 804; public const ItemIDCustom Car = 805; public const ItemIDCustom Boomb = 806; public const ItemIDCustom Grass = 807; public const ItemIDCustom Time = 808; public const ItemIDCustom Oil = 820; public const ItemIDCustom Equip_Start = 850; public const ItemIDCustom Skin_Start = 900; public const ItemIDCustom Rider_Start = 930; public const ItemIDCustom Snow_Board = 960; public const ItemIDCustom DoubleAttack = 1000; public const ItemIDCustom HpRecover = 1001; public const ItemIDCustom YiJiBiSha = 1002; public const ItemIDCustom PAOZHU = 1003; public const ItemIDCustom ReelStart = 1020; public const ItemIDCustom FragmentStart = 1040; public const ItemIDCustom SnowMan = 1041; public const ItemIDCustom Water = 1042; public const ItemIDCustom Fertilize = 1043; public const ItemIDCustom WaterGift = 325; public const ItemIDCustom FertilizeGift = 326; public const ItemIDCustom WeaponStart = 1100; public const ItemIDCustom GameLevel = 1150; public const ItemIDCustom Egg = 1160; public const ItemIDCustom Melon = 5; public const ItemIDCustom Peach = 6; public const ItemIDCustom StartFly = 8; public const ItemIDCustom Apple = 9; public const ItemIDCustom Honey = 10; public const ItemIDCustom AppleGift = 301; public const ItemIDCustom HoneyGift = 302; public const ItemIDCustom ZhuanshuGift = 303; public const ItemIDCustom FightGift = 304; public const ItemIDCustom LimitGift = 305; public const ItemIDCustom FeedPetGift = 306; public const ItemIDCustom LuxuryFeedGift = 307; public const ItemIDCustom ShoesGift = 308; public const ItemIDCustom PropGift = 309; public const ItemIDCustom RoleGift = 310; public const ItemIDCustom CUPayMonthly = 311; public const ItemIDCustom SuperFavorableGift = 315; public const ItemIDCustom CMPayMonthly = 317; public const ItemIDCustom CollectAllCard = 1200; public const ItemIDCustom Coupon_5 = 1300; public const ItemIDCustom Coupon_15 = 1301; public const ItemIDCustom Coupon_30 = 1302; public const ItemIDCustom ShoesStart = 1500; public const ItemIDCustom Hammer = 1044; public const ItemIDCustom whiteCard = 1045; public const ItemIDCustom RedCard = 1046; public const ItemIDCustom LotteryTicket = 1047; public const ItemIDCustom WorldCupTicket = 1048; public const ItemIDCustom ResurrectionTicket = 1049; }
|
我们能看到钻石id是1,金币id是2,苹果id是9,蜂蜜id是10。
public const ItemIDCustom Diamond = 1;
public const ItemIDCustom Coin = 2;
public const ItemIDCustom Apple = 9;
public const ItemIDCustom Honey = 10;
所以对于金币和钻石的修改思路: 当拦截到get_Count或set_Count时,传入的第一个参数永远是 args[0],在面向对象编程底层的C++中,args[0] 就是 this 指针,也就是当前的 Item 对象的内存首地址,往后移动 0x10(16 字节),到达 itemId 变量的内存地址,然后用 readS32() 读出一个32位的整数 ,这个数就是itemId 变量存的值。然后根据读出的数来判断是金币(1),还是钻石(2),还是苹果(9),还是蜂蜜(10)。然后把返回值改成大数。
对于宠物的界面,还有孵化时间,孵化的时间也可以修改。
在cs文件里面能够找到下面这个关键类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| public class PetManager : MonoBehaviour { public static PetManager instance; private const string PETCONFIGNAME = "PetAttrConfig"; private Dictionary<int, PetAttr> m_DicPetAttr; private Item m_PetItem; private int m_PetCount; private OnPetEquipChangeEvent mEquipChangeEvent; private static int _curPetId; public static int m_PetEquipLevel;
public int cur_PetId { get; set; } public int PetCount { get; }
public int get_cur_PetId() { }
public void set_cur_PetId(int value) { }
public int get_PetCount() { }
private void Awake() { }
public void Initialize() { }
private void LoadConfig() { }
private void OnLoadConfig(string _name) { }
public PetAttr GetPetAttribute(int _petId) { }
public int GetPetHatchHoneyNum(int _petId) { }
public void SavePetHatchHoneyNum(int _petId, int _count) { }
public int GetPetUpgradeAppleNum(int _petId) { }
public void SavePetUpgradeAppleNum(int _petId, int _count) { }
public HatchState GetPetHatchStateForOldVersion(int _petId) { }
public void SavePetHatchState(int _petId, HatchState _state) { }
public HatchState GetPetHatchState(int _petId) { }
public void SavePetStartHatchTime(int _petId) { }
public DateTime GetPetStartHatchTime(int _petId) { }
public GameObject CreatePet() { }
public int GetOwnPetCount() { }
public void .ctor() { }
private static void .cctor() { } }
|
能够知道代码中
保存孵化宠物开始孵化时间的函数是 public void SavePetHatchState(int _petId, HatchState _state)
读取开始孵化时间的函数是 public DateTime GetPetStartHatchTime(int _petId) (RVA: 0x19F2D0C)
返回值是 DateTime(时间类型),这个对象本质上是一个64位整数,记录从“公元1年”到现在的滴答数。
根据这个函数名去 ida 里面定位这两个函数

对应的 PetManager$$GetPetStartHatchTime,会从同一个key去读回字符串。

这里能够确定的就是:开始时间就是读档里的开始时间。
然后对GetPetStartHatchTime进行交叉引用,看它那里被调用了,定位到PetUIItem__RefreshItem函数
这是一个很长的UI刷新函数,读不懂,借助ai进行分析
这个函数会进行下面的操作
- GetPetStartHatchTime(petId)
- 取 cur_PetAttr->fields.hatchTime
- 构造 TimeSpan
- 调 System_DateTime__op_Addition(start, span)
在C#里面 TimeSpan 代表的是一段“时间跨度”或“时间间隔”

那就能确定这个计算公式了
结束时间 = 开始的时间 + 孵化的时间
修改思路:
游戏判断孵化是否完成,是要设定条件的,在这个游戏里面就是 已经过去的时间 >= 孵化的时间 。
当前时间 >= 开始的时间 + 孵化的时间(即当前时间要大于等于结束时间)
问题是我们不知道这个孵化的时间是多久,这里能简单修改的就是GetPetStartHatchTime 这个开始的时间。
那么就能通过 当前时间 - 开始时间 > 孵化时间 就可以了。
那就用frida拦截这个函数,并且把开始时间改成0(公元1年),用当前时间来减去0,肯定远远大于系统设定的孵化时间,就能完成秒孵化了。
frida脚本我这里是让ai写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| "use strict";
let installed = false;
const CONFIG = { TargetCount: 999999, Items: { Diamond: 1, Coin: 2, Apple: 9, Honey: 10 } };
const RVAs = { Item_get_Count: 0xEBC754, Item_set_Count: 0xEBC78C, Item_field_itemId: 0x10, PetManager_GetPetStartHatchTime: 0x19F2D0C };
function ptrFromRva(base, rva) { return base.add(rva); }
function getIl2CppBase() { try { const modules = Process.enumerateModules(); for (let i = 0; i < modules.length; i++) { if (modules[i].name.indexOf("libil2cpp.so") !== -1) { return modules[i].base; } } } catch (e) {} return null; }
function isTargetItem(itemId) { return itemId === CONFIG.Items.Coin || itemId === CONFIG.Items.Diamond || itemId === CONFIG.Items.Apple || itemId === CONFIG.Items.Honey; }
function install(base) { if (installed) return; installed = true;
console.log("\n[*] ======================================"); console.log("libil2cpp.so 挂载成功 基址: " + base); console.log("[*] ======================================\n");
try { const itemGetCount = ptrFromRva(base, RVAs.Item_get_Count); const itemSetCount = ptrFromRva(base, RVAs.Item_set_Count);
Interceptor.attach(itemGetCount, { onEnter(args) { this.self = args[0]; }, onLeave(retval) { if (!this.self || this.self.isNull()) return; try { const itemId = this.self.add(RVAs.Item_field_itemId).readS32(); if (isTargetItem(itemId)) { retval.replace(CONFIG.TargetCount); } } catch (e) {} } });
Interceptor.attach(itemSetCount, { onEnter(args) { const self = args[0]; if (!self || self.isNull()) return; try { const itemId = self.add(RVAs.Item_field_itemId).readS32(); if (isTargetItem(itemId)) { args[1] = ptr(CONFIG.TargetCount); } } catch (e) {} } });
console.log("无线物资已启用"); } catch (e) { console.error("[-] 无限物资 Hook 失败: " + e); }
try { const getStartTimeAddr = ptrFromRva(base, RVAs.PetManager_GetPetStartHatchTime); Interceptor.attach(getStartTimeAddr, { onLeave(retval) { retval.replace(ptr(0)); } }); console.log("宠物秒孵化已启用 (RVA: 0x19F2D0C)"); } catch (e) { console.error("秒孵化 Hook 失败: " + e); }
console.log("所有魔法加载完毕"); console.log("\n[*] ======================================"); }
function waitForIl2Cpp() { console.log("[*] 正在扫描内存等待 libil2cpp.so 加载..."); const timer = setInterval(() => { const base = getIl2CppBase(); if (base !== null) { clearInterval(timer); install(base); } }, 500); }
setImmediate(waitForIl2Cpp);
|
hook后的结果,也是直接满级了。

看一下宠物界面,也是无敌了😋
