哥特少女勇闯恶魔城2 Sinisistar2 - AES - Rijindael

哥特少女勇闯恶魔城2 Sinisistar2 - AES - Rijindael

其实隔壁已经给出答案了,但我还是挺好奇是怎么来的,花了几天看了下。仅供参考。

参考文献


电报的资源

都是序列帧动画,然后基本都是拆分成好几个部分了(比如表情和脸),要看的话只能自己去Unity拼图了。

建议用Raz版AS导出,会跳过重复的资产。

虽然解决的不是很干净利索,但也算是完工了,抛砖引玉下吧。


确定加密

搜索decrypt可以发现有很多个加密/解密相关的信息。

有DES RC2 3DES Rijindael AES这几种加密算法的信息,前面这三个都过时了,很显然应该使用的是后面这两个算法,不过也得看引用情况,说不准是混淆之类的。

这里区分一下Rijindael和AES的关系:

项目 Rijndael AES
算法本体 原始算法 Rijndael 子集
分组长度bit 128–256 可变 仅 128
密钥长度bit 128–256 可变 128 / 192 / 256
轮数 依参数变化 固定(10/12/14)

这些Decryptor都是一些工具方法,而不是实际进行数据处理过程,所以需要看System_Security_Cryptography_RijndaelManagedTransform$$DecryptData以及Mono_Security_Cryptography_SymmetricTransform$$FinalDecrypt

这个FinalDecrypt不难猜到应该是对最后一个块的padding进行处理。

DecryptData 反编译后就有将近900行了,看不了一点,大致可以确定是标准的Rijindael算法,还需要判断一下工作模式,填充处理。

看到输入的参数里有paddingMode,以及fLase (应该是判断是否为Last,也就是最后一块)

接下来寻找工作模式,值得一提的是,既然需要选择padding的模式,也就说明工作模式肯定是需要padding的,对应的也就是CBC、ECB、PCBC这三种,根据经验来看,默认是CBC了。

接下来观察到这样的两个分支,很显然对2和4两个模式做了专门的处理,是PKCS7和ANSI X923。

