Friday Frida Hacking Without the Why

Building a small app to display random jokes with all the latest tech is fun and great practice.

It also gives you your own target to hack as a proof-of-concept for hacking Kotlin coroutines - a critical feature for IO where you can find lots of hackable information.

DISCLAIMER: This is partially setting up a pentesting environment for Android. Do not use on targets without authorization (try bugcrowd for a list of projects accepting pentesting within limits).

Overview

  1. Rooted Emulator
    1. Magisk
    2. Frida Server
  2. Hooking Treasure
    1. Network
    2. Database
    3. Sharing

Rooted Emulator

Having a rooted device or emulator image is essential to using frida effectively.

Frida’s tools communicate with a server running on a device to enable hooking into code at runtime.

The modes of operation and installation on devices are:

  1. Injected on the device filesystem (requiring rooted/jailbroken access)

  2. Embedded a gadget inside the application (repacking)

Repackaging an application and embedding a frida gadget is possible but can be more error prone. Rooting and android emulator is easy and risk free.

It’s Magisk

A common tool to root android devices is called Magisk

Magisk has a github repository for installing on an emulator specifically. The instructions are straight forward (some minor manual steps), so in sumary:

  1. backup your emulator ramdisk.img
  2. use the repo to patch the running emulator
  3. move the resulting patched ramdisk.img back to the emulator location

You can check that the device has been rooted inside the magisk app or with a root check application.

Frida Server

Frida is a suite of tools that provides an API for dynamically instrumenting code in native applications.

Installing frida on the host system is as simple as pip install frida-tools. Installing frida server on our rooted device means obtaining a binary for the platform architecture, copying and running.

Frida Loader is an android application that will manage that for you. Grab the APK from the latest release and install with adb install.

When you run the launcher it will give you an option to download, install and run the latest and greatest.

To check the server is functioning first find the device frida-ls-devices should return something like:

Id             Type    Name
-------------  ------  ---------------------
local          local   Local System
emulator-5554  usb     Android Emulator 5554
socket         remote  Local Socket

To view a list of process on the device use frida-ps -D emulator-5554 or frida-ps -U for the USB device.

Hooking the App

Now that we have a rooted device with frida installed, we can install the target the app and make sure it’s available by checking the package name:

adb shell pm list packages | grep jokeapp (package:com.github.ryjen.jokeapp)

Note that I am using a development debug build for the purpose of clarity, in reality the app would be install from an app store and may require dissassembling to find target code.

Hooking is a Frida scripting API to allow replacing code at runtime.

Our goal is to intercept and return spoofed data for:

  • the network API for a random joke
  • saving a joke to the database
  • sharing a joke

I will call these use cases the 3 parrots that will echo back a message we tell it (in an educated consensus).

Network Parrot 🦜

The following frida script intercepts the network service and returns spoofed data:

const Types = {
  Service: "com.github.ryjen.jokeapp.data.api.JokeService",
  Response: "com.github.ryjen.jokeapp.data.model.JokeResponse",
  Continuation: "kotlin.coroutines.Continuation",
};
Java.perform(function () {
  var Service = Java.use(Types.Service);
  var Response = Java.use(Types.Response);

  // Overload a private kotlin suspend function to return a hacked network response
  Service.getRandomJoke.overload(Types.Continuation).implementation = function (
    continuation
  ) {
    var actual = this.getRandomJoke(continuation);
    // suspend functions pass a flag to indicate it will not return
    if (actual.toString() == "COROUTINE_SUSPENDED") {
      return actual;
    }
    // log the actual data
    console.log(actual);
    // return the spoofed data
    return Response.$new("hackID", "This app is HACKED", 200);
  };
});

 

The basic premise of the code is:

  1. Define java types used
  2. Use the java instrumentation to perform the hooking
  3. Define the java classes in the application binary to use
  4. Overload the API method in the network service classes
  5. Log the actual value
  6. Return the spoofed Response

Kotlin coroutines internally pass an intrinsic parameter to signal the function has suspended instead of returned. In frida we need to handle this by checking for COROUTINE_SUSPENDED before performing the overload.

Running frida -U --no-pause -f com.github.ryjen.jokeapp -l network.js the result is the application parrots back the response every network call.

hacked

The logging should look something like this in the frida console after 3 refreshes:

JokeResponse(id=fii3Tv4hFd, joke=Today, my son asked "Can I have a book mark?" and I burst into tears. 11 years old and he still doesn't know my name is Brian., status=200)
JokeResponse(id=IeiyIRSnbxc, joke=I applied to be a doorman but didn't get the job due to lack of experience. That surprised me, I thought it was an entry level position., status=200)
JokeResponse(id=kOfaUvP7Muc, joke=What did the Dorito farmer say to the other Dorito farmer? Cool Ranch!, status=200)

Database Parrot 🦜

When the user adds a favorite joke, we want to hook saving to the database to insert spoofed data. The following script accomplishes the task.

const Types = {
  Joke: "com.github.ryjen.jokeapp.domain.model.Joke",
  Database: "com.github.ryjen.jokeapp.data.repository.joke.JokeRepository",
  Continuation: "kotlin.coroutines.Continuation",
};

Java.perform(function () {
  var Database = Java.use(Types.Database);
  var Joke = Java.use(Types.Joke);

  // Overload a private kotlin suspend function to return a hacked network response
  Database.addFavorite.overload(Types.Joke, Types.Continuation).implementation =
    function (joke, continuation) {
      if (continuation.toString() == "COROUTINE_SUSPENDED") {
        this.addFavorite(joke, continuation);
        return null;
      }
      var spoofed = Joke.$new("jokeID", "This joke is OWNED", null, true);
      console.log(joke);
      this.addFavorite(spoofed, continuation);
      return null; // kotlin unit
    };
});

 

The difference here is replacing the method call with a spoofed parameter instead of returning one.

Running frida -U --no-pause -f com.github.ryjen.jokeapp -l network.js and saving a joke adds the spoofed joke to the database.

database

Again, our logging should log the original data for snooping.

Joke(id=7p41Lmbpjqc, content=What has three letters and starts with gas? A Car., created=null, isFavorite=true)
Joke(id=8p49pWvcxAd, content=Every night at 11:11, I make a wish that someone will come fix my broken clock., created=null, isFavorite=true)
Joke(id=EYo4TCAdUf, content=I tried to write a chemistry joke, but could never get a reaction., created=null, isFavorite=true)

Sharing Parrot 🦜

When the user shares a joke, we want to intercept and send a hacked response. Could be useful to pass private application data to an external resource on the device.

const Types = {
  Share: "com.github.ryjen.jokeapp.ui.components.Share",
  String: "java.lang.String",
  Context: "android.content.Context",
};

Java.perform(function () {
  var Share = Java.use(Types.Share);

  // A message with a exposed data or an attack link
  var spoofed = "Please visit hacked link https://example.com";

  var shareText = Share.text.overload(Types.Context, Types.String);
  shareText.implementation = function (context, content) {
    console.log(content);
    shareText.call(this, context, spoofed);
  };
});

 

Running frida -U --no-pause -f com.github.ryjen.jokeapp -l share.js and sharing a joke results in the spoofed content shared instead. Logging would show the original joke content as expected.

sharing

EOL, Shutdown

Have shown how to run frida on a rooted emulator which is a powerful tool as an alternative to snooping encrypted networks, logging information or other active discovery of information.

Breaching confidentiality, integrity and availability (CIA) on a mobile app becomes a bit of a joke when Frida is available.

If your app requires extra security, should consider a service like NowSecure. Adding detection of rooting/jailbreak and frida server to lockdown the app would also be a very strong blue team defense.

Refs, Also