You can think of Frida as a proxy for code. Its Java bridge allows us to intercept object and method calls in order to modify them and therefore be able to see when they are happening, what arguments are being passed to the related object/method, and what is being returned. Additionally, it also allows us to modify the app's behavior. So as you're probably guessing by now, it is an extremely useful tool commonly used by Android reverse engineers.
In this tutorial we're going to go through basic examples that show how to do all of these things on a simplified proof of concept (POC) app that performs dynamic code loading (DCL) using encrypted strings, a common technique used by malicious apps to hide certain behaviors. You can get everything you need to follow along, such as the apk used in the examples as well as the frida scripts and entire related Android Studio project, from this repository.
Finally before starting, it is worth mentioning that basic Android development concepts (such as the Activity lifecycle) are assumed to be understood and will not be explained.
Analysing the POC
Before we can "hook" (or intercept) anything we need to see the code of the app we're trying to make sense of. For this you can use the free and open source Dex to Java decompiler called Jadx. So install it and open the poc apk with the UI version (jadx-gui
) and check out the MainActivity.
From the above code it is clear that reflection is being used to invoke the method of an encrypted class string and encrypted argument strings are being passed to it. Additionally, it is also clear that the decryption method is defined in the x0.a
class and is called a
.
Hooking the Decryption Method
In order to decrypt the strings without having to go to the decryption method and trying to understand what it is doing, Frida can be leveraged and be used to hook the specific method and read the arguments passed to it as well as its returned value. This can be achieved with the following script:
Java.perform(function() {
Java.use('x0.a').a.overload('java.lang.String').implementation = function(str) {
var result = this.a(str);
console.log('[+] Decrypted ', str, '=> ', result);
return result;
}
Java.use('com.alexisegf.badapp.MainActivity').onCreate.overload('android.os.Bundle').implementation = function(bundle) {
this.onCreate(bundle);
}
});
-
Java.perform
to execute everything inside it as soon as the java bridge is ready and the app process resumed. -
Java.use
to choose the class to target and.a.overload('java.lang.String').implementation
to override the implementation of the method called "a" that receives a string as an argument. -
var result = this.a(str)
to trigger the normal functionality of the overridden method but store the result in the local variabled called "result". -
console.log
to print values to the terminal (such as System.out.println is used in Java). -
return result
to return the expected method output (previously stored in the result variable) in order for the app to keep behaving as it normally would. -
Also hook onCreate and call super to avoid error where other hooks are not applied
Now that that's clear, save this as a .js script (I called mine frida_basics_1.js) and execute it with frida by calling frida -U -f com.alexisegf.badapp -l frida_basics_1.js --no-pause
, where:
-
-U
stands for USB device and attaches to a connected usb device (must be added even if using an emulator), -
-f
force starts the application, -
-l
loads a script from the provided path, -
ā-no-pause
automatically resumes the app process after frida successfully attaches to it.
Hooking a Native Library Method
From the results of the above script it becomes clear that our poc is using reflection to call startActivity and launch BadActivity
.
This Activity turns out to load a native library called bad-native-lib
and call its stringFromJNI()
method from the onCreate callback, although it does not do anything with it. It is also settings a TextView with a "Not ready yet" string. Maybe the returned string is meant to be used in a future version of the app? Let's hook the method to see what it returns:
Java.use('com.alexisegf.badapp.BadActivity').stringFromJNI.implementation = function() {
var result = this.stringFromJNI();
console.log('[+] stringFromJNI => ', result);
return result;
}
And the result:
Seems the native library also uses encrypted strings. Might it use the same encryption/decryption algorithm as the strings in the MainActivity?
Write a Custom Script
It might be obvious by now, but frida scripts can do much more than just intercept returned values. For a final basic example we have a script that intercepts the returned values from the decryption method and force calls it with stringFromJNI being passed as an argument:
Java.perform(function(){
console.log('\n[+] Loaded frida_basics_3.js');
Java.use('x0.a').a.overload('java.lang.String').implementation = function(str) {
var result = this.a(str);
console.log('[+] Decrypted ', str, '=> ', result);
return result;
}
Java.use('com.alexisegf.badapp.BadActivity').onCreate.overload('android.os.Bundle').implementation = function(bundle) {
this.onCreate(bundle);
console.log('[+] Decrypted ', this.stringFromJNI(), ' => ', Java.use('x0.a').a(this.stringFromJNI()))
}
});
After running this you get the flag, but I'll let you get there yourself :wink:
So that's it for now. If you want to dive deeper into Frida I recommend checking out learnfrida.info by @entdark_. And as usual, if you found this article useful make sure to follow me on twitter to not miss the next one.
Thanks for reading!