kulokuro

wow!

随笔记录

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas non dictum orci, non feugiat ex. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi egestas quam in luctus aliquet. Integer orci eros, consequat a pretium at, suscipit in nulla. Etiam ut augue eu felis finibus tempor. Donec sollicitudin a felis sed ornare. Cras vel est lectus. Proin semper euismod posuere. Duis lacinia, ipsum nec facilisis cursus, lacus arcu condimentum risus, ac consequat justo risus ac ligula. Sed ac turpis nec dolor hendrerit hendrerit. Duis id volutpat diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus ac ipsum nisl. Phasellus euismod et elit non rhoncus. Proin egestas eleifend enim a ultricies.
Integer semper imperdiet tortor, at rhoncus nisi vulputate tempor. Morbi sit amet lectus malesuada, molestie ipsum id, lobortis sem. Nam finibus risus et quam consequat, sed bibendum ante dignissim. Ut suscipit varius malesuada. Sed vulputate pulvinar magna at pulvinar. Maecenas sollicitudin faucibus nisl, eu iaculis lorem posuere id. Morbi porttitor mi vitae imperdiet tempus. Fusce malesuada, leo vitae iaculis accumsan, urna libero tristique diam, eu varius felis augue ac ligula. Ut bibendum rhoncus est in rutrum. In neque justo, faucibus ac orci in, facilisis pellentesque nunc. Vestibulum ultrices, turpis id faucibus tincidunt, justo nulla semper lorem, id sollicitudin lacus dui ac nibh. Aliquam quis enim ut ante pretium feugiat. Cras consequat ultrices nunc, ac viverra urna pellentesque sed. Integer auctor quam at nisl vulputate aliquam. Maecenas vitae laoreet quam.

零 - 前言#

作为人菜瘾大的一般音游玩家,看到了各种音游私服和游戏魔改,自己仗着可能懂一点技术也尝试过去学习了一点逆向技能去整点活,但是无一例外都是以失败告终!!!!当然也不是毫无收获吧,起码还是能学到一点啥。
世界计划作为当初开服内测我就有玩的游戏,本来是开服没啥吸引我的就弃坑了,后来机缘巧合下又回去玩了,一直玩到现在。本来一直没有什么动机进行逆向分析,去年为了角色冲了一次榜,冲榜的时候就想写个 API 实时看分数线,产生了分析游戏网络包的想法。最近又想起来有这么一回事,就着手尝试了一下。

一 - 概览#

阅读难度:简单

任务:解密 global-metadata.dat ,方便日后 Il2CppDumper 进行进一步分析。
目标对象:Project Sekai 日服 3.4.0 apk
工具:IDA Pro 7.7 ; 010 Editor
参考资料:https://katyscode.wordpress.com/2021/02/23/il2cpp-finding-obfuscated-global-metadata/

二 - 初探#

逆向 Unity 游戏,基本都是围绕 libil2cpp.so 分析,这是游戏本体的核心逻辑。由于此游戏是用 il2cpp 打包的,把它丢进 IDA 里没有符号表,这时候就需要 global-metadata.dat 了。通过 global-metadata.dat ,我们可以使用 Il2CppDumper 生成很多有用的信息辅助我们逆向分析,如函数名和结构体等。
\lib\arm64-v8a 里找到 libil2cpp.so,在 \assets\bin\Data\Managed\Metadata 里找到 global-metadata.dat ,用 Il2CppDumper 把这俩丢进去试试,不出意外的报错了,提示 ERROR: Metadata file not found or encrypted. ,用 010 Editor 打开看看 global-metadata.dat ,文件头并不是标志性的 AF 1B B1 FA,这基本就是说明文件被加密了。

image

用别的游戏的对比着看,没发现啥规律,排除了异或等简单的混淆方法。那只能手撕源码了。

三 - 源码分析#

如果你在这方面完全零基础,建议先看一下参考资料里的文章后再来继续阅读!

libil2cpp.so 丢进 IDA 里,等待分析完。根据参考资料里面的经验总结,尝试搜索相关字符串。
Shift + F12 打开 String View ,搜索 global-metadata ,出现了唯一的一个结果:

image

点进去,按 X 查找其所有引用,同样是也只有一处地方:

image

进去看这个函数,按 F5 生成伪代码,字符串就是在这行代码出现的

