2024-01-07 修改:感谢b站用户@swordjjjkkk的反馈,修正global-metadata文件大小的计算方法
0x0 前言
0x1 il2cpp源码分析
在获取到il2cpp的源代码后,我们不难看出出,不论是哪个版本的il2cpp,对于global-metadata.dat的加载方法都是不变的
//位于/vm/MetadataCache.cpp
void MetadataCache::Initialize()
{
s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile("global-metadata.dat");
s_GlobalMetadataHeader = (const Il2CppGlobalMetadataHeader*)s_GlobalMetadata;
IL2CPP_ASSERT(s_GlobalMetadataHeader->sanity == 0xFAB11BAF);
IL2CPP_ASSERT(s_GlobalMetadataHeader->version == 23);
//...
}均是调用vm::MetadataLoader::LoadMetadataFile对global-metadata.dat进行加载。
我们进一步分析,分析vm::MetadataLoader::LoadMetadataFile是个什么逻辑
void* MetadataLoader::LoadMetadataFile(const char* fileName)
{
std::string resourcesDirectory = utils::PathUtils::Combine(Runtime::GetDataDir(), utils::StringView<char>("Metadata"));
std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));
int error = 0;
FileHandle* handle = File::Open(resourceFilePath, File::kFileModeOpen, File::kFileAccessRead, File::kFileShareRead, File::kFileOptionsNone, &error);
if (error != 0)
return NULL;
void* fileBuffer = MemoryMappedFile::Map(handle);
File::Close(handle, &error);
if (error != 0)
{
MemoryMappedFile::Unmap(fileBuffer);
fileBuffer = NULL;
return NULL;
}
return fileBuffer;
}这个函数实际上就是将global-metadata.dat加载进内存,并返回首地址,随后,在MetadataCache::Initialize函数中把返回的地址强制转换为Il2CppGlobalMetadataHeader*类型,不难判断出:global-metadata.dat被加载进内存后,s_GlobalMetadataHeader的值就是global-metadata.dat起始地址.
0x2 在ida中对il2cpp.so进行分析
用ida打开il2cpp.so,等待加载完成,在字串中搜索“global-metadata.dat”,并跳转到其交叉引用函数上,这样一来我们直接就定位到了global_metadata.dat的加载函数,即上一段中所提及的MetadataLoader::LoadMetadataFile函数

此处我们直接祭出f5,得到如下代码
int sub_5E53E5A()
{
//...
dword_937B008 = sub_5EB0496("global-metadata.dat");
dword_937B00C = dword_937B008;
v27 = dword_937B008 + *(_DWORD *)(dword_937B008 + 184);
//...
}不难看出,dword_937B008即为源码中的s_GlobalMetadata,dword_937B00C即为源码中的s_GlobalMetadataHeader,sub_5EB0496函数即为MetadataLoader::LoadMetadataFile
0x3 global-metadata.dat文件头分析
我们随便打开一个global-metadata.dat文件,并跑一下模板,不难得知:global-metadata.dat文件头长度为0x110h,起始的4位为魔数(这个在内存中可能会被抹除,我们后面再说),随后的4位代表了Metadata version(这个version我们不用管它,毕竟我们要的是通杀的脚本)


