Search code examples
androidjava-native-interfacereverse-engineeringfrida

Find manually registered (obfuscated) native function address


I'm trying to understand an Android app which contains a native method named foo in class com.app.Bar

Inside class Bar there is a static clause that loads a shared object System.loadLibrary("libfoo.so") which I assume is build with -fvisibility=hidden because the only export is JNI_OnLoad, no JNIEXPORT void JNICALL Java_com_app_Bar_foo which means public native int foo does not follow the naming convention.

  1. What is the process when foo is invoked ?
  2. How can I extract the address of foo ? I'm familiar with Frida
  3. Is there a way to output all JNINativeMethod[] methods ?

What have I tried so far ?

  1. JNIAnalyzer outputs 13K lines that look like JNI_OnLoad@@Base+0x712e

  2. Frida script to try to find foo address (did not work)

function intercept(address) {
    try {
        Interceptor.attach(address, {
            onEnter: function(args) {
                console.log("onEnter", address);
            },
            onLeave: function(ignored) {}
        });
    } catch (e) {
        console.error(e);
    }
}
function Main() {
    var dlopen = new NativeFunction(Module.findExportByName(null, 'dlopen'), 'pointer', ['pointer', 'int']);
    var dlsym = new NativeFunction(Module.findExportByName(null, 'dlsym'), 'pointer', ['pointer', 'pointer']);
    Process.enumerateModulesSync().forEach(function(m) {
        if (m.name === "libfoo.so") {
            console.log("Module", JSON.stringify(m));
            var handle = dlopen(Memory.allocUtf8String(m.path), 1);
            var symb = Memory.allocUtf8String("foo");
            var exports = Module.enumerateExportsSync(m.name);
            console.log(JSON.stringify({
                handle: handle,
                symb: symb,
                dlsym: dlsym(handle, symb),
                exports: exports.map(function(ex){ return ex.address + ": " + ex.name })
            }, null, 2));
            // intercept all exports
            exports.forEach(function(ex){
                intercept(ex.address);
            });
            // explicit intercept foo by known offset
            intercept(m.base.add(0x22334)); // this outputs "Error: unable to intercept function at 0x86c96328; please file a bug"
        }
    });

    console.log("sleep..");
    Thread.sleep(1.5);
    console.log("invoke", Java.use('com.clazz.foo').signToken("A".repeat(32)));
}
Java.perform(Main);


Solution

  • I've solved it using Frida

    Hooking art::JNI::RegisterNativeMethods(_JNIEnv*, _jclass*, JNINativeMethod const*, int, bool) and art::JNI::FindClass after libart.so module is loaded.

    Code

    var RevealNativeMethods = function() {
      var pSize = Process.pointerSize;
      var env = Java.vm.getEnv();
      var RegisterNatives = 215, FindClassIndex = 6; // search "215" @ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html
      var jclassAddress2NameMap = {};
      function getNativeAddress(idx) {
        return env.handle.readPointer().add(idx * pSize).readPointer();
      }
      // intercepting FindClass to populate Map<address, jclass>
      Interceptor.attach(getNativeAddress(FindClassIndex), {
        onEnter: function(args) {
          jclassAddress2NameMap[args[0]] = args[1].readCString();
        }
      });
      // RegisterNative(jClass*, .., JNINativeMethod *methods[nMethods], uint nMethods) // https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#977
      Interceptor.attach(getNativeAddress(RegisterNatives), {
        onEnter: function(args) {
          for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
            /*
              https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#129
              typedef struct {
                 const char* name;
                 const char* signature;
                 void* fnPtr;
              } JNINativeMethod;
            */
            var structSize = pSize * 3; // = sizeof(JNINativeMethod)
            var methodsPtr = ptr(args[2]);
            var signature = methodsPtr.add(i * structSize + pSize).readPointer();
            var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer(); // void* fnPtr
            var jClass = jclassAddress2NameMap[args[0]].split('/');
            console.log('\x1b[3' + '6;01' + 'm', JSON.stringify({
              module: DebugSymbol.fromAddress(fnPtr)['moduleName'], // https://www.frida.re/docs/javascript-api/#debugsymbol
              package: jClass.slice(0, -1).join('.'),
              class: jClass[jClass.length - 1],
              method: methodsPtr.readPointer().readCString(), // char* name
              signature: signature.readCString(), // char* signature TODO Java bytecode signature parser { Z: 'boolean', B: 'byte', C: 'char', S: 'short', I: 'int', J: 'long', F: 'float', D: 'double', L: 'fully-qualified-class;', '[': 'array' } https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java
              address: fnPtr
            }), '\x1b[39;49;00m');
          }
        }
      });
    }
    
    Java.perform(RevealNativeMethods);