逆向记录(代号aria)
前言
: 是新游戏喔 残念,旧的,开服半年了…
某高级计算机体系结构测试deadline中,摸鱼效率MAX,简单看看。
正文
小背景
开服时有扫过一眼,感觉麻烦就没动,今天正好有朋友问,就试试好了。
亮点是剧情全程3D演出,游玩方式印象中是回合制。
我的祖传MI6开最高品质卡得不行了,不好证明是优化问题还是硬件问题。
初探——manifest与URL格式
manifest的包抓了一下,名字是
1 | https://cdn.app.*********-aria.com/v11401_14c67a0bd0d3776b19350*********ca49292a2d43127663a3a2427951f731f8/assetbundle/WebGL_Hash/bf892ddb3aa0f7a9e7597e*********f6fe215dc6861328821b763325953c0 |
(打个电子码)
不得不说有点长,可以猜测v11401那一段是版本号+某个东西的sha256,assetbundle名字也被混淆为某个东西的sha256。格式总结一下就是https://{cdn地址}/{资源版本号相关}/assetbundle/WebGL_hash/{assetbundle的hash}
。
其中资源版本号相关的可以每次拉取manifest的时候记录一下作为常量,不需要也不太可能去搞清楚如何计算的了。
打开manifest看了一眼,里面记录是正常的asset名字,推测与之前某次研究类似,即本地有一个自定义的sha256计算过程,需要走一下逆向。
初探——角色剧情
打开角色剧情看一下相关的文件,加载剧情期间下载了2个文件,猜测是剧情scripts、资源文件(包含语音、图片等)。用AssetStudio(后续简称AS)打开报错了——资源可能加密了。AS报错蕴含的信息量很大:因为如果是AES等能破坏掉header的加密方法,AS不会将之视为有效的Unity assets,也就不会走读取流程;说明是使用了Padding或者局部字符替换等不伤害header的操作。(其实还有可能是Unity版本问题,这里不是)
拖进hex editor一眼看出端倪——存在多个assetbundle头。
删掉第二个header前的部分试了下可以正常读取——是简单的Padding法。
但padding size不是定值,测试只下几个不同的assetbundle具有不同的size。
总之经过读取可以得知上述2个文件分别是图片CG包、语音包。
剧情scripts不在上述文件内,很有可能是在服务器返回等请求里,一般而言它包含各种控制、剧情文本、剧情资源文件url。因此
这个scripts对我们编写脚本作用最大。而服务器下发意味着经历过通信加密部分,想要构造请求意味着要把header里所有需要计算的部分逆向搞清楚,参考以前写过的alicerecode的代码,先看看别的。
推进剧情到动态部分时追加下载了两个文件,URL格式为https://{cdn地址}/{资源版本号相关}/Video/{Video的hash}
。
在额外的Video路径下,说明没有把mp4打包成assetbundle,可能是raw asset。
看了下hex里有ftypisom isomiso2avc1mp41等字样,印证猜想。
幕间——整理一下
目标是提取CG,那么逆向需要弄清楚这些
- 静态png:文件类型是assetbundle,如何正确进行读取、如何计算assetbundle的hash
- 动态mp4:文件类型是mp4,如何计算video的hash
正式研究——dumper & IDA
首先寻找hash部分,在dump.cs里查找hash关键词,翻到最后可以看到
1 | // Namespace: GeePlus.GPUL.AssetBundles |
结合命名空间,GetHashName应该是assetbundle的hash计算过程,GetPaddingSize就是开头填充的长度了。
video的话应该是这里
1 | // Namespace: |
接下来就是静态分析了:
- GetHashName(string assetBundleName, Hash128 assetBundleHash):
在manifest里取得assetbundleName、assetbundleHash,将之按照{assetBundleName}+{assetBundleHash}+gpl_ab
的格式构造一个字符串(加号也属于字符串而不是拼接),对该字符串计算sha256即为结果。assetbundleHash保存格式是bytes[16],需要按照02x格式化并拼接为hex string。 - GetPaddingSize(string assetBundleName, Hash128 assetBundleHash):
似乎是直接对hash128字符串进行循环求值,结果与一个常数相减。但最后看麻了就用了偷懒的方法:读取整个文件,rfind到最后一个匹配的UnityFS头的位置,seek过去进行读取。可行性与开销分析:文件数量未超过1000、平均大小在1MB,存在可行性。 - GetVideoHashName(string videoPath):
难点来了。流程是按照hashFormat去把videoPath格式化为一个字符串,计算sha256即为结果。问题在于,hashFormat疑似app初始化时服务器下发、而videoPath在剧情scripts中,也是服务器下发。
此时遗留问题为,如何简单获取hashFormat与videoPath?猜测的话,由于app初始化了hashFormat作为全局变量,因此所有videoPath都需遵循此格式参与后续计算。考虑到CDN开销,视频文件应当唯一,因此hashFormat应当也是唯一且至少在一个资源版本期间不会发生变动。videoPath也有可能是遵循一定的格式,比如{unit_id}_{scene_id}.mp4
,混淆前具有易读性更符合开发者的习惯。
总结一下:静态分析没找到Format string,那直接动态地从内存中扒出这两个变量即可。
压轴——frida
秒了
可以假定videoPath符合这样的格式:ADV/movie_{unit_id}_{scene_id}.mp4
,视频有两个就是01与02,角色id根据静态资源来就好了。
hashFormat为bless_turn_into_curse_{videoPath}
。
编写脚本进行测试,结论正确。懒得发gist了,贴一下核心部分。
1 | # asset & video path to hash |
1 | # let UnityPy read unpadded part |
写在最后
如果hashFormat发生变动了,再走一遍frida即可解决。
嗯,就这样。
更新:现在早已关服,官方上架了离线版的archive。不算贵,已经买了。