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
1.03k stars 202 forks source link

Il2Cpp.Thread::id doesn't work on Windows #195

Closed lucaloiacono closed 1 year ago

lucaloiacono commented 2 years ago

I'm trying to schedule a function on the main thread but looks like there's no way to do it.

This is what i've tried:

Il2Cpp.perform(() => {

   // This never gets called
   Il2Cpp.scheduleOnInitializerThread(() => {
           console.log("Test");
   });

   // Producing error: access violation accessing 0x...
   Il2Cpp.currentThread?.schedule(() => {
           console.log("Test");
   });

   // Producing error: access violation accessing 0x...
   Il2Cpp.attachedThreads[0].schedule(() => {
           console.log("Test");
   });

});

Am i missing something?

vfsfitvnm commented 2 years ago

Il2Cpp.scheduleOnInitializerThread is a last resort, its wobbly and probably won't work (however, make sure the app is in the foreground).

Il2Cpp.currentThread?.schedule is not expected to work (in you specific scenario), since the Frida thread lacks of a synchronization context.

Il2Cpp.attachedThreads[0].schedule should not throw an exception (access violation is a bad thing), what's the Unity version of that app? If on Android, what's the package name?

lucaloiacono commented 2 years ago

It's a Unity app, version 2020.3.32f1

vfsfitvnm commented 2 years ago

Yeah of course it's a Unity app, but what's its name? I can try to download it so it can reproduce the issue

vfsfitvnm commented 2 years ago

Unfortunately I cannot test right now it as I don't have Windows. Would you attach the whole stack trace? So I can at least know where the problem is (I expect here or here).

lucaloiacono commented 2 years ago

Yeah, here it is:

Error: access violation accessing 0x2a24
    at <anonymous> (frida/runtime/core.js:138)
    at get idOffset (node_modules/frida-il2cpp-bridge/dist/il2cpp/structs/thread.js:19)
    at call (native)
    at <anonymous> (node_modules/decorator-cache-getter/dist/index.js:9)
    at get id (node_modules/frida-il2cpp-bridge/dist/il2cpp/structs/thread.js:28)
    at schedule (node_modules/frida-il2cpp-bridge/dist/il2cpp/structs/thread.js:92)
    at <anonymous> (agent/index.ts:133)
    at perform (node_modules/frida-il2cpp-bridge/dist/il2cpp/base.js:176)

Looks like there is a problem getting the thread id.

vfsfitvnm commented 2 years ago

Yep, unfortunately there's no straightforward way to retrieve the native thread id of a System.Threading.Thread. I'll see what I can do for Windows.

However, why are you using such function (schedule)? I expect it to be seldomly used

lucaloiacono commented 2 years ago

Well, let me explain:

If invoke my function when i load the agent, everything works great, i.e:

Il2Cpp.perform(() => {

   const myClass = ...

   myClass.method("myMethod").invoke(); // <- works!

});

BUT, this is not my case. I need to invoke my methods on demand, after the agent has been loaded, using the Messages system.

Il2Cpp.perform(() => {

   recv("myCommand", onCommandReceived);

});

function onCommandReceived(): void {
   const myClass = ...
   myClass.method("myMethod").invoke(); // <- this produces access violation error
}

I'm pretty sure the problem occurs when the method is called from another thread, that's why i was looking for a way to schedule actions on the main thread.

vfsfitvnm commented 2 years ago

I don't remember if recv is blocking (I guess not), so, when onCommandReceived is invoked, Il2Cpp.perform is already finished, hence the Frida thread is not attached to Il2Cpp anymore.

You should do the following instead:

recv("myCommand", () => Il2Cpp.perform(onCommandReceived));

function onCommandReceived(): void {
   const myClass = ...
   myClass.method("myMethod").invoke();
}

You can verify the connection between the caller thread and Il2Cpp with the property Il2Cpp::currentThread: if it returns null, then the caller thread is not attached, hence the access violation error.

However, if you still get an access violation error, then you are right: you must ensure to call myMethod from the main thread.

lucaloiacono commented 2 years ago

How could I not think about it before... Now it works! Thank you so much.

vfsfitvnm commented 2 years ago

Perfect! I'll leave this issue open until I fix the thread id thing.

wildsheepz commented 2 years ago

Hi @vfsfitvnm,

I tried adding this code in base.js:

    static scheduleOnMainThread(block) {
        const CurrentThreadIsMainThread = this.internalCall("UnityEngine.Object::CurrentThreadIsMainThread", "bool", []);
        return new Promise(resolve => {
            const listener = Interceptor.attach(Il2Cpp.Api._threadCurrent, () => {
                if (CurrentThreadIsMainThread() ) {
                    listener.detach();
                    const result = block();
                    setImmediate(() => resolve(result));
                }
            });
        });
    }

and then I ran this script:

Il2Cpp.scheduleOnMainThread(()=>{
        console.log('mainthread:', Il2Cpp.currentThreadIsMainThread())
    })

I got this output:

Address found: 0x79f88c2000
mainthread: 1

Not sure how to properly test and verify that it is working properly and I'm not sure if UnityEngine.Object::CurrentThreadIsMainThread is consistent across platforms and unity versions.

wildsheepz commented 2 years ago

I just tried it on an invoke that always causes my game to freeze and now it works and doesn't freeze anymore.

wildsheepz commented 2 years ago

Created a pull request

vfsfitvnm commented 2 years ago

@wildsheepz Thanks for looking at this, but your solution only is a partial solution to the problem. The problem here is https://github.com/vfsfitvnm/frida-il2cpp-bridge/blob/master/src/il2cpp/structs/thread.ts#L14 throwing an access violation. We need to find a way to find the thread id offset - so we can get the thread id of every thread - checking if the current thread is the main one is merely a workaround.

I don't have windows now so I can't test, but adding a try/catch on that line may be sufficient:

        for (let i = 0; i < 1024; i++) {
            try {
                const candidate = handle.add(i).readS32();
                if (candidate == currentThreadId) {
                    return i;
                }
            } catch (e: any) {
            }
        }