PentestsPL

PANhandler from /dev/null

Android - Loading Activity from additional apk

( update: source code can be found here: https://github.com/marcing-dev/APK_loading )

Some time ago I’ve found a way of executing Activities in Android that are not located in original apk. Here’s a little story about that!

Every Android developer who was writing big app probably had a problem with limitation of number of references in one apk file. There’re few ways to deal with it for example something called Multidexing (https://developer.android.com/tools/building/multidex.html). Developer can also create an instance of ClassLoader that will load classes from another apk, and then just use these classes.
Code below shows that:

public static void loadClass(String TAG, Context context) {
    Log.i(TAG, "Trying to load new class from apk.");
    final File dexInternalStoragePath = new File(context.getDir("dex", Context.MODE_PRIVATE),
            "test.apk");
    final File optimizedDexOutputPath = context.getDir("outdex", Context.MODE_PRIVATE);
    Log.i(TAG, "dexInternalStoragePath: " + dexInternalStoragePath.getAbsolutePath());
    if(dexInternalStoragePath.exists()) {
        Log.i(TAG, "New apk found!");
        DexClassLoader dexLoader = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
                optimizedDexOutputPath.getAbsolutePath(),
                null,
                ClassLoader.getSystemClassLoader().getParent());
        try {
            Class klazz = dexLoader.loadClass("pl.com.marcing.android.customdex.NewObject");
            Constructor constructor = klazz.getConstructor(String.class);
            Method method = klazz.getDeclaredMethod("getInfo");
            Object newObject = constructor.newInstance("New object info");
            Log.i(TAG, "New object has class: " + newObject.getClass().getName());
            Log.i(TAG, "Invoking getInfo on new object: " + method.invoke(newObject));
        } catch(Exception e) {
            Log.e(TAG, "Exception:", e);
        }
    } else {
        Log.i(TAG, "Sorry new apk doesn't exist.");
    }
}

Here’s a class I’ve created in additional apk:

package pl.com.marcing.android.customdex;

/**
 * Created by MarcinG on 20-02-2015.
 */
public class NewObject {
    private final String info;

    public NewObject(String info) {
        this.info = info;
    }

    public String getInfo() {
        return info;
    }
}

and a log:

I/DynamicClassLoadActivity﹕ Trying to load new class from apk.
I/DynamicClassLoadActivity﹕ dexInternalStoragePath:
    /data/data/pl.com.marcing.android.dynamicactivityloader/app_dex/test.apk
I/DynamicClassLoadActivity﹕ New apk found!
I/DynamicClassLoadActivity﹕ New object has class: pl.com.marcing.android.customdex.NewObject
I/DynamicClassLoadActivity﹕ Invoking getInfo on new object: New object info

This approach is nice, but has its limitations. For example you can’t run new Activities using it, why? Because when you start new Activity original application ClassLoader is used.

ClassLoaders are written in Java and they are normal objects inside app, so one can get access to them. Of course they are written in such way that normal access is impossible. You can’t just simply grab a ClassLoader object and start setting attributes inside it. I could end my story here but fortunately there is a thing called Java Reflection API. It gives you possibility to do almost anything with Java objects. Let’s check an example class:

public class SecuredClass {
	private String guardedAttribute = "I'm private, u can't change me!";

	public void showGuardedAttribute() {
		System.out.println("guardedAttribute: " + guardedAttribute);
	}

}

This class has an attribute named “guardedAttribute”. It’s private and there’s no way you can change it normally. So here’s an output of test app using that class: 

guardedAttribute: I'm private, u can't change me!
guardedAttribute: Oh really?

guardedAttribute value changed :) Here’s code of this app:

public class ReflectionApiTest {
	public static void main(String[] args) throws Exception{
		SecuredClass securedClass = new SecuredClass();
		securedClass.showGuardedAttribute();
		Field field = SecuredClass.class.getDeclaredField("guardedAttribute");
		field.setAccessible(true);
		field.set(securedClass, "Oh really?");
		securedClass.showGuardedAttribute();
	}
}

