vfsfitvnm / frida-il2cpp-bridge

A Frida module to dump, trace or hijack any Il2Cpp application at runtime, without needing the global-metadata.dat file.
https://github.com/vfsfitvnm/frida-il2cpp-bridge/wiki
MIT License
974 stars 199 forks source link

Pass custom callback as parameter when invoking a method #283

Closed commonuserlol closed 1 year ago

commonuserlol commented 1 year ago

I want to pass a function onWindow from typescript as an argument to a method, how can I do this? code:

function onWindow(id: number) {

}
function gui() {
    Il2Cpp.perform(() => {
        const AssemblyCSharp = Il2Cpp.Domain.assembly("Assembly-CSharp").image;
        const ImGui = Il2Cpp.Domain.assembly("UnityEngine.IMGUIModule").image;
        const UnityEngine = Il2Cpp.Domain.assembly("UnityEngine.CoreModule").image;
        const Rect = UnityEngine.class("UnityEngine.Rect");
        const GUI = ImGui.class("UnityEngine.GUI");
        const size = Rect.alloc();
        size.method(".ctor").overload("System.Single", "System.Single", "System.Single", "System.Single").invoke(100, 100, 100, 100);
        const snapshot = Il2Cpp.MemorySnapshot.capture();
        //some code
        instance.method("FixedUpdate").implementation = function() {
           this.method("FixedUpdate").invoke();
           GUI.method("Window").invoke(1, size, onWindow, Il2Cpp.String.from("imgui window"));
        }
        //some code
    });
}

unity docs for UnityEngine.GUI.Window: https://docs.unity3d.com/ScriptReference/GUI.Window.html

vfsfitvnm commented 1 year ago

There's no straightforward way to do it, unfortunately.

Read https://github.com/vfsfitvnm/frida-il2cpp-bridge/discussions/214 and https://github.com/vfsfitvnm/frida-il2cpp-bridge/issues/283.

This implementation might be helpful: https://github.com/vfsfitvnm/frida-il2cpp-bridge/blob/master/src/il2cpp/structs/thread.ts#L96.

However, I could provide an API to do that without all the hassle...

commonuserlol commented 1 year ago

I found a method that works for me, static void CheckEraRange(System.Int32) from the System.Globalization.HijriCalendar class. now my code looks like this

const IntPtr = mscorlib.class("System.IntPtr");
const GUI = ImGui.class("UnityEngine.GUI");
const WindowFunction = GUI.nested("WindowFunction");
const Calendar = mscorlib.class("System.Globalization.HijriCalendar");
Calendar.method("CheckEraRange").implementation = function(id: number) {}
const nullptr = IntPtr.new();
nullptr.method(".ctor").overload("System.Int32").invoke(0);
const callback = WindowFunction.new();
callback.method(".ctor").invoke(Calendar.method("CheckEraRange").object, nullptr); //.ctor(System.Object object, System.IntPtr method);
//some code
GUI.method("Window").invoke(1, size, callback, Il2Cpp.String.from("test window"));

but i still get the error an unexpected native function exception occurred, this is due to parameter types mismatch, do you know why this might be?

commonuserlol commented 1 year ago

i built (not mine) game using Unity and added the code with built-in ImGui. The interesting thing is that the window does not appear when I create it in Update() method and behaves normally in OnGUI().

vfsfitvnm commented 1 year ago

Well, I have no experience there so I can't really help you unfortunately

commonuserlol commented 1 year ago

There's no straightforward way to do it, unfortunately.

Read #214 and #283.

This implementation might be helpful: https://github.com/vfsfitvnm/frida-il2cpp-bridge/blob/master/src/il2cpp/structs/thread.ts#L96.

However, I could provide an API to do that without all the hassle...

what do you mean by API here?

vfsfitvnm commented 1 year ago

Maybe I could provide a class so that you can easily pass a JS callback:

const delegate = Il2Cpp.Delegate.wrap(() => {
    console.log("Hello!");
});

// you can now pass 'delegate' to method invocations
commonuserlol commented 1 year ago

it's a good idea, but as far as I know frida doesn't know how to create methods, so you'll have to use "crutches".

vfsfitvnm commented 1 year ago

Done! https://github.com/vfsfitvnm/frida-il2cpp-bridge/commit/ee0884fb99d500decfc820024aa8abbdb467ab85

vfsfitvnm commented 1 year ago
const IMGUIModule = Il2Cpp.Domain.assembly("UnityEngine.IMGUIModule.dll").image;
const GUI = IMGUIModule.class("UnityEngine.GUI");

