某天刷B站的时候刷到一条视频,看到环行旅舍这个游戏感觉美术挺有意思就入坑试了一下。然后发现作为一款23年的二游多少有点寒颤,虽然是被美术骗进来的,但是实际体验下来这美术风格并不在我的好球区,而且玩法和养成也不对胃口。也不知道看那个视频的时候触到了哪一根弦。

不过本来就是图他美术,游戏不好玩无所谓,直接去找资源解包。

看了眼文件结构,资源主要在两个文件夹“Win\AB”“环行旅舍_Data\StreamingAssets\Win\AB”下(我用的PC端,安卓端目录结构应该不太一样但无所谓)。所有的资源都是“****_****.ab”格式,除了第一个文件夹下还有一个“Win”的unity资源。直接拖入AssetStudio不能正确读取,查看一下文件头,发现所有文件的开头多了8字节的0x20,删去后就能正确读取了。

写了个脚本批量处理完发现有三个文件不符合这个规则,都是“Win\AB”下的文件,一个是前文提到的“Win”文件,这个文件没有加密是manifest文件。剩下是“00000000_3c98fd0db1deabf3.ab”“00000000_85d5b779e578c8b3.ab”这两个文件,打开010editor看看。

两个文件的前32字节是一样的,猜测是加密后的unity资源文件头。用异或试了一下发现并不是简单的异或加密,去网上搜了一圈发现这游戏太小众又或者是因为这两个文件并不是美术资源所以没人解密。“Win\AB”下剩下的文件就是游戏的主要美术资源了,脚本跑一下文件头前缀去掉就能读取了。但我挺好奇这俩文件的,所以就直接上IDA吧。

先用perfare佬的Il2CppDumper跑一下,然后IDA 打开GameAssembly.dll,用脚本恢复一下符号和数据结构。先直球搜索decrypt函数,然后筛查一下,锁定到AES.DecryptToStream函数,往回看调用可以看到这三个函数调用了他。明显是读取asset文件然后解密,这大概率就是我们要找的函数了。

稍微阅读了一下反汇编的代码,发现不是我能处理的。

void __stdcall AES__DecryptToStream(
        System_IO_Stream_o *inputStream,
        System_IO_Stream_o *outputStream,
        const MethodInfo *method)
{
  __int64 v5; // rdx
  __int64 v6; // rdx
  __int64 v7; // rdx
  System_Security_Cryptography_AesManaged_o *KeyAndIv; // rax
  __int64 *v9; // r14
  System_Security_Cryptography_ICryptoTransform_o *v10; // r12
  __int64 *v11; // rsi
  __int64 i; // r15
  int v13; // eax
  IFix_ILFixDynamicMethodWrapper_o *Patch; // rax

  if ( !byte_7FFF49C25F07 )
  {
    sub_7FFF47555410((__int64)&AES_TypeInfo, (__int64)outputStream);
    sub_7FFF47555410((__int64)&byte___TypeInfo, v5);
    sub_7FFF47555410((__int64)&System_Security_Cryptography_CryptoStream_TypeInfo, v6);
    sub_7FFF47555410((__int64)&System_IDisposable_TypeInfo, v7);
    byte_7FFF49C25F07 = 1;
  }
  if ( IFix_WrappersManagerImpl__IsPatched(1265, 0i64) )
  {
    Patch = IFix_WrappersManagerImpl__GetPatch(1265, 0i64);
    if ( !Patch )
      sub_7FFF47555560();
    IFix_ILFixDynamicMethodWrapper____Gen_Wrap_280(
      Patch,
      (BestHTTP_HTTPRequest_o *)inputStream,
      (BestHTTP_HTTPResponse_o *)outputStream,
      0i64);
  }
  else
  {
    if ( (AES_TypeInfo->_2.bitflags2 & 4) != 0 && !AES_TypeInfo->_2.cctor_finished )
      il2cpp_runtime_class_init((__int64)AES_TypeInfo);
    KeyAndIv = AES__GenerateKeyAndIv(0i64);
    v9 = (__int64 *)KeyAndIv;
    if ( !KeyAndIv )
      sub_7FFF47555560();
    v10 = (System_Security_Cryptography_ICryptoTransform_o *)((__int64 (__fastcall *)(System_Security_Cryptography_AesManaged_o *, const MethodInfo *))KeyAndIv->klass->vtable._24_CreateDecryptor.methodPtr)(
                                                               KeyAndIv,
                                                               KeyAndIv->klass->vtable._24_CreateDecryptor.method);
    v11 = sub_7FFF47555520((__int64)System_Security_Cryptography_CryptoStream_TypeInfo);
    System_Security_Cryptography_CryptoStream___ctor(
      (System_Security_Cryptography_CryptoStream_o *)v11,
      inputStream,
      v10,
      0,
      0i64);
    for ( i = sub_7FFF47555010((__int64)byte___TypeInfo, 0x100000u);
          ;
          ((void (__fastcall *)(System_IO_Stream_o *, __int64, _QWORD, _QWORD, const MethodInfo *))outputStream->klass->vtable._31_unknown.methodPtr)(
            outputStream,
            i,
            0i64,
            (unsigned int)v13,
            outputStream->klass->vtable._31_unknown.method) )
    {
      if ( !i )
        sub_7FFF47555560();
      if ( !v11 )
        sub_7FFF47555560();
      v13 = (*(__int64 (__fastcall **)(__int64 *, __int64, _QWORD, _QWORD, _QWORD))(*v11 + 776))(
              v11,
              i,
              0i64,
              *(unsigned int *)(i + 24),
              *(_QWORD *)(*v11 + 784));
      if ( v13 <= 0 )
        break;
      if ( !outputStream )
        sub_7FFF47555560();
    }
    if ( !outputStream )
      sub_7FFF47555560();
    ((void (__fastcall *)(System_IO_Stream_o *, _QWORD, const MethodInfo *, _QWORD))outputStream->klass->vtable._13_unknown.methodPtr)(
      outputStream,
      0i64,
      outputStream->klass->vtable._13_unknown.method,
      (unsigned int)v13);
    sub_7FFF472BF6A0(0, (__int64)System_IDisposable_TypeInfo, v11);
    if ( v10 )
      sub_7FFF472BF6A0(0, (__int64)System_IDisposable_TypeInfo, (__int64 *)v10);
    sub_7FFF472BF6A0(0, (__int64)System_IDisposable_TypeInfo, v9);
  }
}

