很久没写博客了,因为没东西可写,光顾着玩,再加上没有很多感兴趣的东西可以研究(人话:懒),有兴趣的又太难(人话:弱),总之依然是借前人和AI之力开展研究,提取一个游戏的资源。

初步研究:分析结构&确定方向

game_structure

简单判断一下是il2cpp的打包方式,相比mono的最大特征是只有一个单独的GameAssembly.dll,所有游戏代码(包括用户编写的和Unity自带的)均转写为原生的C++,复原和分析难度都更大。在xxx_Data中有StreamingAssets文件夹,证明资源存储在AssetBundle中,但是奇怪的是xxx_Data\StreamingAssets\aa\StandaloneWindows64中没有任何文件,理论上这里应该存放若干*.bundle,甚至还有catalog.json支持(catalog记录了所有ab包的名字),那bundle会在哪呢?

我们回到游戏主程序,它远远大于其它组件,非常异常。这和资源文件一般很大相吻合,很可能出于保护资源文件的目的,将其和游戏主程序打包在一起。

那么我们查一下主程序的壳。

protect

不知道是什么壳,但是有一段高熵异常数据,区段名字也奇奇怪怪,丢进IDA研究一下。

IDA

哈哈,入口点都在这个新区段里,真是乱七八糟,再加上一堆不能被识别为代码的数据和红色的栈指针分析失败,可以直接跑路了()直接硬攻怕是没有好下场,于是我这样分析之后就弃坑了两年。

谁来助我?

资源加载逻辑分析

在漫长的等待下出现了一篇救世主般的文章,解决了最麻烦的查询文档部分

在unity官方文档中我们定位到Addressables位于UnityEngine.ResourceManagement.dll,在vs中我们定位到异步加载bundle的函数入口为UnityEngine.ResourceManagement.ResourceProviders

虽然它是针对mono架构的,il2cpp没有这个单独的UnityEngine.ResourceManagement.dll,但是可以借鉴这个思路,只要定位到相应的函数,就可以在Unity引擎内部进行修改并导出AB包,绕过外部的保护。

那就开始吧,直接Il2cppDumper启动,获取GameAssembly.dll中相应函数的位置。

dnspy

beginoperation

