Reviewing Disassembled Native Code
Following the example from "Disassembling Native Code" we will use different disassemblers to review the disassembled native code.
Once you've opened your file in radare2 you should first get the address of the function you're looking for. You can do this by listing or getting information
i about the symbols
is) and grepping (
~ radare2's built-in grep) for some keyword, in our case we're looking for JNI related symbols so we enter "Java":
$ r2 -A HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so ... [0x00000e3c]> is~Java 003 0x00000e78 0x00000e78 GLOBAL FUNC 16 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
The method can be found at address
0x00000e78. To display its disassembly simply run the following commands:
[0x00000e3c]> e emu.str=true; [0x00000e3c]> s 0x00000e78 [0x00000e78]> af [0x00000e78]> pdf ╭ (fcn) sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI 12 │ sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI (int32_t arg1); │ ; arg int32_t arg1 @ r0 │ 0x00000e78 ~ 0268 ldr r2, [r0] ; arg1 │ ;-- aav.0x00000e79: │ ; UNKNOWN XREF from aav.0x00000189 (+0x3) │ 0x00000e79 unaligned │ 0x00000e7a 0249 ldr r1, aav.0x00000f3c ; [0xe84:4]=0xf3c aav.0x00000f3c │ 0x00000e7c d2f89c22 ldr.w r2, [r2, 0x29c] │ 0x00000e80 7944 add r1, pc ; "Hello from C++" section..rodata ╰ 0x00000e82 1047 bx r2
Let's explain the previous commands:
e emu.str=true;enables radare2's string emulation. Thanks to this, we can see the string we're looking for ("Hello from C++").
s 0x00000e78is a seek to the address
s 0x00000e78, where our target function is located. We do this so that the following commands apply to this address.
Using radare2 you can quickly run commands and exit by using the flags
-qc '<commands>'. From the previous steps we know already what to do so we will simply put everything together:
$ r2 -qc 'e emu.str=true; s 0x00000e78; af; pdf' HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so ╭ (fcn) sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI 12 │ sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI (int32_t arg1); │ ; arg int32_t arg1 @ r0 │ 0x00000e78 0268 ldr r2, [r0] ; arg1 │ 0x00000e7a 0249 ldr r1, [0x00000e84] ; [0xe84:4]=0xf3c │ 0x00000e7c d2f89c22 ldr.w r2, [r2, 0x29c] │ 0x00000e80 7944 add r1, pc ; "Hello from C++" section..rodata ╰ 0x00000e82 1047 bx r2
Notice that in this case we're not starting with the
-A flag not running
aaa. Instead, we just tell radare2 to analyze that one function by using the analyze function
af command. This is one of those cases where we can speed up our workflow because you're focusing on some specific part of an app.
The workflow can be further improved by using r2ghidra, a deep integration of Ghidra decompiler for radare2. r2ghidra generates decompiled C code, which can aid in quickly analyzing the binary.
We assume that you've successfully opened
lib/armeabi-v7a/libnative-lib.so in IDA pro. Once the file is loaded, click into the "Functions" window on the left and press
Alt+t to open the search dialog. Enter "java" and hit enter. This should highlight the
Java_sg_vantagepoint_helloworld_ MainActivity_stringFromJNI function. Double-click the function to jump to its address in the disassembly Window. "Ida View-A" should now show the disassembly of the function.
Not a lot of code there, but you should analyze it. The first thing you need to know is that the first argument passed to every JNI function is a JNI interface pointer. An interface pointer is a pointer to a pointer. This pointer points to a function table: an array of even more pointers, each of which points to a JNI interface function (is your head spinning yet?). The function table is initialized by the Java VM and allows the native function to interact with the Java environment.
With that in mind, let's have a look at each line of assembly code.
LDR R2, [R0]
Remember: the first argument (in R0) is a pointer to the JNI function table pointer. The
LDR instruction loads this function table pointer into R2.
LDR R1, =aHelloFromC
This instruction loads into R1 the PC-relative offset of the string "Hello from C++". Note that this string comes directly after the end of the function block at offset 0xe84. Addressing relative to the program counter allows the code to run independently of its position in memory.
LDR.W R2, [R2, #0x29C]
This instruction loads the function pointer from offset 0x29C into the JNI function pointer table pointed to by R2. This is the
NewStringUTF function. You can look at the list of function pointers in jni.h, which is included in the Android NDK. The function prototype looks like this:
jstring (*NewStringUTF)(JNIEnv*, const char*);
The function takes two arguments: the JNIEnv pointer (already in R0) and a String pointer. Next, the current value of PC is added to R1, resulting in the absolute address of the static string "Hello from C++" (PC + offset).
ADD R1, PC
Finally, the program executes a branch instruction to the
NewStringUTF function pointer loaded into R2:
When this function returns, R0 contains a pointer to the newly constructed UTF string. This is the final return value, so R0 is left unchanged and the function returns.
After opening the library in Ghidra we can see all the functions defined in the Symbol Tree panel under Functions. The native library for the current application is relatively very small. There are three user defined functions:
Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI. The other symbols are not user defined and are generated for proper functioning of the shared library. The instructions in the function
Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI are already discussed in detail in previous sections. In this section we can look into the decompilation of the function.
Inside the current function there is a call to another function, whose address is obtained by accessing an offset in the
JNIEnv pointer (found as
plParm1). This logic has been diagrammatically demonstrated above as well. The corresponding C code for the disassembled function is shown in the Decompiler window. This decompiled C code makes it much easier to understand the function call being made. Since this function is small and extremely simple, the decompilation output is very accurate, this can change drastically when dealing with complex functions.