熊大快跑4.5.0

image-20260406193544889

这个熊大快跑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,发现这个可以搜索到,此刻还是挺开心的,开发者没有混淆这个字符串。

image-20260406214218474

通过这个字符串可以定位到MetadataCache::Initialize()函数,也就是sub_D186D0

image-20260407141138310

这个函数的作用是调用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; // x8
char *v3; // x9
size_t n8_1; // x0
char *v5; // x8
unsigned __int64 n8_7; // x9
char *v7; // x8
unsigned __int64 n8_4; // x9
char *v9; // x8
void *n8_5; // x9
unsigned __int64 n8_2; // x8
char *v12; // x9
__int64 v13; // x0
__int64 v14; // x1
__int64 v15; // x19
__int64 v16; // x20
__int64 v17; // x0
__int64 v18; // x21
__int64 v19; // x19
_QWORD v21[2]; // [xsp+0h] [xbp-C0h] BYREF
void *v22[2]; // [xsp+10h] [xbp-B0h] BYREF
void *v23; // [xsp+20h] [xbp-A0h]
const char *Metadata; // [xsp+28h] [xbp-98h] BYREF
__int64 n8; // [xsp+30h] [xbp-90h]
void *v26; // [xsp+38h] [xbp-88h]
char *v27; // [xsp+40h] [xbp-80h] BYREF
unsigned __int64 n8_8; // [xsp+48h] [xbp-78h]
void *v29; // [xsp+50h] [xbp-70h]
int v30; // [xsp+5Ch] [xbp-64h] BYREF
void *v31[2]; // [xsp+60h] [xbp-60h] BYREF
void *v32; // [xsp+70h] [xbp-50h]
__int64 v33; // [xsp+78h] [xbp-48h] BYREF
unsigned __int64 n8_6; // [xsp+80h] [xbp-40h]
void *v35; // [xsp+88h] [xbp-38h]
char *v36; // [xsp+90h] [xbp-30h] BYREF
unsigned __int64 n8_3; // [xsp+98h] [xbp-28h]

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解密的,跟进看一下

image-20260407144712873

按照这个逻辑,读取的位置是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,那就没问题了

image-20260407162814444

然后使用工具去dump。

image-20260406212122739

