某 Unity 游戏 Firda Hook 实战
前言
最近想着随便逆点东西,来练练手,刚好最近有个梗曲的游戏,就直接拿来练手了,刚好这个游戏没怎么加壳,对于我这个没怎么逆过 Unity 的人来说是个挺好的样本。
本项目的初衷是研究Hook技术与游戏引擎的交互逻辑,请勿将其用于非法盈利等任何商业用途,所有获取的附件请在的24小时内自行删除。
准备
- Il2CppDumper
- frida-il2cpp-bridge
- Frida (本文使用版本为17.1.3)
- IDA Pro
- ilspy-vscode(由于作者使用的是 Linux,体验比较好的 C# 反编译器就这个了,Windows可以使用 ilspy 和 dnSpy)
- JADX 或其它 APK 解包程序/解压缩工具
- 脑子
提取关键文件
安卓 Unity 游戏逆向主要需要 global-metadata.dat 和 libil2cpp.so,这两个文件可以使用 Il2CppDumper 以较为轻松地获取类名/方法名等信息,还可以在 IDA 中恢复符号表,便于逆向分析。
这两个文件的用途分别如下:
-
global-metadata.dat:用于保存 C# 中的元数据,包括但不限于类名、方法名、变量名、字符串内容、特性 Attributes 等,有些游戏可能会加密此内容,可能需要特殊方法来恢复,比如使用 Zygisk-Il2CppDumper,可以在游戏运行时 Dump 在内存中已解密的数据。不过本文样本中此文件并未加密,所以此方法本文并未过多描述。 -
libil2cpp.so:游戏主要的程序逻辑在此处,是 Unity 将 C# 代码翻译为 IL 后再将 IL 转为 C++,最后编译形成,Dump 相关数据后主要分析的就是此文件。
Dump 并恢复符号表
我这里使用了 JADX 来导出两个关键文件,使用其它的 APK 解包工具或直接使用解压缩工具解压都是可以的
还有一个问题就是样本是从 Google Play 上获得的,而直接提取 base.apk 没有 libil2cpp.so,这里我是从 Apkpure 上获取到对应的 apkx,然后从其中的一个 APK 中获取到了这个文件
提取后就可以使用 Il2CppDumper 来 Dump 了
Dump
由于作者使用的是 Linux,所以需要先安装好 .NET 环境才能运行 Il2CppDumper
而且因为这个项目有些年头了,如果使用 Release 中的预编译程序,需要在运行时加上环境变量 DOTNET_ROLL_FORWARD=Major 来向下兼容(Linux 不好安装 .NET 6/7,但预编译的程序是需要 .NET 6/7 的)
DOTNET_ROLL_FORWARD=Major dotnet Il2CppDumper.dll global-metadata.dat libil2cpp.so ./output # 请自行根据具体路径修改指令
Dump 过后可以获得类似下面的文件,不同游戏可能不一样:
├── DummyDll
│ ├── Assembly-CSharp.dll (一般游戏类/方法等会在此文件下,但也有可能修改其它依赖部分)
│ ├── __Generated
│ ├── Il2CppDummyDll.dll
│ ├── Mono.Security.dll
│ ├── mscorlib.dll
│ ├── SimpleGDPRConsent.Runtime.dll
│ ├── System.Configuration.dll
│ ├── System.Core.dll
│ ├── System.dll
│ ├── System.Numerics.dll
│ ├── System.Xml.dll
│ ├── UnityEngine.AndroidJNIModule.dll
│ ├── UnityEngine.AnimationModule.dll
│ ├── UnityEngine.AudioModule.dll
│ ├── UnityEngine.CoreModule.dll
│ ├── UnityEngine.dll
│ ├── UnityEngine.GameCenterModule.dll
│ ├── UnityEngine.GridModule.dll
│ ├── UnityEngine.ImageConversionModule.dll
│ ├── UnityEngine.IMGUIModule.dll
│ ├── UnityEngine.InputLegacyModule.dll
│ ├── UnityEngine.JSONSerializeModule.dll
│ ├── UnityEngine.ParticleSystemModule.dll
│ ├── UnityEngine.Physics2DModule.dll
│ ├── UnityEngine.PhysicsModule.dll
│ ├── UnityEngine.SharedInternalsModule.dll
│ ├── UnityEngine.SpriteShapeModule.dll
│ ├── UnityEngine.TextCoreFontEngineModule.dll
│ ├── UnityEngine.TextCoreTextEngineModule.dll
│ ├── UnityEngine.TextRenderingModule.dll
│ ├── UnityEngine.TilemapModule.dll
│ ├── UnityEngine.UI.dll
│ ├── UnityEngine.UIElementsModule.dll
│ ├── UnityEngine.UIElementsNativeModule.dll
│ ├── UnityEngine.UIModule.dll
│ ├── UnityEngine.UnityAnalyticsModule.dll
│ ├── UnityEngine.UnityWebRequestModule.dll
│ ├── UnityEngine.UnityWebRequestTextureModule.dll
│ └── UnityEngine.UnityWebRequestWWWModule.dll
├── dump.cs
├── il2cpp.h *1
├── script.json *1
└── stringliteral.json *2
恢复符号表
在 IDA 中打开libil2cpp.so,然后在 File -> Script file 加载 il2cppdumper 提供的脚本,脚本ida_py3.py 加载 *2 的文件,脚本ida_with_struct_py3.py加载 *1 的文件来恢复部分符号表
过程中可能会有点卡顿,等就对了
结合 ilspy-vscode 就可以开始静态分析了,使用 ilspy 来逆向 DummyDll 中的内容,来查看有哪些类/方法/属性
frida-il2cpp-bridge 的部署与使用
静态分析完可以看看动态分析(其实我都是直接看调用栈的没怎么静态分析(逃))
部署
在一个空文件夹下使用 frida-create -t agent,然后将生成的 package.json 改为以下内容
{
"name": "your-agent-name",
"version": "1.0.0",
"description": "Frida agent written in TypeScript",
"private": true,
"main": "agent/index.ts",
"type": "module",
"scripts": {
"prepare": "npm run build",
"build": "frida-compile -o _.js -w agent/index.ts",
"attach": "run() { frida -U \"$1\" -l _.js --runtime=v8; }; run",
"spawn": "run() { frida -U -f \"$1\" -l _.js --runtime=v8; }; run",
"app0-spawn": "npm run spawn com.example.application0",
"app1": "npm run \"Application1 Name\"",
"app1-spawn": "npm run spawn com.example.application1"
},
"devDependencies": {
"@types/frida-gum": "^19.0.0",
"@types/node": "^18.14.0",
"frida-il2cpp-bridge": "*"
}
}
相关内容:https://docs.npmjs.com/cli/v7/configuring-npm/package-json/
使用
写完脚本 index.ts 可以开两个终端,一个运行:
npm run build
来实现实时编译
另一个运行:
npm run spawn <游戏包名>
来附加 Frida 到对应游戏上
查看游戏 Unity 版本
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
console.log(Il2Cpp.unityVersion);
});
上面的代码是最简短的 frida-il2cpp-bridge 代码,可以查看游戏的 Unity 版本,同时测试 frida-il2cpp-bridge 是否能正常运行
追踪类
Il2Cpp.trace(true).classes(OnlineManager).and().attach(); //trace()为不显示参数及返回值,传入true则显示,
传入 classes 的类需要提前定义,比如下方:
const Assembly = Il2Cpp.domain.assembly("Assembly-CSharp").image;
const OnlineManager = Assembly.class("OnlineManager");
这个代码可以看调用栈,但我这里测试的时候,如果传入 trace 的参数为 true,即显示方法被调用时的实参与返回值时,程序有的时候会显示 Error: access violation accessing 0x1 之类的错误
这个有点玄学,不知道是为何引起的,重启或许能解决法90%的问题
修改实参或返回值
GetTimeTillBonus.implementation = function () {
console.log("GetTimeTillBonus called, returning 0");
return 0;
}
需要修改的方法需要提前我们定义,比如下方:
const GetTimeTillBonus = OnlineManager.method("GetTimeTillBonus");
正如上面的调用栈所示,这个方法是用来获取每日虫子的剩余时间的,所以使用上方的函数来将剩余时间改为0,这样就实现虫子自由了(虽然改存档也可以,这个游戏的存档在 SharedPrefs 首选项中,用爱玩机工具箱等工具可以直接修改)
修改前:
修改后:
调用方法/查看实参
SeedPecked.implementation = function (...args) {
console.log(`[+] 成功调用: Gameplay::SeedPecked`);
console.log('[+] 参数列表:', args);
if (!alreadyFire) {
alreadyFire = true;
this.method("StartFire").invoke();
}
if (!alreadyUFO) {
alreadyUFO = true;
this.method("LaunchUFOCoroutine").invoke();
}
return this.method("SeedPecked").invoke(...args);
}
调用方法需要先获取对应的实例,上面的示例是 SeedPecked 和 StartFire/LaunchUFOCoroutine 在同一个实例中,所以可以直接用 this 来调用,要注意传入的参数是否符合形参要求,否则会报错或者导致应用闪退
上面的例子也展示了如何查看传入的参数,args 是一个列表,可以打印来查看
上面的例子实现了啄到种子后直接启用两个道具功能,实现了开挂(我去,桂狗)
更多的使用方法可以查看官方文档:Snippets
下面附上我这次测试的完整代码
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
console.log(Il2Cpp.unityVersion);
const Assembly = Il2Cpp.domain.assembly("Assembly-CSharp").image;
const SkinManager = Assembly.class("SkinManager");
const IsSkinUnlocked = SkinManager.method("IsSkinUnlocked");
const Gameplay = Assembly.class("Gameplay");
const RefreshWorm = Gameplay.method("RefreshWorm");
const SeedPecked = Gameplay.method("SeedPecked");
const StartFire = Gameplay.method("StartFire");
const FinishFire = Gameplay.method("FinishFire");
const LaunchUFOCoroutine = Gameplay.method("LaunchUFOCoroutine");
const FinishUFO = Gameplay.method("FinishUFO");
const OnlineManager = Assembly.class("OnlineManager");
const GetTimeTillBonus = OnlineManager.method("GetTimeTillBonus");
const ClaimBonus = OnlineManager.method("ClaimBonus");
let alreadyFire = false;
let alreadyUFO = false;
// 下面是控制某代码片段是否运行的
let traceEnabled = false;
let cheatEnabled = false;
let unlockAllSkinsEnabled = false;
let unlimitedWormsEnabled = false;
if (cheatEnabled) {
SeedPecked.implementation = function (...args) {
console.log(`[+] 成功调用: Gameplay::SeedPecked`);
console.log('[+] 参数列表:', args);
if (!alreadyFire) {
alreadyFire = true;
this.method("StartFire").invoke();
}
if (!alreadyUFO) {
alreadyUFO = true;
this.method("LaunchUFOCoroutine").invoke();
}
return this.method("SeedPecked").invoke(...args);
}
StartFire.implementation = function (...args) {
console.log(`[+] 成功调用: Gameplay::StartFire`);
console.log('[+] 参数列表:', args);
return this.method("StartFire").invoke(...args);
}
FinishFire.implementation = function (...args) {
console.log(`[+] 成功调用: Gameplay::FinishFire`);
console.log('[+] 参数列表:', args);
alreadyFire = false;
return this.method("FinishFire").invoke(...args);
}
LaunchUFOCoroutine.implementation = function (...args) {
console.log(`[+] 成功调用: Gameplay::LaunchUFOCoroutine`);
console.log('[+] 参数列表:', args);
return this.method("LaunchUFOCoroutine").invoke(...args);
}
FinishUFO.implementation = function (...args) {
console.log(`[+] 成功调用: Gameplay::FinishUFO`);
console.log('[+] 参数列表:', args);
alreadyUFO = false;
return this.method("FinishUFO").invoke(...args);
}
}
if (unlimitedWormsEnabled) {
GetTimeTillBonus.implementation = function () {
console.log("GetTimeTillBonus called, returning 0");
return 0;
}
}
if (unlockAllSkinsEnabled){
IsSkinUnlocked.implementation = function (...args) {
const skinId = args[0];
console.log(`[+] 成功调用: SkinManager::IsSkinUnlocked, skinId: ${skinId}`);
return true;
}
}
if (traceEnabled) {
Il2Cpp.trace(true).classes(OnlineManager).and().attach();
}
});
琼公网安备46010602001577号