Automated Frida hook generation with JEB

Introduction

Certain mobile app pentests are done on a recurrent basis (Agile security). Some of these pentests have common repeating tasks. Since repetition is boring, we want to automate as much as possible. In this article, I want to demonstrate how to automatically generate Frida hooks using JEB. The demo use case consists of generating a Frida hook to bypass the TLS pinning for OkHttp. Sometimes the library is obfuscated in the target API, making hooks similar to the following unusable:

var CertificatePinner = Java.use('okhttp3.CertificatePinner');

When the target APK does not obfuscate strings, it is possible to search for known strings in JEB to find the target class quickly. For OkHttp, a good magic string candidate is "Certificate pinning failure!":

JEB script basic anatomy

It is possible to extend and/or leverage JEB's functionalities using JEB scripts and plugins. The documentation suggests scripts for automating simple tasks. The scripts are written in Python and saved in the jeb_dir/scripts/ folder. Jython is used to bridge the gap between Python and Java (JEB is a Java based app). The structure of a JEB script looks as follow:

# -*- coding: utf-8 -*- 
from com.pnfsoftware.jeb.client.api import IScript, IGraphicalClientContext 
from com.pnfsoftware.jeb.core import Artifact 

from java.io import File 

class GenerateFridaHooks(IScript): 
   def run(self, ctx): 
      print(u"?? JEB scripting")

The first line is used to set the encoding. When using UTF-8 strings, make sure to append the u prefix to your strings. Next are the imports. Notice how you can use Java imports. Finally, there's your class definition which extends IScript. This class has a method run which is obviously run everytime the script is invoked.

JEB script CLI vs GUI

JEB scripts can be invoked from both the desktop client or via the command line. If the script is saved in jeb_dir/scripts/ folder, then it is possible to invoke the script by navigating to File > Scripts > Registered > script name. From the command line, navigate to the JEB folder, depending on the operating system you're using, you'll need to use the appropriate bash file:

  • On Windows: jeb_wincon.bat
  • On Linux: jeb_linux.sh
  • On Mac OSX: jeb_macos.sh

To invoke the custom plugin, use:

./jeb_macos.sh -c --srv2 --script=GenerateFridaHooks.py -- "/path/to/target.apk"

Everything after -- are arguments passed to the script which can be retrieved from the context variable ctx.getArguments().

JEB has the concept of Project(s) which contains Artifact(s). When an APK file is opened in the JEB desktop client, a project is created. From the command line, a project needs to be created manually. To support both CLI and GUI, we can check the instance of the context variable:

def run(self, ctx): 
   # Hello world 
   print(u"?? JEB scripting") 

   # If the script is run in JEB GUI 
   if isinstance(ctx, IGraphicalClientContext): 
      project = ctx.getMainProject() 
   else: # assume command line & create a tmp project 
      argv = ctx.getArguments() 
      if len(argv) < 1: 
         print('[-] Did you forget to provide the APK file?') 
         return 
      self.inputApk = argv[0] 

      # Init engine 
      engctx = ctx.getEnginesContext() 
      if not engctx: 
         print('[-] Back-end engines not initialized') 
         return 

      # Create a project 
      project = engctx.loadProject('JebFridaHookProject') 
      if not project: 
         print('[-] Failed to open a new project') 
         return 
       
      # Add artifact to project 
      artifact = Artifact('JebFridaHookArtifact', FileInput(File(self.inputApk))) 
      project.processArtifact(artifact)

Processing DEX with the JEB API

A JEB project can contain several different type of files (units). Since we're only interested in DEX units, it is possible to search for them specifically:

# loop through all dex files in project & search 
for dex in project.findUnits(IDexUnit): 
   pass

To find the specific class and method of interest, I've opted for a naïve signature based algorithm:

  1. Search for the unique magic string such as "Certificate pinning failure!" in OkHttp's case;
  2. Get the class where the string resides and extract the class path;
  3. Loop through each method of the above class, and check if the parameters matches our signature;
  4. Optionally check the return value.

In the case of OkHttp, finding and hooking findMatchingPins(String hostname) could be done by simply iterating through the target class and checking if the parameter is a single String. We can do this in a modular way:


def do_search(self, dex_unit, needle, params, retval = None): 
   results = [] 
   # find string in DEX 
   dex_index = dex_unit.findStringIndex(needle) 
   # cross reference string, most probably used by the same class 
   for ref in dex_unit.getCrossReferences(DexPoolType.STRING, dex_index): 
      # get class name 
      # getInternalAddress() returns something like Lcom/squareup/okhttp/CertificatePinner;->check(Ljava/lang/String;Ljava/util/List;)V+50h 
      fqname = ref.getInternalAddress().split('->')[0] 
      # get class (IDexClass) 
      clazz = dex_unit.getClass(fqname) 
      # From signature to class path 
      # Lcom/squareup/okhttp/CertificatePinner; -> com.squareup.okhttp.CertificatePinner 
      class2hook = clazz.getSignature()[1:-1].replace("/", ".") 
      # loop through each method; check params & retval 
      for method in clazz.getMethods(): 
         if retval is not None and method.getReturnType().getSignature() != retval: continue 
         if self.list_cmp(params, [str(m.getSignature()) for m in method.getParameterTypes()]): 
            method2hook = method.getName() 
            results.append( {"class": class2hook, "method": method2hook}) 
   return results 

