3 ways for Dynamic Code Loading in Android

Dynamic code loading in Android, the methods malware employ to hide malicious behaviour.

Date and Time of last update Tue 21 Feb 2023
  

In this article we are going to see 3 ways which can be used to dynamically load code into an Android application at runtime. To make it clear from the beginning this article is not referring to the Play Feature Delivery. The methods we are going to see are direct sub-classes of the BaseDexClassLoader and malware often employ them in order to hide their true nature.

Then, you might ask, why are you describing how to create such a thing?

The answer is simple. Only when we create something, we are able to fully understand how it operates and then later on figure out ways to detect it.

You can find an example showcase app using all three methods here.

The following sections can be found in the article:

Lets get started.

Setup

First we need to explain what our setup is and what we aim to do. The three different methods do not all support loading the same file-types. Therefore, for consistency reasons we are going to use a .dex file as the file being loaded in each case, since all methods support dex file.

Creating a very simple Android application with an empty activity and a new class named RandomNumber should suffice. The following code is in this class:

package com.erev0s.randomnumber
import kotlin.random.Random

class RandomNumber {
    fun getRandomNumber(): String {
        return Random.nextInt(0, 1000).toString()
    }
}

As you can see, it does not do much, it only has a function which returns a random number. After building the APK, we extract it and retrieve the classes.dex file available in the root folder which contains the class we are interested in. Note that you can verify if the .dex file you have contains the code you are expecting it to have by simply opening it with jadx or a similar tool. Our goal now is to add this .dex file as an asset in our new app and somehow load it dynamically at runtime, and use the function getRandomNumber().

DexClassLoader

DexClassLoader is able to load dex file from apk or jar files as well. The constructor it has, requires four parameters but only two of them are important, the path of the dex file(s) and the parent classloader. So our job is simple, first we need to get the dex file from the asset folder and put it in our internal storage and then point that path in the constructor of the DexClassLoader. The following snippet shows that.

fun cl(dexFileName: String): DexClassLoader {
    val dexFile: File = File.createTempFile("pref", ".dex")
    var inStr: ByteArrayInputStream = ByteArrayInputStream(baseContext.assets.open(dexFileName).readBytes())
    inStr.use { input ->
        dexFile.outputStream().use { output ->
            input.copyTo(output)
        }
    }
    var loader: DexClassLoader = DexClassLoader(
        dexFile.absolutePath,
        null,
        null,
        this.javaClass.classLoader
    )
    return loader
}

Initially we are creating a temporary file using createTempFIle which is later used to hold the contents of the dex file being loaded in line 3. Finally in line 9 the DexClassLoader is being called, and we provide the absolute path of the temporary file that was created right above, along with the parent classloader.

The next steps now are to call the function above with the proper file name and then attempt to call the getRandomNumber function. The following snippet shows how this can be done:

var loader = cl(dexfilename)  // get the DexClassLoader
val loadClass = loader.loadClass("com.erev0s.randomnumber.RandomNumber")  // get the class
val checkMethod = loadClass.getMethod("getRandomNumber")  // get the method
val cl_in = loadClass.newInstance()  // instantiate the class
checkMethod.invoke(cl_in) as String  // invoke the method

PathClassLoader

PathClassLoader is a very simple implementation of the classloader which (as the documentation states) operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. There are very few differences on the implementation part in comparison with the DexClassLoader, let us see them in more detail.

fun path_cl(dexFileName: String): PathClassLoader {
    val dexFile: File = File.createTempFile("pref", ".dex")
    var inStr: ByteArrayInputStream = ByteArrayInputStream(baseContext.assets.open(dexFileName).readBytes())
    inStr.use { input ->
        dexFile.outputStream().use { output ->
            input.copyTo(output)
        }
    }
    var loader: PathClassLoader = PathClassLoader(dexFile.absolutePath, this.javaClass.classLoader)
    return loader
}

The important difference is in line 9 where the constructor of the PathClassLoader takes two arguments, the path of the .dex file and the parent class loader. Calling the function and invoking the method are exactly the same as with the DexClassLoader we saw just above.

InMemoryDexClassLoader

The InMemoryDexClassLoader is, as the name suggests, a class loader which can load classes from a buffer containing a DEX file. The important thing to notice here is that the the dex file is never written in the local file system and only resides in the buffer. A scenario where that would make sense, would be in the case when we are downloading the .dex file from the internet and load it directly. Do note, that while searching to find implementations using this classloader, I could not find one that respects the "do not touch the local file system", what I found was, downloading a file to the local file system and then loading it with the InMemoryDexClassLoader. Which does not really make sense as it beats the purpose of using this class loader in the first place. The following example shows downloading a dex file and saving it to the buffer which is being directly fed to the class loader.

private fun downloadFile(url: String) {
    val thread = Thread {
        try {
            val u = URL(url)
            val conn: URLConnection = u.openConnection()
            val contentLength: Int = conn.getContentLength()
            val stream = DataInputStream(u.openStream())
            buffer = ByteArray(contentLength)
            stream.readFully(buffer)
            stream.close()
            Log.d("seccheck", "Success of download to buffer")
        } catch (e: Exception) {
            Log.e("seccheck", e.message!!)
        }
    }
    thread.start()
}

The following snippet shows an example of how this could have been used within the app.

btBuffer = ByteBuffer.wrap(buffer)
val lder = InMemoryDexClassLoader(btBuffer, this.javaClass.classLoader)
val mt = lder.loadClass("com.erev0s.randomnumber.RandomNumber")
val checkMethodInMemory = mt.getMethod("getRandomNumber")
val newcl = mt.newInstance()
checkMethodInMemory.invoke(newcl)!!.toString()

Example Showcase

The following application is an example where all three methods are used. Three different dex files are created with the only difference between them being in the name of the class they contain (RandomNumber, RandomNumber2, RandomNumber3). The function included in each class is exactly the same. The different name of each class stands as a proof that the correct dex file has been loaded for each case. You can find the application in Github here and check in more detail the steps explained above.

The following is an example of the how the application works. Each button loads the function to generate the random number from a different dex file using a different method.

Notice how for the InMemoryDexClassLoader when the button is clicked a toast is coming up informing us that we need to serve the corresponding .dex file in order to be able to load it. After hitting the download button to fetch the .dex file and load it then the button works.

Conclusion

We now know three ways which can be used to dynamically load code into an application, we also know that the first two require access to the local file system in order to load the file while the third one can be used to load it directly from the internet. Playing around also with the app, can help understand better the implementations and how this knowledge can be used to enhance your skills in reverse engineering malware or creating awesome apps. As always feel free to message me for any questions or remarks.