以Erophone为例:https://www.dlsite.com/maniax/work/=/product_id/RJ331162.html
在游戏_Data\StreamingAssets\...目录下找到.bundle资源文件,打开一看发现是加密的
这样的文件直接丢进AssetStudio根本没法提取,会一直报数组维度超范围的。
平时到这一步就直接放弃了,但是正赶上52论坛开放注册,等10分钟的时间在52学习了各个大佬的思路,突然就觉得我上我也行了。
看完这位大佬https://www.52pojie.cn/thread-1631028-1-1.html的流程后
得知在游戏_Data\Managed 目录下有Assembly-CSharp.dll文件,属于mono
类型,游戏逻辑在 Assembly-CSharp.dll
和其它同路径下的文件里,此时用 dnspy
逆向。
下载个dnspy
,把Assembly-CSharp.dll往里一拖,可以看到很多C#的函数方法。
然后按照思路要找到切入点,这里的切入点可以是游戏引擎从.bundle文件读取AssetBundle、解密、解包从中提取资源的过程。
无需关注如何解密和解密的过程,我们要的只是解密后得到的结果。所以需要在unity引擎读取.bundle文件并解密之后,拿到这个解密后的东西(从逆向出的代码可知它是Stream类型的)
在dnspy保存代码,用vscode打开,这样搜索比较方便。
搜索一下AssetBundle,发现一个class EncryptedAssetBundleProvider中有很多“AssetBundle”字段,其中的属性和方法如下图
发现它还有从网络加载AssetBundle的功能,在方法BeginOperation()中
private void BeginOperation()
{
string text = m_ProvideHandle.ResourceManager.TransformInternalId(m_ProvideHandle.Location);
if (File.Exists(text) || (Application.platform == RuntimePlatform.Android && text.StartsWith("jar:")))
{
m_RequestOperation = _0082_0083_008F_009A_0097_0088_0089_0089_0096._008D_0099_0085_009E_0080_009D_0092_0081_0080(new _0093_0093_0085_0083_0096_008A_0098_0093_008A(text, FileMode.Open, FileAccess.Read));
m_RequestOperation.completed += LocalRequestOperationCompleted;
}
else if (ResourceManagerConfig.ShouldPathUseWebRequest(text))
{
UnityWebRequest unityWebRequest = CreateWebRequest(m_ProvideHandle.Location);
unityWebRequest.disposeDownloadHandlerOnDispose = false;
m_WebRequestQueueOperation = _009B_008D_0092_008B_0083_0082_0090_0081_008E._009C_009E_009D_008B_0098_0088_0098_009D_0098(unityWebRequest);
if (m_WebRequestQueueOperation._008B_007F_0081_0082_0084_008B_0092_0091_008B)
{
m_RequestOperation = m_WebRequestQueueOperation._0093_009A_008B_008E_0095_0093_0097_008F_0091;
m_RequestOperation.completed += WebRequestOperationCompleted;
return;
}
_0090_0088_007F_009A_008E_009A_0093_0097_008B webRequestQueueOperation = m_WebRequestQueueOperation;
webRequestQueueOperation._0097_0092_008F_0081_0088_009C_0099_0097_0090 = (Action<UnityWebRequestAsyncOperation>)Delegate.Combine(webRequestQueueOperation._0097_0092_008F_0081_0088_009C_0099_0097_0090, (Action<UnityWebRequestAsyncOperation>)delegate(UnityWebRequestAsyncOperation asyncOp)
{
m_RequestOperation = asyncOp;
m_RequestOperation.completed += WebRequestOperationCompleted;
});
}
else
{
m_RequestOperation = null;
m_ProvideHandle.Complete<EncryptAssetBundleResource>(null, status: false, new Exception($"Invalid path in AssetBundleProvider: '{text}'."));
}
}
可以看到判断的第一个分支是从本地加载AssetBundle
m_RequestOperation = _0082_0083_008F_009A_0097_0088_0089_0089_0096._008D_0099_0085_009E_0080_009D_0092_0081_0080(new _0093_0093_0085_0083_0096_008A_0098_0093_008A(text, FileMode.Open, FileAccess.Read));
m_RequestOperation.completed += LocalRequestOperationCompleted;
从变量和函数名推测第二行是加载成功后的操作,那么第一行就是加载这个动作本身了
找到第一行中方法定义的位置
这个方法中又调用了上一个方法,在类中插入一个属性作为文件名,在方法中插入一段代码(12-16行,33行)实现把Stream存储到文件的功能。当游戏引擎加载AssetBundle时会多次执行5-9行我们插入的代码。
插入代码的方式一言难尽,只能通过il码的方式编写(新版本的dnspy已经可以直接写C#代码了)。你可能需要安装visual studio来编写C#代码,编译再反编译来查看il码,并且这可能需要一些计算机和编程基础。
这里放上我碰到的一个问题,dnspy一直报index超范围,后来发现是操作码写错了:
对于操作码ldarg:如果是类实例方法的话ldarg.0加载的是对象本身,也就是this,ldarg.1加载的才是方法的第一个参数;如果是类静态方法,ldarg.0就是传入的第一个参数。
il码的教程https://www.cnblogs.com/cdaniu/p/15865337.html
总之,插入的这段代码在游戏启动时被执行了,解密后的数十个AssetBundle被保存到了指定位置,再用AssetStudio就可以完美提取了!
附上修改后的Assembly-CSharp.dll文件,覆盖Managed目录中的文件。再次启动游戏时,解密后的AssetBundle就会被保存到C:\temp目录下了。
解包过程中还参考了下列文章或教程