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

frida-il2cpp-bridge in frida's emulated realm #264

Closed leonitousconforti closed 1 year ago

leonitousconforti commented 1 year ago

Over the past couple of days, I have been attempting to use frida-il2cpp-bridge on an android app that only has arm64 native libraries with frida realms.

I have a script using frida-il2cpp-bridge that works when the app is running in Google Android Emulator using a arm64 system image (api 30, which is android 11) + the arm64 version of frida-server (16.0.11) on my m1 macbook (so arm native and the frida realm=NATIVE). So I know my script is working and I know the app doesn't have any anti-inspection/anti-debugging/anti-il2cpp-apis or whatnot.

I have been trying to use this same script when the app is running in Google Android Emulator using a x64 system image (api 30, which is android 11 with arm translation) + the x86 version of frida-server (16.0.11), however, I have encountered several roadblocks. TLDR: do you think frida-il2cpp-bridge should work with frida's realm feature. If you don't, that's fine I should be able to find an arm client other than my laptop to make it work. If you think this is something that should work, I would love to here your thoughts on how I should continue debugging this as I don't know how I should continue. The debugging steps I have already tried are below.

if I attach the script to the game using

realm=frida.Realm.Native

then this is the output of the log statements I have in my local copy:

before init
before native wait for module
moduleNames=libil2cpp.so
this.modulePath=libndk_translation_proxy_libandroid_runtime.so
this.modulePath=libandroid_runtime.so
this.modulePath=libndk_translation_proxy_libandroid.so
this.modulePath=libandroid.so
this.modulePath=libndk_translation_proxy_libnativewindow.so
this.modulePath=libnativewindow.so
this.modulePath=libndk_translation_proxy_libEGL.so
this.modulePath=libEGL.so
this.modulePath=/vendor/lib64/hw/android.hardware.graphics.mapper@3.0-impl-ranchu.so

and the script hangs here indefinitely, not resolving anything. "before init" statement is from right before this line "before native wait for module" is from right before this line the modulesNames are printed on this line the module paths are each printed after the onEnter from the interceptor in the native wait for module This all makes sense to me, because the app code is arm (translated by the x64 system image) and frida is running in the native realm (x86_64) it can't reach/touch the emulated code.

The logical solution to me was to attach the script to the game using

realm=frida.Realm.Emulated

then this is the output of the log statements I have in my local copy:

before init
before native wait for module
after native wait for module
outside of else statement line 130
_getCorlib start

r start, exportName=il2cpp_get_corlib, retType=pointer, argTypes=
exportedPointer=null
in cModule static get
In get UnityVersion1
in internalCall1
in resolveInternalCall1

r start, exportName=il2cpp_resolve_icall, retType=pointer, argTypes=pointer
exportedPointer=null
in cModule static get
In get UnityVersion1
in internalCall1
in resolveInternalCall1

r start, exportName=il2cpp_resolve_icall, retType=pointer, argTypes=pointer
exportedPointer=null
in cModule static get
In get UnityVersion1
in internalCall1
in resolveInternalCall1
...
[Error: Script is destroyed]

The after native wait for module comes from after this line and suggests to me that the libil2cpp.so was loaded correctly. But now it doesn't print the moduleNames=libil2cpp.so statement which means that the waitForModule in the initialize block must have resolved from this line. Which makes sense to me because the module libil2cpp.so should be loaded by the game process, so I don't think anything is wrong there.

The "outside of else statement line 130" statement comes from this line. And the "_getCorlib start" statement comes from the start of this method.

Everytime this.r is invoked the three parameters are logged. Here are the parts of the this.r method I changed for debugging:

  let exportPointer = Il2Cpp.module.findExportByName(exportName);
  console.log("exportedPointer="+ exportPointer);
  exportPointer = exportPointer  ?? this.cModule[exportName];
  console.log("exportedPointer2="+ exportPointer);

first it tries to do Il2Cpp.module.findExportByName(exportName) and then it falls back to this.cModule[exportName], however as you can see, it never makes it that far because it doesn't reach that print. It never makes it past the first line of the static get cModule because it goes straight to checking the unity version. It never makes it past the first line of the static get unityVersion where it checks unity versions because the log statement right after it "In get UnitVersion2" is never printed. Likewise, it never makes it past the first line of internalCall because the statement "in internalCall2" is never printed. Instead it calls resolveInternalCall which ends up calling this.r("il2cpp_resolve_icall", "pointer", ["pointer"]); and the whole cycle repeats.

It just keeps repeating that loop for about 5 seconds before frida gives an error that the script was destroyed. I am not sure how I should proceed on debugging this, any advice/insight/documentation you could provide about the il2cpp api (it seems to just keep calling il2cpp_resolve_icall and I assume that is an il2cpp api but have not found any documentation about it) or if you could just tell me that this won't ever work because of various reasons would be greatly appreciated.

leonitousconforti commented 1 year ago

As a last ditch debugging effort, I was wondering if maybe the exports should be resolved by the Il2Cpp.module.findExportByName(exportName) call. I added this code to the this.r method:

for (let a of Il2Cpp.module.enumerateExports()) {
    if (a.name === exportName) {
        console.log(JSON.stringify(a));
    }
}

and sure enough the function is printed:

{"type":"function","name":"il2cpp_resolve_icall","address":"0x774042174924"}

but

Il2Cpp.module.findExportByName(exportName);

is still null. There seems to be some difference between module.enumerateExports() and module.findExportByName(exportName) so I think this might be a frida issue and not an il2cpp-frida-bridge issue?

Can confirm that using Il2Cpp.module.enumerateExports() and filtering for a match instead of Il2Cpp.module.findExportByName(exportName) works. It now hangs here which might be resolved after some user interaction in the app - will test tomorrow to see.

vfsfitvnm commented 1 year ago

Yeah, this is a known Frida issue (https://github.com/vfsfitvnm/frida-il2cpp-bridge/issues/160#issuecomment-1076413405) - here's a workaround: https://github.com/vfsfitvnm/frida-il2cpp-bridge/issues/160#issuecomment-1076761759-.

However I'm still interested at that hang...

vfsfitvnm commented 1 year ago

Workaround:

import "frida-il2cpp-bridge";

(globalThis as any).Module = new Proxy(Module, {
    cache: {},

    get(target, property, _): any {
        if (property == "findExportByName") {
            return (moduleName: string | null, exportName: string) => {
                if (moduleName == null) {
                    return Reflect.get(target, property)(moduleName, exportName);
                }
                this.cache[moduleName] ??= (Module as any).enumerateExports(moduleName);
                return this.cache[moduleName].find((_: ModuleExportDetails) => _.name == exportName)?.address;
            };
        } else {
            return Reflect.get(target, property);
        }
    }
} as ProxyHandler<typeof Module> & { cache: Record<string, ModuleExportDetails[]> });

Il2Cpp.perform(() => {
    console.log(Il2Cpp.unityVersion, Process.arch);
};

Prints:

2021.3.20f1 arm
vfsfitvnm commented 1 year ago

Closing as there is no action I can take. I tried to reproduce the hang, and despite succeeding, it didn't seem to be related to frida-il2cpp-bridge. If you are still facing issues, contact me on Discord (vfsfitvnm#7025) so we can share a debug session together.