xdbg启动!我直接xdbg和IDA联合调试(还好你游有PC端)。

众所周知,要读文件肯定要先createfile,先在符号中找到kernel32.dll,在CreateFileA和CreateFileW下断点,然后看rcx,发现先读取的是“00000000_85d5b779e578c8b3.ab”文件。回到IDA中并没有在对应代码的上下文找到明确的调用解密函数的地方。那就先给AES.DecryptToStream函数下个断点,看看读取完文件后是不是会调用这个函数。事实证明猜想是正确的,而且直接看这两个文件的二进制也会发现文件长度都是16的倍数,很符合常见AES加密的输出。

终究还是要对这个函数做分析,不过没必要分析的那么透彻,跟着xdbg单步调试看看需要的逻辑。

KeyAndIv = (__int64 *)AES__GenerateKeyAndIv(0i64);

这里生成了AES的key和初始向量iv

v14 = (System_Security_Cryptography_ICryptoTransform_o *)(*(__int64 (__fastcall **)(__int64 *, _QWORD))(*KeyAndIv + 664))(
                                                               KeyAndIv,
                                                               *(_QWORD *)(*KeyAndIv + 672));

这里构造了一个解密器

 for ( i = sub_7FFF47555010((__int64)byte___TypeInfo, 0x100000u);
          ;
          (*(void (__fastcall **)(__int64 *, __int64, _QWORD, _QWORD, _QWORD))(*v15 + 808))(
            v15,
            i,
            0i64,
            (unsigned int)v17,
            *(_QWORD *)(*v15 + 816)) )
    {
      if ( !i )
        sub_7FFF47555560();
      v17 = ((__int64 (__fastcall *)(System_IO_Stream_o *, __int64, _QWORD, _QWORD, const MethodInfo *))stream->klass->vtable._29_unknown.methodPtr)(
              stream,
              i,
              0i64,
              *(unsigned int *)(i + 24),
              stream->klass->vtable._29_unknown.method);
      if ( v17 <= 0 )
        break;
      if ( !v15 )
        sub_7FFF47555560();
    }

这里是解密的主题逻辑,跟进sub_7FFF47555010发现是il2cpp_array_new_specific_0函数,按下文来看是向内存申请一个array存放解密后的数据。这个循环每次读取0x100000字节的数据然后向解密器传入输入流和输出流指针还有一个标识符。那我们只需要每次解密完把i对应内存的数据dump出来就好了。

dump出来发现第一个文件是Assembly-CSharp_patchV2,第二个文件是shader。好吧确实不是我需要的内容。

至此本次解包结束。不过考虑到很多游戏(比如马上公测的交错战线CrossCore)的资源加密都是在文件头添加无效数据,所以我fork了一份assetstudio然后加了个前缀跳过功能。