# is there a better way? PR/PM please! 
def list_cmp(self, a, b): 
   if len(a) != len(b): return False 
   for x, y in zip(a, b): 
      if x != y: return False 
   return True

The do_search function expects a DEX unit, a needle to search for, an array of parameters that we're looking for and optionally a return value to match against. The function returns an array of dictionaries matching the provided signature. A dictionary contains a class path and a method name.

Putting it all together

First we'll create three variabes: an array which will contain separate Frida hooks, a Frida main template variable and an OkHttp Frida hook template:

class GenerateFridaHooks(IScript): 
   frida_hooks = [] 
   frida_hook_file = u"""'use strict'; 
   // Usage: frida -U -f com.example.app -l generated_hook.js --no-pause 
   Java.perform(function() {\{ 
      {hooks} 
   } 
   }}); 
   """ 
   frida_okhttp3_hook = u""" 
      var okhttp3_CertificatePinner{idx} = Java.use('{java_class}'); 
      var findMatchingPins{idx} = okhttp3_CertificatePinner{idx}.{java_method}.overload('java.lang.String'); 
      findMatchingPins{idx}.implementation = function(hostname) {\{ 
         console.log('[+] okhttp3.CertificatePinner.findMatchingPins(' + hostname + ') # {java_class}.{java_method}()'); 
         return findMatchingPins{idx}.call(this, ''); // replace hostname with empty string 
      }}; """

Next, in the run() method, we'll add code that calls the do_search function with the appropriate parameters to generate our hooks:

def run(self, ctx): 
   # Hello world 
   print(u"?? JEB scripting") 

   # [ ... init project GUI&CLI code omitted... ] 

   # loop through all dex files in project & search 
   for dex in project.findUnits(IDexUnit): 
      # Generating hooks for OkHttp3 
      for idx, result in enumerate(self.do_search(dex, "Certificate pinning failure!", ["Ljava/lang/String;"])): 
         self.frida_hooks.append( 
            self.frida_okhttp3_hook.format(idx=idx, java_class=result.get("class"), java_method=result.get("method"))) 

   # output the Frida script 
   print("-" * 100) 
   print(self.frida_hook_file.format(hooks="\n".join(self.frida_hooks))) 
   print("-" * 100)

Finally we construct the hook by concatenating them all and formatting them in the Frida main hook template. The following is a sample CLI output:


? jeb-pro ./jeb_macos.sh -c --srv2 --script=GenerateFridaHooks.py -- "/path/to/apk/file.apk" 
<JEB startup header omitted> 

?? JEB scripting 
{JebFridaHookArtifact > JebFridaHookArtifact}: 4956 resource files were adjusted 
Attempting to merge the multiple DEX files into a single DEX file... 
<JEB processing omitted> 
{JebFridaHookArtifact > JebFridaHookArtifact}: DEX merger was successful and produced a virtual DEX unit 

?? Fresh Frida Hooks 
---------------------------------------------------------------------------------------------------- 
'use strict'; 
   // Usage: frida -U -f com.example.app -l generated_hook.js --no-pause 
   Java.perform(function() { 
      var okhttp3_CertificatePinner0 = Java.use('<omitted>'); 
      var findMatchingPins0 = okhttp3_CertificatePinner0.a.overload('java.lang.String'); 
      findMatchingPins0.implementation = function(hostname) { 
         console.log('[+] okhttp3.CertificatePinner.findMatchingPins(' + hostname + ') # <omitted>()'); 
         return findMatchingPins0.call(this, ''); // replace hostname with empty string 
      }; 

      var okhttp3_CertificatePinner1 = Java.use('com.squareup.okhttp.CertificatePinner'); 
      var findMatchingPins1 = okhttp3_CertificatePinner1.findMatchingPins.overload('java.lang.String'); 
      findMatchingPins1.implementation = function(hostname) { 
         console.log('[+] okhttp3.CertificatePinner.findMatchingPins(' + hostname + ') # com.squareup.okhttp.CertificatePinner.findMatchingPins()'); 
         return findMatchingPins1.call(this, ''); // replace hostname with empty string 
      }; 
   }); 
---------------------------------------------------------------------------------------------------- 
Done.

Interestingly there were two instances of OkHttp library in this specific app. This is not particularly uncommon as certain dependencies might use their own instance of a library.

It might be handy to check the DEX format, especially when trying to come up with signatures. For example, I wanted to match methods that accept an array of X509Certificate & a String as parameter and returning void. [ is used to denote an array and V is used to denote void:

self.do_search(dex, "NEEDLE", ["[Ljava/security/cert/X509Certificate;", "Ljava/lang/String;"], "V"): # V for Void

Less obvious is Z for boolean though.

Checkout the code from GitHub!

Thanks for reading.

Questions or feedback?