IDOLY PRIDE 资源解析
大概是第一次真正意义上自己动手从零开始拆一个游戏。
这游戏从 release 前一年多就在推上看到广告被吸引了,还大手笔出了高质量的动画,感觉制作组很用心,而且游戏 live 质量也超高,唯一让人愤慨的就是除了泳装所有角色都穿了安全裤,甚至制服下面都是黑成一坨的不明物质,能不能给人一点梦想啊喂(
另外说到 QualiArts 这个开发组,以前大学的时候还玩过挺长一段时间的「オルタナティブガールズ2」,当时也觉得非常不错,所以其实我对这个会社挺有好感的,只是最近听说 AL2 要停服了,感觉挺失落的,毕竟第一次碰到深玩过的游戏停服。嘛,估计 QA 是准备停掉 AL2 全身心投入到 IP 的开发了吧。
其实游戏刚 release 时我就想拆了,但当时只会一些最基本的逆向操作,也没空去研究逆向知识(其实是懒),最近跳槽有了一大段 neet 时间,于是就重新捡起来开始研究了。
话说当时写 ShizuruNotes 的时候也是因为疫情闲在家里的时候写的,莫非我已经社畜到就算闲下来也会干私活的地步了?
正题
不废话了,进入正题。
首先还是拿到游戏丢 IDA 一套操作,这游戏还是挺贴心的,metadata 没有经过任何混淆加密,直接就能用,简直新人之友。大概是我个菜鸡查找方式不对,一开始我在 Solis
命名空间下翻了半天,只翻到个 Solis.Common.Crypto
,向上跟踪半天断链了。然后又翻了一阵,还是经验太少了,不知该从何下手,于是就放着了。
然后晚上我躺床在想,能不能直接从游戏文件下手?于是打开 RE,进入游戏文件夹翻了一会,看到个 octocacheevai
很可疑,初步判定是 asset 的 manifest,打开一看全是乱码,肯定是加密过了。受 pcr 的影响一开始我以为这是个 sqlite 文件,于是我 google 半天 “octo” 和 “octo data” 之类的关键字,全找到些无关的东西。最后死马当活马医直接搜了个 “octocacheevai”,google 居然返回给我唯一的一条搜索结果:
?????
直接就惊了。还是个尼尔的资源解密工具。
点开这个 repo,研究了一下,发现 octocacheevai 其实是个加密过的 protobuf 文件,加密方法是常见的 AES-128-CBC,paddingMode = PKCS7。实际解密时需要先排除掉加密文件的第一字节,该字节必定是 0x01
。解密后得到的文件也要排除掉前16字节,那是 MD5 可以不用管。key 是直接用的一个长度为16的字符串取 MD5,iv 是长度为10也取 MD5。
1 | key = hashlib.md5(key).digest() |
然后直接用 protobuf 反序列化就可以了。
看完之后我管他的先点个 star,把尼尔下下来,用 RE 进入游戏文件一看,整个结构和 IP 完全一模一样(后来搜了才知道 QualiArts 和 applibot 都是 CyberAgent 旗下的,估计两家公司内部有 py)。于是先用尼尔的加密文件试了下这个轮子,能用,那么假设 IP 的解密算法真的和尼尔完全一模一样,现在我只需要知道 IP 的 key 和 iv,剩下的直接用这位大佬造好的的轮子就够了。
问题是,key 和 iv 到哪里去找?回到 dump.cs
,这下有目标了,直接找 Octo 相关的 dll(我怀疑 Octo 是个 CyberAgent 自己社内搞的一个库?)。其中 OctoManager 很可疑,在 Setup 时会传给他一个 IOctoSettings 的接口实例,这个接口里面包含有 key,可以从 Manager 中看到解密时是从这里面取的 key。
于是又向上跟踪,很奇怪的是,跟踪到一定程度之后,又断掉了。不知道这是不是 esterTion 大佬在博客中说的动态指针函数的原因。
詰んだ。
怎么办。第一想到的是动态调试,然而我个菜鸡不会啊。虽然这是迟早必须掌握的技能,但肯定需要研究一段时间,我想先用熟悉的方法搞出来再慢慢研究。
于是我想到能不能直接从内存取?能是肯定能,但内存那么大该到哪里去找呢?然后我又想到,能不能先在尼尔的内存里找已知的那个 key,然后观察内存附近的特征,再到 IP 内存里找到这个特征,key 就应该在附近了。
总之想到了就先试一下吧。打开 GG,到尼尔内存里一搜
bingo! 然后把内存 dump 出来看附近的特征
可以看到上面比较近的地方有个 Octo/Downloader。以此为特征,再把 IP 的整个内存 dump 出来,从里面去找:
只有一个匹配,点开看该地址附近
可以看到两个非常可疑的长度为16的字符串,总之先把两个复制出来。再用同样的方法找到 iv,最后把 key 和 iv 丢进上面那个 repo 去跑。
结果没报错,心里一喜,打开解密出来的文件一看
顺眼了,果然是成功了。
于是简单地改了下轮子,把整个 manifest 用 json 输出
搞定。
剩下就是处理 asset 了(顺带吐槽这游戏的动态卡面居然是 mp4 而不是 l2d,难怪每次点开卡看完一圈动态之后就直接停了)。虽然可以直接从游戏文件里把 asset 全部拷出来,但想了下以后每次更新都要拷一遍好麻烦,还是等有空写个轮子直接从服务器取好了。
最扯的是这游戏的 iv 和尼尔的 iv 完全一样(你们两个真的在搞 py 啊
剩下还有通信和数据库什么的,准备后续继续尝试一下,如果有进展再写一篇文章。话说这游戏抓包,只要我一使用 proxy 就必卡在 start 界面动不了,莫非是做了证书检测?
其实这次拆 IP 主要还是靠运气,正好有个拆尼尔的大佬做好了轮子,正好 IP 和尼尔的加密方式一样,才能让我这菜鸡舔到个边。虽然基本上都是在抱大腿,但实际解密成功还是有一种达成感的。总之第一次拆游戏发现,其实大多数时间都是在搜索资料。当然也可能是因为我什么都不懂,所以才什么都要查,别人大佬可能一看就知道了。
最后说一下存 key 和 iv 的地方。有了答案之后,我在 metadata 里面找到了他们,而 so 文件里没有存任何关于它们的信息,所以可以肯定游戏运行时从 metadata 里取了 key。但是什么时候取的,怎么取的,还没研究出来,等有空的时候我再去研究了,反正能用了就先用着再说。顺便我估计这游戏用到的 string 全都被塞在 metadata 里。
2021-10-03 更新
花了整整一周的时间泡 IDA、查资料,终于把资源搞定了,manifest 的加密部分也用正攻法找到了。这游戏反射调用真是达到变态的程度,追踪起来太恼火了。
先说之前为什么我没有在 IDA 里找到相关的密钥字符串。我去翻了一些关于 IL2CPP metadata 的 分析资料,配合之前看的 Katy 大佬写的一篇非常详尽的 从逆向的角度来看 metadata 初始化过程,当时就觉得如果要从 metadata 读数据的话肯定应该还是在初始化的地方,如果是这样的话 Il2CppDumper 理论上是应该把这些 string 都识别出来的,但为什么没有呢。于是我跑到 Il2CppDumper 的 repo,去翻了下,发现
W T F ?!
这游戏发行我拆的时候还只有 Il2CppDumper v6.6.2,然后 metadata 版本又正好是 v27……然后就懂了。下载最新的 Il2CppDumper,重新一套操作(又跑了我 6 个小时),进 IDA 一看,stringLiteral 全部都有了。
然后可以正式开始分析了。
因为知道了这游戏的 manifest 格式是 protobuf,所以一开始从 Octo.Data
入手。跟着找到 DataManager
和 OctoManager
,其中就有个 OctoFullSettings
,跟着分析了一下里面存了关于游戏 manifest 的一些设置,其中就包括解密 key,字符串是在 Solis.Common.Octo.SolisOctoSetupper.GetC
里(这名字根本想不到),iv 是在 Octo.Data.AESCrypt.Decrypt
内置。另外从枚举可以看出这游戏分了 8 个版本,生产版本(Prd = 0)和开发版本的 key 不同,这也解释了之前我在 dump 的内存附近看到了两个可疑字符串的原因
真正的解密方法是在 Octo.Data.SecureFile
,可以分析出加密数据库的第一个字节必是 0x01 作为 cipher type 使用,所以需要抛弃掉
1 | cipher.decrypt(encryptedBytes[1:]) |
然后解密完成抛弃作为 md5 的前 16 bytes
然后直接把剩下的 bytes 读出来就行了。
manifest 搞定了,于是我准备开始找资源解密,没想到这才是恶梦的开始。
Qua:
你以为 manifest 搞定了就完了?对不起我们还有更抽象的(
我在 dummy dll 和 dump.cs 里翻了整整 3 天,把包括 LocalStorage 和 FileUtil 什么的可疑字样全找遍了,期间还顺便摸清了这游戏资源 url 的格式和构造方法、photo 的读取方法、通信解密处、还有个我曾经特别怀疑的叫 AtlasUtility 的类,然而关键的解密资源毫无踪影,一度找得让我怀疑人生。最后我莫名其妙的想到个方法,直接在 System.IO.File.ReadAllBytes 和 System.IO.File.OpenRead 上进行交叉链接,结果在后者找到个 AssetBundleLoadInterceptor,大概长这个样子
注意第 76 行有一个 256LL
的立即数,当时就让我觉得,我找对地方了。
为什么这样说呢,因为我观察过几个加密的资源文件,这些文件有一个共通的特征,前 0x100
bytes 明显和其他不一样,而且后面还有明显的可读信息出现,所以我估计这游戏资源只加密了前 0x100
bytes,而正好就是这里的 256
。
从现在完事之后来看,没错,确实这个就是混淆资源文件的关键位置,然而当时点进去之后看了半天没看懂,感觉不像是在加密,于是就丢掉了(喂)。结果后来又在 dll 里面绕过来绕过去,还是没找到相应的方法,感觉还是只有这里最像,于是又捡回来开干。
点开 MaskedHeaderStream
可以看到 CryptByString
和 StringToMaskBytes
这两个方法,还记得之前尼尔那个解密 repo,我在想会不会是一样的,于是点进去看了一下,果然尼尔那边也叫这个名字,于是就加深了我这就是解密方法的信心,开始老老实实研究算法。
简单来说,资源混淆分为两个部分,一个是 stringToMaskBytes
。这个方法用于生成长度为 maskStringLength * 2
(也就是 maskStringLength << 1
)的 maskBytes
。方法很简单,先读一个 maskString
,把 maskBytes
从 0 开始的所有偶数位索引全部依序替换为 maskString
,同时把 maskBytes
从最后一位开始所有的奇数位索引全部依序替换为 maskString
从最后一位开始按位取反的值
1 | i = 0; |
这样就得到了一组 maskBytes
,然后再造一个值为 0x9B
(= 10011011)的 32 位立即数,取这个数的最低位向左位移 7 bit(= 10000000),再与这个数本身向右位移 1 bit 的值(= 01001101)做位或运算,得到 11001101
,再把之前的 maskBytes
的每一字节和这个值异或一次得到新的 maskBytes
,最后用这个新的 maskBytes
每一字节与 0x9B
本身做一次异或,就得到了最终的 maskBytes
1 | l = (unsigned int)bytesLength; |
第二部分就是真正做混淆操作的地方了,非常简单,直接拿输入的每个字节和上面生成的 maskBytes
做异或
1 | StringToMaskBytes((uint16_t*)maskString, maskStringlen, maskBytes, byteslen); |
这就是混淆算法的全部,那么现在的问题就是这个 maskString
是哪来的。
我一开始看到传的 abName
,第一反应就是嗯这肯定是文件名!于是就把文件名(MD5)拿来测试,试了半天发现出来还是一片乱码,搞得我还以为是算法我写错了,于是就又去把尼尔的 repo 翻出来看,发现尼尔的解混淆算法也大体上都差不多。我就想干脆先把别人写好的试一下看能不能找到什么头绪。于是从手机上把尼尔的资源文件随便复制了个出来,用 190nm 大佬写好的 c 文件直接拿出来解混淆,仍然传文件名作为 maskString
,好,运行!
……喵?怎么还是一片乱码……?
难道是别人大佬写错了?不应该啊。于是我决定不单独拿 c 文件出来,按着大佬写的步骤一步一步构建运行。
结果跑出来发现是对的,我一阵懵逼 what??????
既然代码是对的,那问题出在哪里呢。看了一会之后发现,所谓的 maskString
其实应该是用 assetBundleName
,而不是文件名……
现在想想那个 abName
不就是暗示了 assetBundleName
吗,我 TM 傻逼啊……
摸清楚之后,一切就顺利了。把解混淆算法用 python 重写了一遍,不得不说 python 对字节类型的处理操作太不友好了,踩了好多个坑才解决,比如 bytes 不能通过 index 取值,没有 unsigned int 等。
资源部分解决了,之后有空准备再摸一下游戏系统的部分,比如技能描述里的 x 段提高具体是多少,还有最关键的 VB 如何决定放技能优先顺序等。API 部分我还没看,但要更艰难一些,听群里 Parsee 大佬说这游戏证书检测似乎不依赖于系统(QualiArts 没想到挺 nb 啊),难怪我之前尝试抓包也根本进不去游戏。
2021-10-12 更新
好了,我觉得大概不用再浪费时间研究这个游戏了。
因为 QualiArts 的操作实在过于精辟。
这游戏 live 的所有的核心方法,包括分数计算、技能段数的具体值、还有最关键的 VB 敌我技能施放优先顺序判定等,都是在服务端执行的,客户端找不到半点代码(您们谁大佬能黑进 QA 服务器偷个代码回来吧),客户端所做的就只是组织好参数发给服务器,然后从服务器获得计算好的结果再展示出来而已。
一开始我得到这个结论的时候都还有点怀疑,QA 的服务器资源都是随便乱用的??直到我看了他们的官方技术博客,才发现还真是全部丢给服务端计算的,甚至还因为这样做导致每次 live 传输的数据量太大而去 customize 了 codec…你们直接交给客户端做要死啊…嘛不过这样也有好处,直接导致增加了使用科技的难度,难怪这游戏从开始以来排行榜都是如此正常,这是好事,但同时也使得内部逻辑无法被逆向了。反正我个菜鸡是摊手了。
References
190nm: rein-kuro
Katy: IL2CPP Tutorial: Finding loaders for obfuscated global-metadata.dat files
Katy: Practical IL2CPP Reverse Engineering: Extracting Protobuf definitions from applications using protobuf-net
esterTion: bang dream proto相关
esterTion: High School Fleet ハイフリ – 拆解相关
Google: Protocal Buffers Documentation