__int64 __fastcall sub_1A5E8BC(_DWORD *a1, int *a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  // ...
  result = sub_1AEC2AC((int)"global-metadata.dat", (int)a2, a3, a4, a5, a6, a7, a8, v21, v22, v23);
  qword_864DEB8 = result;
  if ( result )
  {
    // ...
  }
  return result;
}

根据 il2cpp 源码,这段代码极有可能是 MetadataCache::Initialize()

bool il2cpp::vm::MetadataCache::Initialize()
{
    s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile("global-metadata.dat");
    if (!s_GlobalMetadata)
        return false;
    // ...
}

对照着看,sub_1AEC2AC() 就大概率是 MetadataLoader::LoadMetadataFile() 了。我们点进去看看:

_BYTE *__fastcall sub_1AEC2AC(const char *a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, char a9, int a10, void *a11)
{
  // ... variables

  sub_1AF0D80();
  v29 = "Metadata";
  v30 = 8LL;
  v12 = (unsigned __int64)(unsigned __int8)v23 >> 1;
  if ( (v23 & 1) != 0 )
    v13 = (char *)p;
  else
    v13 = (char *)&v23 + 1;
  if ( (v23 & 1) != 0 )
    v12 = v24;
  v27[0] = (__int64)v13;
  v27[1] = v12;
  sub_1A81EEC(v27, &v29);
  if ( (v23 & 1) != 0 )
    operator delete(p);
  v14 = strlen(a1);
  if ( (v31 & 1) != 0 )
    v15 = v33;
  else
    v15 = v32;
  if ( (v31 & 1) != 0 )
    v16 = *(_QWORD *)&v32[7];
  else
    v16 = (unsigned __int64)v31 >> 1;
  v29 = (char *)a1;
  v30 = v14;
  v23 = (__int64)v15;
  v24 = v16;
  sub_1A81EEC(&v23, &v29);
  if ( (v27[0] & 1) != 0 )
    v17 = v28;
  else
    v17 = (char *)v27 + 1;
  v18 = open(v17, 0);
  if ( v18 == -1 )
    goto LABEL_25;
  if ( fstat(v18, (struct stat *)&v23) == -1 )
  {
    close(v18);
    goto LABEL_25;
  }
  v19 = len[0];
  v20 = mmap(0LL, len[0], 3, 2, v18, 0LL);
  close(v18);
  if ( v20 == (_BYTE *)-1LL )
  {
LABEL_25:
    v20 = 0LL;
    goto LABEL_26;
  }
  if ( v19 >= 1 )
  {
    v21 = 0LL;
    do
    {
      v20[v21] ^= byte_67C9101[v21 & 0x7F];
      ++v21;
    }
    while ( v19 != (_DWORD)v21 );
  }
LABEL_26:
  if ( (v27[0] & 1) != 0 )
    operator delete(v28);
  if ( (v31 & 1) != 0 )
    operator delete(v33);
  return v20;
}

对照 MetadataLoader::LoadMetadataFile()源码:

void* il2cpp::vm::MetadataLoader::LoadMetadataFile(const char* fileName)
{
    std::string resourcesDirectory = utils::PathUtils::Combine(utils::Runtime::GetDataDir(), utils::StringView<char>("Metadata"));
 
    std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));
 
    int error = 0;
    os::FileHandle* handle = os::File::Open(resourceFilePath, kFileModeOpen, kFileAccessRead, kFileShareRead, kFileOptionsNone, &error);
    if (error != 0)
    {
        utils::Logging::Write("ERROR: Could not open %s", resourceFilePath.c_str());
        return NULL;
    }
 
    void* fileBuffer = utils::MemoryMappedFile::Map(handle);
 
    os::File::Close(handle, &error);
    if (error != 0)
    {
        utils::MemoryMappedFile::Unmap(fileBuffer);
        fileBuffer = NULL;
        return NULL;
    }
 
    return fileBuffer;
}

出现了很多共同点,都有 MetaData 字符串和文件操作,能笃定这俩是一个东西了。至于定义了一大坨的这些参数,根据函数调用关系简单分析了下,只看出来 const char *a1 是真正有用的,代表了文件名,在这里传入的即是 global-metadata.dat
对照着源码我们来着手重命名生成的逆向伪代码的变量,最终能得到以下稍微可读的代码:

_BYTE *__fastcall sub_1AEC2AC(char *fileName, ...)
{
  unsigned __int64 v12; // x8
  char *v13; // x9
  __int64 fileNameLength; // x0
  _BYTE *v15; // x8
  unsigned __int64 v16; // x9
  const char *resourceFilePath; // x0
  int fileHandle; // w21
  int fileSize; // w20
  _BYTE *fileBuffer; // x19
  __int64 i; // x8
  __int64 maybe_resourcesDirectory; // [xsp+0h] [xbp-E0h] BYREF
  unsigned __int64 v24; // [xsp+8h] [xbp-D8h]
  void *p; // [xsp+10h] [xbp-D0h]
  size_t len[2]; // [xsp+30h] [xbp-B0h]
  __int64 maybe_dataDictionary[2]; // [xsp+80h] [xbp-60h] BYREF
  char *v28; // [xsp+90h] [xbp-50h]
  char *resourcesFolder; // [xsp+98h] [xbp-48h] BYREF
  __int64 resourcesFolderNameLength; // [xsp+A0h] [xbp-40h]
  unsigned __int8 v31; // [xsp+A8h] [xbp-38h]
  _BYTE v32[15]; // [xsp+A9h] [xbp-37h] BYREF
  _BYTE *v33; // [xsp+B8h] [xbp-28h]

  utils::Runtime::GetDataDir((__int64)&maybe_resourcesDirectory);
  resourcesFolder = "Metadata";
  resourcesFolderNameLength = 8LL;
  v12 = (unsigned __int64)(unsigned __int8)maybe_resourcesDirectory >> 1;
  if ( (maybe_resourcesDirectory & 1) != 0 )
    v13 = (char *)p;
  else
    v13 = (char *)&maybe_resourcesDirectory + 1;
  if ( (maybe_resourcesDirectory & 1) != 0 )
    v12 = v24;
  maybe_dataDictionary[0] = (__int64)v13;
  maybe_dataDictionary[1] = v12;
  utils::PathUtils::Combine(maybe_dataDictionary, &resourcesFolder);
  if ( (maybe_resourcesDirectory & 1) != 0 )
    operator delete(p);
  fileNameLength = strlen(fileName);
  if ( (v31 & 1) != 0 )
    v15 = v33;
  else
    v15 = v32;
  if ( (v31 & 1) != 0 )
    v16 = *(_QWORD *)&v32[7];
  else
    v16 = (unsigned __int64)v31 >> 1;
  resourcesFolder = fileName;
  resourcesFolderNameLength = fileNameLength;
  maybe_resourcesDirectory = (__int64)v15;
  v24 = v16;
  utils::PathUtils::Combine(&maybe_resourcesDirectory, &resourcesFolder);
  if ( (maybe_dataDictionary[0] & 1) != 0 )
    resourceFilePath = v28;
  else
    resourceFilePath = (char *)maybe_dataDictionary + 1;
  fileHandle = open(resourceFilePath, 0);
  if ( fileHandle == -1 )
    goto fileHandleError;
  if ( fstat(fileHandle, (struct stat *)&maybe_resourcesDirectory) == -1 )
  {
    close(fileHandle);
    goto fileHandleError;
  }
  fileSize = len[0];
  fileBuffer = mmap(0LL, len[0], 3, 2, fileHandle, 0LL);
  close(fileHandle);
  if ( fileBuffer == (_BYTE *)-1LL )
  {
fileHandleError:
    fileBuffer = 0LL;
    goto LABEL_26;
  }
  if ( fileSize >= 1 )
  {
    i = 0LL;
    do
    {
      fileBuffer[i] ^= byte_67C9101[v21 & 0x7F];
      ++i;
    }
    while ( fileSize != (_DWORD)i );
  }
LABEL_26:
  if ( (maybe_dataDictionary[0] & 1) != 0 )
    operator delete(v28);
  if ( (v31 & 1) != 0 )
    operator delete(v33);
  return fileBuffer;
}

四 - Capture The Flag#

想必已经很明显了,端倪就在这段代码:

if ( fileSize >= 1 )
{
  i = 0LL;
  do
  {
    fileBuffer[i] ^= byte_67C9101[i & 0x7F];
    ++i;
  }
  while ( fileSize != (_DWORD)i );
}

