Java applications can call native methods stored in dynamic libraries via the Java Native Interface (JNI) framework. Android apps can do the same: developers can use the NDK to write their own .so library to use and distribute.
In this post, we briefly present how the binding mechanisms work, allowing a piece of bytecode to invoke native code routines.
Named Convention Method
The easiest way to call native method is as such:
In Java, class com.example.hellojni.HelloJni:
The native method name adheres to the standard JNI naming convention, allowing automatic resolution and binding.
The corresponding Dalvik bytecode is:
and here are the the corresponding ARM instructions:
JEB automatically binds those methods together, to allow easy debugging from bytecode to native code.
However, there is another way to bind native code to Java.
Dynamic JNI Method
One can decide to bind any function to Java without adhering to the naming convention, by using the JNIEnv->RegisterNatives method.
For example, the following line of code dynamically binds the Java method add(II)I to the native method add():
Due to its dynamic nature, statically resolving those bindings can prove difficult in practice, e.g. if names were removed or mangled, or if the code is obfuscated. Therefore, not all calls to RegisterNatives may be found and/or successfully processed.
However, JEB 3.0-beta.2 (to be released this week) ships with an EnginesPlugin to heuristically detect – some of – these methods, and perform binding – and of course, you will also be able to debug into them.
Once run, it will :
- annotate the dex code with the target addresses:
- rename targets (prefixing names with __jni_) :
- enable you to seamlessly debug into them (jump from Java to this JNI method)
As of this writing, the plugin uses several heuristics, implemented for ARM and ARM64 (Aarch64):
- The first is the simplest one: the JNIEnv->RegisterNatives method is commonly called from the standard JNI initialization function JNI_OnLoad, so JEB searches for this method and attempt to find calls to RegisterNatives.
Once the ‘BL RegisterNatives‘ is found, JEB uses the decompiler to create an IR representation of the block, and determines the values of R2 and R3 (X2 and X3 on Aarch64). R3 indicates the number of native methods to register, R2 is a pointer to the array of JNI native methods (structure with a pointer to method name, a pointer to method signature and a pointer to the native function bound):
Even if accurate, this method does not work when a Branch is issued via a register (BL R4) or method name is hidden.
- The second heuristic is based on method name. First, in Dalvik, we search for all invocations to native methods. Then, for each method found, we search in binaries if there is a String reference matching the method name. (This heuristic is dangerous but yields decent results. A future plugin update may allow users to disable it.)
If found, the plugin looks at cross references of this String and checks if it looks like the expected JNI structure.
- The third and last heuristic is the same as the previous one, but based on arguments. Since names can be shortened, they may not be interpreted as String, and thus not referenced, whereas it is easier to find argument signatures.
These three heuristics only work when methods are defined as a static array variable. Dynamic variables would need some emulation of the JNI_OnLoad method to be resolved.
As you can see, detection is currently based on heuristics, so obfuscated methods may be missing. We will likely release this plugin as open-source on our GitHub repository in the coming days. As usual, feel free to reach out to us (email, Twitter, Slack) if you have questions or suggestions.