前言
-
声明:本文仅用于 技术研究与学习目的,讨论的是 Unity 游戏打包结构、IL2CPP 原理、
GameAssembly.dll分析方法 等知识点,不涉及任何商业游戏资源的发布或破解。 -
本文将使用
Il2CppDumper解析GameAssembly.dll与global-metadata.dat,结合ILSpy查看类与方法结构,再通过Ghidra对机器码函数进行反汇编和伪 C 代码分析,最终可以得到的效果:
-
大致的流程:
GameAssembly.dll + global-metadata.dat
↓
Il2CppDumper → 生成 script.json / stringliteral.json / DummyDll
↓
ILSpy → 查看 DummyDll 类结构
↓
Ghidra → 导入 GameAssembly.dll + JSON 映射 → 查看函数实现
1 原理解析
1-1 Unity打包项目结构
- 当 Unity 项目打包后,通常会生成类似如下的目录结构:
GameName
├─GameName.exe
├─GameName_Data
│ ├─Managed
│ ├─Plugins
│ ├─Resources
│ ├─StreamingAssets
│ └─globalgamemanagers
- 不同脚本后端会导致 代码存储方式不同
- 通常在 Mono 脚本后端 下:
GameName_Data
└─Managed
├─Assembly-CSharp.dll
├─UnityEngine.dll
└─其他程序集
- 如果项目使用 IL2CPP:
GameName
├─GameAssembly.dll
└─GameName_Data
└─il2cpp_data
└─Metadata
└─global-metadata.dat
1-2 Assembly-CSharp.dll
- 在 Unity 项目中:
Assembly-CSharp.dll是 默认的游戏脚本程序集。
- 所有用户编写的脚本,例如:
Player.cs
Enemy.cs
GameManager.cs
- 都会被编译到:
Assembly-CSharp.dll - 因此如果是Mono 脚本后端打包的项目,我们可以直接通过解包工具直接反汇编
Assembly-CSharp.dll得到游戏的工程源码 - 需要注意的是,在 Mono 脚本后端 下,
Assembly-CSharp.dll中存储的并不是机器码,而是 .NET 中间语言(IL,Intermediate Language)。 - IL 是一种 高级虚拟机指令,仍然保留了大量原始程序结构信息,例如:
- 类结构
- 方法名
- 字段
- 控制流程
- 局部变量
- 因此使用
ILSpy、dnSpy等工具反编译时,可以较容易地恢复出 接近原始 C# 的代码结构。 - 例如:
public void Attack()
{
if (hp <= 0)
{
Die();
}
}
1-3 IL2CPP