名称 说明
ANSIX923 4 ANSIX923 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节均填充数字零。
ISO10126 5 ISO10126 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节填充随机数据。
None 1 不填充。
PKCS7 2 PKCS #7 填充字符串由一个字节序列组成,每个字节填充该字节序列的长度。
Zeros 3 填充字符串由设置为零的字节组成。
if ( paddingMode != 1 )
   {
    if ( paddingMode == 2 )
       { ... }
//...
if ( paddingMode != 3 )
    {
      this = (System_Security_Cryptography_RijndaelManagedTransform_o *)(unsigned int)(paddingMode - 4);
      if ( paddingMode == 4 )
         { ... }

接下来只要获取IV和Key就可以尝试破解了。搜索关键词key

发现这个keyExpansion出现的很频繁,但是找不到IV相关的定义,可能是由其他变量名替代了。

System_Security_Cryptography_RijndaelManagedTransform_o *v11; // rbx
v11 = this;
//...
m_decryptKeyExpansion = v11->fields.m_decryptKeyExpansion;

跳转到fields定义

struct System_Security_Cryptography_RijndaelManagedTransform_o // sizeof=0x80
00000000 {
00000000     System_Security_Cryptography_RijndaelManagedTransform_c *klass;
00000008     void *monitor;
00000010     System_Security_Cryptography_RijndaelManagedTransform_Fields fields;
00000080 };

struct __declspec(align(8)) System_Security_Cryptography_RijndaelManagedTransform_Fields // sizeof=0x70
00000000 {                                       // XREF: System_Security_Cryptography_RijndaelManagedTransform_o/r
00000000     int32_t m_cipherMode;
00000004     int32_t m_paddingValue;
00000008     int32_t m_transformMode;
0000000C     int32_t m_blockSizeBits;
00000010     int32_t m_blockSizeBytes;
00000014     int32_t m_inputBlockSize;
00000018     int32_t m_outputBlockSize;
0000001C     // padding byte
0000001D     // padding byte
0000001E     // padding byte
0000001F     // padding byte
00000020     struct System_Int32_array *m_encryptKeyExpansion;
00000028     struct System_Int32_array *m_decryptKeyExpansion;
00000030     int32_t m_Nr;
00000034     int32_t m_Nb;
00000038     int32_t m_Nk;
0000003C     // padding byte
0000003D     // padding byte
0000003E     // padding byte
0000003F     // padding byte
00000040     struct System_Int32_array *m_encryptindex;
00000048     struct System_Int32_array *m_decryptindex;
00000050     struct System_Int32_array *m_IV;
00000058     struct System_Int32_array *m_lastBlockBuffer;
00000060     struct System_Byte_array *m_depadBuffer;
00000068     struct System_Byte_array *m_shiftRegister;
00000070 };

发现了IV的信息。接下来只要找到对应的赋值语句了。

最简单的方式就是去构造函数里面看看

搜搜RijndaelManagedTransform 找到ctor 和 cctor,然后依次向上寻找调用点以及对应的ctor,尽量找相关联的,比如这里有encrypt和decrypt,肯定优先看decrypt

依旧只是调用,不能直接找到赋值。

继续向上。。。

XREF为o以及行数是1的不用看

运气不太好,这些相关的函数都没有实质性的信息。

其实也没有太多固定的方法,大多数时候要点运气,后来在dump.cs里面搜到了一个Util类

然后可以看下Util类的方法

Util
System_Security_Cryptography_RijndaelManaged_o *Util__get_aesManaged(const MethodInfo *method)
{
  Util_c *v1; // rcx
  System_Security_Cryptography_RijndaelManaged_o *v2; // rbx
  struct Util_StaticFields *static_fields; // rdx
  void *aes; // rcx
  struct System_Security_Cryptography_RijndaelManaged_o *v5; // rbx
  System_Text_Encoding_o *UTF8; // rax
  __int64 v7; // rax
  struct System_Security_Cryptography_RijndaelManaged_o *v8; // rbx
  System_Text_Encoding_o *v9; // rax
  __int64 v10; // rax

  if ( !byte_1830E4F1F )
  {
    sub_18038C080(&System_Security_Cryptography_RijndaelManaged_TypeInfo);
    sub_18038C080(&Util_TypeInfo);
    byte_1830E4F1F = 1;
  }
  v1 = Util_TypeInfo;
  if ( !Util_TypeInfo->static_fields->aes )
  {
    Util__Update(0);
    v2 = (System_Security_Cryptography_RijndaelManaged_o *)sub_18033E930(System_Security_Cryptography_RijndaelManaged_TypeInfo);
    System_Security_Cryptography_RijndaelManaged___ctor(v2, 0);
    Util_TypeInfo->static_fields->aes = v2;
    sub_18038B400(&Util_TypeInfo->static_fields->aes, v2);
    aes = Util_TypeInfo->static_fields->aes;
    if ( !aes )
      goto LABEL_14;
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 552LL))(
      aes,
      256,
      *(_QWORD *)(*(_QWORD *)aes + 560LL));
    aes = Util_TypeInfo->static_fields->aes;
    if ( !aes )
      goto LABEL_14;
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 424LL))(
      aes,
      256,
      *(_QWORD *)(*(_QWORD *)aes + 432LL));
    aes = Util_TypeInfo->static_fields->aes;
    if ( !aes )
      goto LABEL_14;
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 584LL))(
      aes,
      1,
      *(_QWORD *)(*(_QWORD *)aes + 592LL));
    v5 = Util_TypeInfo->static_fields->aes;
    UTF8 = System_Text_Encoding__get_UTF8(0);
    if ( !UTF8 )
      goto LABEL_14;
    v7 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, struct System_String_o *, const MethodInfo *))UTF8->klass->vtable._17_GetBytes.methodPtr)(
           UTF8,
           Util_TypeInfo->static_fields->IV,
           UTF8->klass->vtable._17_GetBytes.method);
    if ( !v5 )
      goto LABEL_14;
    ((void (__fastcall *)(struct System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v5->klass->vtable._10_set_IV.methodPtr)(
      v5,
      v7,
      v5->klass->vtable._10_set_IV.method);
    v8 = Util_TypeInfo->static_fields->aes;
    v9 = System_Text_Encoding__get_UTF8(0);
    aes = Util_TypeInfo;
    static_fields = Util_TypeInfo->static_fields;
    if ( !v9
      || (v10 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, struct System_String_o *, const MethodInfo *))v9->klass->vtable._17_GetBytes.methodPtr)(
                  v9,
                  static_fields->K,
                  v9->klass->vtable._17_GetBytes.method),
          !v8)
      || (((void (__fastcall *)(struct System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v8->klass->vtable._12_set_Key.methodPtr)(
            v8,
            v10,
            v8->klass->vtable._12_set_Key.method),
          (aes = Util_TypeInfo->static_fields->aes) == 0) )
    {
LABEL_14:
      sub_18038C2D0(aes, static_fields);
    }
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 616LL))(
      aes,
      2,
      *(_QWORD *)(*(_QWORD *)aes + 624LL));
    v1 = Util_TypeInfo;
  }
  return v1->static_fields->aes;
}