参考文章中的代码,这部分有函数签名的代码也比较好懂,特别是还可以就着mono的一起看。

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
void UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__BeginOperation(
UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource_o *this,
const MethodInfo *method)
{
uint32_t m_Crc; // esi
UnityEngine_ResourceManagement_ResourceManager_o *m_ResourceManager; // xmm1_8
bool v5; // zf
UnityEngine_ResourceManagement_ResourceLocations_IResourceLocation_o *v6; // rax
Il2CppObject *m_TransformedInternalId; // rdx
System_String_o *v8; // r15
UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_o *InternalOp; // rax
UnityEngine_ResourceManagement_ResourceLocations_IResourceLocation_o *v10; // r14
UnityEngine_ResourceManagement_Exceptions_RemoteProviderException_o *v11; // rax
System_Exception_o *v12; // rbx
UnityEngine_Networking_UnityWebRequest_o *WebRequest_6458306688; // rax
UnityEngine_Networking_UnityWebRequest_o *v14; // r14
UnityEngine_Sprite_o *sprite; // rbx
UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_o *v16; // rax
_QWORD *v17; // rax
_QWORD *v18; // r8
UnityEngine_Sprite_o *v19; // rcx
bool v20; // al
__int64 v21; // rax
__int64 typeHierarchyDepth; // rcx
bool v23; // cl
_QWORD *v24; // rax
UnityEngine_Networking_DownloadHandlerAssetBundle_o *v25; // rcx
UnityEngine_ResourceManagement_WebRequestQueueOperation_o *v26; // rax
UnityEngine_ResourceManagement_WebRequestQueueOperation_o *v27; // rbx
System_Delegate_o *OnComplete; // r14
UnityEngine_Events_UnityAction_T0__o *v29; // rax
System_Delegate_o *v30; // rbp
System_Delegate_o *v31; // rax
System_Action_UnityWebRequestAsyncOperation__c *v32; // rbp
System_Delegate_o *v33; // rdi
__int64 v34; // rax
__int64 v35; // r8
System_Action_UnityWebRequestAsyncOperation__c *v36; // rbx
__int64 v37; // r8
struct UnityEngine_ResourceManagement_ResourceProviders_AssetBundleRequestOptions_o *m_Options; // rax
struct UnityEngine_AsyncOperation_o *v39; // rax
UnityEngine_AsyncOperation_o *v40; // rsi
UnityEngine_Events_UnityAction_T0__o *v41; // rax
UnityEngine_Events_UnityAction_T0__o *v42; // rbx
__int64 v43; // rax
System_Exception_o *v44; // rbx
System_String_o *v45; // rax
__int128 v46; // [rsp+30h] [rbp-38h]
int32_t loadType; // [rsp+70h] [rbp+8h] BYREF

if ( !byte_18187C4E0 )
{
sub_1801CAD00(&System_Action_UnityWebRequestAsyncOperation__TypeInfo);
sub_1801CAD00(&System_Action_AsyncOperation__TypeInfo);
sub_1801CAD00(&Method_UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource_LocalRequestOperationCompleted__);
sub_1801CAD00(&Method_UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__BeginOperation_b__34_0__);
sub_1801CAD00(&UnityEngine_Networking_DownloadHandlerAssetBundle_TypeInfo);
sub_1801CAD00(&UnityEngine_ResourceManagement_ResourceProviders_DownloadOnlyLocation_TypeInfo);
sub_1801CAD00(&Method_UnityEngine_ResourceManagement_ResourceProviders_ProvideHandle_Complete_AssetBundleResource___);
sub_1801CAD00(&UnityEngine_ResourceManagement_Exceptions_RemoteProviderException_TypeInfo);
sub_1801CAD00(&UnityEngine_ResourceManagement_WebRequestQueue_TypeInfo);
sub_1801CAD00(&StringLiteral_3528);
byte_18187C4E0 = 1;
}
m_Crc = 0;
m_ResourceManager = this->fields.m_ProvideHandle.fields.m_ResourceManager;
v5 = byte_18187C4F8 == 0;
v46 = *(_OWORD *)&this->fields.m_ProvideHandle.fields.m_Version;
loadType = 0;
this->fields.m_DownloadedBytes = 0;
if ( v5 )
{
sub_1801CAD00(&UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo);
byte_18187C4F8 = 1;
}
if ( !byte_18187C4F6 )
{
sub_1801CAD00(&UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo);
byte_18187C4F6 = 1;
}
if ( !*((_QWORD *)&v46 + 1) )
goto LABEL_54;
if ( (unsigned int)sub_180002860(
2,
UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo,
*((_QWORD *)&v46 + 1)) != (_DWORD)v46 )
{
v43 = sub_1801CAD00(&System_Exception_TypeInfo);
v44 = (System_Exception_o *)sub_1801A8CD0(v43);
sub_180002390(v44);
v45 = (System_String_o *)sub_1801CAD00(&StringLiteral_206);
System_Exception___ctor_6456892944(v44, v45, 0);
sub_1801CAD00(&Method_UnityEngine_ResourceManagement_ResourceProviders_ProvideHandle_get_InternalOp__);
sub_1801CAE10((struct __crt_stdio_stream *)v44);
}
v6 = (UnityEngine_ResourceManagement_ResourceLocations_IResourceLocation_o *)sub_180002860(
3,
UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo,
*((_QWORD *)&v46 + 1));
UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__GetLoadInfo_6458308432(
v6,
m_ResourceManager,
&loadType,
&this->fields.m_TransformedInternalId,
0);
if ( loadType == 1 )
{
m_Options = this->fields.m_Options;
if ( m_Options )
m_Crc = m_Options->fields.m_Crc;
v39 = (struct UnityEngine_AsyncOperation_o *)UnityEngine_AssetBundle__LoadFromFileAsync_6459020096(
this->fields.m_TransformedInternalId,
m_Crc,
0);
this->fields.m_RequestOperation = v39;
v40 = v39;
v41 = (UnityEngine_Events_UnityAction_T0__o *)sub_1801A8CD0(System_Action_AsyncOperation__TypeInfo);
v42 = v41;
if ( !v41 )
goto LABEL_54;
UnityEngine_Events_UnityAction_object____ctor(
v41,
(Il2CppObject *)this,
Method_UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource_LocalRequestOperationCompleted__,
0);
if ( !v40 )
goto LABEL_54;
if ( UnityEngine_AsyncOperation__get_isDone(v40, 0) )
((void (__fastcall *)(intptr_t, UnityEngine_AsyncOperation_o *, intptr_t))v42->fields.invoke_impl)(
v42->fields.method_code,
v40,
v42->fields.method);
else
UnityEngine_AsyncOperation__add_completed(v40, (System_Action_AsyncOperation__o *)v42, 0);
}
else
{
m_TransformedInternalId = (Il2CppObject *)this->fields.m_TransformedInternalId;
if ( loadType != 2 )
{
this->fields.m_RequestOperation = 0;
v8 = System_String__Format(StringLiteral_3528, m_TransformedInternalId, 0);
if ( !byte_18187C4F8 )
{
sub_1801CAD00(&UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo);
byte_18187C4F8 = 1;
}
InternalOp = UnityEngine_ResourceManagement_ResourceProviders_ProvideHandle__get_InternalOp(
&this->fields.m_ProvideHandle,
0);
if ( InternalOp )
{
v10 = (UnityEngine_ResourceManagement_ResourceLocations_IResourceLocation_o *)sub_180002860(
3,
UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo,
InternalOp);
v11 = (UnityEngine_ResourceManagement_Exceptions_RemoteProviderException_o *)sub_1801A8CD0(UnityEngine_ResourceManagement_Exceptions_RemoteProviderException_TypeInfo);
v12 = (System_Exception_o *)v11;
if ( v11 )
{
UnityEngine_ResourceManagement_Exceptions_RemoteProviderException___ctor(v11, v8, v10, 0, 0, 0);
UnityEngine_ResourceManagement_ResourceProviders_ProvideHandle__Complete_object_(
&this->fields.m_ProvideHandle,
0,
0,
v12,
(const MethodInfo_5DB490 *)Method_UnityEngine_ResourceManagement_ResourceProviders_ProvideHandle_Complete_AssetBundleResource___);
this->fields.m_Completed = 1;
return;
}
}
LABEL_54:
sub_1801CAE50(this, method);
}
this->fields.m_WebRequestCompletedCallbackCalled = 0;
WebRequest_6458306688 = UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__CreateWebRequest_6458306688(
this,
(System_String_o *)m_TransformedInternalId,
0);
v14 = WebRequest_6458306688;
if ( !WebRequest_6458306688 )
goto LABEL_54;
sprite = UnityEngine_Tilemaps_Tile__get_sprite((UnityEngine_Tilemaps_Tile_o *)WebRequest_6458306688, 0);
if ( !byte_18187C4F8 )
{
sub_1801CAD00(&UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo);
byte_18187C4F8 = 1;
}
v16 = UnityEngine_ResourceManagement_ResourceProviders_ProvideHandle__get_InternalOp(
&this->fields.m_ProvideHandle,
0);
if ( !v16 )
goto LABEL_54;
v17 = (_QWORD *)sub_180002860(
3,
UnityEngine_ResourceManagement_AsyncOperations_IGenericProviderOperation_TypeInfo,
v16);
method = (const MethodInfo *)UnityEngine_Networking_DownloadHandlerAssetBundle_TypeInfo;
v18 = v17;
if ( !sprite )
goto LABEL_54;
v19 = 0;
if ( (UnityEngine_Networking_DownloadHandlerAssetBundle_c *)sprite->klass == UnityEngine_Networking_DownloadHandlerAssetBundle_TypeInfo )
v19 = sprite;
if ( !v19 )
sub_1801CA880(sprite, UnityEngine_Networking_DownloadHandlerAssetBundle_TypeInfo, v17);
if ( v17 )
{
v21 = *v17;
typeHierarchyDepth = UnityEngine_ResourceManagement_ResourceProviders_DownloadOnlyLocation_TypeInfo->_2.typeHierarchyDepth;
v23 = *(_BYTE *)(v21 + 300) >= (unsigned __int8)typeHierarchyDepth
&& *(UnityEngine_ResourceManagement_ResourceProviders_DownloadOnlyLocation_c **)(*(_QWORD *)(v21 + 200)
+ 8 * typeHierarchyDepth
- 8) == UnityEngine_ResourceManagement_ResourceProviders_DownloadOnlyLocation_TypeInfo;
v24 = 0;
if ( v23 )
v24 = v18;
v20 = v24 == 0;
}
else
{
v20 = 1;
}
v25 = 0;
if ( (UnityEngine_Networking_DownloadHandlerAssetBundle_c *)sprite->klass == UnityEngine_Networking_DownloadHandlerAssetBundle_TypeInfo )
v25 = (UnityEngine_Networking_DownloadHandlerAssetBundle_o *)sprite;
UnityEngine_Networking_DownloadHandlerAssetBundle__set_autoLoadAssetBundle(v25, v20, 0);
v14->fields._disposeDownloadHandlerOnDispose_k__BackingField = 0;
if ( !UnityEngine_ResourceManagement_WebRequestQueue_TypeInfo->_2.cctor_finished )
il2cpp_runtime_class_init();
v26 = UnityEngine_ResourceManagement_WebRequestQueue__QueueRequest(v14, 0);
this->fields.m_WebRequestQueueOperation = v26;
v27 = v26;
if ( !v26 )
goto LABEL_54;
if ( v26->fields.m_Completed || v26->fields.Result )
{
UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__BeginWebRequestOperation(
this,
(UnityEngine_AsyncOperation_o *)v26->fields.Result,
0);
}
else
{
OnComplete = (System_Delegate_o *)v26->fields.OnComplete;
v29 = (UnityEngine_Events_UnityAction_T0__o *)sub_1801A8CD0(System_Action_UnityWebRequestAsyncOperation__TypeInfo);
v30 = (System_Delegate_o *)v29;
if ( !v29 )
goto LABEL_54;
UnityEngine_Events_UnityAction_object____ctor(
v29,
(Il2CppObject *)this,
Method_UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__BeginOperation_b__34_0__,
0);
v31 = System_Delegate__Combine(OnComplete, v30, 0);
v32 = System_Action_UnityWebRequestAsyncOperation__TypeInfo;
v33 = v31;
if ( v31 )
{
v34 = sub_1801CA850(v31, System_Action_UnityWebRequestAsyncOperation__TypeInfo);
if ( !v34 )
sub_1801CA880(v33, v32, v35);
v27->fields.OnComplete = (struct System_Action_UnityWebRequestAsyncOperation__o *)v34;
v36 = System_Action_UnityWebRequestAsyncOperation__TypeInfo;
if ( !sub_1801CA850(v33, System_Action_UnityWebRequestAsyncOperation__TypeInfo) )
sub_1801CA880(v33, v36, v37);
}
else
{
v27->fields.OnComplete = 0;
}
}
}
}