- IL2CPP(Intermediate Language To C++)是 Unity 提供的 AOT(Ahead-of-Time)编译方案。
- 其编译流程如下:
C# Script
↓
IL(Assembly-CSharp.dll)
↓
IL2CPP 转换
↓
C++ 代码
↓
C++ 编译器
↓
GameAssembly.dll
- 与 Mono 不同,IL2CPP 在构建过程中会将 IL 转换为 C++ 再编译为机器码,因此最终生成的
GameAssembly.dll中存储的是 原生机器指令,而不是 .NET 的 IL。 - 这意味着:
GameAssembly.dll中的方法已经被编译为 CPU 指令- 原始的 C# 语法结构已经丢失
- 只能通过反汇编工具查看底层汇编代码
- 同时 Unity 会将 类型信息 单独存储在:
global-metadata.dat
- 该文件中包含:
- 类名
- 方法名
- 字段信息
- 参数类型
- 命名空间
- 因此在 IL2CPP 项目中:
GameAssembly.dll→ 机器码逻辑global-metadata.dat→ 类型与方法信息
- 正是因为 代码与类型信息被拆分存储,普通的 .NET 反编译工具(如
ILSpy、dnSpy)无法直接解析 IL2CPP 程序。 - 如下就是解包后的一段例子:
[Token(Token = "0x6000531")]
[Address(RVA = "0x32B490", Offset = "0x329E90", VA = "0x18032B490")]
public void OnClick_BtnItem()
{
}
- 因此需要借助
Il2CppDumper等工具,将两者结合起来 重建程序集结构,从而恢复出类似Assembly-CSharp.dll的代码框架。 - 这也是 IL2CPP 相比 Mono 更难逆向分析的主要原因。
1-4 Il2CppDumper
- 当然上有政策下有对策
Il2CppDumper是一个常用的 IL2CPP 解析工具。参考下载链接
- 由于 IL2CPP 项目中:
GameAssembly.dll → 机器码逻辑
global-metadata.dat → 类型信息
- 代码逻辑与类型信息被 拆分存储,因此普通的 .NET 反编译工具无法直接解析。
Il2CppDumper的核心作用就是 重新关联这两个文件中的信息,从而恢复程序的结构。- 具体来说,它会完成以下工作:
- 解析
GameAssembly.dll中的 IL2CPP 结构 - 解析
global-metadata.dat中的类型数据
- 解析
- 然后将方法地址与类型信息重新匹配,并重建程序集结构
- 解析完成后,工具会生成如下文件:
DummyDll
script.json
stringliteral.json
dump.cs
- DummyDll
- 生成的 伪程序集 DLL,通常是
Assembly-CSharp.dll - 包含类结构、方法签名、字段信息
- 方法体为空,但可以用于 理解游戏结构和类关系
- 生成的 伪程序集 DLL,通常是
- script.json
- 保存 方法、类型、字段等映射信息
- 可以被 IL2CPP 分析工具或 IDA/Ghidra 脚本读取,用于快速定位方法或类型
- stringliteral.json
- 保存程序中 字符串字面量(string literal) 的信息
- 对分析 UI 文本、配置或提示信息很有帮助
- dump.cs
- 是 Il2CppDumper 自动生成的 C# 脚本
- 作用是 帮助快速定位函数、字段和类型
- 虽然方法体为空,但可以作为 辅助参考,结合反汇编工具进行逻辑分析
1-5 ILSpy
ILSpy是一个 .NET 反编译工具。参考下载链接- 它可以:
- 查看
.dll - 反编译 C# 代码
- 导出 Visual Studio 项目
- 查看
- 在 Mono 项目中,
ILSpy可以直接反编译:
Assembly-CSharp.dll
- 并恢复出接近原始的 C# 源代码。
- 但在 IL2CPP 项目中,
ILSpy的作用主要是:- 读取 Il2CppDumper 生成的 DummyDll
- 通过
ILSpy打开DummyDll/Assembly-CSharp.dll后,可以看到:- 游戏的命名空间结构
- 类之间的继承关系
- 方法签名
- 字段定义
- 因此 IL2CPP 的常见分析流程是:
GameAssembly.dll + global-metadata.dat
↓
Il2CppDumper
↓
DummyDll
↓
ILSpy
↓
Assembly-CSharp.csproj
- 最终可以得到一个 可浏览的伪项目结构,从而更方便地理解程序的整体架构。
1-6 Mono vs IL2CPP 对比表
| 项目 | Mono 脚本后端 | IL2CPP 脚本后端 |
|---|---|---|
| 编译输出 | Assembly-CSharp.dll(IL 中间语言) | GameAssembly.dll(机器码) + global-metadata.dat(类型信息) |
| 代码可读性 | 可直接使用 ILSpy / dnSpy 反编译成接近原始 C# | 方法体为空,需通过 Il2CppDumper + 反汇编工具分析 |
| 方法逻辑存储 | IL 代码包含方法实现 | 机器码在 GameAssembly.dll,DummyDll 仅包含结构信息 |
| 类型信息存储 | 存储在 DLL 中 | 存储在 global-metadata.dat 中,独立于逻辑代码 |
| 工具支持 | ILSpy、dnSpy 直接反编译 | Il2CppDumper 生成 DummyDll + ILSpy 查看结构;IDA/Ghidra 查看逻辑 |
| 分析难度 | 简单,可快速恢复源码 | 较高,需要工具链 + 反汇编配合分析 |
| 优点 | 反编译易,调试简单 | AOT 编译,性能和安全性更高 |
| 缺点 | 性能较低,逆向容易 | 逆向难度大,源码不可直接获取 |
1-7 Ghidra
- Ghidra 是由美国国家安全局(NSA)开源的一款 反汇编与逆向分析工具。官方链接
- 功能特点:
- 支持多种处理器架构(x86, x64, ARM, ARM64, MIPS 等)
- 可以分析 PE、ELF、Mach-O 等可执行文件格式
- 内置反编译器,可将汇编代码转换为 伪 C 代码,便于理解
- 支持脚本扩展(Jython / Java / Python),可自动化批量标记符号与函数
- 在 IL2CPP 项目中,Ghidra 的作用:
- 分析 GameAssembly.dll(机器码逻辑)
- 与 Il2CppDumper 生成的
script.json/stringliteral.json配合,将函数名、类型信息映射回机器码 - 查看具体函数实现、内存布局、调用关系
- 补充 IL2CPP 结构信息中 方法体为空 的情况
- 使用流程:
GameAssembly.dll
↓
Ghidra 导入
↓
分析
↓
使用 Il2CppDumper JSON 映射
↓
查看函数实现
2 教程
2-1 准备文件
- 首先找到两个核心文件:
GameAssembly.dllglobal-metadata.dat
- 一般位于
游戏目录
├─GameAssembly.dll
└─GameName_Data
└─il2cpp_data
└─Metadata
└─global-metadata.dat
2-2 使用 Il2CppDumper
- 打开
Il2CppDumper.exe - 在文件选择窗口中选择:
GameAssembly.dll
global-metadata.dat
- 工具会自动解析并生成输出文件,输出目录中会出现:

- 其中
DummyDll是解包后的文件目录,里头有生成的 伪程序集 DLL,通常是Assembly-CSharp.dll - 而
script.json和stringliteral.json则是这些程序集的方法、类型、字段等映射信息,一会我们会使用Ghidra 脚本读取,用于快速定位方法或类型
2-3 使用 ILSpy 读取 DummyDll
- 打开
ILSpy:- 选择 File → Open
- 打开:
DummyDll/Assembly-CSharp.dll

-
此时可以看到游戏的整个伪项目:

-
虽然方法体通常为空,但可以获得:
- 类结构
- 方法签名
- 字段信息
2-4 (可选)导出 Visual Studio 项目
- 在
ILSpy中:- 右键
Assembly-CSharp - 选择:
Save Code
- 右键
- ILSpy 会自动生成:
Assembly-CSharp.csproj
- 我们可以使用 Visual Studio 打开
Assembly-CSharp.csproj
2-5 使用Ghidra反汇编函数具体实现
2-5-1 准备 Ghidra 项目
-
找到
ghidra下载文件夹下的support打开pyghidraRun.bat
-
注意!!!这里必须使用
pyghidraRun.bat打开,否则一会无法在ghidra中运行python脚本- 参考官方的issues:Python Scripts stopped working in 12.1 #8555
- 参考官方的issues:Python Scripts stopped working in 12.1 #8555
-
打开 Ghidra → File → New Project → 选择 Non-Shared Project

2-5-2 导入 GameAssembly.dll
-
File→ Import File → 选择
GameAssembly.dll,Ghidra 会自动识别文件类型(PE / x86-64 / ARM64 等,根据平台选择)点击 OK 进行导入 -

-
Project Window 中双击导入的 DLL,打开 CodeBrowser 窗口
-
导入完成后,Ghidra 会弹出 “Analyze Imported File” 窗口

-
分析选项可以保持默认,点击 Analyze

-
现在已经可以看到具体实现了但是仍缺乏具体的函数名

2-5-3 加载 Il2CppDumper JSON 数据还原函数名
-
Window → Script Manager, 我们点击新建脚本

-
类型我们选择
PyGhidra -