这里的几个数字很有趣

aes的数据类型一路溯源可以找到

struct __declspec(align(8)) System_Security_Cryptography_SymmetricAlgorithm_Fields // sizeof=0x38
00000000 {                                       // XREF: System_Security_Cryptography_SymmetricAlgorithm_o/r
00000000                                         // System_Security_Cryptography_Aes_Fields/r ...
00000000     int32_t BlockSizeValue;
00000004     int32_t FeedbackSizeValue;
00000008     struct System_Byte_array *IVValue;
00000010     struct System_Byte_array *KeyValue;
00000018     struct System_Security_Cryptography_KeySizes_array *LegalBlockSizesValue;
00000020     struct System_Security_Cryptography_KeySizes_array *LegalKeySizesValue;
00000028     int32_t KeySizeValue;
0000002C     int32_t ModeValue;
00000030     int32_t PaddingValue;
00000034     // padding byte
00000035     // padding byte
00000036     // padding byte
00000037     // padding byte
00000038 };

按照偏移量也就是顺序来看:

256只能是Keysize或者是BlockSize,得出的信息是BlockSize = 256 bit Keysize = 256bit

因为CipherMode.CBC = 1,所以对应工作模式CBC

名称
CBC 1
CFB 4
CTS 5
ECB 2
OFB 3

所以Rijndael-CBC-256

aes.IV = Encoding.UTF8.GetBytes(Util.IV);
aes.Key = Encoding.UTF8.GetBytes(Util.K);

IV 和 Key 来源是 字符串 ,使用 UTF-8编码把ASCII字符转为字节

所以可以确定原始的IV和KEY的长度跟转化后是一致的,32B也就是32个字符,这里需要记住长度,后面会用到。

然后最后这个2毫无疑问对应的是padding了

PaddingMode.PKCS7 = 2所以填充方式是PKCS7

完整的加密方式为:Rijndael-256-CBC-PKCS7


寻找 IV 和 KEY

接下来只需要找出静态域的Key和IV了

继续寻找Util相关函数,发现没有ctor,但是有一个Update完成了初始化数据。

