解密与修改方法
在dnSpy中搜索找到函数 UnityEngine.ResourceManagement.ResourceProviders.AssetBundleResource.LoadWithDataProc
如下
private void LoadWithDataProc(string path, uint crc){
FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
Stream stream = m_dataProc.CreateReadStream(fileStream, m_ProvideHandle.Location.InternalId);
if (stream.CanSeek)
{
m_RequestOperation = AssetBundle.LoadFromStreamAsync(stream, crc);
return;
}
MemoryStream memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
stream.Flush();
stream.Dispose();
fileStream.Dispose();
memoryStream.Position = 0L;
m_RequestOperation = AssetBundle.LoadFromStreamAsync(memoryStream, crc);
}
将其修改为如下
private void LoadWithDataProc(string path, uint crc)
{
//Debug.Log(path);
FileStream fileStream;
Stream stream;
if (File.Exists(path + "uproc"))
{
//Debug.Log("Load " + path + "uproc");
fileStream = new FileStream(path + "uproc", FileMode.Open, FileAccess.Read);
stream = new MemoryStream();
fileStream.CopyTo(stream);
}
else
{
//Debug.Log("Load " + path);
fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
stream = this.m_dataProc.CreateReadStream(fileStream, this.m_ProvideHandle.Location.InternalId);
FileStream fileStreamNu = new FileStream(path + "uproc", FileMode.CreateNew, FileAccess.Write);
MemoryStream memoryStream2 = new MemoryStream();
stream.CopyTo(memoryStream2);
memoryStream2.Position = 0L;
memoryStream2.CopyTo(fileStreamNu);
memoryStream2.Position = 0L;
stream.Flush();
stream.Dispose();
stream = memoryStream2;
fileStreamNu.Flush();
fileStreamNu.Dispose();
}
if (stream.CanSeek)
{
this.m_RequestOperation = AssetBundle.LoadFromStreamAsync(stream, crc);
return;
}
MemoryStream memoryStream3 = new MemoryStream();
stream.CopyTo(memoryStream3);
stream.Flush();
stream.Dispose();
fileStream.Dispose();
memoryStream3.Position = 0L;
this.m_RequestOperation = AssetBundle.LoadFromStreamAsync(memoryStream3, crc);
}
原.bundle文件在加载时将生成一个对应的已解密的.bundleuproc文件,为普通AB格式,可直接在AssetStudio查看,并且游戏受该已解密文件修改的影响,但原资源文件将失去作用
原理简介
上面代码中this.m_dataProc
调用到的是在unity中设置的继承了IDataConverter
的用于加密解密的流处理器类,可能是自定义的也可能不是,不过它是什么对于这种解密方法没有影响。这里看到FileStream可以确定是在最终步骤的直接对文件流进行的加解密,将解密后的文件流从新写到对应的解密后文件,直接增加后缀一一对应,并且如果存在的话直接读取解密后(可能进行修改)的资源进行加载。
不难看出,解密的功能是可以用独立程序完成的,只要可以调用到对应的解密函数,但是这种直接读取解密后的文件的方法可以节省大量繁琐步骤,更方便对资源的修改,事实上通过调用this.m_dataProc.CreateWriteStream
即可将解密数据重新加密。
关于该流处理器类到底存放在什么地方,以及它是如何被引用的
可以追查到在函数UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider.Initialize
中有
SerializedType serializedType = JsonUtility.FromJson<SerializedType>(data);
if (serializedType.Value != null)
{
DataStreamProcessor = Activator.CreateInstance(serializedType.Value) as IDataConverter;
}
是该处理器类引用的来源。对data的追查则到了UnityEngine.ResourceManagement.Util.ObjectInitializationData
结构中的
[FormerlySerializedAs("m_data")]
[SerializeField]
private string m_Data;
而<game>\<game>_Data\StreamingAssets\aa\catalog.json
文件中的内容通过搜索可以确定是UnityEngine.AddressableAssets.ResourceLocators.ContentCatalogData
的序列化,其中的m_ResourceProviderData
中的第一项ObjectInitializationData
中的m_Data
的值则是与运行过程中Debug输出到文件的data一模一样,可以确定这就是它最源头的来源(其它地方也没找到)。
曲折经历与思路
本来打算用AssetStudio找到然后修改某个MonoBehaviour的值(已经提前在ILspy里找到代码中使用MonoBehaviour上的值的内容了),结果找不到,就简单看了看好辨识的资源,结果明显有缺少,才发现一股脑拖进去的这些AB压根没识别。然后对相关资源的代码进行排查追踪,正着找了好半天果然还是太抽象了。决定搜一搜Addressables(aa)这个和AB看起来不太一样的东西,一顿搜索发现了CN版的加密。跟着它的说明文档的思路直接搜AESStreamProcessor
就在游戏代码的程序集里找到了一个,这个是八九不离十。此时突发奇想,向其中安插throw
,直接抓住调用栈,找到这个修改的函数。当然过程中并不一帆风顺,有很多这查一查那追一追的过程,我甚至为了方便获取信息向其中加入了一个用网络进行通信的debugmsg到一个控制台程序打印出来(尤其是前期要大量重复打印各种信息的时候十分的好用),总共用时大约9h左右。