A quick primer on Frida and Android Instrumentation

Hi everyone!

Here is a pretty quick blog post on some Frida/Objection things I’ve been tinkering with.

I had this Android application which had premium features and wanted to understand how that mechanism worked and if it was robust enough. Let’s see what was the journey on how I’ve bypassed it using Frida.

After disassembling the application with Jadx, I was able to perform a simple grep -r -i 'premium' . into the sources and leading me to where the Premium checks were done.

Within the code, I could see things like:

[... redacted ...]
profile.getProfileId() > 1 ? "You are premium!" : "You are simple user!"
[... redacted ...]

Meaning that any profileId greater than 1 would be considered as premium. Am I able to change that? Is that calculated? I started checking the profile object and especially the getProfileId() method which was as easy as:

public final int getProfileId() {
    return this.getProfileId;
}

I think that it was a perfect example to try out Frida and see if I could perform any kind of Android instrumentation in order to hook the getProfileId method and make it return anything > 1, like 3.

Objection

This Objection step was definitely not necessary but Objection is such a great framework and I wanted to re-use it.

First, I started using Objection which is essentialy a runtime mobile exploration toolkit, with Frida under the hood, and this, without needing any jailbreak. However, in my case, I had a spare phone with root access allowing me to interact freely with the device.

First thing first, you need to patch the APK with Objection, like this:

$ objection patchapk --source my.really.nice.apk

Then, you can install it on the phone, using:

$ adb install my.really.nice.apk

Frida

Then, install Frida on the phone. Retrieve the latest version from the Releases page: https://github.com/frida/frida/releases

In my case, it was the 15.0.13-android-arm64 version. Push it on the Android phone.

$ adb push frida-server-15.0.13-android-arm64 /data/local/tmp/

And follow the rest of the instructions here: https://frida.re/docs/android/.

Exploring the app

After installing the app and having Frida up and running, I wanted to analyse return value from the getProfileId() method.

$ objection --gadget "MyCrazyApp" explore
Using USB device `w00tw00t`
Agent injected and responds ok!

     _   _         _   _
 ___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_|  _|  _| | . |   |
|___|___| |___|___|_| |_|___|_|_|
      |___|(object)inject(ion) v1.11.0

     Runtime Mobile Exploration
        by: @leonjza from @sensepost

[tab] for command suggestions
...my.app on (google: 7.1.1) [usb] # android hooking watch class_method path.to.my.class.getProfileId --dump-return
(agent) Attempting to watch class path.to.my.class.getProfileId method getProfileId.
(agent) Hooking path.to.my.class.getProfileId()
(agent) Registering job 923234. Type: watch-method for: path.to.my.class.getProfileId
...my.app on (google: 7.1.1) [usb] # (agent) [923234] Called path.to.my.class.getProfileId()
(agent) [923234] Return Value: 1
(agent) [923234] Called path.to.my.class.getProfileId()
(agent) [923234] Return Value: 1
(agent) [923234] Called path.to.my.class.getProfileId()
(agent) [923234] Return Value: 1

As you can see, we were able to retrieve the return value which was equal to 1, refering to a default user. Now, let’s see how to use Frida to replace that return value with 3.

Frida (Kung)-fu

Objection was running, attached to an already-spawned application. When starting Frida, I just needed to attach to the Application PID that you can retrieve very easily:

$ frida-ps -U | grep -i MyCrazyApp
1337

Since I never used Frida before, let me show you how I went and why it failed. Here is my first JavaScript code:

Java.perform(function () {
    console.log("Inside java perform function");
    var my_class = Java.use("path.to.my.class");
    my_class.getProfileId.implementation = function() {
        console.log("Inside path.to.my.class.class.getProfileId()");
        return 3;
    };
});

However, when I launched this, Frida screamed:

$ frida -U -l hookProfileId.js -p 1337
     ____
    / _  |   Frida 15.0.13 - 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/
Attaching...
Inside java perform function
Error: getProfileId: cannot call instance method without an instance
    at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:961)
    at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:547)
    at <anonymous> (/hookProfileId.js:4)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:16)
    at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:238)
    at <anonymous> (frida/node_modules/frida-java-bridge/index.js:213)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:16)
    at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:232)
    at perform (frida/node_modules/frida-java-bridge/index.js:192)
    at <eval> (/hookProfileId.js:8)

…And Frida was right! I was actually trying to call an instance method, but without any instance.

Here is the final code I used which waiting for an instance and attached to it to hook and override the return value, as expected:


Java.perform(function () {
    console.log("Inside java perform function");
    Java.choose("path.to.my.class", {
        onMatch: function(instance) {
            console.log("Found instance: " + instance);
            instance.getProfileId.overload().implementation = function() {
                console.log("Inside path.to.my.class.getProfileId()");
                return 3;
            }
        },
        onComplete: function() {}
    })
});

And guess what? This resulted in my other Terminal with Objection open with this output:

[...redacted...]
(agent) [923234] Called path.to.my.class.getProfileId()
(agent) [923234] Return Value: 3
(agent) [923234] Called path.to.my.class.getProfileId()
(agent) [923234] Return Value: 3
(agent) [923234] Called path.to.my.class.getProfileId()
(agent) [923234] Return Value: 3
[...redacted...]

Yaaaaaay, I successfuly overrode the return value and this worked! :)

Resources

Here are some links I stumbled upon and which helped me in this small (but fun) journey.

Frida Releases https://github.com/frida/frida/releases
Frida docs https://frida.re/docs/android/
Frida examples https://www.fatalerrors.org/a/java-runtime-for-advanced-usage-of-frida-hook-android-app.html
Frida snippets https://erev0s.com/blog/frida-code-snippets-for-android/
Objection https://github.com/sensepost/objection
Objection Wiki https://github.com/sensepost/objection/wiki/Running-On-A-Rooted-Device