这种

1
2
3
4
5
6
if ( !byte_xx )
{
sub_1801CAD00(...)
...
byte_xx = 1;
}

的模式在前几篇也见到过,用来初始化某些值(字符串,变量)等,然后给一些变量获取值,最需要注意的是loadType = 0;,它取1和2进入两条路线。而这个值由UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource__GetLoadInfo_6458308432函数提供,Deepseek分析的结果大致是根据路径v6判断,如果是一个本地路径字符串则赋值为1,如果含://或者·jar:就认为是远程。远程的部分我们不关心,我们只看本地的部分。

loadtype = 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
25
26
27
28
29
30
if ( loadType == 1 )
{
m_Options = this->fields.m_Options;
if ( m_Options )
m_Crc = m_Options->fields.m_Crc;
v39 = (struct UnityEngine_AsyncOperation_o *)UnityEngine_AssetBundle__LoadFromFileAsync_6459020096(
this->fields.m_TransformedInternalId,
m_Crc,
0);
this->fields.m_RequestOperation = v39;
v40 = v39;
v41 = (UnityEngine_Events_UnityAction_T0__o *)sub_1801A8CD0(System_Action_AsyncOperation__TypeInfo);
v42 = v41;
if ( !v41 )
goto LABEL_54;
UnityEngine_Events_UnityAction_object____ctor(
v41,
(Il2CppObject *)this,
Method_UnityEngine_ResourceManagement_ResourceProviders_AssetBundleResource_LocalRequestOperationCompleted__,
0);
if ( !v40 )
goto LABEL_54;
if ( UnityEngine_AsyncOperation__get_isDone(v40, 0) )
((void (__fastcall *)(intptr_t, UnityEngine_AsyncOperation_o *, intptr_t))v42->fields.invoke_impl)(
v42->fields.method_code,
v40,
v42->fields.method);
else
UnityEngine_AsyncOperation__add_completed(v40, (System_Action_AsyncOperation__o *)v42, 0);
}

这部分主要做两件事:

  • 赋值v39为一个异步方法的参数,从名字可以看出来,这个将要执行的方法就是LoadFromFileAsync,也就是异步从文件加载资源。
  • 执行构造函数UnityEngine_Events_UnityAction_object____ctor,构造委托承接异步方法完成的消息。

然后如果is_doneTrue,也就是已经完成了,就直接调用v42对应的回调函数,获取最终读取的内容;否则,调用UnityEngine_AsyncOperation__add_completed将回调函数加入AsyncOperation 的事件列表,等到读取完成后将会触发该函数。

后面其他内容不关心,我就没让AI分析了。

怎么插入代码呢?

深入考究了这些伪代码的含义之后,还是不知道应该如何插入一些代码,好在在等待的时候出现了一些新工具——il2cpp也可以装载Mod了,而Mod不就是额外插入的一些代码吗,正好符合我们的需求!

下面隆重请出我们的Mod插入工具——MelonLoader

谁来助我?(2)

MelonLoader是一个Mod注入器,在游戏启动前先启动自身并加载Mod,然后再启动游戏,至于原理大概是劫持dll之类的,因为安装它之后游戏文件夹里多了一个不起眼的version.dll

很巧的是Melonloader官网上有一个Mod叫UnityExplorer,安装好之后(主要是指安装.net 6.0)可以直接在游戏界面看到游戏的各种资源和Hook面板,从单纯提取资源的角度其实使用这个资源面板就可以了,像图片资源甚至可以预览。

