五、核心汇编 – UNI'S ON AIR资源提取逆向全记录

  前一篇文章中我确认了游戏数据包中的.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中完成的,而这个方法中又调用了静态类CriWareDecrypterInitialize方法,这个类的结构如下:

// 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的汇编代码:

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,果然有这个函数。

函数CRIWARE3A537A62的部分汇编代码

  发现什么了呢?居然发现了与CriWareDecrypter.Initialize中一模一样的立即数,并且同样是异或操作。仔细看了下豁然开朗了,这个版本的设置密钥过程可以概况为下面几个步骤:

  1. CriWareDecrypter.Initialize接收到key参数后转换为64位整数并与一个固定的立即数异或;
  2. 将异或后的结果保存在类地址加0x5C这个地址上,这个地址估计就是类成员temporalStorage
  3. 调用CRIWARE3A537A62函数,将CriWareDecrypter.CallbackFromNative方法的委托作为参数进去;
  4. CRIWARE3A537A62函数中调用CallbackFromNative方法;
  5. CallbackFromNative方法返回类地址加0x5C这个地址上的值;
  6. CRIWARE3A537A62函数将CallbackFromNative的返回值再与之前那个立即数进行异或;
  7. 利用异或后的数进行真正的设置密钥。

  这说明什么呢?说明一来一回两次异或后,真正的密钥又变成了一开始传入的key参数了,而且我完全搞不懂新版本这样绕一大圈相比原来直接用CriWare.criWareUnity_SetDecryptionKey设置密钥的意义在哪里。另外分析了一圈感觉authenticationFile并没有用到。

  后来想了一下,真正的密钥仍然是key参数大概是为了向后兼容吧,这样用一个旧版本CRIWARE加密的视频,换了新版本后仍然可以正常解密播放,如果只是一次异或的话,换了新版本后,需要输入的key就变了,也就不满足向后兼容了。但是我仍然不懂这样改有什么意义。


  现在对于密钥的设置过程已经清楚了,下面就是找到CriWareDecrypter.Initialize中的key参数究竟是哪里来的。然而奇怪的是,在libil2cpp.so以及global-metadata.dat这两个由所有游戏脚本编译而来的、包含了所有游戏代码逻辑的(除了原生C/C++ Unity插件,例如CRIWARE)文件中没有找到任何疑似该密钥的字符串,也没有找到任何CriWareDecrypterConfig类的赋值代码,这一度让我非常不解。

  其实这还是我之前说的问题,单单逆向过程其实看东西是非常片面的,如果熟悉Unity的开发过程,这个问题实际上是非常简单的。下篇文章就说说我是如何成功获取密钥的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注