My Backdoored Gallery

Aperi'CTF 2019 - Mobile (250 pts).

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 :

logcat.png

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 :


javafn.png

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.


javacode.png

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.png

Flag

APRK{HopeYouManagedToUseFrida}

@Areizen