[转载] [原神] 资源文件解密记录

转载自:https://www.twblogs.net/a/5d11f8ecbd9eee1e5c821fba?lang=zh-cn

关于原神文件解密的一篇流水账。。。


米哈游的原神最近开始内测,发现其所有的通过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。不过想改还是有办法的。