// pretty easy now, isn't it? :P
const delegate = GUI.nested("WindowFunction").delegate((id: number) => console.log("Delegate!", id));

However yes, if you call Il2Cpp.installExceptionListener("all");, you can see it raises You can only call GUI functions from inside OnGUI!

commonuserlol commented 1 year ago

then, is there a way to "create" OnGUI method?

vfsfitvnm commented 1 year ago

Nope, you need to hook OnGUI from whatever MonoBehaviour you have

vfsfitvnm commented 1 year ago

I mean, if we only could replicate what OnGUI does under the hood, then yes...

commonuserlol commented 1 year ago

Nope, you need to hook OnGUI from whatever MonoBehaviour you have

the point is that there is no OnGUI method in the class

commonuserlol commented 1 year ago

I mean, if we only could replicate what OnGUI does under the hood, then yes...

i saw that in UnityEngine.GUI there is a method that checks whether it is running in OnGUI or not. later I will check and try to change its behavior.

commonuserlol commented 1 year ago

I'm sorry, but how to load assemblies now? screenshot upd: frida also throws TypeError: not a function on this line

vfsfitvnm commented 1 year ago

Hey, if you are willing to use master you should read the commit messages and the source code as well; changes are undocumented until the next release

commonuserlol commented 1 year ago

already figured it out. maybe I'm doing something wrong, but I still get the error an unexpected native function exception occurred, this is due to parameter types mismatch

Il2Cpp.perform(() => {
    Il2Cpp.installExceptionListener("all");
    const CoreModule = Il2Cpp.domain.assembly("UnityEngine.CoreModule").image;
    const ImGUIModule = Il2Cpp.domain.assembly("UnityEngine.IMGUIModule").image;
    const AssemblyCSharp = Il2Cpp.domain.assembly("Assembly-CSharp").image;
    const EnemyAI = AssemblyCSharp.class("EnemyAIGranny");
    const Rect = CoreModule.class("UnityEngine.Rect");
    const GUI = ImGUIModule.class("UnityEngine.GUI");
    const Utilitty = ImGUIModule.class("UnityEngine.GUIUtility");
    Utilitty.method("CheckOnGUI").implementation = function () {

    }
    const size = Rect.alloc();
    const delegate = Il2Cpp.delegate(GUI.nested("WindowFunction"), (id: number) => console.log("Delegate!", id));
    size.method(".ctor").overload("System.Single", "System.Single", "System.Single", "System.Single").invoke(100, 100, 100, 100);
    EnemyAI.method("FixedUpdate").implementation = function() {
        this.method("FixedUpdate").invoke();
        GUI.method("Window").invoke(1, size, delegate, Il2Cpp.string("test window"));
    }
});
vfsfitvnm commented 1 year ago

Err, I don't know. Any sample to reproduce (i.e. app name etc whatever to reproduce)?

