前置知识

先网上搜索一些资料,对下面这些概念和问题,有个简单的印象:

il2cpp是什么。

il2cppDumper怎么用:

https://github.com/Perfare/Il2CppDumper

https://bbs.kanxue.com/thread-271191.htm

global-metadata.dat加密了,怎么大概怎么处理:https://cloud.tencent.com/developer/article/2216959

il2cpp加载global-metadata.dat的源码分析:https://bbs.kanxue.com/thread-271191.htm

然后下面借助N1CTF 2018 baby unity3d这题完成从理论到实践的转换。

解题

本文参考该wp步步复现:https://cloud.tencent.com/developer/article/2216959

找解密位置

首先解包apk,拿到libil2cpp.so文件和global-metadata.dat文件。

global-metadata.dat文件是加密过的,所以要在libil2cpp.so中找加密逻辑。

在上面的参考文章里,提到了加载这个文件的调用链是这样的:

il2cpp_init
-> il2cpp::vm::Runtime::Init
-> il2cpp::vm::MetadataCache::Initialize
-> il2cpp::vm::MetadataLoader::LoadMetadataFile

所以去ida里,从il2cpp_init开始看。

于此同时,对着看源码。

所以sub_47C770就是Runtime::Init,可以自己修改。

ida和源码都跟进。

​

​

这么多,哪个才对应的MetadataCache::Initialize?

一个个点进去,看哪个最和源码符合,那个就是。

发现就是他:sub_4B5564。

​

把sub_4B5564改名后跟进:

对比源码之后发现,"CLKFIL\rMETIDITI\nDIT"就是"global-metadata.dat"加密后的文件名。

跟进sub4B5518发现,他把这个文件名还原成global-metadata.dat,然后进入sub513060。

sub_513060估计就是vm::MetadataLoader::LoadMetadataFile。

在这个调用处,v0应该是被传入了sub_513060的,但是伪代码没写出来,估计是解析问题,我不清楚。

进入sub_513060,然后源码也跟进。

比较伪代码和源码的区别,如果找到不一样的地方,那就是解密文件内容的地方了。

对着源码不断修改ida的内容,再靠一点猜测,最终手动还原的伪代码:

所以sub_512FDC就是解密函数。

​

​

手动解密下:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>


int main() {
FILE *file;
unsigned char *buf;
size_t fileSize;
size_t bytesRead;

// 打开文件,文件名为 "example.bin",以 "rb" 模式(只读,二进制)打开
file = fopen("global-metadata.dat", "rb");

// 移动文件指针到文件末尾以计算文件大小
fseek(file, 0, SEEK_END);
fileSize = ftell(file); // 获取文件大小
rewind(file); // 重置文件指针到文件开头

// 分配内存以存储文件内容
buf = (unsigned char *)malloc(fileSize);
// 读取文件内容到缓冲区
bytesRead = fread(buf, 1, fileSize, file);
fclose(file);

unsigned int key[] = {0xF83DA249, 0x15D12772, 0x40C50697, 0x984E2B6B, 0x14EC5FF8, 0xB2E24927, 0x3B8F77AE, 0x472474CD, 0x5B0CE524,
0xA17E1A31, 0x6C60852C, 0xD86AD267, 0x832612B7, 0x1CA03645, 0x5515ABC8, 0xC5FEFF52, 0xFFFFAC00, 0xFE95CB6,
0x79CF43DD, 0xAA48A3FB, 0xE1D71788, 0x97663D3A, 0xF5CFFEA7, 0xEE617632, 0x4B11A7EE, 0x40EF0B5, 0x606FC00,
0xC1530FAE, 0x7A827441, 0xFCE91D44, 0x8C4CC1B1, 0x7294C28D, 0x8D976162, 0x8315435A, 0x3917A408, 0xAF7F1327,
0xD4BFAED7, 0x80D0ABFC, 0x63923DC3, 0xB0E6B35A, 0xB815088F, 0x9BACF123, 0xE32411C3, 0xA026100B, 0xBCF2FF58,
0x641C5CFC, 0xC4A2D7DC, 0x99E05DCA, 0x9DC699F7, 0xB76A8621, 0x8E40E03C, 0x28F3C2D4, 0x40F91223, 0x67A952E0,
0x505F3621, 0xBAF13D33, 0xA75B61CC, 0xAB6AEF54, 0xC4DFB60D, 0xD29D873A, 0x57A77146, 0x393F86B8, 0x2A734A54,
0x31A56AF6, 0xC5D9160, 0xAF83A19A, 0x7FC9B41F, 0xD079EF47, 0xE3295281, 0x5602E3E5, 0xAB915E69, 0x225A1992,
0xA387F6B2, 0x7E981613, 0xFC6CF59A, 0xD34A7378, 0xB608B7D6, 0xA9EB93D9, 0x26DDB218, 0x65F33F5F, 0xF9314442,
0x5D5C0599, 0xEA72E774, 0x1605A502, 0xEC6CBC9F, 0x7F8A1BD1, 0x4DD8CF07, 0x2E6D79E0, 0x6990418F, 0xCF77BAD9,
0xD4FE0147, 0xFEF4A3E8, 0x85C45BDE, 0xB58F8E67, 0xA63EB8D7, 0xC69BD19B, 0xDA442DCA, 0x3C0C1743, 0xE6F39D49,
0x33568804, 0x85EB6320, 0xDA223445, 0x36C4A941, 0xA9185589, 0x71B22D67, 0xF59A2647, 0x3C8B583E, 0xD7717DED,
0xDF05699C, 0x4378367D, 0x1C459339, 0x85133B7F, 0x49800CE2, 0x3666CA0D, 0xAF7AB504, 0x4FF5B8F1, 0xC23772E3,
0x3544F31E, 0xF673A57, 0xF40600E1, 0x7E967417, 0x15A26203, 0x5F2E34CE, 0x70C7921A, 0xD1C190DF, 0x5BB5DA6B,
0x60979C75, 0x4EA758A4, 0x78FE359, 0x1664639C, 0xAE14E73B, 0x2070FF03};

int keySize = sizeof(key);

unsigned int* buff = (unsigned int*)buf;
for (int i = 0; i < fileSize; i+=4){
*((unsigned int *)(buf + i)) ^= key[( i + i/0x84) %0x84];
}

FILE * file_write = fopen("output.bin", "wb");
fwrite(buf, 1, fileSize, file);

return 0;
}

​

解密出来的文件还要把文件头改回去。

然后参考wp到这里就结束了,还没拿到flag,我刚学,也不知道怎么拿,然后下面就是靠我自己一步步调出来的。

Il2CppDumper使用

使用Il2CppDumper,输入libil2cpp.so和解密过的metadata文件,会输出:

​

Assembly-Charp.dll用dnspy打开,可以看到最开始用C#写的类的结构。

​

​

​

打开dump.cs,拉到最下面,给了各个函数在so文件中的偏移。

CheckFlag是0x518A24,打开ida,g跳转,就能到达这个函数的位置。

有脚本批量恢复符号:

ida load script file,选择il2cppdumper自带的ida_py3.py,json选输出的scripts.json。

可以看到Check这个类的方法符号都被恢复了。

CheckFlag

text是a2

password是TypeInfo+80 + 4000

iv是TypeInfo+80 + 2364

flag密文是StringLiteral_2814

TypeInfo

被cctor引用,看看cctor

StringLiteral的值都是能直接看见的。

最后:

flag密文是w0ZyUZAHhn16/MRWie63lK+PuVpZObu/NpQ/E/ucplc=

password是91c775fa0f6a1cba

iv是58f3a445939aeb79

使用frida-il2cpp-bridge

其实,一开始我并没有完全恢复符号,找不到密文和password在哪,就通过frida找。

https://github.com/vfsfitvnm/frida-il2cpp-bridge

寻找思路:

flag的比较逻辑:对输入进行AESEncrypt,再和flag密文比较。

hook AESEncrypt这个函数,就能获取password和iv。

import "frida-il2cpp-bridge";

Il2Cpp.perform(() => {
const assembly = Il2Cpp.domain.assembly("Assembly-CSharp").image;
assembly.class('Check').method('AESEncrypt').implementation = function():Il2Cpp.String{
console.log(arguments[0])
console.log(arguments[1])
console.log(arguments[2])

const result = this.method<Il2Cpp.String>('AESEncrypt').invoke(arguments[0],arguments[1],arguments[2])

return result
}
});

hook 字符串比较函数,就能获取flag密文。字符串比较有String的Compare和Equals,最后hook Equals时得到了密文。

import "frida-il2cpp-bridge";

Il2Cpp.perform(() => {

const SystemString = Il2Cpp.corlib.class("System.String");
var compare = SystemString.method('Equals')
compare.implementation = function(){
console.log(`arg[0]: ${arguments[0]}`)
console.log(`arg[1]: ${arguments[1]}`)
}

});

​

​

然后去调他内部的解密函数

import "frida-il2cpp-bridge";

Il2Cpp.perform(() => {

function getBytes(str:string):Il2Cpp.Parameter.Type{
const val = Il2Cpp.corlib.class("System.Text.UTF8Encoding").new().method('GetBytes',1).invoke(Il2Cpp.string(str));
return val
}

const assembly = Il2Cpp.domain.assembly("Assembly-CSharp").image;
const text = Il2Cpp.string('w0ZyUZAHhn16/MRWie63lK+PuVpZObu/NpQ/E/ucplc=')
const password = getBytes('91c775fa0f6a1cba')
const iv = getBytes('58f3a445939aeb79')
const val = assembly.class('Check').method('AESDecrypt').invoke(text,password,iv)

console.log(val)

});

​

​

​

​

​

​

​

​

​

​

​