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.
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):
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:
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.
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.
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.
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.
Every JNI native method needs
JNIEnv as the first argument. This means the typedef for our function should be:
Unfortunately, if you try executing this function using the above typedef, you’ll get a cryptic error message:
This confused me for a while. I thought maybe I was getting a null reference somewhere so I added lots of
printfs 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.
This creates _org_cf_nativeharness_Cryptor.h_ which contains:
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
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:
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
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.
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:
To push harness to the device,
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.
harness with the path to the target library as the first argument:
To test that it’s all working, in another terminal run:
Finally, bask in your own greatness if you catch the reference, nerd.
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.