I think there’s no need for explanation here. If field is final you can also change its value but if it’s primitive type u basically gain nothing cause classes will use old value. But if that final attribute is some object and you change it to new one, class will use the new one.

Let’s go back to Android…I have created new apk containing Activity like this one:

package pl.com.marcing.android.customdex;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

/**
 * Created by MarcinG on 27-10-2014.
 */
public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onStart() {
        Log.i("TestActivity", "::onStart");
        super.onStart();
        method();
    }

    @Override
    protected void onDestroy() {
        Log.i("TestActivity", "::onDestroy");
        super.onDestroy();
    }

    public void method() {
        Log.i("TestActivity", "action inside TestActivity");
    }
}

I modified method for loading custom classes to look like that:

public static void loadActivity(String TAG, Context context) {
    Log.i(TAG, "Trying to load new activity from apk.");
    final File dexInternalStoragePath = new File(context.getDir("dex", Context.MODE_PRIVATE),
            "test.apk");
    final File optimizedDexOutputPath = context.getDir("outdex", Context.MODE_PRIVATE);
    Log.i(TAG, "dexInternalStoragePath: " + dexInternalStoragePath.getAbsolutePath());
    if(dexInternalStoragePath.exists()) {
        Log.i(TAG, "New apk found!");
        DexClassLoader dexLoader = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
                optimizedDexOutputPath.getAbsolutePath(),
                null,
                ClassLoader.getSystemClassLoader().getParent());
        try {
            Class klazz = dexLoader.loadClass("pl.com.marcing.android.customdex.TestActivity");
            Intent intent = new Intent(context, klazz);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        } catch(Exception e) {
            Log.e(TAG, "Exception:", e);
        }
    } else {
        Log.i(TAG, "Sorry new apk doesn't exist.");
    }
}

When I’ve tried to run new Activity from new apk this error showed up:

android.content.ActivityNotFoundException: Unable to find explicit activity class
{pl.com.marcing.android.dynamicactivityloader/pl.com.marcing.android.customdex.TestActivity};
have you declared this activity in your AndroidManifest.xml?

Activity is not specified in AndroidManifest.xml…So let’s specify it (there was an error….ignored it).

<activity
  android:name="pl.com.marcing.android.customdex.TestActivity"
  android:label="TestActivity">
</activity>

Let’s check it again:

FATAL EXCEPTION: main
    Process: pl.com.marcing.android.dynamicactivityloader, PID: 14987
    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{
      pl.com.marcing.android.dynamicactivityloader/pl.com.marcing.android.customdex.TestActivity}:
    java.lang.ClassNotFoundException: Didn't find class
      "pl.com.marcing.android.customdex.TestActivity" on path:
      DexPathList[[zip file "/data/app/pl.com.marcing.android.dynamicactivityloader-2.apk"],
      nativeLibraryDirectories=[/data/app-lib/pl.com.marcing.android.dynamicactivityloader-2, /system/lib]]

Still error but this time different.  Going through source code of BaseDexClassLoader I’ve found that attribute holding paths to apks and libs is named “pathList”.  So I created some custom ClassLoader and then got out its “pathList” and set it to application ClassLoader. This will force app ClassLoader to load classes from my apk. Everything looks like that:

public static void invokeAdditionalActivity(String TAG, Context context) {
    try {
        Field f = BaseDexClassLoader.class.getDeclaredField("pathList");
        f.setAccessible(true);
        StringBuilder libPath = new StringBuilder();
        StringBuilder apkPath = new StringBuilder();

        Object dexPathObj = f.get(context.getClassLoader());

        Log.i(TAG, "DexPath object class:" + dexPathObj.getClass().getName());
        f = dexPathObj.getClass().getDeclaredField("nativeLibraryDirectories");
        f.setAccessible(true);
        Object libsObj = f.get(dexPathObj);
        Log.i(TAG, "Libs object class:" + libsObj.getClass().getName());
        String delim = "";
        if (libsObj instanceof File[]) {
            Log.i(TAG, "It is file array");
            File[] testArray = (File[]) libsObj;
            for (File libFile : testArray) {
                Log.i(TAG, libFile.getAbsolutePath());
                libPath.append(delim);
                delim = File.pathSeparator;
                libPath.append(libFile.getAbsolutePath());
            }
        }
        //dexElements
        f = dexPathObj.getClass().getDeclaredField("dexElements");
        f.setAccessible(true);
        Object elemsObj = f.get(dexPathObj);
        Object[] elemsArray = (Object[]) elemsObj;
        for (Object element : elemsArray) {
            Log.i(TAG, "Element object class:" + element.getClass().getName());
            Log.i(TAG, "Fields:");
            for (Field elemField : element.getClass().getDeclaredFields()) {
                Log.i(TAG, elemField.getName());
                elemField.setAccessible(true);
                try {
                    Object tmpObj = elemField.get(element);
                    if (tmpObj != null) {
                        if (tmpObj instanceof File) {
                            File testF = (File) tmpObj;
                            if (elemField.getName().equals("file")) {
                                apkPath.append(testF.getAbsolutePath());
                            }
                        }
                    } else {
                        Log.i(TAG, "^^null!!");
                    }
                } catch (Exception e) {
                    Log.i(TAG, "^^can't access");
                }
            }
        }
        final File dexInternalStoragePath = new File(context.getDir("dex", Context.MODE_PRIVATE),
                "test.apk");
        final File optimizedDexOutputPath = context.getDir("outdex", Context.MODE_PRIVATE);
        Log.i(TAG, "apkPath:" + apkPath.toString());
        Log.i(TAG, "libPath:" + libPath.toString());
        DexClassLoader cl = new DexClassLoader(dexInternalStoragePath.getAbsolutePath() + File.pathSeparator + apkPath.toString(),
                optimizedDexOutputPath.getAbsolutePath(),
                libPath.toString(),
                context.getClassLoader());
        Log.i(TAG, "Test: " + cl.toString());
        f = BaseDexClassLoader.class.getDeclaredField("pathList");
        f.setAccessible(true);
        Object dexPathObjNew = f.get(cl);
        f.set(context.getClassLoader(), dexPathObjNew);

        Class klazz = context.getClassLoader().loadClass("pl.com.marcing.android.customdex.TestActivity");
        Intent intent = new Intent(context, klazz);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    } catch (Exception e) {
        Log.e("Invoker", "invoke exception", e);
    }
}

In addition I’m saving old classpath and adding it to the new one so there are no errors. Let’s see what will happen when I’ll try to load new Activity:

I/DynamicActivityLoadActivity﹕ Test: dalvik.system.DexClassLoader[DexPathList[[
    zip file "/data/data/pl.com.marcing.android.dynamicactivityloader/app_dex/test.apk",
    zip file "/data/app/pl.com.marcing.android.dynamicactivityloader-1.apk"],
    nativeLibraryDirectories=[/data/app-lib/pl.com.marcing.android.dynamicactivityloader-1, /system/lib, /system/lib]]]
I/TestActivity﹕ ::onStart
I/TestActivity﹕ action inside TestActivity

This time everything went smooth and new Activity has been started :) Impossible is nothing! 

That’s all, I think that people will find some interesting tricks that can be done using this technique. I for example, have made an app that gives user option to add new features to it. User clicks button, app connects to server, downloads required apk and runs it. Activity loader can be inside new apk. Application loads loader class from apk and fires it passing context to it. The best thing about that approach is that if u load “different” apk you can get different results. Have fun!

Source code can be found here: https://github.com/marcing-dev/APK_loading

Written on February 23, 2015