-
将 Il2CppDumper 提供的 ghidra.py 脚本 拷贝到新建脚本
-
注意在脚本开头加上
# @runtime jython
- 这里也提供完整的代码实现
# -*- coding: utf-8 -*-
# @runtime jython
import json
processFields = [
"ScriptMethod",
"ScriptString",
"ScriptMetadata",
"ScriptMetadataMethod",
"Addresses",
]
functionManager = currentProgram.getFunctionManager()
baseAddress = currentProgram.getImageBase()
USER_DEFINED = ghidra.program.model.symbol.SourceType.USER_DEFINED
def get_addr(addr):
return baseAddress.add(addr)
def set_name(addr, name):
name = name.replace(' ', '-')
createLabel(addr, name, True, USER_DEFINED)
def make_function(start):
func = getFunctionAt(start)
if func is None:
createFunction(start, None)
f = askFile("script.json from Il2cppdumper", "Open")
data = json.loads(open(f.absolutePath, 'rb').read().decode('utf-8'))
if "ScriptMethod" in data and "ScriptMethod" in processFields:
scriptMethods = data["ScriptMethod"]
monitor.initialize(len(scriptMethods))
monitor.setMessage("Methods")
for scriptMethod in scriptMethods:
addr = get_addr(scriptMethod["Address"])
name = scriptMethod["Name"].encode("utf-8")
set_name(addr, name)
monitor.incrementProgress(1)
if "ScriptString" in data and "ScriptString" in processFields:
index = 1
scriptStrings = data["ScriptString"]
monitor.initialize(len(scriptStrings))
monitor.setMessage("Strings")
for scriptString in scriptStrings:
addr = get_addr(scriptString["Address"])
value = scriptString["Value"].encode("utf-8")
name = "StringLiteral_" + str(index)
createLabel(addr, name, True, USER_DEFINED)
setEOLComment(addr, value)
index += 1
monitor.incrementProgress(1)
if "ScriptMetadata" in data and "ScriptMetadata" in processFields:
scriptMetadatas = data["ScriptMetadata"]
monitor.initialize(len(scriptMetadatas))
monitor.setMessage("Metadata")
for scriptMetadata in scriptMetadatas:
addr = get_addr(scriptMetadata["Address"])
name = scriptMetadata["Name"].encode("utf-8")
set_name(addr, name)
setEOLComment(addr, name)
monitor.incrementProgress(1)
if "ScriptMetadataMethod" in data and "ScriptMetadataMethod" in processFields:
scriptMetadataMethods = data["ScriptMetadataMethod"]
monitor.initialize(len(scriptMetadataMethods))
monitor.setMessage("Metadata Methods")
for scriptMetadataMethod in scriptMetadataMethods:
addr = get_addr(scriptMetadataMethod["Address"])
name = scriptMetadataMethod["Name"].encode("utf-8")
methodAddr = get_addr(scriptMetadataMethod["MethodAddress"])
set_name(addr, name)
setEOLComment(addr, name)
monitor.incrementProgress(1)
if "Addresses" in data and "Addresses" in processFields:
addresses = data["Addresses"]
monitor.initialize(len(addresses))
monitor.setMessage("Addresses")
for index in range(len(addresses) - 1):
start = get_addr(addresses[index])
make_function(start)
monitor.incrementProgress(1)
print 'Script finished!'
-
点击运行按钮

-
如果运行python脚本报错:
- 请确保打开ghidra的方式为
pyghidraRun.bat - 且脚本开头添加了
# @runtime jython
- 请确保打开ghidra的方式为
-
运行脚本 → 弹出文件选择窗口 → 选择:
script.json(方法、类型映射)stringliteral.json(字符串映射)
-
脚本执行后:
- 函数、类、字段、字符串会被自动标记和命名
- 函数名和类型信息会与 IL2CPP 结构对应
- 可在 Functions Window 中查看已映射的函数列表

2-5-4 查看函数实现
-
在 Functions Window 中双击函数 → 打开 Listing 或 Decompile 窗口

-
可以看到伪 C 代码(伪汇编)

-
注意:
- 方法内部逻辑可读
- 局部变量名通常无法还原,需人工命名分析

2-6 流程总结
GameAssembly.dll + global-metadata.dat
↓
Il2CppDumper → 生成 script.json / stringliteral.json / DummyDll
↓
ILSpy → 查看 DummyDll 类结构
↓
Ghidra → 导入 GameAssembly.dll + JSON 映射 → 查看函数实现
总结
- 本文系统讲解了 Unity 游戏打包结构、Mono 与 IL2CPP 的差异、以及 IL2CPP 项目的逆向分析流程,重点介绍了如何利用
Il2CppDumper解析GameAssembly.dll与global-metadata.dat,结合ILSpy查看类与方法结构,再通过Ghidra对机器码函数进行反汇编和伪 C 代码分析,从而在方法体不可直接还原的情况下理解游戏逻辑和调用关系,为学习和研究 Unity 游戏逆向提供了完整、可操作的技术方案。 - 如有错误欢迎指出!感谢大家的观看
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/m0_73800387/article/details/159156747



