EzUnity
难度Super Damn Hard Pro的Unity
![[BL_Endian.png]]
你知道il2cpp吗?
难度:hard pro
出题人:@yuro
💡或许你需要学习一下il2cpp逆向相关的知识?
💡il2cpp是开放源代码的
💡修复gm.dat好像很麻烦,有没有不通过gm.dat也能dump的方法呢
文件{:download="EzUnity.zip"}
概览#
- 脱壳
- 修补global-metadata.bat
- 看il2cpp源码+ida静态分析
- 修改Il2CppDumper代码,dump,导入ida
- ida静态分析
0. upx -d#
第一步,先查壳,扔进die或者exeinfo
都有upx壳,全 upx -d
脱了
1. 修补gm.dat#
直接 Il2CppDumper
Dump不出来
Il2CppDumper.exe .\GameAssembly.dll '.\ez unity_Data\il2cpp_data\Metadata\global-metadata.dat' .\dumped\
ERROR: Metadata file not found or encrypted.
gm.dat 是加密过的,使用HxD或者010Editor查看
59 55 52 4F 1D 00 00 00 59 55 52 4F 00 01 00 00
小知识
C#是小端序(little-endian),所以实际的值是:
AF 1B B1 FA
->0xFAB11BAF
;00 01 00 00
->0x0100
正常的头是 AF 1B B1 FA
,改了之后重新运行,报错
Initializing metadata...
System.ArgumentException: An item with the same key has already been added. Key: 13
at System.Collections.Generic.Dictionary`2.TryInsert(TKey , TValue , InsertionBehavior )
at System.Linq.Enumerable.ToDictionary[TSource,TKey](TSource[] , Func`2 , IEqualityComparer`1 )
at Il2CppDumper.Metadata..ctor(Stream stream) in C:\projects\il2cppdumper\Il2CppDumper\Il2Cpp\Metadata.cs:line 102
at Il2CppDumper.Program.Init(String il2cppPath, String metadataPath, Metadata& metadata, Il2Cpp& il2Cpp) in C:\projects\il2cppdumper\Il2CppDumper\Program.cs:line 124
at Il2CppDumper.Program.Main(String[] args) in C:\projects\il2cppdumper\Il2CppDumper\Program.cs:line 98
看来没这么简单,扔进ida里静态分析一下
2. ilcpp源码分析#
【腾讯开发者社区】Il2cpp逆向:global-metadata解密
文章中的il2cpp调用链与实际不符,因为unity版本对不上,需要找到对应版本的il2cpp
查看
ez unity\ez unity_Data\level0
文件可以直接查看版本,ez unity是2022.3.17f1 (参考)在这里可以下载到所有版本的Unity和Il2Cpp
il2cpp_init
-> il2cpp::vm::Runtime::Init
-> il2cpp::vm::MetadataCache::Initialize
-> il2cpp::vm::GlobalMetadata::Initialize
-> vm::MetadataLoader::LoadMetadataFile
对比源码,找到了 il2cpp::vm::GlobalMetadata::Initialize
,过程附在了[[#2|下面]]
strcpy(v5, "fnlfdj*el~jhlzn>usg");
while ( 1 )
{
v7 = &v13;
if ( v6 > 0xF )
v7 = (__int128 *)v5;
*((_BYTE *)v7 + v4) ^= (_BYTE)v4 + 1;
if ( (unsigned __int64)++v4 >= v14.m128i_i64[0] )
break;
v6 = v14.m128i_u64[1];
v5 = (char *)v13;
}
v15 = v13;
这里是将字符串解密成 "global-metadata.bat"
让gpt反混淆了一下,直接得到解密算法
解密脚本:
enc = "fnlfdj*el~jhlzn>usg"
result = ''
for i in range(len(enc)):
result += chr(ord(enc[i]) ^ (i + 1))
print(result)
最后 global-metadata.dat
放在的是v13,即 [rsp+68h+var_48]
往下看代码
v9 = sub_7FFE44AFA8A0(v8);
这里的 sub_7FFE44AFA8A0
即对应 MetadataLoader::LoadMetadataFile
,直接加载文件了
继续往下看,发现并没有解密global-metadata.bat的地方,内存中的gm.bat也和实际的一样。
只是魔数不对,即应该是 AF 1B B1 FA
,不过程序也并没有校验魔数,不影响运行
继续比对源码发现,有4个字节 YURO 是多出来的,程序并没有删除掉,
而是直接跳过读取下一个 00 01 00 00
59 55 52 4F 1D 00 00 00 59 55 52 4F 00 01 00 00 | YURO....YURO....
把这4个字节删掉,用Dumper依然报错,真玄乎。
静态分析 一段时间之后,没什么成果,转换了个思路,尝试修改Il2CppDumper的源码
启发改源码的文章 【知乎】标题太长了就不放了
神奇,修改源码编译运行Dumper,就出来了(仓库)
挺玄乎的,这里不能直接把多出来的 YURO 直接删掉,需要在 Il2CppDumper 源码的
Il2CppDumper\Il2Cpp\MetadataClass.cs -> class Il2CppGlobalMetadataHeader
的
version
和stringLiteralOffset
之间 添加一个int来占位置
2024-04-07更新
学长说他们的办法是,和正常的gm.dat比较,就发现:
- 头是错的
- 只需要在头的末尾加4个字节的0就可以了
感觉这个才是比较通用的方法
剩下就是简单的静态分析找flag~
# 和BabyUnity一样,定位Wrong!就好
# pip install pycryptodome
import base64
from Crypto.Cipher import AES
encflag = base64.b64decode("pNufkEIU9dHjKXYXWiFyrthHYFEfqJAWcPM/t8/zX1w=")
key = "a216d5d34c2723f5"
iv = "9f68268f755b1363"
def bytes2str(bs):
result = ''
for b in bs:
result += hex(b)[2:]
return result
cipher = AES.new(key.encode(),AES.MODE_CBC, iv.encode())
dec_text = cipher.decrypt(encflag)
print(dec_text.decode())
逆向详细过程归档#
1. il2cpp版本#
一开始是自己摸索的,后来搜索了下原来可以直接在level0看
在ida中找到了il2cpp_init函数,通过对比il2cpp源码,推断版本是2019或2020
2. 最晕的逆向环节#
调用链如下(回去的传送门)
il2cpp_init
-> il2cpp::vm::Runtime::Init
-> il2cpp::vm::MetadataCache::Initialize
-> il2cpp::vm::MetadataLoader::LoadMetadataFile
晕了,反正大概跟着教程走吧
__int64 __fastcall il2cpp_init(__int64 a1)
{
sub_18025CDE8(0i64, &unk_1810C19BD);
return (unsigned __int8)sub_1801E8FD0(a1);
}
通过看源码对比出来这个 sub_1801E8FD0
就是 Runtime::Init
函数,按 N 重命名为 Runtime_Init,
点进去看,继续对比源码
qword_1813339D0 = (__int64)"4.0";
sub_1801BC610();
sub_1801A5E30();
sub_180181960();
off_181238E28();
if ( (unsigned __int8)sub_1801F5DE0() )
{
mono_thread_suspend_all_other_threads();
sub_18020F220();
sub_18018EC10();
sub_1801D1070(sub_1801DADC0, sub_1801DAD80);
sub_180277540(&qword_1813335A0, 0i64, 768i64);
v6 = sub_18021C4B0("mscorlib.dll");
v7 = sub_18021C4B0("__Generated");
这里的 sub_1801F5DE0
就是 MetadataCache::Initialize()
,点进去看
result = sub_18018E9C0(&dword_181333A00, &dword_181333A10);
if ( result )
{
sub_18017E590(*(_QWORD *)(qword_181333E70 + 8), *(unsigned int *)qword_181333E70);
这里的 sub_18018E9C0
就是加载 global-metadata.bat
的地方,点进去看
3. 高手gpt的反混淆#
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
__int128 v13;
int v14[2] = {19, 31};
// 初始化
char* v5 = (char *)malloc(0x20);
strcpy(v5, "fnlfdj*el~jhlzn>usg");
v13 = (__int128)(unsigned __int64)v5;
// 加密循环
for (int i = 0; i < v14[0]; ++i) {
((__int8*)v13)[i] ^= i + 1;
}
// 输出加密后的字符串
printf("%s\n", v5);
// 释放内存
free(v5);
return 0;
}
4. dumper不出来~硬来静态分析#
那只能等加载完之后,再找到对应的地址
先找字符串,在 GlobalMetadata.cpp
有 static Il2CppString** s_StringLiteralTable = NULL;
其中一个调用链是
il2cpp_init
-> Runtime::Init
-> MetadataCache::InitializeGCSafe
-> il2cpp::vm::GlobalMetadata::InitializeStringLiteralTable
-> s_StringLiteralTable
比对源码,sub_7FFE478A63E0()
就是 MetadataCache::InitializeGCSafe()
往下看, qword_7FFE489E2B50
就是存 s_StringLiteralTable
的地方,打个断点
试试能不能断到
GlobalMetadata::GetStringLiteralFromIndex
看来常规的调试很难有入口,尝试修改il2cppdumper的代码
难,感觉更简单的方法是,导入GlobalMetadata的结构体,然后对比源码,有错的地方就改
调试,没找到什么错误