然后就是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
// Namespace: 
[Serializable]
public class Item // TypeDefIndex: 6927
{
// Fields
public int itemId; // 0x10
public string name; // 0x18
public string spriteName; // 0x20
public int price; // 0x28
public string description; // 0x30
public string commond; // 0x38
public string tag; // 0x40
private string CountStr; // 0x48
public int quality; // 0x50
private string LevelStr; // 0x58
private const string KEY_COUNT = "itemNum";
private const string KEY_LEVEL = "itemLevel";

// Properties
public int Count { get; set; }
public int Level { get; set; }

// Methods

// RVA: 0xEC033C Offset: 0xEBC33C VA: 0xEC033C
public void .ctor() { }

// RVA: 0xEC0410 Offset: 0xEBC410 VA: 0xEC0410
public void .ctor(int itemId) { }

// RVA: 0xEC04F0 Offset: 0xEBC4F0 VA: 0xEC04F0 Slot: 4
public virtual void Init() { }

[Obsolete("该方法仅用于在木块化代码时候修改数据使用", False)]
// RVA: 0xEC0640 Offset: 0xEBC640 VA: 0xEC0640 Slot: 5
public virtual void InitOld() { }

// RVA: 0xEBC754 Offset: 0xEB8754 VA: 0xEBC754
public int get_Count() { }

// RVA: 0xEBC78C Offset: 0xEB878C VA: 0xEBC78C
public void set_Count(int value) { }

// RVA: 0xEC076C Offset: 0xEBC76C VA: 0xEC076C
public int get_Level() { }

// RVA: 0xEC07A4 Offset: 0xEBC7A4 VA: 0xEC07A4
public void set_Level(int value) { }

// RVA: 0xEC07E0 Offset: 0xEBC7E0 VA: 0xEC07E0
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
// Namespace: 
public enum ItemIDCustom // TypeDefIndex: 6712
{
// Fields
public int value__; // 0x0
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
// Namespace: 
public class PetManager : MonoBehaviour // TypeDefIndex: 3545
{
// Fields
public static PetManager instance; // 0x0
private const string PETCONFIGNAME = "PetAttrConfig";
private Dictionary<int, PetAttr> m_DicPetAttr; // 0x20
private Item m_PetItem; // 0x28
private int m_PetCount; // 0x30
private OnPetEquipChangeEvent mEquipChangeEvent; // 0x38
private static int _curPetId; // 0x8
public static int m_PetEquipLevel; // 0xC

// Properties
public int cur_PetId { get; set; }
public int PetCount { get; }

// Methods

// RVA: 0x19F19C0 Offset: 0x19ED9C0 VA: 0x19F19C0
public int get_cur_PetId() { }

// RVA: 0x19F1AAC Offset: 0x19EDAAC VA: 0x19F1AAC
public void set_cur_PetId(int value) { }

// RVA: 0x19F1BCC Offset: 0x19EDBCC VA: 0x19F1BCC
public int get_PetCount() { }

// RVA: 0x19F1BD4 Offset: 0x19EDBD4 VA: 0x19F1BD4
private void Awake() { }

// RVA: 0x19F1C3C Offset: 0x19EDC3C VA: 0x19F1C3C
public void Initialize() { }

// RVA: 0x19F1C40 Offset: 0x19EDC40 VA: 0x19F1C40
private void LoadConfig() { }

// RVA: 0x19F1E4C Offset: 0x19EDE4C VA: 0x19F1E4C
private void OnLoadConfig(string _name) { }

// RVA: 0x19F1EBC Offset: 0x19EDEBC VA: 0x19F1EBC
public PetAttr GetPetAttribute(int _petId) { }

// RVA: 0x19F1FA0 Offset: 0x19EDFA0 VA: 0x19F1FA0
public int GetPetHatchHoneyNum(int _petId) { }

// RVA: 0x19F2050 Offset: 0x19EE050 VA: 0x19F2050
public void SavePetHatchHoneyNum(int _petId, int _count) { }

// RVA: 0x19F210C Offset: 0x19EE10C VA: 0x19F210C
public int GetPetUpgradeAppleNum(int _petId) { }

// RVA: 0x19F2224 Offset: 0x19EE224 VA: 0x19F2224
public void SavePetUpgradeAppleNum(int _petId, int _count) { }

// RVA: 0x19F2348 Offset: 0x19EE348 VA: 0x19F2348
public HatchState GetPetHatchStateForOldVersion(int _petId) { }

// RVA: 0x19F08E8 Offset: 0x19EC8E8 VA: 0x19F08E8
public void SavePetHatchState(int _petId, HatchState _state) { }

// RVA: 0x19F2B1C Offset: 0x19EEB1C VA: 0x19F2B1C
public HatchState GetPetHatchState(int _petId) { }

// RVA: 0x19F2C1C Offset: 0x19EEC1C VA: 0x19F2C1C
public void SavePetStartHatchTime(int _petId) { }

// RVA: 0x19F2D0C Offset: 0x19EED0C VA: 0x19F2D0C
public DateTime GetPetStartHatchTime(int _petId) { }

// RVA: 0x19F2E44 Offset: 0x19EEE44 VA: 0x19F2E44
public GameObject CreatePet() { }

// RVA: 0x19F2E4C Offset: 0x19EEE4C VA: 0x19F2E4C
public int GetOwnPetCount() { }

// RVA: 0x19F2EF8 Offset: 0x19EEEF8 VA: 0x19F2EF8
public void .ctor() { }

// RVA: 0x19F2FC0 Offset: 0x19EEFC0 VA: 0x19F2FC0
private static void .cctor() { }
}

能够知道代码中

保存孵化宠物开始孵化时间的函数是 public void SavePetHatchState(int _petId, HatchState _state)

读取开始孵化时间的函数是 public DateTime GetPetStartHatchTime(int _petId) (RVA: 0x19F2D0C)

返回值是 DateTime(时间类型),这个对象本质上是一个64位整数,记录从“公元1年”到现在的滴答数。

根据这个函数名去 ida 里面定位这两个函数

image-20260408203837345

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

image-20260408205832656

这里能够确定的就是:开始时间就是读档里的开始时间。

然后对GetPetStartHatchTime进行交叉引用,看它那里被调用了,定位到PetUIItem__RefreshItem函数

这是一个很长的UI刷新函数,读不懂,借助ai进行分析

这个函数会进行下面的操作

  1. GetPetStartHatchTime(petId)
  2. 取 cur_PetAttr->fields.hatchTime
  3. 构造 TimeSpan
  4. 调 System_DateTime__op_Addition(start, span)

在C#里面 TimeSpan 代表的是一段“时间跨度”或“时间间隔”

image-20260408213208429

那就能确定这个计算公式了

结束时间 = 开始的时间 + 孵化的时间

修改思路:

游戏判断孵化是否完成,是要设定条件的,在这个游戏里面就是 已经过去的时间 >= 孵化的时间 。

当前时间 >= 开始的时间 + 孵化的时间(即当前时间要大于等于结束时间)

问题是我们不知道这个孵化的时间是多久,这里能简单修改的就是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类偏移
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;
}

// 判断当前处理的道具 ID 是否在我们的“无限清单”中
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);

// 拦截读取 (get_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();//读出id
if (isTargetItem(itemId)) { //判断id是什么
retval.replace(CONFIG.TargetCount);//把结果改为 999999
}
} catch (e) {}
}
});

// 拦截写入 (set_Count) 不让扣钱
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); //把写入的值改为 999999
}
} catch (e) {}
}
});

console.log("无线物资已启用");
} catch (e) {
console.error("[-] 无限物资 Hook 失败: " + e);
}

//宠物秒孵化
try {
const getStartTimeAddr = ptrFromRva(base, RVAs.PetManager_GetPetStartHatchTime);
Interceptor.attach(getStartTimeAddr, {
onLeave(retval) {
// 强制返回 0 (公元1年),制造极大时间差
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); // 500ms轮询,防止设备卡顿
}

setImmediate(waitForIl2Cpp);

hook后的结果,也是直接满级了。

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

image-20260408172727880