zep's blog

follow me into the rabbit hole of ITsec

Make Flarebear dance with flag using Frida (part three)


In this third and last part of this blog post, we'll finally use Frida to hook some interesting Java methods inside of the Flarebear Android application. This will allow us to dump and to set the internal state of the Flarebear application, which finally allows us to set the correct state to make Flarebear dance ;-)

Frida installation

First we need to install Frida on the running virtual Android device and also locally on the host machine. Frida's website is located at frida.re and hosts howto's and documentation. The source code of Frida is hosted at Github: github.com/frida

We'll use the python bindings of Frida on the host machine. To install these bindings, use pip3 install frida-tools. The corresponding Frida executables are now located at ~/.local/bin. To make them easily available, add this directory to $PATH:

zep@base:~/$ export PATH="~/.local/bin:$PATH"

We also need the "agent" part of Frida: A small executable running inside the actual Android device, this is called "Frida server". The various different Frida server executables are available at github.com/frida/frida/releases.

It's very important to pick the correct Frida server executable for the corresponding platform and architecture, otherwise Frida will not work. In our case we have an Android device which runs on x86_64. Therefore we'll using frida-server-12.8.20-android-x86_64

Download the corresponding file and extract it like this:

zep@base:~/$ unxz frida-server-12.8.20-android-x86_64.xz
zep@base:~/$ mv frida-server-12.8.20-android-x86_64 frida-server

Then we nee to install the extracted agent on the virtual Android device. We'll do this using adb:

zep@base:~/$ ./adb push ~/Downloads/frida-server /data/local/tmp/
zep@base:~/$ ./adb shell "chmod 755 /data/local/tmp/frida-server"

Remember adb is located in the Android SDK in the platform-tools folder. In my case the adb is located at ~/Android/Sdk/platform-tools. Finally we can start frida-server like this:

zep@base:~/$ ./adb shell "/data/local/tmp/frida-server &"

To test the connection, we can use frida-ps:

zep@base:~/$ frida-ps -U

frida-ps should now list all running processes on the virtual Android device.

If you get instead following error message while trying to start frida-server, then you have probably the wrong frida-server executable:

zep@base:~/$ ./adb shell "/data/local/tmp/frida-serverd &"
/system/bin/sh: /data/local/tmp/frida-server: No such file or directory

Frida is now installed and ready to use. But first, how does Frida actually work and why do we need an "agent" executable? The functionality of Frida is described like this in the documentation:

This functionality is provided by frida-core, which acts as a logistics
layer that packages up GumJS into a shared library that it injects into
existing software, and provides a two-way communication channel for
talking to your scripts, if needed, and later unload them. Beside this
core functionality, frida-core also lets you enumerate installed apps,
running processes, and connected devices. The connected devices are
typically iOS and Android devices where frida-server is running.
That component is essentially just a daemon that exposes frida-core
over TCP, listening on localhost:27042 by default.

There are many different modes of operation for Frida. It also possible to bundle Frida gadget directly with the target Android application.

Dumping the state of Flarebear

From part one and two we already know that three visible buttons of the Flarebear application call the three functions play, feed and clean inside of the Flarebear activity. These functions are therefore good candidates to hook, since we can easily trigger their execution. Let's take the play function and hook it and redirect execution to our own defined snipped of Javascript code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import frida
import sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

jscode = """
Java.perform(function () {

  var MainActivity = Java.use('com.fireeye.flarebear.FlareBearActivity');
  var play = MainActivity.play;

  // Function to hook is defined here
  play.implementation = function (v) {

      // Show a message to know that the function got called
      send('play fn hooked');

      // define function to call instead of original play function
      var getState = MainActivity.getState.overload('java.lang.String', 'java.lang.String');
      var getStat = MainActivity.getStat;

      // call the getState function and send back the result
      var state = getState.call(this, 'activity', '');
      send("get activity shared preferences: " + state);
  };
});
"""

process = frida.get_usb_device().attach('com.fireeye.flarebear')

script = process.create_script(jscode)
script.on('message', on_message)

print('[*] tracing flarebar...')

script.load()
sys.stdin.read()

To use the Frida Python bindings, the frida module has to be imported. At line 33, we instruct frida-server to attach to the Flarebear application and to inject a small Javascript engine (which is actually part of Frida gadget). This Javascript code communicates with frida-server and is completely scriptable. To actually hook the play function, we have to write a short snipped of Javascript code, which does all the work.

At line 10, the used Javascript code is defined. At line 13 and 14 a "reference" to the play function is defined. At line 17, the play function is actually hooked by replacing its implementation. The Javascript code defined there is executed as soon as the original play function is called. We are interested in the internal state of the Flarebear application. To get this state, we can call the getState function supplying the right parameter, which are "activity" and "" (line 27). Finally we're sending back the resulting data (line 28).

