Mobile Hacking: Using Frida to Monitor Encryption

This post will walk you through the creation of a Frida script that will be used to demonstrate the usage of the Frida Python bindings. The Frida script will be used to monitor encryption calls and capture details about the encryption type and keys in use. We will learn how to send messages from Frida to a Python script and output them in a customized format. We will also learn a few tricks that can be applied to our Frida scripts to gather some useful information while performing dynamic instrumentation.

Note: This post will assume you are already familiar with Frida.

Before we begin, feel free to grab the code from GitHub to follow along with the examples. We are going to be using the Android app from OWASP's MSTG-Hacking-Playground project on GitHub. A direct link to the apk can be found here. Install the apk on your Android device. You can use the following adb command to install it:

adb install app-arm-debug.apk

We are going to be testing against the KeyStore activity in the OWASP application. This activity will generate an RSA key pair inside the Android KeyStore and use the key pair to encrypt and decrypt the text that you enter.

Writing our Initial Hook

Let's write a basic Frida hook to intercept calls to Cipher.init(). We are going to hook the parameters that are passed into the function and print them on the console. Create a file named cipher.js and insert the following code:

'use strict;'

if (Java.available) {
  Java.perform(function() {

    //Cipher stuff
    const Cipher = Java.use('javax.crypto.Cipher');

    Cipher.init.overload('int', 'java.security.Key').implementation = function (opmode, key) {

      console.log('[+] Entering Cipher.init()');
      console.log('[ ] opmode: ' + opmode);
      console.log('[ ] key: ' + key.toString());
      console.log('[-] Leaving Cipher.init()');
      console.log('');

      // call original init method
      this.init.overload('int', 'java.security.Key').call(this, opmode, key);
    }

  }
)}

cipher.js

This hook will intercept calls to Cipher.init() and print the parameters passed to the function to the console. Rather than running our script using the Frida cli, we are going to use the Frida Python bindings. Create a file named test.py and insert the following code:

import frida
import os
import sys
import argparse


def parse_hook(filename):
    print('[*] Parsing hook: ' + filename)
    hook = open(filename, 'r')
    script = session.create_script(hook.read())
    script.load()


if __name__ == '__main__':
    try:
        parser = argparse.ArgumentParser()
        parser.add_argument('package', help='Spawn a new process and attach')
        parser.add_argument('script', help='Print stack trace for each hook')
        args = parser.parse_args()


        print('[*] Spawning ' + args.package)
        pid = frida.get_usb_device().spawn(args.package)
        session = frida.get_usb_device().attach(pid)
        parse_hook(args.script)
        frida.get_usb_device().resume(pid)
        print('')
        sys.stdin.read()

    except KeyboardInterrupt:
        sys.exit(0)
test.py

The script takes two (2) arguments: the name of a package to spawn on the Android device and the path to a script that will be injected into the process.

Python Script Arguments

We can get the name of the process a few ways, but the easiest would be to run frida-ps -Ua while the application is running on the Android device. We are looking for the string under the identifier column.

Output from frida-ps

Now we will run our script passing in the package name and the cipher.js script we created using the following command:

python test.py sg.vp.owasp_mobile.omtg_android cipher.js

Once the application spawns, we want to choose OMTG-DATAST-001-KEYSTORE

OWASP Main Screen

Enter some text and then click the encrypt button followed by the decrypt button. The application should have encrypted your entered text and then decrypted it successfully.

KeyStore Activity

Now if we take a look back at our Python script, you should see our log messages displayed on the console.

Python Script Output

Handling Messages in Python

OK, so now that we have a basic example working, we will make some updates. Rather than outputting messages to the console from our Frida script, we will send some messages back to our Python script and let it handle the logging.

First, modify the cipher.js script to swap out the console.log() lines. Modify the Cipher.init() hook to look like the following:

Cipher.init.overload('int', 'java.security.Key').implementation = function (opmode, key) {

  send('Entering Cipher.init()');
  send('opmode: ' + opmode);
  send('key: ' + key);
  send('Leaving Cipher.init()');
  //console.log('');

  // call original init method
  this.init.overload('int', 'java.security.Key').call(this, opmode, key);
}

Here we have replaced the console.log() for the send() function instead. This will pass the messages back to our Python script for further processing. We will modify the parse_hook method in our Python script to handle these new messages:

def parse_hook(filename):
    print('[*] Parsing hook: ' + filename)
    hook = open(filename, 'r')
    script = session.create_script(hook.read())
    script.on('message', on_message)
    script.load()

We added the line script.on('message', on_message), which sets a callback function that will handle messages we send from the Frida script. Next, we will need to create the new on_message() callback to handle the messages.

def on_message(message, data):
    try:
        if message:
            print(message)

    except Exception as e:
        print('exception: ' + e)

The function takes two (2) parameters: message and data. The message parameter is where our log messages will be. The data parameter can be ignored for now. The function simply prints the message parameter to the console. Next, we will run our updated script and see what it looks like.

Python Script Output

The message parameter is a dictionary that contains the type of message sent and the payload. The payload contains the data that we sent from our cipher.js script.

