Logo WP

EzUnity

难度Super Damn Hard Pro的Unity

![[BL_Endian.png]]

你知道il2cpp吗?

难度:hard pro

出题人:@yuro

💡或许你需要学习一下il2cpp逆向相关的知识?

💡il2cpp是开放源代码的

💡修复gm.dat好像很麻烦,有没有不通过gm.dat也能dump的方法呢

文件{:download="EzUnity.zip"}

概览#

  1. 脱壳
  2. 修补global-metadata.bat
  3. 看il2cpp源码+ida静态分析
  4. 修改Il2CppDumper代码,dump,导入ida
  5. 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解密

【看雪论坛】IDA技巧——结构体

文章中的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

versionstringLiteralOffset之间 添加一个int来占位置

2024-04-07更新

学长说他们的办法是,和正常的gm.dat比较,就发现:

  1. 头是错的
  2. 只需要在头的末尾加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.cppstatic 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的结构体,然后对比源码,有错的地方就改

调试,没找到什么错误