main_title

那我们的分析到这里圆满结束……才怪。在享用了一些图片资源之后我发现还有更多类型的资源无法预览和保存,例如视频,而这是不可接受的。因此还是要Hook这个加载资源的函数,在加载之前先读取要加载的ab包,输出到本地,再把控制权交还给Unity引擎。

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
static void Prefix(UnityEngine.ResourceManagement.ResourceProviders.AssetBundleResource __instance)
{
try
{
// 1. 从 __instance 中提取出文件的本地路径 text
var handle = __instance.m_ProvideHandle;
string text = handle.ResourceManager.TransformInternalId(handle.Location);

UnityExplorer.ExplorerCore.Log("准备加载并尝试 Dump: " + text);

// 2. 只有当它是本地文件时才进行 Dump
if (System.IO.File.Exists(text))
{
string exportDir = @"C:\AB_Dump\";
if (!System.IO.Directory.Exists(exportDir))
{
System.IO.Directory.CreateDirectory(exportDir);
}

string fileName = System.IO.Path.GetFileName(text);
string destPath = System.IO.Path.Combine(exportDir, fileName);

// 如果还没 dump 过,就执行文件流拷贝
if (!System.IO.File.Exists(destPath))
{
using (System.IO.FileStream sourceStream = new System.IO.FileStream(text, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
using (System.IO.FileStream destStream = new System.IO.FileStream(destPath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
{
byte[] buffer = new byte[4 * 1024 * 1024];
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
destStream.Write(buffer, 0, bytesRead);
}
}
UnityExplorer.ExplorerCore.Log($"[Dump成功] {fileName} 提取完毕!");
}
}
}
catch (System.Exception ex)
{
UnityExplorer.ExplorerCore.LogWarning($"[Dump失败] Exception in AssetBundleResource::BeginOperation():\n{ex}");
}
}

Gemini老师提供了一段Hook的代码,它将会在BeginOperation执行之前执行,获取待加载的ab包的路径,然后尝试读取并直接原样写入另一个目录。执行看看。

执行就不放图了,结果是无事发生,添加了Hook之后我就等着,然后什么也没有。

前版本的馈赠

没有哪个游戏一开始就是il2cpp()毕竟这个太难了。

于是我决定先从上版本入手,上版本同样隐藏了bundle文件,但是好在它使用mono打包,可以直接看这部分代码——而且比起C++好看了不止一点。

mono

插入代码也极其简单,直接右键编辑类,加入一个新函数DumpAssetBundle,然后每次BeginOperation进入第一个本地文件分支的时候调用一下。

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
private void DumpAssetBundle(string sourcePath)
{
try
{
string text = "C:\\AB_Dump\\";
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
string fileName = Path.GetFileName(sourcePath);
string path = Path.Combine(text, fileName);
if (!File.Exists(path))
{
using (FileStream fileStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (FileStream fileStream2 = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
byte[] array = new byte[4194304]; //其实是4 * 1024 * 1024,编译后简化了
int count;
while ((count = fileStream.Read(array, 0, array.Length)) > 0)
{
fileStream2.Write(array, 0, count);
}
}
}
File.AppendAllText("C:\\AB_Dump\\AB_Debug_Log.txt", "成功 Dump 文件: " + fileName + "\n");
}
}
catch (Exception ex)
{
File.AppendAllText("C:\\AB_Dump\\AB_Debug_Log.txt", string.Concat(new string[]
{
"Dump 失败 [",
sourcePath,
"]: ",
ex.Message,
"\n"
}));
}
}

就可以dump出所有的.bundle文件。但这不是重点,重点在于,游戏主界面出现之前,所有ab包已经生成了。(下图中 export 文件夹)

faster

那么同理可以推断,如果我在进入主界面之后才调用UnityExplorer的Hook功能,此时ab包早就加载完毕了,自然不会有任何作用,而想要更早调用Hook,UnityExplorer也没有显示出来。

(省流直达)最终方案

在咨询(拷打)了Gemini老师之后,它终于把焚诀交出来了。我们只要自己写一个Mod就好,MelonLoader会在游戏加载前加载所有的Mod,此时Hook便是万无一失。


以下是手把手教你如何把这段代码写成一个自动运行的 MelonLoader Mod。

第一步:准备开发环境

  1. 打开 Visual Studio
  2. 创建一个新的项目,选择 类库 (Class Library)
  3. 给项目起个名字,比如 ABDumperMod

第二步:添加必要的引用 (References)

右键项目 -> 添加引用 (Add Reference) -> 浏览,找到你游戏目录下的以下 DLL 文件并添加:

  1. MelonLoader\net6\MelonLoader.dll
  2. MelonLoader\net6\0Harmony.dll
  3. MelonLoader\net6\Il2CppInterop.Runtime.dll
  4. MelonLoader\Il2CppAssemblies\Il2Cppmscorlib.dll
  5. MelonLoader\Il2CppAssemblies\Unity.ResourceManager.dll
  6. MelonLoader\Il2CppAssemblies\UnityEngine.CoreModule.dll
  7. MelonLoader\Il2CppAssemblies\UnityEngine.AssetBundleModule.dll

第三步:复制完整的 Mod 代码

将你的 Class1.cs 重命名为 DumperMod.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
83
84
85
using System;
using System.IO;
using MelonLoader;
using HarmonyLib;
using UnityEngine;

[assembly: MelonInfo(typeof(ABDumperMod.Dumper), "Auto AB Dumper", "1.0.0", "YourName")]
[assembly: MelonGame(null, null)]

namespace ABDumperMod
{
public class Dumper : MelonMod
{
public override void OnInitializeMelon()
{
MelonLogger.Msg("=====================================================");
MelonLogger.Msg("Auto AB Dumper 已经启动!");
MelonLogger.Msg("=====================================================");
}

// Dump 逻辑
private static void DumpAB(string path)
{
try
{
if (string.IsNullOrEmpty(path)) return;

MelonLogger.Msg($"[触发拦截] 获取到路径: {path}");

if (!File.Exists(path))
{
MelonLogger.Msg($"[跳过] 物理文件不存在: {path}");
return;
}

string exportDir = @"C:\AB_Dump\";
if (!Directory.Exists(exportDir))
Directory.CreateDirectory(exportDir);

string fileName = Path.GetFileName(path);
string destPath = Path.Combine(exportDir, fileName);

if (!File.Exists(destPath))
{
MelonLogger.Msg($"[准备 Dump] 正在复制文件: {fileName}");

using (FileStream sourceStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (FileStream destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
byte[] buffer = new byte[4 * 1024 * 1024]; // 4MB
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
destStream.Write(buffer, 0, bytesRead);
}
}
MelonLogger.Msg($"[Dump 成功] {fileName} 提取完毕!");
}
}
catch (Exception ex)
{
MelonLogger.Warning($"[Dump 失败] 路径: {path}, 错误: {ex.Message}");
}
}

// 在BeginOperation执行前拦截它
[HarmonyPatch(typeof(UnityEngine.ResourceManagement.ResourceProviders.AssetBundleResource), "BeginOperation")]
public class Patch_BeginOperation
{
public static void Prefix(UnityEngine.ResourceManagement.ResourceProviders.AssetBundleResource __instance)
{
try
{
var handle = __instance.m_ProvideHandle;
string text = handle.ResourceManager.TransformInternalId(handle.Location);
DumpAB(text);
}
catch (Exception e)
{
MelonLogger.Warning("拦截 BeginOperation 失败: " + e.Message);
}
}
}
}
}

生成的ABDumperMod.dll即可导出加载过的ab包到指定目录,而且和unity引擎读取的一致。

后续工作

有了隐藏的ab包剩下的用AssetRipper解决就可以了,想要啥就要啥,拿下!

预计该Mod对所有unity引擎生成的游戏都适用,虽然据说中国版unity有加密assetbundle的特别功能,那个暂时没有解决,“谁来助我”章节提供的文章已经处理掉了。

在五月份之前的最后几分钟(4月30日23:58)提取出了这些ab包,解决了一件沉淀几年的大事,还是挺愉快的。更重要的是,所有fvn都被拿下了(小声)。虽然初心是看不惯某些游戏做个开头然后美美卖几百块的周边,但不知不觉也是提高了自己的技术——至于技术有没有用,那就不好说了。