Let's try out this script and see what internal state we can get.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
zep@base:~/$ python3 play.py
[*] tracing flarebar...
[*] play fn hooked
[*] get activity shared preferences:
[*] play fn hooked
[*] get activity shared preferences:
[*] play fn hooked
[*] get activity shared preferences:
[*] play fn hooked
[*] get activity shared preferences: f
[*] play fn hooked
[*] get activity shared preferences: ff
[*] play fn hooked
[*] get activity shared preferences: fff
[*] play fn hooked
[*] get activity shared preferences: fffc
[*] play fn hooked
[*] get activity shared preferences: fffcc
[*] play fn hooked
[*] get activity shared preferences: fffccc

When pressing the play button, the internal state obviously does not change, since we hooked this function and do not call the original implementation. But when pressing the clean or the feed button, we can observe that the state changes.

  • When pressing the feed button, the char f is added to the state.
  • When pressing the clean button, the char c is added to the state.

Now remember the function getPassword which uses the function getStat to retrieve the count of the chars f, c and p from the sharedPreferences ? These counts are then used to generate a password, which is then used to decrypt the flag which is stored as an encrypted image file.

This means following: The right number of clicks on the play, feed or clean button results in the correct counts of the chars f, c and p which results in the correct password. But which combination of these chars is "correct" ?

We can search for cross-references of the getPassword function and analyze the calling function. In this case, getPassword is called by the function danceWithFlag. This function is called by the function setMood:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public final void setMood() {
        if (isHappy()) {
            ((ImageView) _$_findCachedViewById(R.id.flareBearImageView)).setTag("happy");
            if (isEcstatic()) {
                danceWithFlag();
                return;
            }
            return;
        }
        ((ImageView) _$_findCachedViewById(R.id.flareBearImageView)).setTag("sad");
    }

Analyzing the setMood function, we can deduce that Flarebear must be "happy" and "ecstatic" before "dancingWithFlag". The corresponding function looks like this:

1
2
3
4
 public final boolean isHappy() {
     double stat = (double) (((float) getStat('f')) / ((float) getStat('p')));
     return stat >= 2.0d && stat <= 2.5d;
 }

We can see that the function isHappy checks the relation between the counts of the char f and p. This relation actually models an constraint, which expresses that Flarebear must at least "eat" two times more that that it "plays".

1
2
3
4
5
6
7
8
9
 public final boolean isEcstatic() {
     int state = getState("mass", 0);
     int state2 = getState("happy", 0);
     int state3 = getState("clean", 0);
     if (state == 72 && state2 == 30 && state3 == 0) {
         return true;
     }
     return false;
 }

Also the function isEcstatic models constraints which define when Flarebear is "ecstatic" and gives the flag:

  • mass must be 72
  • happy must be 30
  • clean must be 0

Note that this function calls getState and not getStat, this are actually two different functions which sound quite similar..

1
2
3
4
5
6
7
public final String getState(@NotNull String str, @NotNull String str2) {
        Intrinsics.checkParameterIsNotNull(str, "key");
        Intrinsics.checkParameterIsNotNull(str2, "defValue");
        String string = PreferenceManager.getDefaultSharedPreferences(this).getString(str, str2);
        Intrinsics.checkExpressionValueIsNotNull(string, "sharedPref.getString(key, defValue)");
        return string;
}

The function getState actually returns the value for a given key from the sharedPreferences. We already have found out that there are four different used keys for the sharedPreferences:

  • activity, used by the saveActivity function
  • clean, used by the changeClean function
  • happy, used by the changeHappy function
  • mass, used by the changeMass function

Let's have a look at the changeHappy function:

1
2
3
4
public final void changeHappy(int i) {
    String str = "happy";
    setState(str, getState(str, 0) + i);
}

This function increases the stored integer for the key "happy" by the amount of i. But from where is this function changeHappy actually called ? A quick search reveals the three already known functions play, feed and clean. Let's have a look at the feed function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public final void feed(@NotNull View view) {
       Intrinsics.checkParameterIsNotNull(view, "view");

       saveActivity("f"); // this concatenates 'f' to the activity sharedPreferences string

       changeMass(10);    // this adds 10 to   the mass  sharedPreferences integer
       changeHappy(2);    // this adds 2  to   the happy sharedPreferences integer
       changeClean(-1);   // this adds -1 from the clen  sharedPreferences integer

       incrementPooCount();
       feedUi();
   }

We can see that a press on the feed button and the corresponding execution of the feed function calls the three functions changeMass, changeHappy and changeClean which in turn add or subtract the given value from the integers stored in the corresponding sharedPreferences values. The correct amount of clicks on the buttons changes the internal state of Flarebear to the correct state string, which consists of the chars f, c and p.

