One think you sometimes like to do is patching Apps - you can do that dynamically using tools like Frida.
Dynamic instrumentation is excellent but has strong requirements (often requires rooting your phone), so one thing one could do is patch Apps statically.
Of course, if you want to patch native code (called via JNI) you can easily patch the *.so files in the App and repackage, but how do you patch the Java bytecode?
For that, we'll introduce how Java Apps bytecode looks like, how to patch it, repackage the App etc., without rooting your phone.
You should only patch Apps that you own or have permissions to patch.
This guide is only going to be used for information on how to patch Apps, and in no way encourages bypassing DRM or creating fake Apps.
In other words - use at your own risk!
There are many tools out there, but my favorite is probably apktool.
We'll need a few tools to resign the App, and maybe understand where to patch. So:
- apktool is going to be used for decoding and rebuilding the App.
- keytool that comes with JDK would allow us to re-sign the App.
- adb is the Android Debug Bridge and will allow us to debug Apps.
apktoolis quite easy to install - it's aJARfile so it can be run with Java. However, you can install it on a Linux box easily withapt.- For
keytool, install the defaultJDK. - For
adb, install the Android debugging tools. Again I will be usingaptfor that on a modern Linux, but you can easily install those on Windows too. - We will need
apksignerto sign our APK, it comes with the Android debugging tools too.
# apktool
sudo apt install apktool
# JDK
sudo apt install default-jdk
# adb and apksigner
sudo apt install android-tools-adb android-tools-fastbootAs I mentioned in a previous blogpost, Android Apps are bundled as *.apk files.
Those are essentially zip files, with a predefined structure:
- A metadata file called
AndroidManifest.xml. - Bytecode files: those are
*.dexfiles - you will most likely examineclasses.dexeven thoughclasses-2.dex(etc.) files do exist. - Native libraries: will be
*.sofiles underlib/. - Resources and assets: such as pictures, audio and others - will be under
res/andassets/, with the difference being resources are compiled into a class (namedR) and assets are "raw".
Since we will be focusing on modifying the bytecode, classes.dex is our primary target, but we will have to start with AndroidManifest.xml to understand what we'd like to focus on in the App.
I did not mention it previously, but when treating *.apk files as archives - this file is binary (even though it has a .xml extension). Tools like apktool can convert it to a readable form - more on that later.
Tools like apktool will extract all definitions and code in classes.dex and extract them to files with the extension *.smali.
Each file will commonly be a Java class, and contain the definitions of the class, as well as the Android bytecode.
Thus, let us start by describing the bytecode structure itself in each *.smali file.
So, some starting points:
- In Smali, registers are 32-bit long and hold 32-bit values. To handle 64-bit values (e.g. Java
longordoubleinstances), two registers are used. - Smali files contain declaration of a class (with a
.classdirective), that includes information about the class - its superclass, its members and methods. Our patching will mostly focus on methods. - Methods can get parameters in registers, and will be references as
p0,p1and so on. Note that non-static methods will have the Javathisinstance be assigned top0! - Each method declares how many registers it uses. There are two ways to do so -
.registers(total number of registers) and.locals(the number of non-parameter registers). - Method declaration contains complete type information (including the return type and the input types). Each type can be a primitive type or a class instance. Return type can also be
V(void). - All method invocations also contain type information about the called method and resulting type. It makes sense since in Java you can have multiple methods with the same name, differentiated by argument types.
- Stack is heavily used: arguments are pushed into the call stack, the stack is used to sometimes handle large data, exception handling uses the stack and so on.
Basic types supported are the following:
| Java type | Smali | Description | Remarks |
|---|---|---|---|
| boolean | Z | Java falseor true |
|
| char | C | Single character | |
| byte | B | Single byte | |
| short | S | Signed 16-bit integer | |
| int | I | Signed 32-bit integer | |
| long | J | Signed 64-bit integer | Uses 2 registers |
| float | F | 32-bit IEEE754 | |
| double | D | 64-bit IEEE754 | Uses 2 registers |
| Class instance | Lclass; | Class instance | Example: Ljava/lang/String; is a String type |
Let us start with a toy example - I have uploaded an toy.apk to this repository.
It has one activity (MainActivity) that will call a method called showToast, and here is the relevant source code:
private void showToast() {
Random random = new Random();
boolean randomBoolean = random.nextBoolean();
String title = getTitle(randomBoolean);
Toast toast = Toast.makeText(getApplicationContext(), title, Toast.LENGTH_SHORT);
toast.show();
}
private static String getTitle(boolean showName) {
if (showName) {
return "JBO (@yo_yo_yo_jbo)";
}
else {
return "https://jonathanbaror.com";
}
}Let's run apktool with the d flag (for decoding) and examine the output directory:
$ jbo@nix:~ apktool d ./toy.apk
I: Using Apktool 2.5.0-dirty on toy.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/jbo/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes3.dex...
I: Baksmaling classes2.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
I: Copying META-INF/services directory
$ jbo@nix:~ cd toy/
$ jbo@nix:~/toy ll
total 48
drwxrwxr-x 10 jbo jbo 4096 Sep 21 07:59 ./
drwxrwxr-x 3 jbo jbo 4096 Sep 21 07:59 ../
-rw-rw-r-- 1 jbo jbo 2618 Sep 21 07:59 AndroidManifest.xml
-rw-rw-r-- 1 jbo jbo 2765 Sep 21 07:59 apktool.yml
drwxrwxr-x 8 jbo jbo 4096 Sep 21 07:59 kotlin/
drwxrwxr-x 3 jbo jbo 4096 Sep 21 07:59 META-INF/
drwxrwxr-x 3 jbo jbo 4096 Sep 21 07:59 original/
drwxrwxr-x 144 jbo jbo 4096 Sep 21 07:59 res/
drwxrwxr-x 9 jbo jbo 4096 Sep 21 07:59 smali/
drwxrwxr-x 4 jbo jbo 4096 Sep 21 07:59 smali_classes2/
drwxrwxr-x 3 jbo jbo 4096 Sep 21 07:59 smali_classes3/
drwxrwxr-x 2 jbo jbo 4096 Sep 21 07:59 unknown/
You see here AndroidManifest.xml, and if you examine it you'll see there is one activity - com.jbo.toy.MainActivity.
Now we need to find that activity as a *.smali file - apktool might generate several SMALI class directories, so we need to find it!
$ jbo@nix:~/toy find . -name MainActivity.smali
./smali_classes3/com/jbo/toy/MainActivity.smali
At this point, we need to examine that smali file, I will be focusing on small parts of it to keep things neat.
However, this is how the file starts:
.class public Lcom/jbo/toy/MainActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MainActivity.java"
# direct methods
.method public constructor <init>()V
.locals 0
.line 14
invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V
return-void
.end methodLet's analyze line by line:
.classdescribes the class - its name iscom.jbo.toy.MainActivity(note.is replaced by/). As I mentioned, class types start with anLand end with a semicolon..superdeclares the superclass - our class derives from a base classandroidx.appcompat.app.AppCompatActivity..sourceis helpful for deubgging - the source file is calledMainActivity.java. Note this will be omitted in release builds.- We then start with our first method - the class's constructor, which is called
<init>, gets no arguments and returns void - thus declared.method public constructor <init>()V. - Our method does not use any local registers, so it says
.locals 0. As I mentioned previously,p0in non-static methods isthis, so it's well-defined. - The
.linedirective is useful for debugging, but will be omitted in release builds. - Our first instruction is
invoke-direct, which is a method call. In this case it calls another method -AppCompatActivity's constructor! Note it pushesp0as an argument, since the constructor must get athisinstance. - Lastly,
return-voidinstruction basically means we return from the method without specifying a return value, as expected.
Note the JVM will check types of everything - so, if we try returning an integer in this function the JVM will crash the App.
Let us continue and examine the getTitle method:
.method private static getTitle(Z)Ljava/lang/String;
.locals 1
.param p0, "showName" # Z
.line 38
if-eqz p0, :cond_0
.line 39
const-string v0, "JBO (@yo_yo_yo_jbo)"
return-object v0
.line 42
:cond_0
const-string v0, "https://jonathanbaror.com"
return-object v0
.end methodNotes:
- This method is
privateandstatic. It gets a boolean (Z) and returns a String, thus declared.method private static getTitle(Z)Ljava/lang/String;. - This method declares it requires one local register (
.locals 1). It will use the registerv0for that. Note JVM will crash if you try to refer to higher numbers (e.g.v1). - The
.paramdirective is useful for debugging, as it shows you parameterp0(the first argument, since this is a static method) is calledshowName. You won't find it in release builds though. - Logic starts later at a condition:
if-eqzshould be read as "if equal to zero" - in this case,p0is our boolean, so it'd be equal to 0 if it'sfalse(in JVM, zero is false and non-zero is true). Ifp0is zero we jump to a label named:cond_0. Labels start with a colon. - Assuming we did not jump, we assign register
v0a constant string (viaconst-stringinstruction). If this looks odd (how can a 32-bit register contain an entire long string?) keep in mind this is implemented by having v0 refer to a data table in the class in which that string resides. We then callreturn-object v0to exit the function and return that string. - If we do jump, we end up in
:cond_0, which assigns a different string and returns, similarly.
To get a complete understanding, let us analyze the showToast method:
.method private showToast()V
.locals 5
.line 30
new-instance v0, Ljava/util/Random;
invoke-direct {v0}, Ljava/util/Random;-><init>()V
.line 31
.local v0, "random":Ljava/util/Random;
invoke-virtual {v0}, Ljava/util/Random;->nextBoolean()Z
move-result v1
.line 32
.local v1, "randomBoolean":Z
invoke-static {v1}, Lcom/jbo/toy/MainActivity;->getTitle(Z)Ljava/lang/String;
move-result-object v2
.line 33
.local v2, "title":Ljava/lang/String;
invoke-virtual {p0}, Lcom/jbo/toy/MainActivity;->getApplicationContext()Landroid/content/Context;
move-result-object v3
const/4 v4, 0x0
invoke-static {v3, v2, v4}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v3
.line 34
.local v3, "toast":Landroid/widget/Toast;
invoke-virtual {v3}, Landroid/widget/Toast;->show()V
.line 35
return-void
.end methodLet us quickly analyze it:
- This method uses 5 local variables - registers
v0,v1,v2,v3andv4- that's significantly higher than before! - We start by creating a new instance of the Java
Randomclass and assigning it tov0. Notenew-instancesimply allocates bytes for the instance - we need to initialize it by calling its constructor withinvoke-directand callingRandom's<init>method as we saw earlier! - We then use
invoke-virtualto call a virtual function -Random's nextBoolean method. - We save its result (a boolean, as indicated by its signature
nextBoolean()Z) intov1, usingmove-result. - We then use
invoke-staticto call a static methodgetTitle, which we have seen. We pushv1as its argument and expecting get a resultingStringinstance. - We use
move-result-objectto save thatStringinstance intov2. - Use call another virtual function - this time
getApplicationContextfunction from ourMainActivity- we pushthis(i.e.p0) and expect to get aContextback. - We save the
Contextintov3. - We assign
v4the value of 0. Noteconst/4specifies the number of bytes - 4 corresponds to a 32-bit variable. - We invoke a static method -
Toast.makeTextand pushv3,v2andv4. NotemakeTextgets 3 arguments: theContext, theCharSequence(whichStringis) and an integer (0 in our case). - We save the resulting
Toastintov3. Yes - we can overridev3with a completely different type and the JVM will keep track of that change. - We call the virtual method
showon the resultingToast. - We finish it off by invoking
return-void.
Let's do something easy - let's make the functionality not so random and always show the website! We have several options:
- Patching the
showToastmethod and passingfalsealways togetTitle. - Patching
getTitleand making it return what we want, always. - Ignoring
getTitlecompletely - patchshowToastand assign the string we want to the right register before creating theToastobject.
I chose option #2 just for the sake of demonstration, but you are free to try with anything you like.
For that, we'd want to change getTitle's if-eqz p0, :cond_0 to goto :cond_0. How did I know goto is a valid instruction and its meaning?
Well, I look it up - a great reference exists here and shows all instructions (and their opcodes too!).
Sometimes I'd also like to add some sort of debug strings - for that, using the Log class really helps out!
I'll call Log.d and add a message, implementing that only in Smali!
So, I modified the if condition and added an invocation to Log.d - this is the "final" result:
.method private static getTitle(Z)Ljava/lang/String;
.locals 1
.param p0, "showName" # Z
.line 38
const-string v0, "Patched by JBO"
invoke-static {v0, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :cond_0
.line 39
const-string v0, "JBO (@yo_yo_yo_jbo)"
return-object v0
.line 42
:cond_0
const-string v0, "https://jonathanbaror.com"
return-object v0
.end methodNote how I simply ignore the resulting int (I) from Log.d and how I used v0 for the log message. I could do other things (like increasing .locals by one and using v1) but for the sake of simplicity I chose not to.
Okay, now it's time to repack the App, again by using apktool:
$ jbo@nix:~ apktool b ./toy -otoy_patched.apk
If all goes well this will repackage the App and generate a new apk called toy_patched.apk!
However, we still need to digitally sign it (as apktool does not have the private key for the original toy.apk).
To do that, we could use keytool as such:
$ jbo@nix:~ keytool -genkeypair -v -keystore my-release-key.keystore -alias myalias -keyalg RSA -keysize 2048 -validity 10000
This will generate a new RSA keypair and save it into a file my-release-key.keystore. We can then use apksigner:
$ jbo@nix:~ apksigner sign --ks my-release-key.keystore --ks-key-alias myalias ./toy_patched.apk
This was a practical guide on patching Apps, but most importantly - how to read and modify Smali code.
Note there are modern obfuscation tools (e.g. ProGuard) that make things quite challenging, but obfuscation should not stop a dedicated researcher.
I hope to give a better, concrete example that will show a modern App being patched end-to-end, in the future.
Stay tuned!
Jonathan Bar Or (https://jonathanbaror.com)