void Util__Update(const MethodInfo *method)
{
  Il2CppObject *object; // rax
  __int64 v2; // rdx
  __int64 v3; // rcx
  Il2CppObject *v4; // rbx
  struct System_String_o *v5; // rax
  struct System_String_o *v6; // rax

  if ( !byte_1830E4F20 )
  {
    sub_18038C080(&Method_UnityEngine_Resources_Load_Unique___);
    sub_18038C080(&Util_TypeInfo);
    sub_18038C080(&StringLiteral_6649);
    sub_18038C080(&StringLiteral_10860);
    sub_18038C080(&StringLiteral_6614);
    byte_1830E4F20 = 1;
  }
  if ( System_String__IsNullOrEmpty(Util_TypeInfo->static_fields->IV, 0) )
  {
    object = UnityEngine_Resources__Load_object_(StringLiteral_10860, Method_UnityEngine_Resources_Load_Unique___);
    v4 = object;
    if ( !object )
      sub_18038C2D0(v3, v2);
    v5 = System_String__Concat_6464675520((System_String_o *)object[2].klass, StringLiteral_6614, 0);
    Util_TypeInfo->static_fields->IV = v5;
    sub_18038B400(Util_TypeInfo->static_fields, v5);
    v6 = System_String__Concat_6464675520((System_String_o *)v4[2].monitor, StringLiteral_6649, 0);
    Util_TypeInfo->static_fields->K = v6;
    sub_18038B400(&Util_TypeInfo->static_fields->K, v6);
  }
}

查看汇编视图直接得到了这几个常量的字符值。

可以初步得到的信息:

object = UnityEngine_Resources__Load_object_("Manager/unique", Method_UnityEngine_Resources_Load_Unique___);
K  = object[2].monitor + "06789412023ED45";
IV = object[2].klass + "01127802CDEF00BC";

# Il2CppObject是某一个没有确定的类型,所以取了一个笼统的名字object
00000000 struct Il2CppObject // sizeof=0x10
00000000 {                                       // XREF: MidasTouch_State_array/r
00000000                                         // UnityEngine_GameObject_array/r ...
00000000     Il2CppClass *klass;                 // XREF: sub_180248F60+48/w
00000008     void *monitor;                      // XREF: sub_180248F60+4D/w
00000010 };

object应该是从这个路径"Manager/unique"上加载的一个文件,同时Method_UnityEngine_Resources_Load_Unique___这里说明了类型是<Unique>

以及拼接使用的字符值偏移量20h,28h

可以在dump.cs找到Unique定义,发现这里的偏移量也是20和28,正好对应上了。

// Namespace: 
public class Unique : MonoBehaviour // TypeDefIndex: 673
{
	// Fields
	public string x; // 0x20
	public string y; // 0x28

	// Methods

	// RVA: 0x3FA3D0 Offset: 0x3F97D0 VA: 0x1803FA3D0
	public void .ctor() { }
}

x -> klass

y -> monitor

可以进一步细化结果

K  = y + "06789412023ED45"; # y是 32-15 = 17个字符
IV = x + "01127802CDEF00BC"; # x是 32-16 = 16个字符

所以接下来只需要找到这个unique文件即可。

但是对于Unity而言Resources资源存储在resources.assetsresources.assets.resSsharedassets0.assetssharedassets0.assets.resS等文件中,不确定最好全部导AS里面,然后查找。

大概就是这些玩意

设置里需要打开Display all assets然后导入文件,不然看不到unique。

虽然找到了这个东西,但是几乎没有任何有意义的信息。

不知道是不是使用方式有问题。


这里之前没有x和y的值,因为之前选择Monobehaviour时候AS会弹出一个框让你选择assembly加载dll还原数据,之前没想太多就关了


顺利导入后就会正常显示x和y

或者导出原始数据查看。



CE搜索内存

用IDA调试的时候游戏里一读档就一直报错卡异常,无语了。

突然发现可以用CE硬搜,前文已经得出了两个关键的拼接串,所以可以用CE直接搜索内存,但是不知道为什么这个CE没有搜索子串的方法(也可能是我没找到),只能搜索精确值。

然后可以枚举一下,因为这里的IV和KEY都是 32个字符

所以IV需要枚举前16位,KEY需要枚举前17位

其实最多也就 16 * 16 + 16 * 17 = 528 次就能搞定。

比如这里搜01127802CDEF00BC会显示有50个地址。(注意搜索的值类型)

然后向前添加一位,尝试0~F,发现添加0之后还是可以搜索到50多个地址,但是添加1就什么都没有,可以说明添加0是对的。