猜测这里是使用了一个密码表对读进去的 fileBuffer 作 xor 解密,这个 0x7F 说明密码表大小为 128 ,和下标 i 作 and 从而实现循环使用。点进去这个 byte_67C9101 ,其内容也验证了我们的猜想。

image

那么接下来,就是写解密脚本了。
异或是对称的,因此加密和解密可以用同一套代码得到。我们右键选择 Convert - Conver to C/C++ Array ,导出密码表,然后写一个 C++ 脚本:

// Generated by New Bing
#include <iostream>
#include <fstream>
#include <vector>

unsigned char KeyTable[128] = {
    0xF7, 0xA9, 0x40, 0x2F, 0x25, 0x46, 0xC2, 0xF3, 0x2C, 0xC3, 0x5C, 0x8B, 0x04, 0x15, 0xA9, 0x7E, 
    0x7A, 0xDE, 0xB6, 0x16, 0x05, 0xD7, 0xE8, 0x50, 0x44, 0x6E, 0x8B, 0x9F, 0xB7, 0xFD, 0x93, 0xEF, 
    0x5C, 0x7C, 0x2A, 0x3A, 0xD3, 0x07, 0x1B, 0x3E, 0x0E, 0xC3, 0x10, 0xB9, 0x0E, 0x93, 0x09, 0xEF, 
    0xD7, 0x7F, 0x56, 0x93, 0xFB, 0x44, 0x16, 0x1F, 0xC1, 0xF0, 0xEB, 0xB7, 0xBA, 0x22, 0x3B, 0x2E, 
    0x2F, 0x6C, 0x2E, 0x88, 0xEA, 0x6E, 0xAA, 0x72, 0xC2, 0x77, 0x08, 0x42, 0xC7, 0x2E, 0x42, 0xB0, 
    0x65, 0x9C, 0xC8, 0x38, 0xA7, 0xFF, 0xF4, 0x17, 0x65, 0x0B, 0x79, 0xE3, 0x1C, 0x63, 0x83, 0xEB, 
    0x88, 0xE1, 0x89, 0xA1, 0x65, 0x80, 0x04, 0x68, 0xD4, 0xE3, 0xD0, 0x83, 0x3C, 0xD4, 0x57, 0x92, 
    0xAE, 0x3A, 0x52, 0x1E, 0x5A, 0x02, 0x86, 0x30, 0x77, 0x16, 0x23, 0xDF, 0xF5, 0xC7, 0x55, 0x5F
};

int main()
{
    std::ifstream inputFile("global-metadata.dat", std::ios::binary);

    inputFile.seekg(0, std::ios::end);
    size_t fileSize = inputFile.tellg();
    inputFile.seekg(0, std::ios::beg);

    std::vector<uint8_t> mmapPtr(fileSize);
    inputFile.read(reinterpret_cast<char*>(mmapPtr.data()), fileSize);
    inputFile.close();

    for (size_t i = 0; i < fileSize; ++i)
    {
        mmapPtr[i] ^= KeyTable[i & 0x7F];
    }

    std::ofstream decryptedFile("DecryptedMetadata.dat", std::ios::binary);
    decryptedFile.write(reinterpret_cast<char*>(mmapPtr.data()), fileSize);
    decryptedFile.close();

    std::cout << "Finish Decrypt" << std::endl;

    return 0;
}

运行一下,打开解密后的文件:

image

可以发现文件头变成了我们熟悉的 AF 1B B1 FA ,用 MetaDataStringEditor 也能成功读取

image

至此,我们便完成了对 global-metadata.dat 的破解。

后记:#

实际上,那段伪代码其实并不需要重命名这么多,我当初把 fileBuffer 看出来后就没管这么多了。这种对 global-metadata.dat 的加密方法其实算是比较简单的了,属于是初学者都能花点时间破出来的练手题。
说起来,我前脚刚破完 3.3.1 版本,第二天就更新了 3.4.0 ,有点小无语...
新版本的密码表和老的不一样,还好游戏本体不会隔三岔五更新,要不然每次更新都要重新来一遭等 IDA 在后台吭哧吭哧分析一个多小时,还是有点难熬的(
如果有时间试着研究下能不能不需要 IDA 直接读 libil2cpp.so 拿到密码表,不过这对于我这么一个连汇编都不懂的人来说估计很折磨就是了(

下一篇文章就是解密网络通信加密了

image

加载中...
此页面数据所有权由区块链加密技术和智能合约保障仅归创作者所有。