中间的可以全部忽略,跳到0x108h,从这个位置开始,exportedTypeDefinitionsOffset和exportedTypeDefinitionsCount两个值与整个global-metadata.dat的文件大小息息相关,我们可以通过将两值相加得到global-metadata.dat的大小。
此处需要额外注意的是这两个值的起始位置不一定是0x108h和0x10Ch,需要以0x108h和0x10Ch为起始点,每次对两个起始点分别-0x4,并读取int值,如果两个值的和小于10,则需要继续-0x4直到和大于10为止。
0x4 frida脚本
由于MetadataCache::Initialize函数仅会在游戏刚进入时被调用,因此我们需要通过使用frida唤起进程的方式进行hook(你手速如果够快也可以不用这么弄)
因此我们就可以得到frida的最终运行命令
frida -U -l hookagent.js -f "com.example.demo"
在0x2板块中,我们使用ida对libil2cpp.so进行了反汇编分析,得到sub_5EB0496即为MetadataLoader::LoadMetadataFile,所以我们直接对sub_5EB0496函数进行hook(这个值是不固定的,每个so都不一样,要具体情况具体分析)
至此,我们就可以得到脚本第一部分的代码了:
const addr = Process.findModuleByName("libil2cpp.so");
const offset = 0x5EB0496;
const funcOff = addr.base.add(offset);
Interceptor.attach(funcOff,{
onEnter:function(args){
console.log("MetadataLoader::LoadMetadataFile")
},
onLeave:function(retval){
console.log("s_GlobalMetadataHeader:0x"+retval.toString(16))
}
})
但是实际运行后,我们会发现进程在刚刚被启动的时候libil2cpp.so还并未加载,因此我们的脚本会报错,想解决这个问题只需要使用setInternal设置一下定时执行,并使用try catch块防止报错导致的脚本退出
setInterval(() => {
try {
const addr = Process.findModuleByName("libil2cpp.so");
const offset = 0x5EB0496;
const funcOff = addr.base.add(offset);
Interceptor.attach(funcOff, {
onEnter: function (args) {
console.log("MetadataLoader::LoadMetadataFile")
},
onLeave: function (retval) {
console.log("s_GlobalMetadataHeader:0x" + retval.toString(16))
}
})
}catch(e){
//ignored
}
},1)//设置每1ms执行一次
以上就是对s_GlobalMetadataHeader的值的获取,接下来我们对global-metadata.dat的大小进行计算
在0x3板块中,我们得知想要计算global-metadata.dat的大小需要获取到exportedTypeDefinitionsOffset和exportedTypeDefinitionsCount两个值的大小,即读取s_GlobalMetadataHeader + 0x108h和s_GlobalMetadataHeader + 0x10Ch两个地址上的int值
function get_size() {
const metadataHeader = s_GlobalMetadataHeader;
let fileOffset = 0x10C;
let lastCount = 0;
let lastOffset = 0;
while (true) {
lastCount = Memory.readInt(ptr(metadataHeader).add(fileOffset));
if (lastCount !== 0) {
lastOffset = Memory.readInt(ptr(metadataHeader).add(fileOffset-4));
break;
}
fileOffset -= 8;
if(fileOffset<=0)
{
console.log("get size failed!");
break;
}
}
return lastOffset+lastCount;
}
综合以上两段代码,我们可以就得出完整的脚本,如下:
var finded = false
var addr;
var notFirstTime = true
var s_GlobalMetadataHeader;
setInterval(() => {
if(finded){
if(addr!=null){
let offset = 0x5EB0496; //此值需要更改
let funcOff = addr.base.add(offset);
if(notFirstTime){
notFirst = false;
Interceptor.attach(funcOff,{
onEnter:function(args){
console.log("MetadataLoader::LoadMetadataFile");
},
onLeave:function(retval){
console.log("s_GlobalMetadataHeader:"+retval.toString(16));
s_GlobalMetadataHeader = retval;
save(get_size())
}
})
}
}else{
addr = Process.findModuleByName("libil2cpp.so");
let offset = 0x5EB0496; //此值需要更改
let funcOff = addr.base.add(offset);
if(notFirstTime){
notFirst = false;
Interceptor.attach(funcOff,{
onEnter:function(args){
console.log("MetadataLoader::LoadMetadataFile");
},
onLeave:function(retval){
console.log("s_GlobalMetadataHeader:"+retval.toString(16));
s_GlobalMetadataHeader = retval;
save(get_size())
}
})
}
}
}else{
try{
addr = Process.findModuleByName("libil2cpp.so");
finded=true;
}catch(ex){
}
}
}, 5);
function get_size() {
const metadataHeader = s_GlobalMetadataHeader;
let fileOffset = 0x10C;
let lastCount = 0;
let lastOffset = 0;
while (true) {
lastCount = Memory.readInt(ptr(metadataHeader).add(fileOffset));
if (lastCount !== 0) {
lastOffset = Memory.readInt(ptr(metadataHeader).add(fileOffset-4));
break;
}
fileOffset -= 8;
if(fileOffset<=0)
{
console.log("get size failed!");
break;
}
}
return lastOffset+lastCount;
}
function save(size){
var file = new File("/data/local/tmp/global-metadata.dump.dat","wb");
var contentBuffer = Memory.readByteArray(s_GlobalMetadataHeader,size);
file.write(contentBuffer);
file.flush();
file.close;
console.log("global-metadata已导出到/data/local/tmp/global-metadata.dump.dat")
}0x5 后记
其实这个脚本并不能做到完全的通杀,因为还有一部分游戏采取了按需加载,此时使用这种方法导出的global-metadata.dat是不完整的。
填个坑,前面0x3说加载到内存中会抹除魔数(AF 1B B1 FA),这其实是为了防止直接搜索到global-metadata.dat在内存中的位置,但是并没卵用,我们通过hook的防止,不搜索内存直接拿到地址并dump,通杀
如果喜欢的话投个币或者点个赞啥的都行,谢谢各位读者
参考:
1.il2cpp源代码 https://github.com/4ch12dy/il2cpp