DexPatcher: Patch Android APKs Using Java

DexPatcher: Patch Android APKs Using Java

We may earn a commission for purchases made using our links.

You’ve probably seen or installed modified applications, be it a patched dialer for your resolution or a custom WhatsApp version with added features. How do developers do that, though? A lot of the time, the applications’ source code isn’t even available, so how does it all work? We’ll see that first, then take a look at a new tool that aims to make the process much easier, and finally compare it to the popular Xposed framework to see how they differ.

Modifying APKs: How Does It Work?

You might have heard about how APKs are usually modified — developers plug themselves into the matrix, start seeing everything in Smali, and gain the ability to modify things using the power of the Source. A phone call is enough to get them out once that’s done, at which point they’re ready to share the shiny new APKs.

More seriously… Let’s start at the beginning. If you’re not familiar with modding Android applications, you might be wondering what smali is. Developers usually use the Java programming language to code Android apps in. A program (the compiler) then “translates” that code to another format suitable for your device, resulting in .dex files contained inside the application package (or APK).

At that point, you can’t access the original source code anymore (unless you’re the developer or the application is open source). However, what you do have is the APK, since it’s what is installed on your device. From it, you can get the dex files (usually classes.dex) and then try to translate it back to a format you can understand. That’s where smali comes in, as a more readable but faithful translation. You can go one step further and translate it back to Java, though that process isn’t faithful enough — you’ll get an understandable result, but chances are you won’t be able to translate it the other way around again as some details will be lost along the way. In other words, any modifications you might make will be for naught since you won’t be able to turn it back into an APK again to install it on your device… at least not without a lot of effort.

smali/baksmali is actually an assembler/dissembler for the dex format — that’s what it literally means in Icelandic. However, we usually refer to the format smali understands when we say ‘Smali’ (think of it as instructions defining every little detail, even if it’s not all needed by us humans — it’s therefore more verbose than Java). Also note that the above explanation is a bit simplified, but should be a close analogy while still being easy to understand.

What would a developer need to do to modify an app (without access to the source), then? The process is more or less as follows:

  1. Get the APK (from the web or from the device).
  2. Use something like apktool to decompile the APK to Smali. (apktool makes use of smali/baksmali, but makes it much easier to decompile and rebuild APKs, and also takes care of decoding resources like XML files.)
  3. Extract classes.dex from the APK, then use dex2jar and finally a Java decompiler to get (incomplete, often broken, but mostly understandable) Java code. (This is optional, but can be helpful as Smali is much more difficult to understand.)
  4. Identify what to modify.
  5. Actually modify it by editing the Smali code directly.
  6. Alternatively, write the modification in Java, compile it, decompile it again to Smali, then copy the resulting Smali code over.
  7. Once all is over, use apktool again to rebuild the APK.
  8. Sign the APK (to verify the identify of the author; all packages must be signed) and finally install it.

Writing Smali code is quite difficult and prone to error. Smaller changes can be made in Smali, but adding new features with it is more challenging. Additionally, you won’t have any compile time errors, so even typos may only be detected at runtime. Expanding and sharing Smali patches can also be troublesome, as diffs tend to be very specific to a particular APK version. Although some tools exist to make parts of the process explained above easier (Virtuous Ten Studio comes to mind), it can still get tiresome.

Introducing DexPatcher

DexPatcher by XDA Senior Member Lanchon aims to fix these issues, by making the process simpler and allowing developers to completely avoid dealing with Smali. Instead, devs can write patches in Java alone and have DexPatcher handle everything else.

This has the main advantage of having easily readable and manageable patch files. Patching APKs also becomes more convenient in general. We’ll see a full example on how to use DexPatcher in a bit, but here’s a quick overview of what it offers first:

  • Open source.
  • Cross-platform: it should run on Linux, Mac and Windows.
  • Patch files: modifications you make are contained in Java patch files you can independently share.
  • Java: it’s not Smali.

