In this blog post, I describe how to use Soot to read an Android APK (without the source code), change its methods and classes(even add a new class), and write the new code into a working APK. A few notes:
One way to analyze Android apps is by running them on a device (or an emulator) and observe the logs to capture some desired information. If you have the app’s source code, you can log whatever you want, e.g., record the execution of a method at its beginning. However, in the cases that the source code is not available, such as in security analysis where you are dealing with possible malicious apps, you need to instrument the APK, which is compiled inDalvik byte code. And as you may have guessed, Soot is going to save the day.
The following figure shows an overview of how Soot reads/modifies/writes an APK. First, Soot usesDexplerto convert the Dalvik byte code to Jimple bodies. Then it runsWhole Packs that can transform, optimize, and annotate the whole program (for example, call graph generation). Next, the Jimple Transformation Packs will run on each Jimple body (this is the part that we modify the code). Finally, Soot converts all Jimple bodies to Baf (a low-level intermediate representation in Soot), and using Dexpler the whole code will be compiled into an APK. The instrumented APK can be installed on an Android device (you just need to sign it first). Now, let’s instrument some apps!
We are going to add a simple statement(System.out.println("Beginning of method: " + METHOD_NAME)
) at the beginning of each APK method using a BodyTransfomer. Before reading further, please clone theSootTutorial repository and haveAndroidLogger.java in front of you.
In order to analyze an Android APK with Soot, you need to install the Android SDK in your machine. You can either use the SootTutorial docker image (docker run -it noidsirius/soot_tutorial:lates
) or followthis link.
Soot needs a special configuration for analyzing Android apps. The following code shows the options that I used for the instrumentation. Each option accompanies with a comment that describes it.
The last part of the setup code is not setting an option but resolving the required classes for the instrumentation. Recall that we want to add a new statement at the beginning of each APK method, which its Jimple representation is:
$r1 = <java.lang.System: java.io.PrintStream out>
virtualinvoke $r1.<java.io.PrintStream: void println(java.lang.String)>("<SOOT_TUTORIAL> Beginning of method METHOD_NAME")
Since these statements require classesjava.lang.System
andjava.io.PrintStream
, we should resolve them in Soot, which is done in lines 20–21 insetupSoot
.
Now, we’re ready to write a BodyTransformer to modify the code. The following code showsmyLogger
BodyTransformer. Note that the transformer is added to thejtp
pack and it will be applied to all methods’ bodies.
First of all, we need to filter out non-APK methods (lines 5–6), because Soot loads these methods and may not be aware that they belong to Android libraries (checkisAndroidMethod
). Then, we can create the content that we want to log (line 11, the method’s signature isbody.getMethod().getSignature()
). We add atagto the content so we can retrieve these logs later. Recall that Jimple statements are three-address code; so, in order to invokeSystem.out.println
we need to first create a local variable,psLocals
and$r1
in the Jimple code, that points toSystem.out
(lines 13–16), then invoke theprintln
method using that local variable (lines 19–21).
For adding a local variable we input the body and the type of the local variable to a LocalGenerator (can be foundhere). To create a Jimple statement, we use the singletonJimple.v()
. For example, line 16 creates anAssignStmt
equivalent to$r1 = <java.lang.System: java.io.PrintStream out>
(note that you need to pass the reference of a SootField or SootMethod). Similarly, line 21 creates anInvokeStmt
that has a virtual invoke expression equivalent tovirtualinvoke $r1.<java.io.PrintStream: void println(java.lang.String)>("<SOOT_TUTORIAL> Beginning of method METHOD_NAME")
. Note that the parameter of this invocation must be aValue
; therefore, we useStringConstant
to create a String constantValue
equal to the content.
So far, we have created two Jimple statements (lines 16 and 21). Note that, the IdentitiyStmts of Jimple bodies (that determines the parameters andthis
pointer) must appear at the beginning; therefore, we need to find the first non-identity statements and insert our code before it (look at lines 17, 22, and 24). Finally, we have to validate the new modified body to make sure no problems exist (at least statically). The final step is to just run the packs (PackManager.v().runPacks()
) and write the output into an APK (PackManager.v().writeOutput()
)
That’s it! You add a Java statement at the beginning of all APK methods under 20 lines of code in Soot (actually it should be less than 10 if I didn’t expand all statements to write comments :) ). However, there is one more step before you can install the instrumented APK: you need to sign it. You can use the bash scriptsign.sh that basically first runzipalign (that aligns the APK) and then runapksigner using a keyset.
In summary, runAndroidLogger
, either in Intellij or CLI by running./gradlew run --args="AndroidLogger"
. It should createdemo/Android/Instrumented/calc.apk
and you can sign and install it by running the following commands (don’t forget to connect your device to your machine or run an Android emulator):
cd ./demo/Android
./sign.sh Instrumented/calc.apk key "android"
adb install -r -t Instrumented/calc.apk
To see the logs, runadb logcat | grep -e "<SOOT_TUTORIAL>"
and then use your device and openNumix Calculator. You should see something like this on your terminal:
07-07 11:41:26.570 32487 32487 I System.out: <SOOT_TUTORIAL>Beginning of method <com.numix.calculator.view.CalculatorDisplay: void onSizeChanged(int,int,int,int)>
07-07 11:41:26.571 32487 32487 I System.out: <SOOT_TUTORIAL>Beginning of method <com.numix.calculator.view.ScrollableDisplay: void onLayout(boolean,int,int,int,int)>
07-07 11:41:26.571 32487 32487 I System.out: <SOOT_TUTORIAL>Beginning of method <com.numix.calculator.view.ScrollableDisplay: com.numix.calculator.view.AdvancedDisplay getView()>
07-07 11:41:26.589 32487 32487 I System.out: <SOOT_TUTORIAL>Beginning of method <com.numix.calculator.view.ScrollableDisplay: void scrollTo(int,int)>
If you’re interested in some specific methods you can filter it by pipinggrep -e "METHOD_SIGNATURE"
toadb logcat
Let’s do some more existing things. Assume in the previous example, instead of just logging the name of the method, we wanted to keep track of the number of methods that have been called so far (or simply count the executed method). One way to do this is to have a static integer field and increase it by one when a method is executed. Now, I show how to create a new class, field, and method from scratch, add them to the APK, and more importantly, use them in other methods. The whole code can be found inAndroidClassInjector.java andInstrumentUtil.java and the following code shows creating a class:
The new class must be in the same package of APK in order that other methods could access it (line 3). Also, it should be public (Modifier at line 4), a subclass of Object (line 5), and a Soot Application class (line 6). At the end ofcreateCounterClass
the generated class has been created and added to theScene
. Now let’s create a static integer field for this class:
As can be seen, it’s so simple: just instantiate aSootField
, provide its name, type, and its modifiers (note that the modifiers are aggregated by or,|
binary operator). Then we add this field to the class. Now, we are going to create a method that increments this field and prints it:
It’s a little bit more complex than creating the field. For instantiating aSootMethod
you need to pass name, parameters’ types, return type, and modifier (lines 3–5). In addition to that, a method needs to have a body (line 7) that represents the functionality of the method. First, we increment thecounter
field by assigning its value to a temporary local variable (recall Jimple has three-addressed statements), add the local by one, and reassign the field to the incremented number (lines 11–15). Next, we log this number similarly to AndroidLogger; however, since we have two things to print (the string"Counter's value"
and the localcounterLocal
) we need to contact them using StringBuilder (for more info look at the code inInstrumentUtil.java).The final statement must be a return statement (line 21). Next, we validatebody
and make it the active body ofincMethod
(lines 23-24).
We’re done! A new class, field, and method have been created and added to the APK. To invoke incrementAndLog method we use a similar approach to AndroidLoggger and you can find the corresponding BodyTransformerhere. Once you runAndroidClassInjector.java, sign the instrumented app, install and open it, you should see something like this in the adb logcat:
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>: Beginning of method <com.numix.calculator.view.ScrollableDisplay: void onMeasure(int,int)>
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>:Counter's value: 3935
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>: Beginning of method <com.numix.calculator.view.ScrollableDisplay: void onMeasure(int,int)>
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>:Counter's value: 3936
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>: Beginning of method <com.numix.calculator.view.ScrollableDisplay: void onMeasure(int,int)>
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>:Counter's value: 3937
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>: Beginning of method <com.numix.calculator.view.ScrollableDisplay: void onMeasure(int,int)>
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>:Counter's value: 3938
07-07 14:33:03.177 3967 3967 I <SOOT_TUTORIAL>: Beginning of method <com.numix.calculator.view.ScrollableDisplay: void onMeasure(int,int)>
In this blog post, we reviewed how Soot can reads/modifies/writes an Android APK and get familiar with Soot packs, in particular, Jimple Transformer. Feel free and play with the code, especiallyAndroidClassInjector.java, to create more interesting applications. For example, you can log the running thread or count the execution of each method separately.
If you’re interested in this work and you want to use it in a real project, I suggest you take a look atAndroid Soot Instrumentor repository which is designed for CLI and is more configurable. I created this repository based on my experience in Software Engineering research projects that I was involved and it would be great if you can collaborate to make it more useful.
Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +772K followers.
I’m a Ph.D. student in Software Engineering at UCI. I like to automate things and play music.