When analyzing malware or penetration testing an app which uses a native library, it’s helpful to isolate and execute the library’s functions. This opens the door for debugging and using the malware’s own code against it. For example, if the malware has encrypted strings and the decryption is done by a native function, you could either spend a bunch of time reversing the algorithm to write your own decryption routine or you could just harness the function such that you can execute it with arbitrary inputs. If the malware author completely changes their decryption, you might not have to change anything. In this post, I’ll explain how to harness a native library and execute its functions even if they require arguments from a live JVM instance.
In a previous post, I explained how to create a Java VM from Android native code but I didn’t give any real examples of how to use it. In this post, I’ll give a concrete example.
There are at least two approaches to harness a native function. The first is to modify the app to accept some input from you and pass that to the native function. For example, you can write an intent filter, convert it to Smali, add the code to the target app, modify the manifest, run the app, and send it intents via adb
with your arguments. Even better, you could add a small socket or web server instead of an intent filter and send curl
requests, which doesn’t require modifying the manifest.
The second approach is to create a small native executable which loads the library, calls the target function, can be executed from the command line, and passes whatever arguments you give it. This makes it easier to debug since you’re just running an executable rather than an entire app.
Target App
I created an example app so you can follow along at home. It’s called native-harness-target. To clone and build (of course replace $ANDROID_*
vars for yourself):
1 | git clone https://github.com/CalebFenton/native-harness-target.git |
The APKs will be in app/build/outputs/apk/. For this post, I’ll be using an x86 emulator image and app-universal-debug.apk.
The app has an encrypted string and uses a native library to decrypt the string at run time. Here’s how the string decryption looks in Smali:
1 | const/16 v3, 0x57 |
Building the Harness
I started with a tool called native-shim by Tim “diff” Strazzere (a fellow RedNaga member!) as a foundation for the harness. What shim does is load a library and call its JNI_OnLoad
. This makes debugging easy because you can just tell your debugger to start shim
and pass the path to the target library as an argument. Set your debugger to break on library load and you can step through the JNI_OnLoad
. Also, native-shim is great because it shows how to do almost everything you need to make harness work: load libraries (.so files), get references to functions, and call them.
First, I added code to initialize a Java VM instance and passed that instance to JNI_OnLoad
. This makes for a more realistic JNI initialization. Without a real VM instance, the internal state of the JNI library may be a little weird. It really depends on how JNI_OnLoad
is implemented by the particular library. It may not matter at all, but it’s common to check the JNI version from the little code I’ve seen, and to do that you need an instance of the VM.
1 | printf(" [+] Initializing JavaVM Instance\n"); |
Tim, just let me know if you want this in native-shim and I’ll send a pull request.
Eventually, the goal was to make the harness open a socket server, read arguments over the socket, and call the function with those arguments. This way, the decryption function just becomes a service, and a Python script could easily interface with it.
Understand the Target Function
To call a function you need the function signature and the return type. To get this, let’s look at a decompilation of org.cf.nativeharness.Cryptor
which declares the decryptString
native method.
1 | public class Cryptor { |
From this code, you can see the method takes a String
and returns a String
. Seems simple. Let’s convert that to a native function method signature.
1 | Java_org_cf_nativeharness_Cryptor_decryptString(JNIEnv *env, jstring encryptedString) |
Every JNI native method needs JNIEnv
as the first argument. This means the typedef for our function should be:
1 | typedef jstring(*decryptString_t)(JNIEnv *, jstring); |
Unfortunately, if you try executing this function using the above typedef, you’ll get a cryptic error message:
1 | E/dalvikvm: JNI ERROR (app bug): attempt to use stale local reference 0x1 |
This confused me for a while. I thought maybe I was getting a null reference somewhere so I added lots of printf
s to show the memory locations of all the relevant pointers. The error really sounds like there’s something wrong with one of the arguments, but all of the pointers looked good, none were null.
I had the idea to be extra super double sure I got the method signature right. Maybe there’s some JNI boiler plate I was forgetting? To do this, I used javah
which generates C header and source files that are needed to implement native methods.
To do this, you’ll need dex2jar installed and on your class path and you need to change platforms/android-19
to point to whichever platform you have installed.
1 | $ d2j-dex2jar.sh app-universal-debug.apk |
This creates org_cf_nativeharness_Cryptor.h which contains:
1 | JNIEXPORT jstring JNICALL Java_org_cf_nativeharness_Cryptor_decryptString |
This has jobject
as the second argument. WHY? What gives? If already know the answer to this, I bet you’ve spent a lot of time looking at Smali, specifically invoke-virtual
. Whenever you call a virtual method (i.e. usually anything non-static), the first argument is an instance of the object which implements the method. In this case, the first argument should be an instance of org.cf.nativeharness.Cryptor
.
Of course, you could cheat and just look at str-crypt.c to find the signature but if you’re really reverse engineering or pen-testing, you won’t have the source.
The real function typedef should have a jobject
for the Cryptor
instance as the first argument:
1 | typedef jstring(*decryptString_t)(JNIEnv *, jobject, jstring); |
You may be wondering why the method is not static to begin with. There isn’t a good reason for it to be static, true. But in the original app which made me write this blog, the target method wasn’t static and I ran into this problem.
The lesson here is if you’re not sure what the signature is, try javah
and keep in mind virtual methods take an instance for the first argument, similar to Java’s Method#invoke()
.
Building the Socket Server
This is the least interesting part of harness. If you don’t mind, I’m just going to skip this. You can see the code for yourself. Also, I’m just a C tourist. If you think the code is shit, I believe you. But if you want to tell me it’s shit, it must come with a pull request.
Using the Harness
Here’s an overview of the steps required to test the harness:
- start an emulator
- push the harness to the device
- push target native library and any dependencies to the device (in this case, there are no dependencies)
- push the native harness target app to the device
- start the harness
- forward ports from the emulator to the host
- run decrypt_string.py and cross your fingers
To push the app and native library to the device:
1 | $ adb push app/build/output/apk/app-universal-debug.apk /data/local/tmp/target-app.apk |
To push harness to the device,
1 | cd harness |
Note: this pushes the x86 library to the device. If you really want to use another emulator image, replace make install
with adb push libs/<your emulator flavor>/harness /data/local/tmp
.
Now, run harness
with the path to the target library as the first argument:
1 | $ adb shell /data/local/tmp/harness /data/local/tmp/libstr-crypt.so |
To test that it’s all working, in another terminal run:
1 | $ ./decrypt_string.py |
Finally, bask in your own greatness if you catch the reference, nerd.
Conclusion
You should be able to take the harness code and modify the target function to run whatever function you want. This won’t always work 100% reliably because programs are arbitrarily complicated and can do all kinds of weird shit.