上一篇文章里面解决了UnityCN加密的问题,按理来说修改文件是一个小case因为有很多现成的工具比如UABEA。但是实际上使用uabea修改texture2D之后导入游戏会在logcat中出现报错并且游戏会在调用相关文件的时候崩溃。

这里提到了.resS文件,也就是上篇文章里按下不表的部分。我们使用assetstudio打开文件,找到texture2D文件查看dump信息,原始文件中可以看到这么一段信息

	StreamingInfo m_StreamData

unsigned int offset = 0
unsigned int size = 8388608
string path = "archive:/CAB-7cad85440178f2bd1acbf08ce895972f/CAB-7cad85440178f2bd1acbf08ce895972f.resS"

而使用uabea修改后的文件中size变成了0而path信息被抹去了。结合之前反编译il2cpp.so的内容,推测是因为游戏使用动态加载资源的方式,而修改后抹去了path信息所以无法索引到文件所以报错。但是具体什么过程我也没搞明白。

什么是.resS文件?在pc端打包的unity游戏中常常能看到level0.resS文件,在使用uabea修改文件的时候也常常看到有cab-*****和cab-****.resS两个部分。在uabea中我们常常是打开前者进行修改的,而当尝试打开后者时会被拒绝,似乎所有可读写内容都存储在前者中。我们这里遇到的是后者,也是前一篇文章里提到的Directory Info里面出现的内容。解压之后的block内容其实又可以按Directory分割,这里path里.resS指向的就是Directory Info: offset: 0x00038A6C | size: 0x00800000 | flags: 0 | path: CAB-7cad85440178f2bd1acbf08ce895972f.resS对应的数据内容。

引用UABEA作者的说法,实际上对于Texture2D和mesh这种大体积文件,在cab-*****中其实没有保存完整的数据,图片的内容是放在.resS中的。

查看UABEA和UnityPy中修改Texture2D的部分可以看到程序主动抹去了StreamingInfo m_StreamData的信息,我尝试在UnityPy的基础上做修改,因为相比UABEA,UnityPy的代码似乎更容易理解。

    @image_data.setter
    def image_data(self, data: bytes):
        self._image_data = data
        # ignore writing to cab for now until it's more stable
        # if self.version >= (5, 3) and self.m_StreamData.path:
        #     cab = self.assets_file.get_writeable_cab()
        #     if cab:
        #         self.m_StreamData.offset = cab.Position
        #         cab.write(data)
        #         self.m_StreamData.size = len(data)
        #         self.m_StreamData.path = cab.path
        #     else:
        #         self.reset_streamdata()
        self.reset_streamdata()

self.reset_streamdata()函数抹去了上面提到的StreamingInfo m_StreamData的信息。可以看到实际上是可以直接修改cab内容(也就是.resS部分)。尝试去掉注释运行,发现在原有的cab-****.resS上新增了一个.resS部分,显然这不是我想要的,我希望直接修改.resS文件。因为UnityPy没有提供删除cab文件的函数,所以我尝试使用UABEA的remove功能移除.resS文件再通过UnityPy添加。但是这损坏了文件,为什么。

在Unity\2019.4.40f1c1\Editor\Data\Tools下有一个可执行程序叫WebExtract.exe,通过`WebExtract.exe test.asset`可以分别导出上午提到的cab-****和cab-****.resS的数据。查看修改前后的数据,发现修改后的cab-****中多了一次cab-****.resS的引用。猜测是使用remove在UABEA中移除cab-****.resS数据的时候并不会从cab-****中移除引用,而unitypy添加.resS部分的时候新建了一个引用。这也只是个人猜测,具体什么原理我也不确定。

那怎么办呢。翻看源码发现实际上关键的数据替换是如下实现的

        img_data, tex_format = Texture2DConverter.image_to_texture2d(
            img, self.m_TextureFormat
        )
        
        
        self.image_data = img_data

那实际上只需要将转换后的数据直接在.resS数据中替换就可以了,而不通过unitypy

import os
from PIL import Image
import UnityPy
from UnityPy.export import Texture2DConverter

dir = r"work"
src = r"test.asset"
dst = r"work\test.asset.fix"

# set unityCN key for unityCN
# bundle_key = ""
# UnityPy.set_assetbundle_decrypt_key(bundle_key)

# load file
env = UnityPy.load(os.path.join(dir, src))

# view Texture2D info
# some file has more than one texture2D data store in .resS
textureList = {}
totalSize = 0
for obj in env.objects:
    if obj.type.name == "Texture2D":
        data = obj.read()
        # print(data.name)
        # print(f"offset:{data.m_StreamData.offset}")
        # print(f"size:{data.m_StreamData.size}")
        # print(f"path:{data.m_StreamData.path}")
        # print(f"TextureFormat:{data.m_TextureFormat}")
        textureList[data.name] = {
            "offset": data.m_StreamData.offset,
            "size": data.m_StreamData.size,
            "path": data.m_StreamData.path,
            "TextureFormat": data.m_TextureFormat
        }
        totalSize += data.m_StreamData.size

# convert data
# toLoadImg should be the same wide/height as toEditTexture
# so that img_data has the same size as data.m_StreamData.size
toEditTextureName = "example"
toLoadImg = os.path.join(dir, "example.fix.png")
img = Image.open(toLoadImg)
toEditTexture = textureList[toEditTextureName]
img_data, tex_format = Texture2DConverter.image_to_texture2d(
    img, toEditTexture["TextureFormat"]
)
# replace data with different size is possible
# when needed i will add support
if len(img_data) != toEditTexture["size"]:
    raise ValueError(f"data size missmatch,except {toEditTexture['size']} but recieved {len(img_data)}")

# save file with no compression
newFileData = bytearray(env.file.save(packer="none"))
size = len(newFileData)
begin = size - totalSize + toEditTexture["offset"]
newFileData[begin:begin + toEditTexture["size"]] = img_data
with open(dst, 'wb') as f:
    f.write(newFileData)

env = UnityPy.load(dst)
with open(dst, 'wb') as f:
    f.write(env.file.save(packer="lz4"))

Texture2DConverter.image_to_texture2d保证了相同尺寸的图片会得到相同长度的数据,如果要替换不等尺寸的图片,理论上手动修改其他部分的信息就行。但是我没这个需求也没尝试过。

文章组织的可能有点凌乱,因为很多细节被忽略掉了,如果你对BundleFile的文件结构本身有一定了解可能会容易理解一点。更多的内容可以看看这个Issue

顺手写了个web应用自动替换和打包,https://lsns.axix.top/因为似乎没什么人使用,我暂时下线了这个应用,如果你有需求的话请在评论区留言或者给我发邮件,我会考虑完善项目在github上发布