从N1CTF2018 baby unity3d入门il2cpp逆向
从实际案例入门il2cpp逆向的基本步骤,介绍了常用工具与分析方法
前置知识
先网上搜索一些资料,对下面这些概念和问题,有个简单的印象:
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)
});