如果我们搜到了同一个串,那么每次添加一个位,地址应该是邻近的。

这里保存下前三个搜索结果,然后重新进行搜索:

发现地址确实跟假设的一样,是相邻的。不妨大胆一点,直接算一下地址,这里已经找到了1位,所以还剩15位,也就是地址偏移0x0F

0x1CA76F2607F - 0x0F = 0x1CA76F26070

手动添加一下地址0x1CA76F26070 ,这里自动读出来了内容。

成功得到了IVD9AB89AA56F5673001127802CDEF00BC

同理可得KEY0BFAB106A793DCA7F06789412023ED45

0x1C98502A9E0 - Ox10 = 0x1C98502A9D0

虽然有点丑陋,不过好在没有烂尾。


总结

  1. 确定加密方式Rijndael-256-CBC-PKCS7
  2. 找到IV和KEY的生成方式
  3. CE

Rijndael-256-CBC-PKCS7

IVD9AB89AA56F5673001127802CDEF00BC

KEY0BFAB106A793DCA7F06789412023ED45

注意这里的IV和KEY需要用UTF-8编码

aes.IV = Encoding.UTF8.GetBytes(Util.IV);
aes.Key = Encoding.UTF8.GetBytes(Util.K);

之前用py3rijindael来算,发现太慢了这也,byd满载跑了一晚上快8个小时,最后一个50M的文件跑了一个小时还没跑出来,不建议使用。

Rijindael.py
pythonnet甚至不要10分钟:clown_face::clown_face::clown_face:

只存了图像和音频,自取。
GamesArchive/Sinisistar2

若内容对你有所帮助,欢迎点击右下角:heart:表示支持,你的鼓励将成为我持续更新的动力(maybe)。

3 个赞

大佬厉害,就是有点小问题,我点你git下资源一直失败,好像git对大资源不是很支持,能不能用网盘传一份呢?:grinning_face:

我不用网盘,你用浏览器插件Gitzip试一下吧。
MicrosoftEdge使用Gitzip - 知乎

从git上面下载的gitzip gitdown都是没用的

主要是我本来就没打算分享资源,授人以鱼不如授人以渔的道理不必多讲,懒人和伸手党太多了,甚至还有倒卖的。
Downgit和gitzip如果目录下的文件数量太多会解析失败,如果你只下载很小的一部分就没什么问题。

方法和脚本都给了,无非就算照着做一遍

有点马后炮了其实拿il2cppdump还原出dll后就可以用assetstudio选择dump出的dll文件夹直接看到x和y的赋值了。

第一个gameobject类型是预制体,monobehaviour应该是继承monobehaviour类的挂载脚本。


反正我这是空的,不知道什么情况:thinking:

CE地址可以直接右键-浏览内存区域 不需要去硬猜
用上CE的话还有更简单的方法,对于unity游戏CE顶栏有个mono,里面有 .net info工具,可以直接查找内存中的实例并读数据 也能调用方法

CE也有调试器,也可以去给对应的方法打断点看出入参

此外 MonoBehaviour 有时加载不出来或者懒得去搞dump dll的话也可以先export raw导出原始数据然后用二进制编辑器查看

按自己的经验只是解密的话padding一般不用管 大部分文件不会因为尾部多几个字节就无法读取,也可以直接根据解出来的文件尾部字节特征确定padding类型

1 个赞

很实用,感谢补充

可能是assetstudio不太一样,有些点到monobehaviour类型时会弹出选择文件夹提示,这个时候基本上就是选择dll所在文件夹,如果是il2cpp打包则需要选择dump出的dll。

提一嘴,如果是expect raw的话,可以先在dump.cs中确定类型,再看导出的raw文件,一般是4字节小端一段一段读取,int是8字节,string是4字节长度加相应长度字节,数组是4字节数量,后面跟相应数量的类型。

1 个赞

长知识了:smiling_face_with_sunglasses::+1: