前一篇文章中我确认了游戏数据包中的.usme文件就是CRIWARE的usm视频格式文件。这说明加解密部分并不是游戏开发者自己完成的,而应该是CRIWARE Unity插件中自带的,那么找到对应的密钥应该就可以直接利用2ch帖子中的工具CRID(.usm)分離ツール v1.01进行视频的解密和转换。
0x01 libil2cpp.so
粗略看一下dump.cs
可以发现,UNI'S ON AIR中有两种播放视频的组件,一种是UnityEngine
自带的VideoPlayer
类,另一种就是CRIWARE Unity插件的Player
。我要找的Live视频正是通过CRIWARE插件播放的,但是自带的VideoPlayer
似乎也被用来播放一些其他的视频。那为什么放着现成的VideoPlayer
不用还要同时用两套视频播放组件呢?可能是因为CRIWARE插件加密起来更方便吧。
根据Headcrabbed大佬『CGSS 核心反向过程实录』中的经验,我找到了CriWareInitializer
这个类,并且在它的成员中发现了CriWareDecrypterConfig
这个类,它的结构如下:
// Namespace: [Serializable] public class CriWareDecrypterConfig // TypeDefIndex: 4643 { // Fields public string key; // 0x8 public string authenticationFile; // 0xC public bool enableAtomDecryption; // 0x10 public bool enableManaDecryption; // 0x11 // Methods public void .ctor(); // RVA: 0xF62C84 Offset: 0xF62C84 }
显然这是用来设置解密参数的,而且这大概意味着音频和视频用的是同一个密钥进行加密,找到密钥还可以顺带解密音频玩一玩。具体的设置密钥过程应该是在CriWareInitializer.InitializeDecrypter
中完成的,而这个方法中又调用了静态类CriWareDecrypter
的Initialize
方法,这个类的结构如下:
// Namespace: public static class CriWareDecrypter // TypeDefIndex: 4623 { // Fields private static ulong temporalStorage; // 0x0 // Methods public static bool Initialize(CriWareDecrypterConfig config); // RVA: 0xF62238 Offset: 0xF62238 public static bool Initialize(string key, string authenticationFile, bool enableAtomDecryption, bool enableManaDecryption); // RVA: 0xF62330 Offset: 0xF62330 [MonoPInvokeCallbackAttribute] // RVA: 0x48AD30 Offset: 0x48AD30 private static ulong CallbackFromNative(IntPtr ptr1); // RVA: 0xF621AC Offset: 0xF621AC public static extern int CRIWARE3A537A62(bool enable_atom_decryption, bool enable_mana_decryption, CriWareDecrypter.CallbackFromNativeDelegate func, IntPtr obj); // RVA: 0xF625F0 Offset: 0xF625F0 private static void .cctor(); // RVA: 0xF62718 Offset: 0xF62718 }
然而奇怪的是,在CRIWARE插件的官方开发文档中并没有任何CriWareDecrypterConfig
类或者CriWareDecrypter
类的手册,而CriWareInitializer
这个类的说明及手册让人感觉根本不存在解密设置一样。所以会不会是游戏开发者根本不需要进行加解密设置,CRIWARE会自动进行加解密这个过程呢?
来看一下CriWareDecrypter.Initialize
的汇编代码:
看到什么了吗,立即数!在这种地方出现这么大的立即数,会不会就是写死在代码里的密钥呢?再来看看代码逻辑,如果传入了string key
这个参数,则会将其传入System.Convert.ToInt64
中转换成整数,之后与0xD47EB533AEF7E5
这个数进行异或操作。那是不是说,这个0xD47EB533AEF7E5
就是默认密码呢?我立刻用这个数解密视频试了试,然而很遗憾,得到的仍然全是花屏。方法参数中还有一个string authenticationFile
,说明密钥也可以由文件来提供,但是看了看软件包中似乎并不像是有认证文件这种东西,于是我还是把突破点放在寻找这个key
参数上。
按照一般的编程习惯猜测,应该可以在字符串常量中找到这个key
参数。刚才提到key
会被放到System.Convert.ToInt64
转换成整数,而System.Convert
是.NET框架下的类库,可以查到它接收字符串的格式,格式不对或者超过范围都会报错,这就为搜索字符串缩小了范围。接下来我在stringliteral.json
中搜索了所有符合条件的字符串常量,然而找到的数不管是它本身还是和0xD47EB533AEF7E5
这个数异或,都没能成功解密视频。
于是回过头来再研究下CriWareDecrypter.Initialize
这个方法,这个方法中key
参数和立即数异或完了之后,似乎就再没有使用过了,authenticationFile
参数进行了一些路径拼接操作之后,似乎也没有使用过了,就最后调用了一下CriWareDecrypter.CRIWARE3A537A62
这个静态外部方法,但是这个方法看上去并没有传递密钥等参数。在Headcrabbed大佬的博客中,CRIWARE Unity插件是通过CriWare.criWareUnity_SetDecryptionKey
这个方法来调用libcri_ware_unity.so
库中的函数进行设置密钥的,然而我并没有看到有这个函数。
0x02 libcri_ware_unity.so
CRIWARE的核心代码都在预先编译好的libcri_ware_unity.so
以及libcri_mana_vpx.so
库里面,要设置密钥的话,肯定要将密钥传递给这些库。猜测可能是由于CRIWARE版本更新的原因,这部分代码有所变动所以和之前的版本不一样了,对于现在这个版本的CRIWARE来说,最有可能传递密钥的就是这个CRIWARE3A537A62
了,这个应该也是libcri_ware_unity.so
中的导出函数。所以需要反编译libcri_ware_unity.so
来看看密钥到底是怎么传递的,同样打开IDA把它加载进来进行分析,然后在函数窗口搜索CRIWARE3A537A62
,果然有这个函数。
发现什么了呢?居然发现了与CriWareDecrypter.Initialize
中一模一样的立即数,并且同样是异或操作。仔细看了下豁然开朗了,这个版本的设置密钥过程可以概况为下面几个步骤:
CriWareDecrypter.Initialize
接收到key
参数后转换为64位整数并与一个固定的立即数异或;- 将异或后的结果保存在类地址加
0x5C
这个地址上,这个地址估计就是类成员temporalStorage
; - 调用
CRIWARE3A537A62
函数,将CriWareDecrypter.CallbackFromNative
方法的委托作为参数进去; - 在
CRIWARE3A537A62
函数中调用CallbackFromNative
方法; CallbackFromNative
方法返回类地址加0x5C
这个地址上的值;CRIWARE3A537A62
函数将CallbackFromNative
的返回值再与之前那个立即数进行异或;- 利用异或后的数进行真正的设置密钥。
这说明什么呢?说明一来一回两次异或后,真正的密钥又变成了一开始传入的key
参数了,而且我完全搞不懂新版本这样绕一大圈相比原来直接用CriWare.criWareUnity_SetDecryptionKey
设置密钥的意义在哪里。另外分析了一圈感觉authenticationFile
并没有用到。
后来想了一下,真正的密钥仍然是key
参数大概是为了向后兼容吧,这样用一个旧版本CRIWARE加密的视频,换了新版本后仍然可以正常解密播放,如果只是一次异或的话,换了新版本后,需要输入的key
就变了,也就不满足向后兼容了。但是我仍然不懂这样改有什么意义。
现在对于密钥的设置过程已经清楚了,下面就是找到CriWareDecrypter.Initialize
中的key
参数究竟是哪里来的。然而奇怪的是,在libil2cpp.so
以及global-metadata.dat
这两个由所有游戏脚本编译而来的、包含了所有游戏代码逻辑的(除了原生C/C++ Unity插件,例如CRIWARE)文件中没有找到任何疑似该密钥的字符串,也没有找到任何CriWareDecrypterConfig
类的赋值代码,这一度让我非常不解。
其实这还是我之前说的问题,单单逆向过程其实看东西是非常片面的,如果熟悉Unity的开发过程,这个问题实际上是非常简单的。下篇文章就说说我是如何成功获取密钥的。