Aperi’CTF 2019 - My Backdoored Gallery
Challenge details
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
Aperi’CTF 2019 | My Backdoored Gallery | Mobile | 250 | 0 |
Here the goal is to find from where log message are coming from. But first, you need to launch the application on a non-root device with x86 arch.
Not everybody have the luck to have such device so we will make the application executable by an emulator.
Files: mygallery.apk
Setting up an emulator
How to choose the emulator
To know which type of device can execute the app, you just have to open it in jadx-gui
and search for minsdk
value in the Manifest.xml.
To know the arch, in the folder lib, there are all arch supported. In our case x86
So for this challenge we need an Android device with Oreo or higher in x86.
But the application, still doesn’t fully launch.
Bypass anti-root
In the code, we can quickly spot an anti-root dectection
private static boolean k() {
for (String file : new String[]{"/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su"}) {
if (new File(file).exists()) {
Process.killProcess(Process.myPid());
}
}
return false;
}
If this code detect that you have common binary installed on your device, it kills the current process.
To bypass this behaviour I used Frida.
To install it : pip install frida-tools
Then you need to deploy the agent on your device. It can quickly be done with : pip install frida-push; frida-push
We will need frida-compile
afterward : npm install frida-compile
Now here is the simple script that allow us to bypass the anti-root.
hooks.js :
Java.perform(function(){
const MainActivity = Java.use("reverse.areizen.mygallery.MainActivity");
MainActivity.k.implementation = function(){
console.log("[+] Bypassed root check");
return false;
}
})
Launch it with : frida -U -f reverse.areizen.mygallery -l hooks.js --no-pause
Now that the application doesn’t close at start we can see the following message in the log by using adb logcat
:
Finding the suspect code
Still in jadx-gui
from Java code, we don’t see call to Log.d()
.
In the java code we can quickly see that images folder assets
are loaded and are displayed.
If we click on an image, the code launch ImageViewActivity
to display the image fullscreen.
However in MainActivity
we can see a strange code calling a native-lib :
static {
System.loadLibrary("native-lib");
}
...
public native void loadImages(AssetManager assetManager);
protected void onCreate(Bundle bundle) {
...
AssetManager assets = getAssets();
loadImages(assets);
}
So the function loadImages()
is called from a native lib : libnative-lib.so
.
Native lib analysis
So now that we have a native lib, we open it in Ghidra to know what it is doing.
Many functions startswith png_
, after checking on internet, we see that these function are from libpng
and it is doing image manipulation.
Then, we need to analyse the function Java_reverse_areizen_mygallery_MainActivity_loadImages
( in Java native libs follow the scheme 'Java_'+class_name_with_package+'_'+method_name
). By following the JNI specifications : JNI Specs we can redefine the prototype of the function :
Then we propagate all the variable by doing Right Click -> Commit locals
, it permit use to have a code that is cleaner.
We have now a bloc of code that seems readable.
Many functions belonging to the JNIEnv
struct are called, we have two way to solve it by statically analysing what it does or by a dynamic approach.
I my case, I choose the second solution.
To create my hooks I referred to the android definition of struct JNIEnv
: https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h
An other point to consider to attach these function, is that we need to wait until this librarie is loaded before hooking it. In android source code we can see that it call android_dlopen_ext
to open native libs. So e wait until it load the good library and then we set our hooks. ( source ) [https://android.googlesource.com/platform/system/core/+/master/libnativeloader/native_loader.cpp#746]
code that wait for the librarie to be loaded:
library_name = "libnative-lib.so"
library_loaded = 0
Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'),{
onEnter: function(args){
// first arg is the path to the library loaded
library_path = Memory.readCString(args[0])
if( library_path.includes(library_name)){
console.log("[.] Loading library : " + library_path)
library_loaded = 1
}
},
onLeave: function(args){
// if it's the library we want to hook, hooking it
if(library_loaded == 1){
console.log("[+] Loaded")
//Now we will hook the callback func
hook_func()
library_loaded = 0
}
}
})
After creating this chall, I developped a tool to make my next reversing of JNI more easy. (https://github.com/Areizen/JNI-Frida-Hook/)[https://github.com/Areizen/JNI-Frida-Hook/]
So you can hook all JNI function with the following code :
const jni = require("./utils/jni_struct.js")
function_name = "Java_reverse_areizen_mygallery_MainActivity_loadImages"
function hook_func(){
// To get the list of exports
Module.enumerateExportsSync(library_name).forEach(function(symbol){
// console.log(symbol.name)
if(symbol.name == function_name){
console.log("[...] Hooking : " + library_name + " -> " + function_name + " at " + symbol.address)
Interceptor.attach(symbol.address,{
onEnter: function(args){
jnienv_addr = Memory.readPointer(args[0])
console.log("[+] Hooked successfully, JNIEnv base adress :" + jnienv_addr)
// here we hook all functions
jni.hook_all(jnienv_addr)
},
onLeave: function(args){
// Prevent from displaying junk from other functions
Interceptor.detachAll()
console.log("[-] Detaching all interceptors")
}
})
}
})
}
We compile it with frida-compile
and we launch the application :
frida-compile hooks.js -o _hooks.js && frida -U -f reverse.areizen.mygallery -l _hooks.js --no-pause
We get the following output:
[.] Loading library : /data/app/reverse.areizen.mygallery-BRWKXc9urHhVZ4hBb8cCGw==/lib/x86/libnative-lib.so
[+] Loaded
[...] Hooking : libnative-lib.so -> Java_reverse_areizen_mygallery_MainActivity_loadImages at 0xcec838b0
[+] Bypassed root check
[+] Hooked successfully, JNIEnv base adress :0xea7ef9c8
[+] Entered : GetLongField
[+] Entered : NewByteArray
[+] Entered : SetByteArrayRegion
[+] Entered : FindClass
[+] Entered : GetStaticMethodID
[+] Entered : CallStaticObjectMethodV
[+] Entered : FindClass
[+] Entered : GetMethodID
[+] Entered : CallObjectMethodV
[+] Entered : FindClass
[+] Entered : GetStaticMethodID
[+] Entered : CallStaticObjectMethodV
[+] Entered : FindClass
[+] Entered : GetStaticMethodID
[+] Entered : CallStaticObjectMethodV
[+] Entered : FindClass
[+] Entered : GetMethodID
[+] Entered : NewObjectV
[+] Entered : GetStringUTFChars
[+] Entered : NewObjectV
[+] Entered : NewObjectV
[+] Entered : NewObjectV
[+] Entered : NewObjectV
[+] Entered : ReleaseStringUTFChars
...
So we have all JNIEnv
function called. We can see that Java code is called by using JNIEnv
.
The nex step is to hook FindClass
to see class used ( we can also strings | grep
but we are not in a forensic challenge ;) ).
The prototype of FindClass
is the following : jclass FindClass(JNIEnv *env, const char *name);
So we will need the second arg :
Interceptor.attach(jni.getJNIFunctionAdress(jnienv_addr,"FindClass"),{
onEnter: function(args){
console.log("env->FindClass(\"" + Memory.readCString(args[1]) + "\")")
}
})
We launch the script and get the following output :
[.] Loading library : /data/app/reverse.areizen.mygallery-BRWKXc9urHhVZ4hBb8cCGw==/lib/x86/libnative-lib.so
[+] Loaded
[...] Hooking : libnative-lib.so -> Java_reverse_areizen_mygallery_MainActivity_loadImages at 0xced768b0
[+] Bypassed root check
[+] Hooked successfully, JNIEnv base adress :0xea7ef9c8
env->FindClass("java/util/Base64")
env->FindClass("java/util/Base64$Decoder")
env->FindClass("java/nio/ByteBuffer")
env->FindClass("java/lang/ClassLoader")
env->FindClass("dalvik/system/InMemoryDexClassLoader")
[-] Detaching all interceptors
Here we have clues about what the application is doing :
+ It get back a Base64
that it decode
+ It store it in a ByteBuffer
+ It loads it with ClassLoader ( InMemoryClassLoader
has been released under Oreo
it’s why you need an Oreo device )
Our goal is now to get the dex
loaded by the application. To do so we will try to get back the Base64
loaded.
In the first log of JNIEnv
function we can see that SetByteArrayRegion
is called before FindClass
. The doc of SetByteArrayRegion
is the following :
Set<PrimitiveType>ArrayRegion Routines
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,
jsize start, jsize len, const NativeType *buf);
A family of functions that copies back a region of a primitive array from a buffer.
It’s a function that will convert a memory region from C
to Java
, we can deduce that it’s converting the Base64
array to Java
side :
Interceptor.attach(jni.getJNIFunctionAdress(jnienv_addr,"SetByteArrayRegion"),{
onEnter: function(args){
var bytebuffer = Memory.readCString(args[4])
console.log("Memory region content : \"" + bytebuffer + "\"")
}
})
To get :
[.] Loading library : /data/app/reverse.areizen.mygallery-BRWKXc9urHhVZ4hBb8cCGw==/lib/x86/libnative-lib.so
[+] Loaded
[...] Hooking : libnative-lib.so -> Java_reverse_areizen_mygallery_MainActivity_loadImages at 0xced7c8b0
[+] Bypassed root check
[+] Hooked successfully, JNIEnv base adress :0xea7ef9c8
Memory region content : "ZGV4CjAzNQBlobXDqAn2Ln4RodOHG2/fw8eS2ZG/NMFMBAAAcAAAAHhWNBIAAAAAAAAAAKADAAAcAAAAcAAAAAgAAADgAAAAAgAAAAABAAAHAAAAGAEAAAQAAABQAQAAAgAAAHABAACcAgAAsAEAAPgBAAD6AQAA/wEAAAcCAAAXAgAANwIAAEMCAABVAgAAXAIAAGQCAACCAgAAjgIAAJECAACWAgAAqgIAAL4CAADSAgAA8AIAAAgDAAAWAwAAGQMAACcDAAA1AwAAOAMAAD4DAABBAwAASgMAAFoDAAALAAAADQAAAA4AAAAPAAAAEAAAABEAAAATAAAAFgAAAAwAAAAAAAAA8AEAABMAAAAGAAAAAAAAAAQAAwADAAAABAADAAUAAAAEAAcABwAAAAQAAwAIAAAABAAAABQAAAAEAAMAFQAAAAUAAwAXAAAAAQAAABgAAAACAAEAAgAAAAQAAQACAAAABQABAAIAAAAEAAAAEQAAAAIAAAAAAAAABgAAAAAAAAB7AwAAbAMAAAUAAAABAAAAAgAAAAAAAAAKAAAAAAAAAJEDAAB4AwAAAQABAAEAAABgAwAABAAAAHAQAQAAAA4AAwABAAIAAABlAwAACwAAAHAQAQACABoAEgAaAQkAcSAAABAADgAAAAIAAAADAAMAAAADMS4wAAY8aW5pdD4ADkFQUExJQ0FUSU9OX0lEAB5BUFJLe0hvcGVZb3VNYW5hZ2VkVG9Vc2VGcmlkYX0ACkJVSUxEX1RZUEUAEEJ1aWxkQ29uZmlnLmphdmEABURFQlVHAAZGTEFWT1IAHEhlbGxvIGZyb20gdGhlIG90dGVyIHNpZGUgOikACkhlbGxvLmphdmEAAUkAA0lMTAASTGFuZHJvaWQvdXRpbC9Mb2c7ABJMamF2YS9sYW5nL09iamVjdDsAEkxqYXZhL2xhbmcvU3RyaW5nOwAcTHRoZS9vdHRlci9zaWRlL0J1aWxkQ29uZmlnOwAWTHRoZS9vdHRlci9zaWRlL0hlbGxvOwAMVGhlT3R0ZXJTaWRlAAFWAAxWRVJTSU9OX0NPREUADFZFUlNJT05fTkFNRQABWgAEZmxhZwABaQAHcmVsZWFzZQAOdGhlLm90dGVyLnNpZGUABHRoaXMABgAHDgAIAAcOPHgABhcaFxkfFwAE/xcBARcEBgABAAAZARkBGQEZARkBGQKBgASwAwEAAQAGGgOBgATIAwAAAA4AAAAAAAAAAQAAAAAAAAABAAAAHAAAAHAAAAACAAAACAAAAOAAAAADAAAAAgAAAAABAAAEAAAABwAAABgBAAAFAAAABAAAAFABAAAGAAAAAgAAAHABAAABIAAAAgAAALABAAABEAAAAQAAAPABAAACIAAAHAAAAPgBAAADIAAAAgAAAGADAAAFIAAAAgAAAGwDAAAAIAAAAgAAAHsDAAAAEAAAAQAAAKADAAA="
[-] Detaching all interceptors
After extracting the base64 and opening it in jadx-gui
, the dex
contains the following code :
Flag
APRK{HopeYouManagedToUseFrida}
@Areizen