DEV Community

Max Baumann
Max Baumann

Posted on

I wrote an open source mod of an Android App

Before we start: Acting on some of the information shared in this article may or may not violate the terms of service of third party apps. You are responsible for any damages, not me.

This article assumes familiarity with Android App Development

The last few month I have spend working on an open source mod of the Twitch Android App. In this post I want to present you the challenges I have faced, the solutions I came up with and we will also learn how Android Apps work.

Anatomy of an apk file

Before we can start thinking about how we can modify an Android App we have to understand the structure of an .apk file, the installation format for Android apps.

These application files are just signed zip archives which contain everything the app needs to run properly. This first and foremost includes the compiled byte-code of the code, but also resources like images, layouts, native libraries, fonts and other assets.

Just rename any .apk file to a .zip file and have a look inside.

Your results may vary quite a lot, as some applications make use of some techniques others do not.

AndroidManifest.xml is pretty much the same AndroidManifest.xml you already know, just in the xml binary format. It also includes information that is not explicitly added to it by a developer, but is derived from their gradle settings.

resources.arsc contains all resources used in the app in an Android specific format, but don't worry there are tools to extract it's contents.

If you find a lib/ directory, that is because the app uses the NDK and it contains native libraries as ELF Shared Objects.

classes.dex contains the Android byte code of the app and is thus the most important result of the compilation process. You likely find files like classes2.dex or classes3.dex as well.

Split apks

Some vendors split their apks into so called splits; They release a base.apk file which includes most of the app and multiple additional splits for different devices. There is no point in delivering xxxhdpi images to small devices or French strings to Arabic users. Fortunately it is a near trivial task to merge them back together.

Reverse Engineering

Apktool

The avid reader noticed nothing is human readable, which makes sense as apk files should be as small as possible. A tool called Apktool comes to the rescue.

You give it an apk file and it will

  1. extract all resources,
  2. disassemble the .dex files into directories of .smali files and
  3. convert the AndroidManifest.xml back into a readable format. (java -jar apktool.jar d file.apk)

If you deal with a split apk you can run apktool on all of them and merge the directories. Make sure you keep the AndroidManifest and public.xml of the base and update the latter to reference all external resources (apktool will create stubs form them). I wrote a bash script and a simple go tool for this.

The great thing about this is that all of these steps can be reverted again, so we can make changes and produce a new .apk file using java -jar apktool.jar b /path/to/target-directory.

You can't install it yet, as it needs to be signed first, more on this later.

JADX

But before we take a dive into the smali code, let's look at some good ol' Java code. Android byte-code is an optimized form of Java byte-code, which is very easily and reliably decompilable. Don't get me wrong, the result will contain lots of issues and won't be compilable. The tool of choice for Android decompilation is called JADX. Just hand it an apk file and it will produce an entire Android Studio project for you:

jadx -e --show-bad-code --no-imports --deobf -d decompiled ./your.apk`
Enter fullscreen mode Exit fullscreen mode

I added --no-imports as IDEA had some issues with resolving imports for me, you can try out jadx without this flag, wit will produce far more friendlier code.

Actually doing it

When it comes to the actual task of reverse engineering there is little I can tell you, you just read code, lots of it.
Here are a few tips though:

  • Have a goal in mind. Most of the time you will not work with small hobby apps, but huge applications with thousands of classes. You don't want to (and probably can't) read all of it. You objective is not to understand everything about the way your sample works. You want to achieve something.
  • Learn your tools Android Studio (IntelliJ IDEA) is incredibly complex and also incredibly helpful if you know what it can do. You should spend some time getting to know it better.
  • Keep it simple You always want to find the easiest, most simple way of achieving the functionality you want. This makes rebasing to new versions easier (and is easier to debug).
  • Follow the strings Most of the time when something happens on your screen some text will appear somewhere. If you look for usages of those strings you can quickly find the parts of the codebase that are of interest for you.

Modding

So you now 1. have an idea for a modification and 2. have read the relevant code to 3. know what you need to change and now want to do it. But how? There are two ways you can add custom code to an app, which I call patching and including.

Patching

Example

The app you want to modify opens a dialog bar to trick the user into giving away their data and you want to modify it to automatically click the reject button.

After reverse engineering you found this code snippet in a file called CookiesLauncher.java and had an idea:

public class CookiesLauncher {
  public void showBarToUser(final LinearLayout layout) {
    final CookieBanner banner = new CookieBanner(layout.getContext());
    banner.setOnReject(new OnClickListener() {
      public void onClick(View v) {
        CookiesLauncher.this.dontStealData();
        layout.removeView(banner);
      }
    });
    layout.addView(banner);
    // <-- Idea: call the reject handler here
  }

  private void dontStealData() {
    this.stealDataAnyway();
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a simple task and can be done by simply adding a patch.

The first step is always to find the smali version of your java code. For this you simply look for the class name and add a .smali to it. If it is an anonymous class embedded somewhere you it will be called something like like: ParentClass$1.smali or ParentClass$OnClickListener.smali.

In our example we simple edit the CookiesLauncher.smali which will be in one of the smali_classes directories.

.class public Lcom/test/app/CookiesLauncher;
.super Ljava/lang/Object;
.source "CookiesLauncher.java"


.method public showBarToUser(Landroid/widget/LinearLayout;)V
    .locals 2

    .line 10
    new-instance v0, Lcom/test/app/CookieBanner;

    invoke-virtual {p1}, Landroid/widget/LinearLayout;->getContext()Landroid/content/Context;

    move-result-object v1

    invoke-direct {v0, v1}, Lcom/test/app/CookieBanner;-><init>(Landroid/content/Context;)V

    .line 11
    new-instance v1, Lcom/test/app/CookiesLauncher$1;

    invoke-direct {v1, p0, p1, v0}, Lcom/test/app/CookiesLauncher$1;-><init>(Lcom/test/app/CookiesLauncher;Landroid/widget/LinearLayout;Lcom/test/app/CookieBanner;)V

    invoke-virtual {v0, v1}, Lcom/test/app/CookieBanner;->setOnReject(Landroid/view/View$OnClickListener;)V

    .line 17
    invoke-virtual {p1, v0}, Landroid/widget/LinearLayout;->addView(Landroid/view/View;)V

    .line 19
    return-void
.end method

Enter fullscreen mode Exit fullscreen mode

As you can see, smali is much lower level than Java is. We don't work with variables, but with registers, we don't just call a method but invoke them and it matters whether we invoke a static method (invoke-static), a method on an interface (invoke-interface) or a virtual one (invoke-virtual). The whole instruction set is available here: https://source.android.com/devices/tech/dalvik/dalvik-bytecode

We need to call the onClick(View v) interface method on the android.view.View.OnClickListener which is in register v1 and pass the LinearLayout in p1 to it:

.local v0, "banner":Lcom/test/app/CookieBanner;
new-instance v1, Lcom/test/app/CookiesLauncher$1;

invoke-direct {v1, p0, p1, v0}, Lcom/test/app/CookiesLauncher$1;-><init>(Lcom/test/app/CookiesLauncher;Landroid/widget/LinearLayout;Lcom/test/app/CookieBanner;)V

invoke-virtual {v0, v1}, Lcom/test/app/CookieBanner;->setOnReject(Landroid/view/View$OnClickListener;)V

.line 17
invoke-virtual {p1, v0}, Landroid/widget/LinearLayout;->addView(Landroid/view/View;)V

invoke-interface {v1, p1}, android/view/View$OnClickListener;->onClick(Landroid/view/View;)V

.line 19
return-void
Enter fullscreen mode Exit fullscreen mode

Now we can rebuild the apk file using apktool and have a patched application file. There is one more issue though:

Signing

The original app was signed using the developer's private key. Before installing the apk the Android operating system will confirm that the signature is still valid, and thus whether the apk has not been tempered with. If this verification fails, Android will refuse to install it.

So we need to sign the apk ourselves. For security purposes Android will refuse any signature that was not made by the same authors of previous installed version of the app, so we need to uninstall the original app first.

Pro tip: You can get around this by changing the app's id. This comes with side effect though.

I used a tool called Uber apk signer for this.

Now you can install your patched app. Congrats!

Including

If you want to add logic that is more complex that a simple if-else or two, you probably want to write the code in Java (or kotlin), compile it, add it to the smali directories and then simply patch in a call to your methods. This is what I call including.

The following figure roughly illustrates this process:

While this takes the burden of writing proper smali code off you just made another enemy: The Java Compiler.

Mocks

The compiler has no idea about the classes that exist in your App, so when you try to call them from your code you are out of luck, you have to mock them.

This means you copy the call signatures of the methods you want to call. Of course you don't want to actually build the mocks. I fixed this by putting the mocks in a separate Android Studio Module.

This module is then imported by the other module as an Android library and thus not build by the compiler.

dependencies {
  // ...
  implementation project(path: ':othermodulesnamehere')
}
Enter fullscreen mode Exit fullscreen mode

Summary

Using all of these techniques this is how the mod will be created:

  1. Use apktool to convert the apk to editable files
  2. Build any new classes you may want to add
  3. Apply your patches
  4. Rebuild the apk file
  5. Sign it

Open source and its constrains

Ok, now that you know how to mod an Android application it is time to talk about why there are pretty much no open source mods.
Whatever I do must be shareable and reproducible. At the same time I can't share the original code, as it is not my IP. At the heart of open source software is always the idea of collaborative work, so that must be possible. The solution is simple: Git.

Instead of sharing the modified code we can simply track the changes made using git diff and share the result of that.

For this to work we initialize a git repository and make a commit tagged base right after running apktool:

java -jar apktool.jar d file.apk -o disass && # disassemble
  cd disass && 
  git init && # init git repo
  git commit -m "base" && # create commit
  git tag base # and tag it
Enter fullscreen mode Exit fullscreen mode

To save our changes we generate a .patch file like this:

cd disass &&
  git add . && # make sure all changes are tracked
  git diff base --minimal > ../ourmod.patch # generate .patch file
Enter fullscreen mode Exit fullscreen mode

This .patch file can then be published.

Your project structure will look like this:

workspace/
- .git/ # of course we track our changes to the workspace using git
- .gitignore
- disass/ # (in .gitignore) contains our result of `apktool d`
  - .git/
- ourmod.patch # our changes
- project/ # android studio project used for inclusion

- a.sh
- bunch.sh
- of.sh
- scripts.sh
Enter fullscreen mode Exit fullscreen mode

Using git apply it is possible for another dev to apply those changes after disassembling the app themselves (as long as they use the same version of the app, more on updates later).

This is not a perfect solution though, as the output of git diff moves quite a lot sometimes and having all patches in one file is also a lot of fun to merge. /s. This is why I wrote a script that generates patch files for each changed file, but that only helps so much.

As you can guess bash scripts are the duck-tape holding this project together

Updates

Due to the usage of git it is easy to apply the changes to a new version of the Twitch app, when it comes out. Of course there are always merge conflicts but most of the patches can be merged surprisingly easily. What is a problem though are the thousands of classes in the Twitch app. I can't verify that nothing significant has changed and update the mocks by hand. That's why I wrote a tool that checks for changes in methods and properties of classes between updates: ubi. It is quite verbose and a lot of the reports it generates can be ignored, but it does improve the quality of the mod as I can act on changes statically, before running into crashes.

Conclusion

I was able to put the years of picking up little skills on the side, like bash scripting or git magic to use in solving a problem where you (unfortunately) could not simply read the docs and holy sh#t I'm proud of that. Every version of bttv-android gets 3k+ downloads now and I am amazed by the incredible support of the Twitch Community, which helps with feature requests, bug reports and translations.
The solutions I came up with are probably not the best way of producing a somewhat high quality mod and heavily relies on bash-duck-tape-f#ckery and if you have better Ideas please let me know!

https://blog.bmn.dev/I-wrote-an-open-source-mod-of-an-Android-App

Discussion (0)