Skip to content

Reverse-engineering Android apps

One definition of hackers includes the interest in understanding how things work. Android apps are such things that many people use but few people know how they work. I always wanted to build my own apps, but also understand how others do their thing. When I came into security research, my focus shifted to What is this app doing with the data it gets?

Theory

Reverse engineering is the process of analyzing the final app to understand what code lead to it. As you may know, most software is compiled, which means that the developer’s code is translated into a code that machines understand. Most compilers do this translation:

source code (S) => intermediate language (I) => machine language (M)

Going from the machine language back to the source code is more difficult, as most of the information is literally “lost in translation”. Therefore we talk about engineering back to some code that we can understand, as not many people understand machine language.

Android apps are coded in Java or Kotlin (S), which are two programming languages that compile to Java Bytecode (I) which is then translated by the Dalvik Virtual Machine to ARM instructions (M). Compared to other languages, Android apps are not shipped in a machine language representation, but as an intermediate format that can be easily translated back to human-readable source code. If you ever wondered how there are so many clones of popular games in the Play Store, this is one reason.

For Android app analysis, there are better representations of the code. As said before, going back to source code from compiled code is difficult and much is lost, such as variable names, file names and comments. The process is called decompilation. From a legal point of view, many countries include decompilation in their cybercriminals jurisdictions, in other countries the companies behind apps can forbid decompilation and sue you. So decompilation in security research is a minefield that can bring you a lot of trouble.

Therefore smart people built tools to more easily visualize the compiled code without needing to decompile the app. These are not 100% foolproof and may get you also in trouble, but these are used all over the community to analyze, debug and test Android apps. We are going to look into the process of using these tools.

Getting an APK

The first problem when we want to analyze an app is getting the file containing the app. We do not see this file when we install an app from the Play Store. The file is an Android application package (.apk) file and there are multiple ways to get it. If you have a rooted phone, you can install an app and pull the APK to your computer using adb. In most cases I simply look for the APK on the internet, as there are many websites and alternative app stores that offer to download these.

For the following analysis, I will be looking at Pizza Comparison, an app that helps with your risk-benefit analysis of your pizza purchases. For the APK file, we look at APKPure, version 1.0.3-11:

https://apkpure.com/pizza-comparison/de.tetrabude.android.pizzacomparison

We want to find their secret calculation method, to check if they reaaaally are the best app for pizza shoppers. The app does a price calculation and our goal is to check the correctness of it.

The tools

We will be using apktool to translate the bytecode into an intermediate representation called smali. Smali code has similarities to Java code, but is a bit more easy to extract from compiled dex files. Dex files contain the Java bytecode of the app.

Download from here and install. There is a version for Mac, Windows and Linux. I will be using the Linux version.

Extract the raw files

APK files are basically ZIP files. You can test this by renaming your APK to .zip and extract it. For Linux, you can do:

unzip pizza-comparison.apk

You will find a number of files, a lot of them are XML files that are not readable by your text editor. This is because they are encoded, they are packed to be smaller. You want any apps on your device to be small when you download them from the Play Store.

You will also find one (or multiple) classes.dex files which contain the app’s code. Now let’s decode everything and look into it.

Decode and read

Use the following command to decode only the source code (and no resources such as images, layouts etc) and the AndroidManifest file:

apktool decode --no-res --force-manifest pizza-comparison.apk

The above will extract everything into a new directory. If you now look into the files, you will be able to open AndroidManifest.xml with an editor.

The most interesting folder now is smali/, which is structured as a regular Android code base. The folder structure mirrors the different Java packages. In this app, there is one regular code package smali/de/tetrabude/android/pizzacomparison/ and Android’s standard support package. We can look at the main Android activity MainActivity.smali. Remember, we look for secret formulas, so we need to search strategically. If you run the app on your phone, you can “add pizza”, and there you get to input the parameters. So we are looking for an onClick event. Another fact is that new screens are started using Intents. Our smali code contains the same identifiers:

.method public onClick(Landroid/view/View;)V
    // ...

    if-eqz v1, :cond_1

    .line 248
    new-instance v0, Landroid/content/Intent;

    const-class v1, Lde/tetrabude/android/pizzacomparison/edit/PizzaEditRound;

    invoke-direct {v0, p0, v1}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V


    // ...
    if-eqz v1, :cond_0

        .line 254
        new-instance v0, Landroid/content/Intent;

        const-class v1, Lde/tetrabude/android/pizzacomparison/edit/PizzaEditRectangular;

        invoke-direct {v0, p0, v1}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

I cut out some lines, but in this snippet we see a condition (if-eqz => if equal zero) then a new Intent is created and used with class PizzaEditRound or with PizzaEditRectangular. So these might be an interesting starting point. We look into the file edit/PizzaEditRound.smali . When Activities are started, the onCreate method is called. What we see is the code used to create this:

The Save button is the most relevant here. When a new Pizza is saved, the calculation is started. This is where we want to look at. We look at the onClick method:

.method public onClick(Landroid/view/View;)V
    // ...

    if-eqz v0, :cond_0

    .line 45
    new-instance v0, Lde/tetrabude/android/pizzacomparison/pizza/PizzaRound;

    invoke-direct {v0}, Lde/tetrabude/android/pizzacomparison/pizza/PizzaRound;-><init>()V

    iput-object v0, p0, Lde/tetrabude/android/pizzacomparison/edit/PizzaEditRound;->newPizza:Lde/tetrabude/android/pizzacomparison/pizza/Pizza;

    // ...
    invoke-virtual {v0, v2, v3}, Lde/tetrabude/android/pizzacomparison/pizza/PizzaRound;->setDiameter(D)V

Okay, so there is a third class that is used: PizzaRound. The last line shows that the method PizzaRound.setDiameter is called. That sounds like secret calculations! Let’s go into pizza/PizzaRound.smali . For an overview let’s look at all the methods in the class:

grep "\.method" pizza/PizzaRound.smali

.method static constructor <clinit>()V
.method public constructor <init>()V
.method public constructor <init>(Landroid/os/Parcel;)V
.method public describeContents()I
.method public getDiameter()D
.method public getDimension()Ljava/lang/String;
.method public getSquareSize()D
.method public setDiameter(D)V
.method public writeToParcel(Landroid/os/Parcel;I)V

Nothing too interesting here. Let’s look at the file’s first few lines:

.class public Lde/tetrabude/android/pizzacomparison/pizza/PizzaRound;
.super Lde/tetrabude/android/pizzacomparison/pizza/Pizza;
.source "PizzaRound.java"

Okay, we see that PizzaRound has a superclass, called Pizza (damn, getting hungry here). Let’s look into pizza/Pizza.smali. We list the methods:

grep "\.method" pizza/Pizza.smali

.method public constructor <init>()V
.method protected constructor <init>(Landroid/os/Parcel;)V
.method public compareTo(Lde/tetrabude/android/pizzacomparison/pizza/Pizza;)I
.method public bridge synthetic compareTo(Ljava/lang/Object;)I
.method public describeContents()I
.method public abstract getDimension()Ljava/lang/String;
.method public getPizzaName()Ljava/lang/String;
.method public getPrize()D
.method public getSquarePrice()D
.method public getSquarePrice(DD)D
.method public abstract getSquareSize()D
.method public setPizzaName(Ljava/lang/String;)V
.method public setPrize(D)V
.method public writeToParcel(Landroid/os/Parcel;I)V

The getSquarePrice methods look interesting. There are two, let’s start with:

.method public getSquarePrice()D
    .locals 2

    .prologue
    const-wide/high16 v0, 0x3ff0000000000000L    # 1.0

    .line 43
    invoke-virtual {p0, v0, v1, v0, v1}, Lde/tetrabude/android/pizzacomparison/pizza/Pizza;->getSquarePrice(DD)D

    move-result-wide v0

    return-wide v0
.end method

Okay, this does not look like a calculation. This method has no parameters, so it is probably a method that uses default values. You can see that it calls the second method, which takes two double values:

.method public getSquarePrice(DD)D
    .locals 5
    .param p1, "factor_to_square_currency"    # D
    .param p3, "factor_to_square_unit"    # D

    .prologue
    .line 38
    iget-wide v0, p0, Lde/tetrabude/android/pizzacomparison/pizza/Pizza;->sellingPrice:D

    mul-double/2addr v0, p1

    invoke-virtual {p0}, Lde/tetrabude/android/pizzacomparison/pizza/Pizza;->getSquareSize()D

    move-result-wide v2

    div-double/2addr v2, p3

    div-double/2addr v0, v2

    return-wide v0
.end method

We see mul-double => multiply-double, div-double etc. Here is the secret (pizza) sauce of the app. We can now try to reverse this into (pseudo-)Java code:

double getSquarePrice(double factor_to_square_currency, double factor_to_square_unit) {
    double v0 = this.sellingPrice * factor_to_square_currency;
    double v2 = this.getSquareSize() / factor_to_square_unit;
    v0 = v0 / v2;
    return v0;
}

Okay, this should be it, more or less. Of course the real code might contain less temporary variables, but at least the names should be real. We found the price calculation of the app, and can now verify it calculates correctly.

Conclusion and remarks

You see, reverse engineering an app is not difficult, but it requires some knowledge about Android and a lot of experience. In my post you saw that I looked directly at specific methods, because I know from experience where to look. Also, this was a toy example, and you can check if my manual reverse engineering process was good enough since the Pizza Comparison app is open-source and available on GitHub. Thanks to the developers for that!

Published inAndroidIT-Security

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *