24kcsplus
文章24
标签40
分类0
某 Unity 游戏 Firda Hook 实战

某 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.datlibil2cpp.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 中获取到了这个文件

global-metadata.dat,右键此文件有导出选项

libil2cpp.so,如果右键没有反应,你可能需要升级 JADX 了

提取后就可以使用 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);
}

调用方法需要先获取对应的实例,上面的示例是 SeedPeckedStartFire/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();
    }

});
本文作者:24kcsplus
本文链接:https://24kblog.top/posts/2611483955/
版权声明:除非特别声明,否则本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×