Since we can analyze the changes the corresponding functions changeClean, changeHappy and changeMass have to the state string, we can also calculate the correct state string. Once we have the correct internal state, we can set it using Frida and call the function danceWithFlag. This function will then call the other mentioned functions to decrypt and display the flag.

Calculating the state string

First lets summarize the found constraints.

Final values for Flarebear to be "ecstatic":

  • mass must be 72
  • happy must be 30
  • clean must be 0

We can influence these values by pressing the corresponding buttons in the application.

Play button

  • add -2 to mass
  • add 4 to happy
  • add -1 to clean

Clean button

  • add 0 to mass
  • add -1 to happy
  • add 6 to clean

Feed button

  • add 10 to mass
  • add 2 to happy
  • add -1 to clean

Defining a system of linear equations

To calculate a working solution of the correct buttons clicks, we can define a system of linear equations and solve this system using well known linear algebra techniques.

For the mass value we get following equation:

equation for mass constraints

For the happy value we get following equation:

equation for happy constraints

For the clean value we get following equation:

equation for clean constraints

The equations above are intentionally very verbose and can be further simplified (e.g -1x equals -x).

  • x represents the number of button clicks for the feed button
  • y represents the number of button clicks for the clean button
  • z represents the number of button clicks for the play button

These equations can be transformed to following system of equations:

not simplified system of linear equations

This can be simplified to:

system of linear equations

This system of equations can now be solved easily by hand or by using help from the numpy python library:

import numpy as np

A = np.array([[10, 0,-2], [2, -1,4], [-1,6,-1]])
B = np.array([72, 30,0])
X = np.linalg.solve(A,B)

print(X)

There is only one solution for this system:

solution of system of linear equations

To get the flag, we have to press

  • 8 times the feed button
  • 2 times the clean button
  • 4 times the play button

We could now press these buttons by hand to get the flag or we can use Frida to set the calculated internal state. This would also work if the actual number of button clicks would be much higher.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import frida
import sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)


jscode = """
Java.perform(function () {
  // Function to hook is defined here
  var MainActivity = Java.use('com.fireeye.flarebear.FlareBearActivity');

  var play = MainActivity.play;
  play.implementation = function (v) {

    // Show a message to know that the function got called
    send('play fn hooked');

    var setStateStr = MainActivity.setState.overload('java.lang.String', 'java.lang.String');
    var getStateStr = MainActivity.getState.overload('java.lang.String', 'java.lang.String');

    send("set activity shared prefs to calculated solution [8,2,4]");
    setStateStr.call(this, 'activity', 'ffffffffccpppp');

    send("get activity shared prefs: " + getStateStr.call(this, 'activity', ''));

    var getStat = MainActivity.getStat;

    send("getStat for value f: " + getStat.call(this, 'f'));
    send("getStat for value p: " + getStat.call(this, 'p'));
    send("getStat for value c: " + getStat.call(this, 'c'));

    var setState = MainActivity.setState.overload('java.lang.String', 'int');

    send("setting mass to 72");
    setState.call(this, 'mass', 72);

    send("setting happy to 30");
    setState.call(this, 'happy', 30);

    send("setting clean to 0");
    setState.call(this, 'clean', 0);

    var getState = MainActivity.getState.overload('java.lang.String', 'int');

    send("actual mass: " + getState.call(this, 'mass', 0));
    send("actual clean: " + getState.call(this, 'clean', 0));
    send("actual happy: " + getState.call(this, 'happy', 0));

    var isEcstatic = MainActivity.isEcstatic;
    send("is Ecstatic? " + isEcstatic.call(this));

    var getPassword = MainActivity.getPassword;
    send("password is: " + getPassword.call(this));

    var danceWithFlag = MainActivity.danceWithFlag;
    send("called danceWithFlag");
    danceWithFlag.call(this);

    // Call the original function
    //play.call(this, v);
  };
});
"""


process = frida.get_usb_device().attach('com.fireeye.flarebear')

script = process.create_script(jscode)
script.on('message', on_message)

print('[*] tracing flarebar...')

script.load()
sys.stdin.read()

Lets use the script above to get the flag:

zep@base:~/$ python3 flare.py
[*] tracing flarebar...
[*] play fn hooked
[*] set activity shared prefs to calculated solution [8,2,4]
[*] get activity shared prefs: ffffffffccpppp
[*] getStat for value f: 8
[*] getStat for value p: 4
[*] getStat for value c: 2
[*] setting mass to 72
[*] setting happy to 30
[*] setting clean to 0
[*] actual mass: 72
[*] actual clean: 0
[*] actual happy: 30
[*] is Ecstatic? true
[*] password is: flareflareflareflare*BEARBEARBEARBEARBEARBEARBEARBEAR+yeahyeah
[*] called danceWithFlag

This gives us following flag:

flag