六、获取密钥 – UNI'S ON AIR资源提取逆向全记录

  之前的文章中讲了那么多,终于要说到获取密钥了。看了看perfare大佬和Headcrabbed大佬的博客,他们的方法要么是插桩改包,要么是IDA动态调试打断点。这对于手上Android设备仅有一台没有root的Lenovo Z5的我来说太过麻烦了,而且我也不怎么会。主要还是我觉得这破联想手机root起来简直太麻烦了,都没有可以一键root的软件,以后还是买主流Android机型吧。

  前面说了在libil2cpp.so以及global-metadata.dat中没有找到任何疑似该密钥的字符串,而这两个文件又是由所有C#脚本文件编译而来的,那就说明这个密钥应该不是在C#脚本文件中赋值的。在看libil2cpp.so的汇编代码时也发现了一些可能是与动态创建C#类相关的方法,又结合我之前Cocos的开发经验,我猜C#脚本中的类是可以作为组件(Component)绑定到某一个对象(Object)上的,然后这个类的公开成员变量就可以通过Unity面板上来赋值。如果是这样的话,密钥应该会被打包进场景(Sence)的资源文件中,前面也介绍了AssetStudio可以用来解包Unity的资源文件,所以我们就用它加载apk包中的资源文件assets\bin\Data\data.unity3d看一看。

利用AssetStudio加载assets\bin\Data\data.unity3d后,在Asset List中找到了CriWareInitializer这个类

  果然,资源列表有CriWareInitializer类,这说明这个类应该如同我猜想的一样,是被绑定到某个对象上了,但是貌似看不到变量的值(其实是能看到的,后面会讲)。

  现在我有90%的把握密钥是从面板上输入的,那这样的话只要知道了面板上的参数在编译完成之后最终保存在哪里就可以找到密钥了。为此我决定自己去编译一个Unity游戏试一试。好在Unity个人版可以免费下载和使用,安装打开之后发现面板上果然有Add Component。创建一个C#脚本文件,然后参照CriWareInitializer的结构创建一个继承MonoBehaviour的类,然后就可以把脚本附加到对象上了。附加后果然可以从面板上输入参数,注意类成员如果也是自己写的类的话,这个类要加上[System.Serializable]才可以在面板上显示出来。

Unity的开发界面,当选中某一对象时,可以看到在右下角有Add Component的按钮。图中我给Main Camera添加了NewBehaviourScript这个脚本组件,可以看到类成员变量可以直接通过面板进行输入

  然后我们在面板上随便输入一串容易搜索的字符串,就可以进行编译了。不过在此之前先看看未编译的时候这个字符串是保存在哪里。进入到Unity项目文件夹下的Assets目录,里面后缀名为.unity的就是Unity的场景文件了。场景文件在编译之前是可读的文本文件,用编辑器打开它可以看到它的首行内容为%YAML 1.1,这说明场景文件其实是YAML格式的数据文件,刚才我们附加上去的脚本组件在场景文件中是这样保存的:

--- !u!114 &686896590
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 686896589}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 825c1f9618784a048a180e65249a5b20, type: 3}
  m_Name: 
  m_EditorClassIdentifier: 
  useDecrypter: 1
  decrypterConfig:
    key: testkeyy
    authenticationFile: 
    enableAtomDecryption: 0
    enableManaDecryption: 1
  key: teststringtest

  可以看到对脚本中成员变量的赋值以键值对的形式存储在了场景文件中,接下来就看看编译后的储存形式了。打开File -> Build Settings,选择Android后点左下角的Player Settings,然后Scripting Backend选IL2CPP,就可以点击Build进行编译了。

