Mobile Hacking: Using Frida to Monitor Encryption
Overview
This guide demonstrates creating a Frida script to monitor encryption operations and capture encryption details using Python bindings. We’ll walk through intercepting Cipher.init() calls, handling messages between Frida and Python, and extracting key information from Android KeyStore operations.
Writing the Initial Hook
Create cipher.js to intercept calls to Cipher.init():
'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);
}
})
}Create test.py to use Frida Python bindings:
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)Execute with: python test.py sg.vp.owasp_mobile.omtg_android cipher.js
Handling Messages in Python
Modify cipher.js to use send() instead of console.log():
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);
}Update parse_hook() in test.py:
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()Add callback function:
def on_message(message, data):
try:
if message:
print(message)
except Exception as e:
print('exception: ' + e)Packing Data Efficiently
Consolidate multiple messages into a single structured payload to prevent output interleaving in multithreaded applications:
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);
}Cleaning Up Output and Adding Details
Call private methods to extract meaningful information from parameters:
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);
}Update Python message handler:
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)Casting for More Details
Add class definitions at the top of cipher.js:
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');Handle 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()});
}Handle 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()});
}This post was originally written for TrustedSec and is reproduced with permission. Complete code is available on GitHub.