Packing up our Data

So why do we want to handle the messages in Python? Why not just sprinkle some console.log() calls throughout our Frida scripts? If the application you are instrumenting uses multiple threads, you may start to have several hooks firing at the same time. If you are using multiple console.log() statements in your hook, it is possible your output from one (1) hook may start getting mixed in with output from another hook, making the log output harder to use.

A better way would be to collect all the data we want to log, pack it all up, and then fire off one (1) send() message at the end of our hook to make sure all of our relevant data stays together. Then we can parse the message from our Python script and handle the data however we want.

Next, we will modify our cipher.js script to pack up all of our logged data and send it in one (1) message.

Cipher.init.overload('int', 'java.security.Key').implementation = function (opmode, key) {

  var args = [];
  args.push({'name': 'opmode', 'value': opmode});
  args.push({'name': 'key', 'value': key.toString()});

  var send_message = {
    'method': 'javax.crypto.Cipher.init',
    'args': args
  };

  send(send_message);

  // call original init method
  this.init.overload('int', 'java.security.Key').call(this, opmode, key);
}

The first change is that we added an array called args that will store a dictionary for each argument passed into our hooked function. The dictionary contains a key for name and value. We will use this to name each argument and store its value.

var args = [];
args.push({'name': 'opmode', 'value': opmode});
args.push({'name': 'key', 'value': key.toString()});

The next section creates a dictionary called send_message which will contain the payload that we send back to our Python script.

var send_message = {
'method': 'javax.crypto.Cipher.init',
'args': args
};

send(send_message);

The dictionary contains the method key, which is a string containing the method name that we hooked. It also contains the args key, which we will use to pass our array of arguments. Finally, we call send(send_message) to pass this back to our Python script. We should not need to make any changes to our Python script just yet. We will run the script passing our updated cipher.js file and see what the output looks like.

Python script Output

So now we only have two (2) messages that were sent to the Python script: one (1) for the encrypt call and one (1) for the decrypt call.

Cleaning up the Output and Adding More Detail

At this point, the way we are outputting the message is making it challenging to read. It would also be nice to get a little bit more detail on the parameters that we are printing out to make the output of our script more useable.

After inspecting our previous output when encrypting and decrypting, we can determine that an opmode of '1' is an encryption and an opmode of '2' is a decryption. We could update our cipher.js script to test for this and send us a text string, but we can do something better.

Looking at the source code for the Cipher class, we can see there is a private method called getOpmodeString().

private static String getOpmodeString(int opmode) {
    switch (opmode) {
        case ENCRYPT_MODE:
            return "encryption";
        case DECRYPT_MODE:
            return "decryption";
        case WRAP_MODE:
            return "key wrapping";
        case UNWRAP_MODE:
            return "key unwrapping";
        default:
            return "";
    }
}

Rather that hardcoding the same logic in our cipher.js script, we will call this private method and return the result instead of the integer value passed to Cipher.init().

var opmodeString = this.getOpmodeString(opmode);
args.push({'name': 'opmode', 'value': opmodeString});

We can also make calls to instance methods, which can be useful for getting some context around the function that was hooked. Since the Cipher class can be used for several different encryption algorithms, we will make a call to this.getAlgorithm() to determine which encryption algorithm is in use. Because this extra data is not part of the function arguments, we will also make a new array to store the 'extra details' that we gather to send back in our log message.

var details = [];
var algo = this.getAlgorithm();
details.push({'name': 'key', 'value': algo});

Next, we will change the key.toString() call to key.$className, which will give us the fully qualified class name of the object. This will be important later so that we can attempt to cast it to the correct type in order to access its methods.

The last change to the script will be adding the new details array into our send_message dictionary that we send back to the Python script.

var send_message = {
    'method': 'javax.crypto.Cipher.init',
    'args': args,
    'details': details
  };

Our updated Cipher.init() hook should now look like the following:

Cipher.init.overload('int', 'java.security.Key').implementation = function (opmode, key) {

  var args = [];
  var details = [];

  var opmodeString = this.getOpmodeString(opmode);
  var algo = this.getAlgorithm();

  args.push({'name': 'opmode', 'value': opmodeString});
  args.push({'name': 'key', 'value': key.$className});

  details.push({'name': 'key', 'value': algo});

  var send_message = {
    'method': 'javax.crypto.Cipher.init',
    'args': args,
    'details': details
  };

  send(send_message);

  // call original init method
  this.init.overload('int', 'java.security.Key').call(this, opmode, key);
}

OK, at this point, we have added a bit more detail to our cipher.js script. Now we will add some code on the Python side to perform better processing of the message format we are sending over. If you recall, our previous output looked like the below:

{'type': 'send', 'payload': {'method': 'javax.crypto.Cipher.init', 'args': [{'name': 'opmode', 'value': 1}, {'name': 'key', 'value': '[object Object]'}]}}

We have a dictionary with our message in the payload key. From there, we sent a method, args, and we just added details for our extra information. Let's modify the on_message callback in our Python script like the below example:

