關於原神文件解密的一篇流水賬。。。
米哈遊的原神最近開始內測,發現其所有的通過unity加載的資源文件都被加密了。下面就講一下尋找解密的方法的過程。
遊戲是unity 2017.1版本的win64平臺,但是卻使用了il2cpp,unity2017.1的文檔裏說明win平臺是不支持il2cpp的,不知道怎麼做到的,也許是跟unity官方py的?
慣例第一步,先把程序(UserAssembly.dll)丟進IDA裏分析。il2cpp第一個加載的文件一定是global-metadata.dat。所以搜索字符串“global-metadata.dat”直接找到加載的函數。
下面是原版的加載函數。
void MetadataCache::Initialize()
{
s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile ("global-metadata.dat");
s_GlobalMetadataHeader = (const Il2CppGlobalMetadataHeader*)s_GlobalMetadata;
assert (s_GlobalMetadataHeader->sanity == 0xFAB11BAF);
assert (s_GlobalMetadataHeader->version == 21);
const Il2CppAssembly* assemblies = (const Il2CppAssembly*)((const char*)s_GlobalMetadata + s_GlobalMetadataHeader->assembliesOffset);
for (uint32_t i = 0; i < s_GlobalMetadataHeader->assembliesCount / sizeof(Il2CppAssembly); i++)
il2cpp::vm::Assembly::Register(assemblies + i);
.....
.....
}
對照發現沒有改動,所以解密部分應該在“vm::MetadataLoader::LoadMetadataFile”函數內部。
//原版的加載函數
void* MetadataLoader::LoadMetadataFile (const char* fileName)
{
std::string resourcesDirectory = utils::PathUtils::Combine (Runtime::GetDataDir (), "Metadata");
std::string resourceFilePath = utils::PathUtils::Combine (resourcesDirectory, fileName);
int error = 0;
FileHandle* handle = File::Open (resourceFilePath, File::kFileModeOpen, File::kFileAccessRead, File::kFileShareRead, File::kFileOptionsNone, &error);
if (error != 0)
return NULL;
//調用CreateFileMappingW將文件加載到內存
void* fileBuffer = MemoryMappedFile::Map (handle);
//在這裏添加了解密函數
File::Close (handle, &error);
if (error != 0)
{
MemoryMappedFile::Unmap (fileBuffer);
fileBuffer = NULL;
return NULL;
}
return fileBuffer;
}
//添加的解密部分反編譯結果
//確認文件開頭"mark"標誌
if ( !byte_7FFE06F3B110|| _filelen.QuadPart < 4ui64
|| *_filedata != 109 || _filedata[1] != 97
|| _filedata[2] != 114 || _filedata[3] != 107 )
goto LABEL_46;
//調用qword_7FFE06F3B100指向的函數,功能爲由加密文件長度計算解密後的長度
v35 = ((__int64 (__fastcall *)(_QWORD))qword_7FFE06F3B100)((LARGE_INTEGER)_filelen.QuadPart);// 獲取文件長度
_filelen_1 = v35;
_addr = malloc_(v35, 16i64);
_newaddr = (void *)_addr;
if ( !_addr )
{
UnMapFile(_filedata);
goto LABEL_47;
}
//調用qword_7FFE06F3B108指向的解密函數
v38 = ((__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))qword_7FFE06F3B108)(
_filedata,
(LARGE_INTEGER)_filelen.QuadPart,
_addr,
_filelen_1,
v39,
&err,
-2i64);
......
繼續查找引用兩個函數指針的函數發現下面一個函數。
void __fastcall il2cpp_enable_confuse(__int64 (__fastcall *a1)(_QWORD), __int64 (__fastcall *a2)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))
{
qword_7FFE06F3B100 = a1;
qword_7FFE06F3B108 = a2;
byte_7FFE06F3B110 = 1;
}
函數並沒有被dll內部調用,通過動態調試,發現是主程序GS.exe調用的,並且兩個解密相關的函數也位於GS.exe內。
接着IDA反編譯GS.exe發現解密函數經過了混淆,應該是叫做控制流扁平化。
IDA F5結果
IDA 圖形視圖
因爲混淆後代碼太難理解所以放棄從代碼找出算法。。。。
根據米哈遊在崩壞三中的做法(詳見我在52pojie的文章),感覺文件依然是異或爲主的算法,不同bundle文件的頭部也有重合。
然後與未加密的bundle的文件頭比較手動算出64字節密鑰,發現並不能完全解密文件,於是動態調試從內存中dump出了解密後的global-metadata.dat,發現其長度小於加密的文件(這時候才明白第一個函數是計算大小的),與加密文件算出的密鑰,開頭是64字節循環,然後在一段數據後就無法解出正確的數據。然後動態調試下斷點發現,在將原數據複製到解密數據的地址時有些數據被忽略了。
所以,爲了測試算法(其實直接dump出複製到解密數據存儲位置的數據對比一下就能知道哪些數據被去掉了),準備了兩個文件,一個是全爲0x00的文件0.dat,另一個是包含遞增的int值的文件123.dat,開頭四字節設爲“mark”,兩個文件分別替換global-metadata.dat,並dump出”解密“後的數據0.dnmp和123.dump。其實只是簡單的異或的原理了。因爲0 xor key = key,所以0.dat經過解密函數後其實就是密鑰了。123.dat經過解密函數可以看作加密,密鑰就是0.dnmp,123.dump再與0.dump異或即可得到”解密"的文件0123.dat,通過對比123.dat和0123.dat即可知道哪些數據被去掉了。
結果如下圖
綜上,文件的結構爲:
以mark開頭,2580(0xA14)字節爲一個循環,每個循環按順序包含四個大塊和一個小塊,大塊爲612(0x264)字節數據加4字節無關數據,小塊爲112(0x70)字節數據加4字節無關數據。最後不足的部分直接結束,不需要補齊。
將開頭的mark及無關數據去除後即可循環與64字節的密鑰異或解密。經過測試,加密時的無關數據可以隨意設置不會有影響。
另外,關於網絡修改,https還是抓不到,資源文件流量不走代理,而且服務器會將http重定向到https。不過想改還是有辦法的。