2022-12-03:
停服前资源汇总:https://pan.baidu.com/s/1eoeaeHBbgw8sHEhsrJVHag
起源:hozuki昨天晚上在群里发的一张卡面
我刚开始看到其实还以为是同人图,后来试了一阵游戏后回去看才看出来是卡面
hzk说他是研究模型,我下载玩了一阵果然手游rpg依然不是我的爱好,就去看了看资源是怎么搞的
题外话,ios12上clutch目前没法用,我可能还是需要继续用se才能提取prcn
搞出来扔进il2cppdumper 然后 ida一条龙,后面慢慢进行初始分析的时候就可以开始看dump.cs了
基本上一上来就是搜索AssetBundle和StreamingAsset,慢慢排查就能看到有个 RAssetBundleManager ,然后类成员那里就有个明显的不得了的CryptoParameter,然后就直接找到CryptoParameter类,ctor就是目标了
// Namespace: RFW.Unity.Security.Rijndael public sealed class CryptoParameter // TypeDefIndex: 8836 { // Fields // ... // Properties // ... // Methods public void .ctor(); // RVA: 0x10081CB34 Offset: 0x81CB34 public void .ctor(int blockSize, int keySize, byte[] key, byte[] initialVector); // RVA: 0x10081CB84 Offset: 0x81CB84 public void .ctor(int blockSize, int keySize, string key, string initialVector); // RVA: 0x10081244C Offset: 0x81244C // ... }
可以看到第一个ctor涉及了RequestBase(很明显是api请求)和MDLoader3,第二个ctor涉及RAssetBundleManager。目前为了直接尝试查看资源就先直接去看asset了
然后从这个函数里就能看到很有意思的UrbanLegend类,这个类就是提供各种密钥,包括MDLoader的key【j6GWCVK9UMKKd3pnNDtxYFSZ4zHiQ9xD】+ iv【dQATZ4QY7gahQaT5】,以及资源的key【Y2VJOCQpWSNZcyRyNVJGNVd8NFctXzE7Kkw7KVZVenc=】+ iv【ZTBnJDJuUnAmIWRBUVJXP2pxeCxXPn1FI2ZKRzFEKkw=】
资源解密逻辑倒是挺有意思
$dat = file_get_contents('cr0026_c005_0.abip'); $key = implode('', array_reverse(str_split(base64_decode('Y2VJOCQpWSNZcyRyNVJGNVd8NFctXzE7Kkw7KVZVenc='), 1))); $iv = implode('', array_reverse(str_split(base64_decode('ZTBnJDJuUnAmIWRBUVJXP2pxeCxXPn1FI2ZKRzFEKkw='), 1))); $head = substr($dat, 0, 0x400); $body = substr($dat, 0x400); $headdec = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $head, MCRYPT_MODE_CBC, $iv); file_put_contents('cr0026_c005_0-dec.abip', $headdec.$body);
将两个值b64 dec,反转字符串就是key+iv,不过我最早直接全文件decrypt,然后出错。观察了文件后可以发现在0x400之后会出现明显的明文,而且这个部分用的是256bit block,取到0x400也很合理。在只解密前512字节写入文件后就能直接AssetStudio打开了
另外,资源在下载的时候和本地缓存有文件头的区别,但大体一看就可以猜出文件头的格式:
ppir魔术头,长度1 0x10,长度2 0x20,头部长度 0x30,0,文件名长度 0xD,0,文件长度 0x1D0175
好,到此资源的读取就结束了,然后就是第二部分manifest
因为首次启动没抓包,当我后续继续的时候游戏直接登陆完就进主界面了,所以应该是已经被缓存了。document底下只有两个大量文件的目录,prim都是资源,那就只能是md了。直接文件大小排序然后一秒就看到 MD_AssetbundleDLPackVer.snd
那么这个目录就是用到刚才的MDLoader的key了呗,直接扔进aes出来,打开一看文件头1f8b可还行,于是继续gzdecode,很快就搞到了manifest列表。不过这个文件是自定义格式,并不是已知的msgpack之类的。但文件格式也不算很复杂,直接看都能看出怎么读取,稍微错几下调试一波就能发现变长参数的问题,然后也很快能解决。(代码里的注释是后来看到 MD_AssetbundlePackData 类才加的)
require 'UnityBundle.php'; $dat = file_get_contents('MD_AssetbundleDLPackVer.snd'); $mdkey = 'j6GWCVK9UMKKd3pnNDtxYFSZ4zHiQ9xD'; $mdiv = 'dQATZ4QY7gahQaT5'; $dec = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $mdkey, $dat, MCRYPT_MODE_CBC, $mdiv); $pad = ord(substr($dec, -1, 1)); $dec = substr($dec, 0, -$pad); file_put_contents('MD_AssetbundleDLPackVer-dec.snd.gz', $dec); file_put_contents('MD_AssetbundleDLPackVer-dec.snd', gzdecode($dec)); $st = new FileStream('MD_AssetbundleDLPackVer-dec.snd'); $st->littleEndian = true; $st->long; $entries = $st->long; $st->long;$st->long; $map = []; echo "$entries\n"; for ($i=0; $i<$entries; $i++) { $name = readString($st); $val = [ $st->long, // size android $st->long, // size ios readString($st), // version_ios readString($st) // version android ]; $resCount = readString($st); $val[] = $resCount; $resCount = intval(substr($resCount, 1)); for ($j = 0; $j<$resCount; $j++) { $val[] = readString($st); } $map[$name] = $val; echo "\r$i"; //var_dump($name); //print_r($map[$name]); //sleep(1); } //print_r($map); file_put_contents('MD_AssetbundleDLPackVer-dec.json', json_encode($map)); function readString($stream) { $len = ord($stream->byte); return $stream->readData($len); }
public sealed class MD_AssetbundlePackData : ISerializeData, DataID // TypeDefIndex: 6604 { // Fields public string nm; // 0x10 public int sa; // 0x18 public int si; // 0x1C public string vr_ios; // 0x20 public string vr_dro; // 0x28 public string[] files; // 0x30 [JsonIgnoreAttribute] // RVA: 0x1014B716C Offset: 0x14B716C public int version; // 0x38 }
manifest是搞定了,但是素材url长这样:
http://cache.projecttokyodolls.jp/40e9d7b96420e59099eb672f65b569c0/ios/21_UI_Illust_01_Costume_005_idl.abip
这个hash还是得继续翻才能找到,继续之前的经验搜到 RAssetBundleDownloader,里面不管哪个Request最终都会汇总到 RAssetBundleDownloader__RequestPackage,然后下发到 AssetBundleEnvironment 下的 MakeURL ,再发到 MakeURLCore。虽然ida的反编译有点问题,不过基本能看出来这个hash是 md5(“${backetName}/${version}”) ,version在看到manifest具体成员名称后就能搞懂是那个字符串,但是这个backetName在搜索后发现和cdn域名一起放在了api的响应里面,所以到头来还是得拆开真正的api通信。
继续从CryptoParameter$$.ctor上去到RequestBase$$MakePostRequestParameter 里面,可以看到iv是EnvironmentData__get_CurrentApiKey() 前16字节,key是EnvironmentData__get_CurrentApiKeyNext()
03/04:
卡关了,看不懂api用的密钥是怎么来的。从类属性能知道有4个key来回循环使用,但是初始化的地方是个全局变量?而且处理函数里面一大片各种奇怪的调用和运算。debugserver挂不上,因为游戏反调试,编写插件hook函数也会导致自己退出,大概也有篡改检测。
今天早上甚至重新研究了hashcat想直接把backetName暴力出来,但我电脑太垃圾了,跑了一上午才跑完8位长度,这样下去怎么也跑不到真实桶名的
03/04 – 2:
搜api域名竟然看到旁边就是密钥……逆向迫真需要运气才能搞,不然累死都看不到东西。
查看字符串使用的地方只有一个MakeNetworkEnvironment,里面get一次调一个函数传一个字符串,所以大概上面那个大概是初始化一个长度4的数组?然后在这里才append到数组里。
大概写了一下之后,好像是出来结果了,但是iv应该是不对,因为第一个区块是错误的。然后就第二次卡关了,按照代码来看key是CurrentApiKeyNext,iv是CurrentApiKey前16字节。我甚至试了全部4个都不对,就很头疼。
更气人的是这个b64解码之后应该是gzip,gzip头10字节还有两个字节无法确定,但我遍历字节测试gzdecode居然全都失败。
救救菜鸡.jpg
03/06:
昨晚求助了hozuki看一眼到底问题出在了哪里,结果人家半小时光速代码直接完成api解码……
现在再看看上面那个代码,问题在哪里呢?
- openssl_decrypt 在此处如果不指定OPENSSL_ZERO_PADDING将会返回false
- mcrypt_decrypt里面我不知道什么时候塞了个substr($dat, 64) (????)
被自己弱智到了
login的响应里面的到backetName是dolls.prd.cdn,我在没电边缘挣扎验证 md5(“dolls.prd.cdn/60200”) 结果hash并不是这个,然后我就彻底没电睡了,hzk继续研究得出的是hash还有一步。
function MakeUrlCore(string $domain, string $backet, string $fileName, int $version, string $platform) { $hash = md5("${backet}/${version}"); return "http://${domain}/".md5( substr($hash, hexdec($hash[0])) )."/${platform}/${fileName}"; } echo MakeUrlCore( "cache.projecttokyodolls.jp", "dolls.prd.cdn", "21_UI_Illust_01_Costume_006_got.abip", 60200, "ios" ); // http://cache.projecttokyodolls.jp/40e9d7b96420e59099eb672f65b569c0/ios/21_UI_Illust_01_Costume_006_got.abip
我的主要目的并不是拆api,所以就不去研究login之后的sharedSecret加密部分了,开始着手卡面dump
任务完成
https://redive.estertion.win/tokyodolls/card/
https://gist.github.com/esterTion/6b3fd18e2f67d72fdb6d28c5fe195e8a
Bonus round:
卡面只是一堆id实在是难看,md目录下很容易看到MD_Costume.snd,但是这个解密后打开看着就很奇怪,感觉看不到正确内容。直接在dump.cs里面搜能看到MD_Costume类,成员很多,试着和之前一样反序列化之后比对指针位置,看起来确实是一个成员,但内容都是乱码。
那么应该是混淆了,去看了眼get_Name和get_Rarity,发现输出的数字要 ^ 0x7FEF,字符要通过MDManager__MDStrShift(string source, bool isAdd),其中再次调用了SimpleModules__Shift(string source, int shift, int step),根据isAdd shift和step分别是11 3和-11 -3,不过这些反序列化的调用都是isAdd=true。
内部的实现看起来有很多奇怪的地方,不过粗略抄了一个ord(source[i]) + shift后ASCII旧可以了,unicode却不对,去看了眼msdoc原来c#的char包含unicode。但是我直接mb_substr之后有的可以有的不行
这次我学会了,直接打开vs用c#试了一个,于是直接就出来了
断点看了后c#里面获取到的char并不是utf8原始字节,需要转成unicode。php有个md_ord函数是7.2+才有的,但万能的gayhub帮我找到了一个polyfill,于是虽然技能名称好像还是有点问题,但我只要卡面名所以就不关我事啦(
MD_Costume相关代码已扔在gist里
到此这次《东京「偶」像计划》的研究应该算是彻底结束了
几个吐槽:
- 你永远会在自己意识不到的时候犯几个事后觉得智障的不得了的错误
- 逆向中经验是能看懂F5和asm的前提,但运气是能找到正确地方的前提
- 前一阵主要研究了arcaea,安卓端的libcocos2dcpp.so所有函数名都在,研究起来难度就比ios什么都没有要低很多。在里面挣扎找出了Ayu碎片、血条、解歌相关的算法,然后都扔在cnwiki里面了(叉会儿腰)
- 你大佬永远是你大佬,hozuki nb!(破音)我单推hzk!
dalao能不能帮忙看下arcaea中un_k的相关算法