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
,這基本就是說明文件被加密了。
用別的遊戲的對比著看,沒發現啥規律,排除了異或等簡單的混淆方法。那只能手撕源碼了。
三 - 源碼分析#
如果你在這方面完全零基礎,建議先看一下參考資料裡的文章後再來繼續閱讀!
把 libil2cpp.so
丟進 IDA 裡,等待分析完。根據參考資料裡面的經驗總結,嘗試搜索相關字符串。
按 Shift + F12
打開 String View ,搜索 global-metadata
,出現了唯一的結果:
點進去,按 X
查找其所有引用,同樣是也只有一處地方:
進去看這個函數,按 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[i & 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
,其內容也驗證了我們的猜想。
那麼接下來,就是寫解密腳本了。
異或是對稱的,因此加密和解密可以用同一套代碼得到。我們右鍵選擇 Convert - Convert 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;
}
運行一下,打開解密後的文件:
可以發現文件頭變成了我們熟悉的 AF 1B B1 FA
,用 MetaDataStringEditor 也能成功讀取
至此,我們便完成了對 global-metadata.dat
的破解。
後記:#
實際上,那段伪代码其實並不需要重命名這麼多,我當初把 fileBuffer
看出來後就沒管這麼多了。這種對 global-metadata.dat
的加密方法其實算是比較簡單的了,屬於是初學者都能花點時間破出來的練手題。
說起來,我前腳剛破完 3.3.1 版本,第二天就更新了 3.4.0 ,有點小無語...
新版本的密碼表和老的不一樣,還好遊戲本體不會隔三岔五更新,要不然每次更新都要重新來一遭等 IDA 在後台吭哧吭哧分析一個多小時,還是有點難熬的(
如果有時間試著研究下能不能不需要 IDA 直接讀 libil2cpp.so
拿到密碼表,不過這對於我這麼一個連匯編都不懂的人來說估計很折磨就是了(
下一篇文章就是解密網絡通信加密了