(I'm closing this issue)

commonuserlol commented 1 year ago

game: granny (you can download the latest version in google play or https://apkpure.com/granny/com.dvloper.granny) i am using frida server latest version used esbuild to compile the script: .\node_modules\.bin\esbuild index.ts --bundle --outfile=_.js (windows) frida command: frida -Uf com.dvloper.granny -l _.js my script:

import "frida-il2cpp-bridge";
function GUI() {
    Il2Cpp.perform(() => {
        Il2Cpp.installExceptionListener("all");
        const CoreModule = Il2Cpp.domain.assembly("UnityEngine.CoreModule").image;
        const ImGUIModule = Il2Cpp.domain.assembly("UnityEngine.IMGUIModule").image;
        const AssemblyCSharp = Il2Cpp.domain.assembly("Assembly-CSharp").image;
        const EnemyAI = AssemblyCSharp.class("EnemyAIGranny");
        const Rect = CoreModule.class("UnityEngine.Rect");
        const GUI = ImGUIModule.class("UnityEngine.GUI");
        const Utilitty = ImGUIModule.class("UnityEngine.GUIUtility");
        Utilitty.method("CheckOnGUI").implementation = function () {

        }
        const size = Rect.alloc();
        const delegate = Il2Cpp.delegate(GUI.nested("WindowFunction"), (id: number) => console.log("Delegate!", id));
        size.method(".ctor").overload("System.Single", "System.Single", "System.Single", "System.Single").invoke(100, 100, 100, 100);
        EnemyAI.method("FixedUpdate").implementation = function() {
            this.method("FixedUpdate").invoke();
            GUI.method("Window").invoke(1, size, delegate, Il2Cpp.string("test window"));
        }
    });
}
//@ts-ignore
globalThis.GUI = GUI

Sample steps for playback: start game, press play, then Continue and again Continue. after you have GUI elements (joystick, etc.) enter GUI() in frida repl

vfsfitvnm commented 1 year ago

Thanks, I could reproduce. In the meantime, since you have Windows, would you try this snippet? You could test it against https://github.com/kidagine/Darklings-FightingGame (or whatever Windows game you have installed, if any)

Il2Cpp.perform(() => {
    // should be the same!
    console.log(Il2Cpp.currentThread?.id, Process.getCurrentThreadId());
});

Thanks :P

commonuserlol commented 1 year ago

it looks like you misunderstood me. the game is on android, but my pc works on windows (which most likely does not affect anything)

vfsfitvnm commented 1 year ago

I meant if you could test that snippet for me since I do not have Windows - it's not related to this issue (but to #195)! It's just a side thing

vfsfitvnm commented 1 year ago

Alright, UnityEngine.Rect is a value type, and you are passing it as a reference type, pass size.unbox() instead of size.

Now there's another error System.NullReferenceException: Object reference not set to an instance of an object. - which is related to the fact you are bypassing CheckOnGUI I guess...

commonuserlol commented 1 year ago

I meant if you could test that snippet for me since I do not have Windows - it's not related to this issue (but to #195)! It's just a side thing

frida -f C:\Users\User\Downloads\Darklings\Darklings.exe -l _.js
     ____
    / _  |   Frida 16.0.18 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Local System (id=local)
Spawned `C:\Users\User\Downloads\Darklings\Darklings.exe`. Resuming main thread!

[Local::Darklings.exe ]-> Godot Engine v3.5.2.stable.official.170ba337a - https://godotengine.org
OpenGL ES 3.0 Renderer: Intel(R) HD Graphics 505
Async. shader compilation: OFF

[Local::Darklings.exe ]->
[Local::Darklings.exe ]-> Il2Cpp.perform(() => {console.log(Il2Cpp.currentThread?.id, Process.getCurrentThreadId());})
{}

as I understand it, the game uses godot, does this mean that there is no il2cpp here?

vfsfitvnm commented 1 year ago

Uh, it looks like they switched to Godot since the last time I checked! Version 0.2.9 is the last one using Unity: https://github.com/kidagine/Darklings-FightingGame/releases/tag/0.2.9

commonuserlol commented 1 year ago

same result

[Local::Darklings.exe ]-> Il2Cpp.perform(() => {console.log(Il2Cpp.currentThread?.id, Process.getCurrentThreadId());})
{}
[Local::Darklings.exe ]-> Process.getCurrentThreadId()
4292
vfsfitvnm commented 1 year ago

Uhm, weird...

vfsfitvnm commented 1 year ago

Alright, UnityEngine.Rect is a value type, and you are passing it as a reference type, pass size.unbox() instead of size.

Regarding this one, I cannot automate this step: it's unclear to me if value types are always passed as value types, so I just let the user "guess" it. However, when the exception occurs, I can definitely check for value types passed as reference types, so that a better error message would be raised

commonuserlol commented 1 year ago

this is not relevant to this problem, but frida-il2pp-bridge does not define the class correctly

        const AssemblyCSharp = Il2Cpp.domain.assembly("Assembly-CSharp").image;
        const EnemyAI = AssemblyCSharp.class("EnemyAIGranny");
        const TestFPS = AssemblyCSharp.class("TestFPS");
        TestFPS.method("OnGUI").implementation = function() {
            this.method("OnGUI").invoke();
            GUI.method("Window").invoke(1, size.unbox(), delegate, Il2Cpp.string("test window"));
        }
        const testFPS = TestFPS.new();
        EnemyAI.method("FixedUpdate").implementation = function() {
            this.method("FixedUpdate").invoke();
            testFPS.method("OnGUI").invoke();
        }

but console output is il2cpp: couldn't find method OnGUI in class changeTextureNightmare

vfsfitvnm commented 1 year ago

Uhm, it works fine on my end. PS: TestFPS is a MonoBehaviour, and you can't create a MonoBehaviour using the new operator

commonuserlol commented 1 year ago

hmm, calling the OnGUI method manually is also useless. well, I'll come back to this question when I find a game that uses OnGUI