def on_message(message, data):
    try:
        if message:
            if message['type'] == 'send':
                payload = message['payload']
                method = payload['method']
                args = payload['args']
                details = payload['details']
                # print('[ ] {0}'.format(message['payload']))
                print('[+] Method: {0}'.format(method))

                print('[ ] Arguments:')
                for item in args:
                    print('[ ]   {0}: {1}'.format(item['name'], item['value']))

                print('[ ] Additional Details:')
                for item in details:
                    print('[ ]   {0}: {1}'.format(item['name'], item['value']))

                print('')

    except Exception as e:
        print('exception: ' + e)

This will look for any message where the type is equal to send and then extract the payload. Since our payload is also a dictionary, we will access the method, args, and details keys to print their values. Because both args and details are arrays of dictionaries, we will need to loop through each item and print the values for the name and value keys as well. Once we are done, we should have some nicely formatted output. Then we will run the updated Python script and check it out.

Python Script Output

Now our output is back to being much more readable and we have included a few extra details to make the messages more useful when hooking this function. We can now see that the application iis using RSA to encrypt and decrypt our data.

Casting for More Details

The key passed into the Cipher.init() call is a Key interface rather than a class type. Now that we know the fully qualified class name, we can add some code in our cipher.js file to cast the key to the correct type to get some additional information. The application is using an android.security.keystore.AndroidKeyStoreRSAPublicKey for the encryption and an android.security.keystore.AndroidKeyStoreRSAPrivateKey for the decryption.

We will start by adding a few new definitions to the top of our cipher.js script. These will be used later when casting the key to the correct type.

const AndroidKeyStoreKey = Java.use('android.security.keystore.AndroidKeyStoreKey');
const AndroidKeyStoreRSAPublicKey = Java.use('android.security.keystore.AndroidKeyStoreRSAPublicKey');
const AndroidKeyStoreRSAPrivateKey = Java.use('android.security.keystore.AndroidKeyStoreRSAPrivateKey');

const KeyFactory = Java.use('java.security.KeyFactory');
const KeyInfo = Java.use('android.security.keystore.KeyInfo');

Next, we will add some code to deal with the case where we have an AndroidKeyStoreRSAPublicKey.

if (key.$className === 'android.security.keystore.AndroidKeyStoreRSAPublicKey') {
    var pub_key = Java.cast(key, AndroidKeyStoreRSAPublicKey);
    var keystoreKey = Java.cast(key, AndroidKeyStoreKey);
    
    details.push({'name': 'AndroidKeyStoreKey.getAlias()', 'value': keystoreKey.getAlias()});
    details.push({'name': 'key.getPublicExponent()', 'value': pub_key.getPublicExponent().toString()});
    details.push({'name': 'key.getModulus()', 'value': pub_key.getModulus().toString()});
}

The line var pub_key = Java.cast(key, AndroidKeyStoreRSAPublicKey); takes our key interface and casts it to the AndroidKeyStoreRSAPublicKey class storing it in pub_key for later use. Since AndroidKeyStoreRSAPublicKey inherits from AndroidKeyStoreKey, we can also cast the key to an AndroidKeyStoreKey to access some methods of that class. Now we can call the methods defined on those classes to get even more details about the key. Because this is an AndroidKeyStoreKey, we will get the alias it is stored under in the KeyStore as well as the exponent and modulus.

Now we will handle the case where we have a AndroidKeyStoreRSAPrivateKey.

if (key.$className === 'android.security.keystore.AndroidKeyStoreRSAPrivateKey') {
    var priv_key = Java.cast(key, AndroidKeyStoreRSAPrivateKey);
    
    var factory = KeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore");
    var keyInfo = Java.cast(factory.getKeySpec(key, KeyInfo.class), KeyInfo);
    
    details.push({'name': 'keyInfo.getKeystoreAlias()', 'value': keyInfo.getKeystoreAlias()});
    details.push({'name': 'keyInfo.getKeySize()', 'value': keyInfo.getKeySize().toString()});
    details.push({'name': 'keyInfo.isInsideSecureHardware()', 'value': keyInfo.isInsideSecureHardware().toString()});
    
    details.push({'name': 'key.getModulus()', 'value': priv_key.getModulus().toString()});
}

In this case, we need to create a KeyInfo object from our private key to be able to get some additional details. Because the key is an AndroidKeyStoreKey, we will not be able to access the private key material since is it protected by hardware. We can confirm this by looking at the return value from keyInfo.isInsideSecureHardware(). The rest of the code makes a few calls to get some additional information about the private key that will be sent back to our Python script.

Now we will run our Python script one more time and see what the updated output looks like:

Python Script Output

We now have a ton of useful information and we can also confirm that this key is indeed protected by hardware. We now also have the alias that is used to access the key pair for encryption and decryption operations. Although we do not have the private key, we could add some code to our script to have the process encrypt or decrypt arbitrary data with the key by the alias. We may explore this in a later blog post but for now, we will leave this as an exercise for the reader.


This post originally written for TrustedSec and posted here with permission.