What is Dynamic Instrumentation?#
The more straight forward approach of understanding these android applications is by decompiling them and examining the code - after all the apk
is just an archive. The issue is that some of these applications can be too large to understand, obfuscated or packed, or full of useless logic.
Dynamic Instrumentation is the process of analyzing the behavior of an application at runtime through the injection of instrumentation code. There are two main types of instrumentation - those being embedded and injection. Embedded instrumentation can be done by patching the application with our own instrumentation agent, usually with a shared library or something similar. Injection is when we spawn the instrumentation agent as another process on the system that will inject into the target runtime environment.
The main reason you’d want to choose embedded over injection is that embedding can still be done in a jailed environment while injection needs higher privileges.
This post explains in further detail and shows an example. There is a ton of functionality we can use in frida
which I want to touch on but we won’t be able to comprehensively cover it.
Using frida
#
Frida is a dynamic instrumentation toolkit that we will use to look at processes, hook methods, run scripts, and more that I don’t yet know of. We will also be using objection to perform some of these actions and make our life a bit easier.
Let’s say that we have an app that we want to observe and interact with - I will be using the FridaTarget.apk
from Hextree.io’s android course.
We can first patch the APK using objection
to include frida
:
╰─ objection patchapk -s FridaTarget.apk
No architecture specified. Determining it using `adb`...
Detected target device architecture as: x86_64
---SNIP---
Copying final apk from /tmp/tmpfl41j233.apktemp.aligned.objection.apk to FridaTarget.objection.apk in current directory...
Cleaning up temp files...
╰─ ls -lah
total 38M
drwxr-xr-x. 1 gabe gabe 80 Oct 26 21:50 .
drwxr-xr-x. 1 gabe gabe 86 Oct 26 16:18 ..
-rw-r--r--. 1 gabe gabe 15M Oct 26 21:36 FridaTarget.apk
-rw-r--r--. 1 gabe gabe 24M Oct 26 21:50 FridaTarget.objection.apk
Then we can install this patched version of the app:
╰─ adb install FridaTarget.objection.apk
Performing Streamed Install
Success
Then, once we have the app running we can connect to the process with frida
:
╰─ frida -U FridaTarget
____
/ _ | Frida 16.5.6 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
[Android Emulator 5554::FridaTarget ]->
That’s great and all, but what can we actually use this for?
Frida Scripts#
Continuing with the example, we can look for some sensitive functionality within the application. Maybe a sensitive method or something similar - in our case the MainActivity
doesn’t do anything interesting but this ExampleClass
does:
We can write some JavaScript in a file (script.js
) to call this function:
Java.perform(() => {
let ExampleClass = Java.use("io.hextree.fridatarget.ExampleClass");
let ExampleInstance = ExampleClass.$new();
console.log(ExampleInstance.returnDecryptedString());
})
Then we use -l
on frida
to load the script:
╰─ frida -U -l script.js FridaTarget
____
/ _ | Frida 16.5.6 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
Attaching...
[Android Emulator 5554::FridaTarget ]->
I am very securely encrypted!
I am very securely encrypted!
It goes without saying that this is easier that reverse-engineering the encryption process for the string. We can modify the script.js
to get the next one:
Java.perform(() => {
let ExampleClass = Java.use("io.hextree.fridatarget.ExampleClass");
let ExampleInstance = ExampleClass.$new();
console.log(ExampleInstance.returnDecryptedString());
console.log(ExampleInstance.returnDecryptedStringIfPasswordCorrect("VerySecret"));
})
Then we see the output:
I am very securely encrypted!
Luckily I was password protected!
I am very securely encrypted!
Luckily I was password protected!
Pretty straight forward - we can do the same with the FlagClass
:
Modifying our code:
Java.perform(() => {
let ExampleClass = Java.use("io.hextree.fridatarget.FlagClass");
let ExampleInstance = ExampleClass.$new();
console.log(ExampleInstance.flagFromStaticMethod());
console.log(ExampleInstance.flagFromInstanceMethod());
console.log(ExampleInstance.flagIfYouCallMeWithSesame("sesame"));
})
Tracing Activities and Fragments#
Pretty simple so far right? We can also make a script to trace an activity like this:
Java.perform(() => {
let ActivityClass = Java.use("android.app.Activity");
ActivityClass.onResume.implementation = function() {
console.log("Activity resumed:", this.getClass().getName());
// Call original onResume method
this.onResume();
}
})
You’ll notice that this works in logging the main activity when we launch and resume the app - but it doesn’t work for those smaller activities within the app. That is because those smaller buttons are implemented using Fragments and we need to call those separately. We can modify our script:
Java.perform(() => {
let ActivityClass = Java.use("android.app.Activity");
ActivityClass.onResume.implementation = function() {
console.log("Activity resumed:", this.getClass().getName());
// Call original onResume method
this.onResume();
}
let FragmentClass = Java.use("androidx.fragment.app.Fragment");
FragmentClass.onResume.implementation = function() {
console.log("Fragment resumed:", this.getClass().getName());
// Call original onResume method
this.onResume();
}
})
Now when we select those navigation links we see the corresponding fragments get logged:
We can also use frida-trace
to trace function calls in the application. For this to work we need to instrument each function - by telling frida
which functions and classes we are interested in. For example if we wanted to track each button click we might use this syntax:
io.hextree.Class!button_click_handler
We can use wildcards though to track them more broadly:
io.hextree.*!*
This will match all classes and methods in the io.hextree
space and when we run the following command:
frida-trace -U -j 'io.hextree.*!*' FridaTarget
We will see certain actions getting traced:
We can also trace into specific native objects:
frida-trace -U -I 'libhextree.so' -j 'io.hextree.*!*' FridaTarget
Intercepting Function Calls#
We can use frida
to replace arguments given to a function or replace certain return values. This can be useful when trying to bypass things like license checks or accessing hidden parts of an application. Let’s try and bypass the ones built into the application:
We can again make a script to interact with these, starting with the function that seems to just take an input and capitalize it:
Java.perform(() => {
var InterceptionFragment = Java.use("io.hextree.fridatarget.ui.InterceptionFragment");
InterceptionFragment.function_to_intercept.implementation = function(argument) {
this.function_to_intercept(argument);
return "Gabe Rocks";
// or use the argument itself instead of the return value
//console.log("Argument:",argument);
//argument = "Test Argument";
//return this.function_to_intercept(argument);
}
})
One of the other exercises here we can practice with is the dice game where in order to win we need to roll so all the dice match six (technically 5
bc counting from zero). We can first trace the call:
╰─ frida-trace -U -j 'io.hextree.*!*' FridaTarget
Instrumenting...
---SNIP---
Started tracing 57 functions. Web UI available at http://localhost:33293/
/* TID 0xf4c */
1473 ms DiceGameFragment$1.onClick("<instance: android.view.View, $className: com.google.android.material.button.MaterialButton>")
1473 ms | DiceGameFragment.rollDice()
1473 ms | | DiceGameFragment.randomDice()
1473 ms | | <= 4
1473 ms | | DiceGameFragment.randomDice()
1473 ms | | <= 0
1473 ms | | DiceGameFragment.randomDice()
1473 ms | | <= 1
1473 ms | | DiceGameFragment.randomDice()
1473 ms | | <= 3
1473 ms | | DiceGameFragment.randomDice()
1473 ms | | <= 4
So we can just always return the value 5
so that we win the game:
Java.perform(() => {
var DiceGameFragment = Java.use("io.hextree.fridatarget.ui.DiceGameFragment");
DiceGameFragment.randomDice.implementation = function() {
this.randomDice();
return 5; // Always return 5 to guarantee a win
};
});
Then run it and get the flag:
HTB Supermarket#
This is a medium-difficulty challenge where we download and install the APK and then try and get the flag. We are tasked with finding a discount code specifically.
We can patch the APK with objection
:
╰─ objection patchapk -s supermarket.apk
Then once we’ve installed it we can investigate how it runs:
╰─ frida-ps -Uia
PID Name Identifier
---- -------------- --------------------------
2947 Calendar com.android.calendar
1584 Files com.android.documentsui
1900 Search com.android.quicksearchbox
4504 Supermarket com.example.supermarket
---SNIP---
Let’s trace it:
╰─ frida-trace -U -j 'com.example.supermarket.*!*' Supermarket
--SNIP---
Started tracing 21 functions. Web UI available at http://localhost:43613/
/* TID 0x1503 */
9734 ms MainActivity$b.beforeTextChanged("<instance: java.lang.CharSequence, $className: androidx.emoji2.text.n>", 0, 7, 8)
9734 ms MainActivity$b.onTextChanged("<instance: java.lang.CharSequence, $className: androidx.emoji2.text.n>", 0, 7, 8)
9734 ms | MainActivity.stringFromJNI()
9734 ms | <= "FqVu3UluTNtSELauTRPFvq9wBdfXmbzbOgq4NS/KasE="
9734 ms | MainActivity.stringFromJNI2()
9734 ms | <= "2mubW7SBIsaFkTXE"
9734 ms | MainActivity.stringFromJNI3()
9734 ms | <= "AES"
9734 ms | MainActivity.stringFromJNI3()
9734 ms | <= "AES"
9734 ms | MainActivity.s()
9736 ms | | MainActivity$c.$init("<instance: com.example.supermarket.MainActivity>", "<instance: android.content.Context, $className: com.example.supermarket.MainActivity>", 17367043, "<instance: java.util.List, $className: java.util.ArrayList>")
Strangely it seems to leak something kinda important - let’s look at the code. We see the JNIs at the end of the MainActivity
:
Looking further in the code we see how it is used:
Without even needing to really understand the application’s way of using these we already know that the JNI2
variable has getBytes()
run against it and that AES
is being used somewhere. We also know that the first JNI
is being base64 encoded and decoded.
We can just toss it all into CyberChef until it makes sense
First we convert the key into bytes:
Then we can just base64 decode the JNI
input and mess with AES decrypt until we get the flag:
This isn’t really a great example of all that we learned but still worth the practice.