When analyzing an Android application, we often end up playing with the Smali intermediate representation... Way more human readable than the binary DEX code itself, but still not that user friendly. This blog post gives some guidelines on how to read Smali, and start writing you own Smali code!
Most of the time, we prefer to read Java code, but when analyzing an Android application, we often need to read or write in Smali. After reading this blogpost, you will be able to understand and speak Parseltongue... aka Smali. In addition, you should be able to repackage an APK after having altered the content without errors.
What is Smali
Android applications run inside the Dalvik Virtual machine, and that binary needs to read DEX (Dalvik EXecutable) format in order to execute the application.
In this article, I will only focus on Smali, the intermediate representation (IR) for DEX files.
The syntax for Smali is loosely based on Jasmin's/dedexer's syntax. As this is just a quick introduction, for a more detailed guide of the full language, readers can follow the white rabbit here: 🐇 [JASMIN]
To contextualize Smali within the production pipeline of an APK, the following graphic shows a global represention of how an APK is produced:
However, if we add the intermediate representation on the same figure we get:
Why write in Smali
The first question we can ask ourselves is: why would we want to program in Smali, a difficult language where every mistake is an unforgivable curse?
Actually, several motivations can be the answers to this question, and I list some of those below.
Sometimes, it is necessary to modify the code of an application even when we do not have the source code, in order to:
- improve its features;
- falsify its behaviors;
- inject code to better understand how it works.
Indeed, when an application has anti-debug features such as:
- the verification of the debug flag: getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE;
- the parameters inside the Manifest to avoid extraction of native library [NATIVE_EXTRACTION] (this parameters can be automatically patched by some tools like [APKPATCHER]);
- a routine to check if extra libs have been added;
- etc.
As such, it is sometimes easier to edit the application's code to, for example, log the values of a variable.
For instance, here is a code snippet I used to log a simple String:
# Use an unused register, v8 and v9 # the trick here is to expand the number size of register with `locals X` const-string v8, "MY_TAG" invoke-static {v8, v9}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
Another classical way to use Smali is to remove x509 certificate pinning from the application to be able to carry a Man In The Middle (MITM) [SSLPINNING]. Or, we can add a code to load a library such as [FRIDA] near the entrypoint of the application, to be able to instrument the application as the [APKPATCHER] tool does.
A more specific experience where I needed to modify the Smali code of an application was during an audit of IoT devices, which ran on an Android system. I found a vulnerability that let me have a restricted access on the bootloader of the device.
Using this, I was able to alter some applications that got privileged access on the system. Then, I used this vulnerability to patch one of the applications and insert a backdoor to gain full access after a reboot of the device.
In other cases, when you program in Smali, you probably don't follow the same code generation patterns that a classical compiler would use. These patterns are usually used by Java decompilers such as [CFR], [PROCYON], [FERNFLOWER], [JADX], or [JDCORE] to rebuild the DEX bytecode into Java code. It is common to simply see some decompiled methods vanish when writing Smali code. When we know these rules, it is easier to eliminate methods we do not want to see on the decompiled code. Note that this "feature" could be used to obfuscate the application, but maybe that will be the subject of another blogpost...
Write directly in Smali
When you want to modify the Smali code of an APK, you should follow certain rules to avoid getting incomprehensible issues that will make you tear your hair out.
When you compile a Java program, you give the compiler the architecture of your code:
java/ └── exploit └── intent └── exploit.java
This architecture is kept in Java compiled code and is given to the DEX compiler:
class/ └── exploit └── intent └── exploit.class
Smali keeps the same architecture as Java with class files:
smali/ └── exploit └── intent └── exploit.smali
But when you decompile a more recent APK, you will find multiples folders such as: smali, smali\_classes2, ..., smali\_classesXX. These folders have the same architecture as the Java class folder, but they seem to be incomplete. In fact, DEX files cannot contain a number of methods exceeding 64K (as in Kilo), in other words, no more than 65,536 methods. Since Android 5.0, it is possible to configure your APK to have more than one DEX file with the library multidex, and if your Android SDK level is greater than 21, multidex is natively integrated.
Another limitation that can come up when playing with DEX files is the size of the classes: the size of classesN-1.dex should always be larger than classesN.dex. Otherwise, the application may not respond. In fact, you can launch the application, but in some cases it will crash and in some others it will not respond and no interaction will be possible.
However, if your DEX file has more than 64K methods, you could move the excess number of methods to another DEX. Note that all the classes and nested classes which contain those excess methods should be moved too.
If you want more informations about [MULTIDEX]
Let's take an example
First of all, if you want to reproduce what follows, verify that you use the appropriate version of Java.
apktool b [redacted]/ -o output.apk I: Using Apktool 2.5.0-dirty I: Checking whether sources has changed... I: Smaling smali folder into classes.dex... Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer; at org.jf.dexlib2.writer.DexWriter.writeAnnotationDirectories(DexWriter.java:919) at org.jf.dexlib2.writer.DexWriter.writeTo(DexWriter.java:344) at org.jf.dexlib2.writer.DexWriter.writeTo(DexWriter.java:300) at brut.androlib.src.SmaliBuilder.build(SmaliBuilder.java:61) at brut.androlib.src.SmaliBuilder.build(SmaliBuilder.java:36) at brut.androlib.Androlib.buildSourcesSmali(Androlib.java:420) at brut.androlib.Androlib.buildSources(Androlib.java:351) at brut.androlib.Androlib.build(Androlib.java:303) at brut.androlib.Androlib.build(Androlib.java:270) at brut.apktool.Main.cmdBuild(Main.java:259) at brut.apktool.Main.main(Main.java:85)
If you have this error, then you probably do not have the correct version of Java for your Apktool. In the following example, I use openjdk-17.
First, I decompile the application with Apktool:
apktool d ~/git/asthook/misc/[redacted].apk I: Using Apktool 2.5.0-dirty on [redacted].apk I: Loading resource table... I: Decoding AndroidManifest.xml with resources... I: Loading resource table from file: /home/madsquirrel/.local/share/apktool/framework/1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Baksmaling classes2.dex... I: Baksmaling classes3.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files... I: Copying META-INF/services directory
Then, I get a folder [redacted] with 3 subfolders which contain Smali code smali, smali\_classes2, smali\_classes3.
If I rebuild it directly, there is no error:
apktool b -f -o output.apk [redacted]/ I: Using Apktool 2.5.0-dirty I: Smaling smali folder into classes.dex... I: Smaling smali_classes3 folder into classes3.dex... I: Smaling smali_classes2 folder into classes2.dex... I: Copying raw resources... I: Copying libs... (/lib) I: Copying libs... (/kotlin) I: Copying libs... (/META-INF/services) I: Building apk file... I: Copying unknown files/dir... I: Built apk...
At this point, if I modify a Smali file in the first directory [redacted]/smali/com/[redacted]/[redacted]/appUpgrade/AppUpgrade.smali and add a simple method toto:
.method public static toto()V locals 0 return-void .end method
And try to recompile:
apktool b -f -o output.apk [redacted]/ I: Using Apktool 2.5.0-dirty I: Smaling smali folder into classes.dex... Exception in thread "main" org.jf.util.ExceptionWithContext: Exception occurred while writing code_item for method Landroidx/collection/LongSparseArray;->clone()Landroidx/collection/LongSparseArray; at org.jf.dexlib2.writer.DexWriter.writeDebugAndCodeItems(DexWriter.java:1046) at org.jf.dexlib2.writer.DexWriter.writeTo(DexWriter.java:345) at org.jf.dexlib2.writer.DexWriter.writeTo(DexWriter.java:300) at brut.androlib.src.SmaliBuilder.build(SmaliBuilder.java:61) at brut.androlib.src.SmaliBuilder.build(SmaliBuilder.java:36) at brut.androlib.Androlib.buildSourcesSmali(Androlib.java:420) at brut.androlib.Androlib.buildSources(Androlib.java:351) at brut.androlib.Androlib.build(Androlib.java:303) at brut.androlib.Androlib.build(Androlib.java:270) at brut.apktool.Main.cmdBuild(Main.java:259) at brut.apktool.Main.main(Main.java:85) Caused by: org.jf.util.ExceptionWithContext: Error while writing instruction at code offset 0x12 at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:1319) at org.jf.dexlib2.writer.DexWriter.writeDebugAndCodeItems(DexWriter.java:1042) ... 10 more Caused by: org.jf.util.ExceptionWithContext: Unsigned short value out of range: 65536 at org.jf.dexlib2.writer.DexDataWriter.writeUshort(DexDataWriter.java:116) at org.jf.dexlib2.writer.InstructionWriter.write(InstructionWriter.java:356) at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:1279) ... 11 more
The error occurs randomly in the method Landroidx/collection/LongSparseArray;->clone()Landroidx/collection/LongSparseArray;, but we can clearly understand what happens when we see this error "Caused by: org.jf.util.ExceptionWithContext: Unsigned short value out of range: 65536". Indeed, 65536 is the limit for the allowed number of methods.
Warning: if during the recompilation step you did not get the above-mentioned error, this probably means that the number of methods did not go over the authorized limit.
Repackage the APK
We still need to sign our APK to finalize the repackaging. We can use a quick'n'dirty script to do this, as follows:
#!/bin/bash folder="$1" app="$2" # genkey keytool -genkey -keyalg RSA -keysize 2048 -validity 700 -noprompt -alias apkpatcheralias1 -dname "CN=apk.patcher.com, OU=ID, O=APK, L=Patcher, S=Patch, C=BR" -keystore apkpatcherkeystore -storepass password -keypass password 2> /dev/null # repackage apk apktool b -f -o "$app" "$folder" # sign apk jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore apkpatcherkeystore -storepass password "$app" apkpatcheralias1 >/dev/null 2>&1 # zipalign zipalign -c 4 "$app"
Now, you have all the clues to easily repackage an APK. However, be careful if you repackage an APK: I invite you to remove the build/ folder after each rebuild since some modifications may not be correctly set, thus, at runtime, the application may not open the DEX, as illustrated in the following example:
10-27 05:04:36.359 11241 11241 E AndroidRuntime: FATAL EXCEPTION: main 10-27 05:04:36.359 11241 11241 E AndroidRuntime: Process: com.example.myapplication, PID: 11241 10-27 05:04:36.359 11241 11241 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.myapplication/com.example.myapplication.MainActivity}: java.lang.ClassNotFoundException: Didn't find class "com.example.myapplication.MainActivity" on path: DexPathList[[zip file "/data/app/com.example.myapplication-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.myapplication-1/lib/x86_64, /system/lib64, /vendor/lib64]] 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2567) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.-wrap12(ActivityThread.java) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:102) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.os.Looper.loop(Looper.java:154) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:6119) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "com.example.myapplication.MainActivity" on path: DexPathList[[zip file "/data/app/com.example.myapplication-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.myapplication-1/lib/x86_64, /system/lib64, /vendor/lib64]] 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at java.lang.ClassLoader.loadClass(ClassLoader.java:380) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at java.lang.ClassLoader.loadClass(ClassLoader.java:312) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.Instrumentation.newActivity(Instrumentation.java:1078) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2557) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: ... 9 more 10-27 05:04:36.359 11241 11241 E AndroidRuntime: Suppressed: java.io.IOException: Failed to open dex files from /data/app/com.example.myapplication-1/base.apk because: Failed to open dex file '/data/app/com.example.myapplication-1/base.apk' from memory: Unrecognized magic number in /data/app/com.example.myapplication-1/base.apk: 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexFile.openDexFileNative(Native Method) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexFile.openDexFile(DexFile.java:367) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexFile.<init>(DexFile.java:112) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexFile.<init>(DexFile.java:77) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexPathList.loadDexFile(DexPathList.java:359) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexPathList.makeElements(DexPathList.java:323) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexPathList.makeDexElements(DexPathList.java:263) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.DexPathList.<init>(DexPathList.java:126) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:48) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at dalvik.system.PathClassLoader.<init>(PathClassLoader.java:64) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at com.android.internal.os.PathClassLoaderFactory.createClassLoader(PathClassLoaderFactory.java:43) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ApplicationLoaders.getClassLoader(ApplicationLoaders.java:58) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:520) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.LoadedApk.getClassLoader(LoadedApk.java:553) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.getTopLevelResources(ActivityThread.java:1866) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.LoadedApk.getResources(LoadedApk.java:766) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ContextImpl.<init>(ContextImpl.java:2038) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ContextImpl.createAppContext(ContextImpl.java:1983) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5294) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread.-wrap2(ActivityThread.java) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545) 10-27 05:04:36.359 11241 11241 E AndroidRuntime: ... 6 more
Let me introduce you to Smali programming
This tutorial is not exhaustive, but will help you with where to begin.
To start, Smali files follow a nomenclature.
Naming nomenclature of file
A file is named with Class Name, as in Java, but the code of the Class is not always located in a single file.
For each nested Class (or other artefacts such as Thread, Anonymous, etc.), the code of these artefacts is on another file with the nomenclature <class_name>$<nested_class>.smali. For all Thread objects, they are called like this: <class_name>$N.smali where N is an integer number (greater than 0).
Each Smali file begins with the definition of the Class, as follow:
.class <public|private|synthetic> <static?> L<path>/<class_name>; # class name could be: "Test" for the file Test.smali # If Test have a nested class "nestedTest" the name could be "Test$nestedTest" .super L<parent_class>; # As in Java, the mother of all classes is java/lang/Object;
Then, to introduce what follows, we talk about types as defined in Smali.
Types
Native types are the following:
- V void, which can only be used for return value types
- Z boolean
- B byte
- S short
- C char
- I int
- J long (64 bit)
- F float
- D double (64 bit)
For reference types (classes and arrays), we have:
- L<object> which is an Object type used as follow Lpackage/ObjectName; is equivalent to package.ObjectName; in Java;
- \[<type> which is an simple Array for an integer one-dimensional array we should have \[I equivalent to Java int\[\];
- more complexe case where type can be concatenate as \[\[I which is equivalent to int\[\]\[\] in Java;
So now if you want to define some fields in the class:
.field <public|private> <field_name>:<type>
Examples:
# define a public field named defaultLeftPad, which is an Integer .field public defaultLeftPad:I # define a public field named defaultLongOptPrefix, which is an array of String .field public defaultLongOptPrefix:Ljava/lang/String;
Now to define a method:
# definition of a method: # the method can be private / protected / public # Can be call static or need to be instanciate, if the method is static the class should be static .method <public|private> <static?> <function_name>(<type>)<return_type> # .locals will define the number of registers use # .locals 3 allow you to use v0, v1 and v2 .locals <X> # In function of return type the method could be end: # If the return type is V (void) return-void # If the return type is natives types: return <register> # Where register contain the correct type of return # If the return type is an object return-object <register> # Finally a method always finish with: .end method
Full example of patching an APK
Now, let me show you a full example of Smali modification.
Here, we want to inject Frida to use it on a non-rooted phone. Frida provided a shared library (frida-gadget) meant to be loaded by programs to be instrumented.
To instrument the application as soon as the it starts, I first look for the entrypoint of this application, i.e., the launchable activity.
To find it, I enumerate the list of labels declared on the appplication and filter it to get the launchable activity.
$ aapt dump badging [redacted].apk | grep launchable-activity launchable-activity: name='com.[redacted].[redacted].SplashActivity' label='' icon=''
I find the class in the Smali code ~/.asthook/[redacted].apk/decompiled_app/apktools/smali/com/[redacted]/[redacted]/SplashActivity.smali. Inside this class, I look for the entrypoint function <clinit> as defined in documentation [JVM]. In this class, this function is implicitly created by the JVM, so I will create this function explicitly.
When this function is present in the code, you should add the following portion of code at the beginning of the function without the preamble and postamble.
# define a static constructor of the class SplashActivity called clinit .method static constructor <clinit>()V # define 1 local variable .locals 1 # define to the JVM that function begin here .prologue # store the string "frida-gadget" to register v0 const-string v0, "frida-gadget" # call the method System.loadLibrary(v0) invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method
After the declarion of # direct methods I put this portion of code: it defines a static constructor called <clinit> and returns a Void.
- I defined only one register, so it will be v0;
- I set .prologue to indicate the beginning of the code of the application (it is only needed for this special method);
- I put the String "frida-gadget" in the register v0;
- I call the method System.loadLibrary(v0);
- I return Void.
Finally, we need to add the library libfrida-gadget.so inside ~/.asthook/[redacted].apk/decompiled_app/apktools/lib/<arch>/ and rebuild, as explain here: repackage
Conclusion
Now you should be able to become a perfect Smali Parselmouth! To analyse Smali in more depth, I encourage you to decompile code extracts that you would have written in Java. You will learn a lot about building Smali code from Java code.
Finally I would like to give a big thank you for all the quarkslab team who took the time to review this article.