You also gain the advantage of build-time error checking, so bugs show up early in the development cycle. The Java compiled provides its usual compile time checking (with access to the original APK symbols), and DexPatcher enforces compatibility of source and patch when patching, providing helpful information and giving warnings when you seem to be doing something legal but fishy.

In addition to that, DexPatcher comes with a set of helper scripts (only available on Linux, though they could be ported over to other platforms as well). These take care of setting up the workspace, extracting the target APK’s classes and resources, decompiling the classes to Java (the CFR Java decompiler is used for the latter), and finally building and signing the patched APK once you’re done.

Let’s take a look at an example (on Linux):

Install the DexPatcher Scripts

$ # Make a directory where we can test stuff out and enter it.
$ mkdir xda-test
$ cd xda-test
$ git clone https://github.com/Lanchon/DexPatcher-scripts.git dexpatcher  # Clone the DexPatcher helper scripts repo.
$ cd dexpatcher
$ chmod +x dxp-*  # Not necessary, but for clarity: we need to make sure the files we'll call later are executable.

Configure the DexPatcher Scripts

Open dxp.config in your favorite text editor and make sure to change the necessary variables to suit your system. You only need to change the following line to point to your Android SDK’s installation location instead:

dxp_android_sdk_dir=(~/android/sdk)

(DexPatcher will automatically pick the highest platform version available. Additionally, you can also modify other config options to have it use your own versions of some tools instead of the bundled defaults.)
For ease of access, we can add the dexpatcher directory to our PATH, or even symlink the different dxp-* scripts to a location that is already in your PATH, such as ~/bin:

export PATH=$PWD:$PATH

Modify an Application

For this example, we’ll use a simple and open source application. Of course, patching the source code directly would be possible in this particular case, but that’s no fun at all!

We’ll take the “Get ID” application by basil2style, an application that shows you some details about your device. Our goal is to modify the “Copy” button for the “Device ID” and have it share this ID instead:

DexPatcher Before Patch

  • First, let’s download the APK we’re going to modify: Get ID.
  • Decompile the application.
  • Create the signing key that we’ll later use to sign the APK.

We can also do it all via the shell, using the helper scripts:

$ cd dexpatcher  # Go to our working directory.
$ curl -O https://f-droid.org/repo/makeinfo.com.getid_1.apk  # Download the APK.
$ dxp-setup-for-apk makeinfo.com.getid_1.apk  # Unpack and decompile the APK.
$ cd makeinfo.com.getid_1  # Go to the newly created directory where everything is unpacked/decompiled to.
$ dxp-create-keystore  # Create the APK signing key. Press  6 times (or fill out the info), then "yes".

You’ll notice a few different directories in there:

  • decode: you’ll find the resources and Smali here, as decoded by apktool.
  • src: Empty directory. This is where we’ll place our patch files.
  • src-cfr: this is where cfr decompiled the app (along with errors). A good to place to look in to decide on what to change (you might also need resources and their IDs from the decode directory above, but not for this particular example).
  • src-cfr-nodecode: same as above, but containing only empty stubs (no code, just skeletons). You can use these files as a basis for your patch as we’ll see in a bit.

As we’ve mentioned before, we want to change the Device ID “Copy” button to share the ID text instead. If we look around the source code, we’ll notice that the Device ID Copy button (device_copy) onClick event is handled in by anonymous class in src-cfr/makeinfo/com/getid/MainActivity.java. While we could mod it here, it’s usually better to find an alternate way to do it as anonymous classes have numeric names (MainClassName$SomeNumber, e.g. MainActivity$3) which might change unpredictably between versions.

Instead, we will register our own class for the event by modifying the MainActivity class. First, let’s copy the “skeleton” version from src-cfr-nocode/makeinfo/com/getid/MainActivity.java to src/makeinfo/com/getid/MainActivity.java (remember that src is where our patch will live). (You can also copy the version with the full code if you prefer, this is purely a matter of taste.)

We can now edit it as follows:

  • Add the necessary import for the DexPatcher annotation:
import lanchon.dexpatcher.annotation.*;
  • Add a tag to indicate we’re editing the class. We also set the default action for members of the patch class to IGNORE, which means that the members are there to be referenced by our code during Java compilation, but will be ignored by DexPatcher.
@DexEdit(defaultAction = DexAction.IGNORE)
public class MainActivity
// The reference to ActionBarActivity will be satisfied by symbols
// extracted from the app when we build the patch.
extends ActionBarActivity {
  • Additionally, add empty bodies to the constructor and onCreate method, as well as all other methods we plan to use (remember that they’ll be ignored when our patch is actually applied — we’re just adding them so we can refer to them here if we need to). You can also just add the native keyword instead.
  • We can already build the patch at this point, if you’re curious:
    $ dxp-make  # Output: `patched.apk`.

    Pretty simple, right? Let’s keep going, though — we’re still not done yet.

  • Let’s edit onCreate now to set out own OnClickListener so that we can share the device ID instead of copying it to the clipboard:
    // Rename the target method so that we can still call it (the original)
    // if needed.
    @DexEdit(target = "onCreate")
    protected void source_onCreate(Bundle var1) {}
    // Add our new custom method.
    @Override
    @DexAdd
    protected void onCreate(Bundle var1){
        // Call the original method:
        source_onCreate(var1);
    
        // Replace the text and handler:
        device_copy.setText("Share");
        device_copy.setOnClickListener(new DeviceCopyOnClick());
    }
    
    // Note that we don't use an anonymous class to avoid nameclashing with
    // MainActivity$1, which already exists.
    // We also could've defined a nested MainActivity.Patch class and used
    // an anonymous class in MainActivity.Patch.onCreate(), and then called
    // MainActivity.Patch.onCreate() from MainActivity.onCreate().
    @DexAdd
    class DeviceCopyOnClick implements View.OnClickListener {
        @Override
        public void onClick(View object) {
            if (MainActivity.this.val) {
                Intent intent = new Intent(Intent.ACTION_SEND);
                intent.setType("text/plain");
                intent.putExtra(Intent.EXTRA_SUBJECT, "Device ID");
                intent.putExtra(Intent.EXTRA_TEXT, device.getText().toString());
                startActivity(Intent.createChooser(intent, "Share Device ID"));
            } else {
                Toast.makeText(MainActivity.this.getApplicationContext(), "Nothing to Share", 0).show();
            }
        }
    }
  • Looks like we’re done now! The full patch should look like this. We can now build the patched APK and install it:
    $ dxp-make
    $ adb install patched.apk
  • Let’s have a look at the result:

DexPatcher After Patch

(Thanks to Lanchon for helping out with the sample code!)

How DexPatcher Differs From Xposed

Xposed is immensely popular, and for a good reason — it makes building, sharing and installing mods much simpler for developers and users alike. There are a few differences between DexPatcher and Xposed that may make some prefer one over the other:

  1. Xposed does its magic by hooking methods at runtime and allowing developers to do something before, after or instead any method. DexPatcher, on the other hand, modifies everything ahead of runtime and produces a standalone, modified APK — running code before, after or instead of methods is still possible, and you actually have some extra freedom.
  2. Producing a standalone APK means it doesn’t depend on any external framework. This also means root is not required for modifying user apps.
  3. Since you’re created a new APK with DexPatcher, it’ll be signed differently. This means users can’t receive official updates from the original author, and may cause some issues with apps like Google Apps if the signatures are checked.
  4. Both modules’ and DexPatcher patches’ source code can be easily distributed and modified. They also share many similarities if you get a bit familiar with each.

Get It Now

We’ve talked enough about DexPatcher. It’s your turn to give it a shot now, so head over to the DexPatcher Forum Thread to get started straight away!