Unity的编译设置

  不过编译过程也是相当麻烦了,一会儿要安装Android SDK,一会儿又要安装Android NDK,再加上Android又是Google的产品,国内由于墙的原因连接Google又很麻烦,这个编译可是折腾了我一阵子。

  编译时间一般也是需要挺久的,即使测试项目里面什么内容都没有。编译成功后会直接生成apk文件,然后解压开看一看。在包中lib\armeabi-v7a目录下有libil2cpp.solibmain.so以及libunity.so,也就是说之前在UNI'S ON AIR里看到的其他.so文件都是插件。在assets\bin\Data中可以看到Unity编译后的资源文件,与UNI'S ON AIR只有一个data.unity3d文件不同,编译的这个测试项目下有许多以十六进制命名的文件,以及level0sharedassets0.assets等文件:

assets\bin\Data目录下保存着各种Unity资源文件

  用AssetStudio分析了一下,这种level{x}文件应该就是编译后的场景文件,虽然是二进制文件,但是用编辑器打开level0后就可以搜索到之前设置的字符串,并且AssetStudio的Asset List中也可以找到NewBehaviourScript这个资源。而sharedassets{x}.assets查了一下应该存储着场景引用的预置资源(Prefab)。

  之前在UNI'S ON AIR的资源列表中也看到了CriWareInitializer,那说明靠暴力搜索也可以找到密钥(不过要注意data.unity3d是对编译后资源文件的再一次打包,打包成了UnityFS格式,需要利用像AssetStudio这样的工具把资源文件释放出来)。

  不过对于一个纯数字的字符串来说,暴力搜索的话有点困难,符合匹配条件的太多了。如果能知道编译后文件的数据对应方式,那这个就很简单了,但是这要求对Unity的源代码非常熟悉,我现在还没那个能力。

  不过转念一想这种普适的东西难道之前没有人做过吗?突然间我发现我之前疏忽掉了一个重要的东西,那就是之前在AssetStudio中点击某一个TypeMonoBehaviour的资源后,会弹出一个选择文件夹的窗口。我之前一种不知道这个选择文件夹是干什么用的,好像关了也能用,直到我发现窗口左上角标题小小的Select Assembly Folder字样。不得不吐槽Windows的这个提示实在是太不起眼了。

点击任意MonoBehaviour资源后,弹出Select Assembly Folder对话框

  那这一下就清楚了,这是让选择编译后存放dll文件的目录,那正好之前Il2CppDumper这个工具生成的DummyDll文件夹派上了用场,选择它之后发现,参数全部显示出来了,密钥也直接看到了!

成功加载Assembly Folder后会显示出脚本组件的所有属性及其值

  不得不说看到密钥的时候真的是太激动了,这可是我第一次成功逆出来东西啊!赶快拿着密钥用CRID(.usm)分離ツール v1.01去解密.usme文件试了试,终于出图像不是花屏了!!我真的是太激动了!!!

  到这里我的逆向过程已经可以算是成功了,剩下的获取视频资源及重组就简单了。这里不得不佩服perfare大佬,他的AssetStudio和Il2CppDumper真的给Unity逆向提供了巨大的便利,没有这两个工具几乎不可能找到密钥。

  回过头来看我整个的逆向过程,由于过程中不知道路在哪里走了许多弯路,现在把路摸清楚了,获取密钥只需要这么简单的三步——解压apk、用Il2CppDumper得到DummyDll、打开AssetStudio加载资源文件以及DummyDll。不过我觉得逆向的乐趣也正是在于一步步摸清路的过程吧,如果一开始就知道路怎么走那这个逆向也就没什么意思了。

6条评论

  1. 非常感谢大佬的教程
    有一个问题,用CRID(.usm)分離ツール v1.01的时候,这个小工具的readme里面,命令是把密钥分两段了,变成-a,-b的形式,我得出的密钥和大佬一样,是13个数字,这样应该怎么分段呢?

  2. 感谢大佬的教程!
    不过有一个地方我还是不太明白,这个密钥转换成16进制后只有11位,何为低32位与高32位?希望能够得到您的指点。

    1. 密钥在代码中对应的是一个长整型 (long),long的长度是64 bit,但是CRID那个工具密钥是分成两个32 bit的int输入的,所以CRID输入的密钥是两个32 bit整数转成十六进制后的字符串。对应到16进制就是高8位和低8位,高8位不够就在前面补零。

发表评论

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