Setup for testing Android app vulnerabilities

In the previous article I documented my approach for reverse engineering an Android game. But getting my hands on the code is only one part of security research. Once a potential issue is identified, I need to verify that it is actually exploitable. So there is no way around messing with an actual live app. Ideally that has to happen in a controlled environment with emulated hardware. As before, this is mostly me writing things down for my future self, but it might come useful for other people as well.

Choosing a virtualization approach

Historically, the official Android emulator has been barely usable, which is why I’ve been using Androix x86 images in VirtualBox for a long time. This solution doesn’t work here however: the Android game in question contains binary code compiled for the ARM platform, it cannot be executed directly on x86 hardware.

But don’t despair, Android 11 comes with a built-in ARM emulator! So when this game is run on x86 hardware, the OS should automatically translate ARM processor instructions and things will magically work. Sounds great, except for the fact that at the time of writing the Android x86 project only provides Android 9 images and nothing above that.

Further searching brought me to Bliss OS 14, based on Android 11 and available as an Alpha release. I tried it in VirtualBox, and it failed installing. Trying to run it directly from the CD image failed as well. Some investigation on my part revealed wrong paths in the install script as well as a broken Grub build. And that’s not even the parts changing on OS upgrades, the OS would get stuck on boot despite my workarounds. Supposedly, things would work on non-virtualized hardware but I have strong doubts about that. I rather suspect that this “release” is only usable for people installing everything manually and having really deep understanding of the operating system. So either the developers have a different understanding of the phrase “alpha release” than me (mine implies something that’s at least somewhat usable) or the whole thing is a pure publicity stunt: “We are first to bring Android 11 to the desktop!”

I then circled back to the Android emulator. And who would have thought, it worked flawlessly! All the issues making it unusable for me in the past were gone. So the next section is about getting this emulator to your machine.

Setting up Android SDK

The Android SDK is going to be a hefty download no matter what. So you are probably well-advised to go to the download page and download Android Studio. It is easier to set up and better documented.

Personally however, I don’t need this IDE. I prefer working with command line tools where it’s easier to get reproducible results which is particularly important for automation. So for me it’s the “Command line tools only” download further down on the same page.

Android SDK requires Java, but Java doesn’t have to be installed globally on the system. You can download Java JDK and unpack it anywhere on your system. As long as you expose its path in the JAVA_HOME environment variable, all the command line tools will work correctly.

The command line tools expect to be unpacked to the cmdline-tools/latest directory of the Android SDK, even if your Android SDK install is initially nothing but the command line tools. So you better put them there unless you want to specify the Android SDK path explicitly every time. Add /path/to/sdk/cmdline-tools/latest/bin to your PATH environment variable to use the commands. This allows running sdkmanager to install further components:

sdkmanager 'system-images;android-30;google_apis_playstore;x86_64' \
           'build-tools;30.0.3' 'platforms;android-30' 'platform-tools'

Here system-images;android-30;google_apis_playstore;x86_64 is the Android 11 image (corresponding to API version 30) with the Play Store app. The emulator will be installed automatically and you’ll need to add /path/to/sdk/emulator to PATH for the emulator command.

build-tools;30.0.3 and platforms;android-30 are needed in order to build apps for the Android 11 platform, /path/to/sdk/build-tools/30.0.3 needs to be added to PATH for the APK signing commands. And platform-tools contain the adb binary, so /path/to/sdk/platform-tools should be added to PATH as well.

You can now create a device image for the emulator. I named the image Android11 and used a tablet as a usable initial hardware configuration. While this command will suggest creating a custom hardware profile, it’s hardly advisable to do so. That is, unless you fancy answering a hundred irrelevant questions of course where each mistake will make you repeat the process.

avdmanager create avd -n Android11 -d '10.1in WXGA (Tablet)' \
           -k 'system-images;android-30;google_apis_playstore;x86_64'

You can still change the hardware configuration by editing ~/.android/avd/Android11.avd/config.ini manually. For me, the following values needed adjusting or adding:

  • hw.ramSize: the default 2048 MB weren’t sufficient for the game app to run, I changed this to 10240 MB
  • hw.keyboard: this should be set to yes if you prefer your computer’s keyboard over the on-screen variant
  • hw.keyboard.charmap: the default qwerty2 should be ok for US keyboards, but German keyboard layout is qwertz

Now you should be able to run the emulator:

emulator -avd Android 11

You can install the app to be tested via Play Store. But then again, you probably have the APK package anyway, in my case named game.apk. So you can install it directly:

adb install game.apk

Minimal proof of concept Android app

The previous article mentioned an exposed service. In order to attempt accessing it, one needs to run some code via another Android app. Obviously, I’d rather avoid complicating matters, so how does one build a minimal Android app?

As this or this blog articles explain, the answer is surprisingly complicated. In the end, it’s probably easiest to use Gradle. So I downloaded the latest package which at the time of writing was gradle-6.8.3-all.zip. After unpacking you have one more value to be added to the PATH environment variable: /path/to/gradle/bin. Now the gradle command can be used.

The project root needs a build.gradle configuration file:

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0+'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30
    buildToolsVersion '30.0.3'
}

Most of this is generic, only the version of the Android plugin for Gradle is worth mentioning. The documentation says that 4.1.0+ is the correct plugin version for current Gradle releases. Future Gradle versions might require a different value here.

Gradle expects a complex directory structure, so AndroidManifest.xml has to be located under src/main in the project. Mine is very basic:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test">
    <application android:label="Test app">
        <activity android:name="MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

If you keep the package ID as com.example.test, then your MainActivity.java file has to be placed into the src/main/java/com/example/test subdirectory of the project. Something like this will do:

package com.example.test;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;

public class MainActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent();
        intent.setComponent(new ComponentName(
          "com.example.funnygame",
          "com.example.funnygame.MyFirebaseMessagingService"
        ));
        intent.setAction("com.google.firebase.MESSAGING_EVENT");
        this.bindService(intent, new ServiceConnection()
        {
          @Override
          public void onServiceConnected(ComponentName name, IBinder service)
          {
            System.out.println("Service connected: " + name);
          }
          @Override
          public void onServiceDisconnected(ComponentName name) {}
        }, BIND_AUTO_CREATE );
    }
}

Now you can run gradle installDebug (expects ANDROID_HOME environment variable to be set to the Android SDK path). This command will build the app and install it to the emulator automatically. When you run it, it will immediately attempt to connect to a particular service from another app. And maybe it can then do something interesting with it.

Adding debugging output to the target application

Now my first attempt didn’t go anywhere. Nothing happened and from the source code I couldn’t quite tell why. So there is no way around debugging the target application to see what’s going on there. Except: all the documentation says that debugging only works with debug builds. And I cannot exactly ask the vendor to provide me a debug build of their app.

Luckily, there is still a way. If an Android app can be decompiled, it can also be modified. It seems that the most up-to-date framework to do this is Soot and there is a detailed article on how it can be used. It also provides a repository with working code which is really helpful because you don’t have to start from scratch with your Java code.

Edit (2021-02-24): I somewhat generalized and streamlined the approach described here, the code is now available in this repository.

What I ended up doing is very similar to the AndroidLogger.java example. It adds a System.out.println() call at the beginning of each method and will log both the method and the parameters it receives. You have to keep in mind that Jimple is a low-level representation of Java code that doesn’t support nested expressions. So the result of each intermediate expression has to be saved into a local variable which complicates things somewhat. The resulting body transformer looks like this:

@Override
protected void internalTransform(Body b, String phaseName, Map<String, String> options)
{
  JimpleBody body = (JimpleBody)b;

  // Only add logging to com.example.funnygame package
  String className = body.getMethod().getDeclaringClass().getName();
  if (!className.startsWith("com.example.funnygame."))
    return;

  // Instructions to be added
  List<Unit> units = new ArrayList<>();

  // StringBuilder message = new StringBuilder("Entered method with parameters: ");
  Local message = generateNewLocal(body, RefType.v("java.lang.StringBuilder"));
  units.add(
    Jimple.v().newAssignStmt(
      message,
      Jimple.v().newNewExpr(RefType.v("java.lang.StringBuilder"))
    )
  );
  units.add(
    Jimple.v().newInvokeStmt(
      Jimple.v().newSpecialInvokeExpr(
        message,
        Scene.v().getMethod(
          "<java.lang.StringBuilder: void <init>(java.lang.String)>"
        ).makeRef(),
        StringConstant.v(
          "Entered method " + body.getMethod().getSignature() +
          " with parameters: "
        )
      )
    )
  );

  List<Local> parameters = body.getParameterLocals();
  boolean first = true;
  for (Local parameter: parameters)
  {
    if (first)
      first = false;
    else
    {
      // message.append(", ");
      units.add(
        Jimple.v().newInvokeStmt(
          Jimple.v().newVirtualInvokeExpr(
            message,
            Scene.v().getMethod(
              "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>"
            ).makeRef(),
            StringConstant.v(", ")
          )
        )
      );
    }

    // message.append(String.valueOf(parameter));
    Local stringified = generateNewLocal(body, RefType.v("java.lang.String"));
    units.add(stringify(parameter, stringified));
    units.add(
      Jimple.v().newInvokeStmt(
        Jimple.v().newVirtualInvokeExpr(
          message,
          Scene.v().getMethod(
            "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>"
          ).makeRef(),
          stringified
        )
      )
    );
  }

  // System.out.println(message.toString());
  Local messageStringified = generateNewLocal(body, RefType.v("java.lang.String"));
  units.add(
    Jimple.v().newAssignStmt(
      messageStringified,
      Jimple.v().newVirtualInvokeExpr(
        message,
        Scene.v().getMethod(
          "<java.lang.Object: java.lang.String toString()>"
        ).makeRef()
      )
    )
  );

  Local printStream = generateNewLocal(body, RefType.v("java.io.PrintStream"));
  units.add(
    Jimple.v().newAssignStmt(
      printStream,
      Jimple.v().newStaticFieldRef(
        Scene.v().getField("<java.lang.System: java.io.PrintStream out>").makeRef()
      )
    )
  );
  units.add(
    Jimple.v().newInvokeStmt(
      Jimple.v().newVirtualInvokeExpr(
        printStream,
        Scene.v().getMethod(
          "<java.io.PrintStream: void println(java.lang.String)>"
        ).makeRef(),
        messageStringified
      )
    )
  );

  // Insert new code at the beginning of the method and validate
  body.getUnits().insertBefore(units, body.getFirstNonIdentityStmt());
  body.validate();
}

This uses two helper functions:

private static Local generateNewLocal(Body body, Type type)
{
  LocalGenerator lg = new LocalGenerator(body);
  return lg.generateLocal(type);
}

private static Unit stringify(Local value, Local result)
{
  Type type = value.getType();
  String typeSignature = (
    type instanceof PrimType ? type.toString() : "java.lang.Object"
  );
  if (typeSignature == "byte" || typeSignature == "short")
    typeSignature = "int";
  return Jimple.v().newAssignStmt(
    result,
    Jimple.v().newStaticInvokeExpr(
      Scene.v().getMethod(
        "<java.lang.String: java.lang.String valueOf(" + typeSignature + ")>"
      ).makeRef(),
      value
    )
  );
}

Using the setup from the original article I’ve hit two issues. First one was that the APK file generated by Soot was subtly broken. I decided that it would be best to let Soot process only the .dex files. As it turned out, process_dir option doesn’t have to be set to the location of the APK file, it can also be an individual .dex file. Soot will then write a classes.dex file to the output directory. So I generate the modified APK using the following quick and dirty Python script:

import os
import shutil
import subprocess
import zipfile

env = dict(os.environ)
env['CLASSPATH'] = '.:/path/to/soot-4.2.1-jar-with-dependencies.jar'
env['ANDROID_HOME'] = '/path/to/sdk/'

with zipfile.ZipFile('game.apk', 'r') as archive_in:
  with zipfile.ZipFile('game-instrumented.apk', 'w') as archive_out:
    for entry in archive_in.infolist():
      if entry.filename.startswith('classes') and entry.filename.endswith('.dex'):
        archive_in.extract(entry)
        subprocess.check_call([
          'java', 'APKConverter', entry.filename, 'instrumented'
        ], env=env)
        os.unlink(entry.filename)
        archive_out.write(os.path.join('instrumented', 'classes.dex'), entry.filename)
        shutil.rmtree('instrumented')
      else:
        archive_out.writestr(entry, archive_in.read(entry))

The second issue is rather weird: a particular class in a Google API would error out due to an invalid cast. The problematic code was in a static class initializer and the decompiled line looked like this:

zzao = (zzl)new zzt();

This is indeed an invalid cast, the zzt type doesn’t implement the (empty) zzl interface. How did this ever work? I can only imagine that this particular construct used invalid bytecode, meant to turn into an error upon recompilation. The zzao variable initialized here is never used.

But since we are rewriting code, we can simply remove this assignment! Adding the following code to the body transformer did the job:

if (className.equals("com.google.android.gms.games.Games") &&
    body.getMethod().getName().equals("<clinit>"))
{
  body.getUnits().removeIf(unit -> {
    if (unit instanceof AssignStmt)
    {
      String typeName = ((AssignStmt)unit).getLeftOp().getType().toString();
      if (typeName.equals("com.google.android.gms.games.appcontent.zzl"))
        return true;
    }
    return false;
  });
}

The final touch is signing the modified APK. A test certificate is good enough to install the app in the emulator, but you’ll have to uninstall the original app first. I used the following command to generate a certificate (keytool is part of the Java install):

keytool -genkey -v -keystore test.jks -alias test -keyalg RSA \
        -keysize 2048 -keypass 123456 -validity 10000

Now signing is a matter of running the following two commands from the Android SDK:

zipalign -f 4 game-instrumented.apk game-signed.apk
apksigner sign --ks test.jks --ks-pass 'pass:123456' game-signed.apk

Running adb install game-signed.apk can install the modified app now, and it should produce nice debugging messages visible in adb logcat output.

Comments

  • D'Franco

    Thank you for sharing this. App safety is a huge deal, now you're practically getting spied by every app you install and that is without getting hacked. Good post.