Introduction

Brida is a Burp Suite Extension that, working as a bridge between Burp Suite and Frida, lets you use and manipulate applications’ own methods while tampering the traffic exchanged between the applications and their back-end servers. The idea of a tool like this came into mind during the analysis of a mobile app that used symmetric crypto with random keys and, being unable to tamper its traffic without knowing the correct secrets, all data exchanged was not modifiable via Burp.

What is Frida?

Frida is an amazing tool to “inject JavaScript to explore native apps on Windows, macOS, Linux, iOS, Android, and QNX” or, more precisely, “it’s a dynamic code instrumentation toolkit”. For the purpose of this document, we’re going to expose only few of its many features, more information about Frida can be found at: https://frida.re/docs/home/. We strongly suggest you to familiarize with its key concepts and functionalities before you continue reading.

Requisites

This tool was made to speed-up our daily tasks as penetration testers therefore the reader might need to know few basic concepts about application’s and OS internals, penetration testing, de-compiling, reverse engineering, etc. A working knowledge of Burp Suite may help. The following software is required (was tested on):

Burp Suite Pro 1.7.25

Frida 10.11

Pyro 4.60 (pip install pyro4)

Python 2.7

Java 1.8

Your favourite de-compiler

We decided to use Pyro4 as an interface between Burp and Frida to allow direct access from Java and Python extensions.

Brida can be found at https://github.com/federicodotta/Brida and soon at Burp BAppStore

Common Scenario

You have to perform a complete assessment on a native application (Android, iOS, etc.), which means you are also required to investigate the interactions between the application and the back-end servers. You do this pretty much everyday so let’s assume you’re already able to redirect the device/app traffic through your host; let’s also assume you have managed to overcome SSL Pinning mechanisms and/or anti-root checks , so fire up Burp Suite Pro and let’s have a look on what’s happening on the wire.

Damn! This application looks like it uses some sort of (custom) encoding/encryption routines in order to send and receive data to/from the back-ends.

More generally, applications’ logic could be based on cryptographic tokens, could use a complex challenge-response algorithm as well, and so on. How can we tamper the messages? Most of the times the only viable approach is to decompile/disassemble the application, identify the functions or methods we’re interested in AND re-implement them.

This approach is obviously time consuming and not always really viable: i.e. the generation of tokens and/or the encryption routines could be based on cryptographic material strictly tied to the device (state) or stored inside protected areas and thus not directly accessible… That’s when Brida comes in handy: instead of trying to extract keys/certificates and re-writing the routines we’re interested in, why don’t we let the application do the dirty work for us?

How it works?

Brida is made of three components:

Brida.jar is the Burp Suite Extension bridaServicePyro is a python script that glues Frida to Burp, is stored inside the extension and copied in a temporary directory during the execution of Brida. script.js is the JavaScript you’re going to inject into the target application, it exposes its functionalities to the extension via Frida’s own rpc.exports.

'use strict'; // 1 - FRIDA EXPORTS rpc.exports = { exportedFunction: function() { // Do stuff... // This functions can be called from custom plugins or from Brida "Execute method" dedicated tab }, // Function executed when executed Brida contextual menu option 1. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom1: function(message) { return "6566"; }, // Function executed when executed Brida contextual menu option 2. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom2: function(message) { return "6768"; }, // Function executed when executed Brida contextual menu option 3. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom3: function(message) { return "6768"; }, // Function executed when executed Brida contextual menu option 4. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom4: function(message) { return "6768"; } } // 2 - AUXILIARY FUNCTIONS // Convert a hex string to a byte array function hexToBytes(hex) { for (var bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)); return bytes; } // Convert a ASCII string to a hex string function stringToHex(str) { return str.split("").map(function(c) { return ("0" + c.charCodeAt(0).toString(16)).slice(-2); }).join(""); } // Convert a hex string to a ASCII string function hexToString(hexStr) { var hex = hexStr.toString();//force conversion var str = ''; for (var i = 0; i < hex.length; i += 2) str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); return str; } // Convert a byte array to a hex string function bytesToHex(bytes) { for (var hex = [], i = 0; i < bytes.length; i++) { hex.push((bytes[i] >>> 4).toString(16)); hex.push((bytes[i] & 0xF).toString(16)); } return hex.join(""); } // 3 - FRIDA HOOKS (if needed) if(ObjC.available) { // Insert here Frida interception methods, if needed // (es. Bypass Pinning, save values, etc.) } 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 79 80 81 82 83 84 85 'use strict' ; // 1 - FRIDA EXPORTS rpc . exports = { exportedFunction : function ( ) { // Do stuff... // This functions can be called from custom plugins or from Brida "Execute method" dedicated tab } , // Function executed when executed Brida contextual menu option 1. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom1 : function ( message ) { return "6566" ; } , // Function executed when executed Brida contextual menu option 2. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom2 : function ( message ) { return "6768" ; } , // Function executed when executed Brida contextual menu option 3. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom3 : function ( message ) { return "6768" ; } , // Function executed when executed Brida contextual menu option 4. // Input is passed from Brida encoded in ASCII HEX and must be returned in ASCII HEX (because Brida will decode the output // from ASCII HEX). Use auxiliary functions for the conversions. contextcustom4 : function ( message ) { return "6768" ; } } // 2 - AUXILIARY FUNCTIONS // Convert a hex string to a byte array function hexToBytes ( hex ) { for ( var bytes = [ ] , c = 0 ; c < hex . length ; c += 2 ) bytes . push ( parseInt ( hex . substr ( c , 2 ) , 16 ) ) ; return bytes ; } // Convert a ASCII string to a hex string function stringToHex ( str ) { return str . split ( "" ) . map ( function ( c ) { return ( "0" + c . charCodeAt ( 0 ) . toString ( 16 ) ) . slice ( - 2 ) ; } ) . join ( "" ) ; } // Convert a hex string to a ASCII string function hexToString ( hexStr ) { var hex = hexStr . toString ( ) ; //force conversion var str = '' ; for ( var i = 0 ; i < hex . length ; i += 2 ) str += String . fromCharCode ( parseInt ( hex . substr ( i , 2 ) , 16 ) ) ; return str ; } // Convert a byte array to a hex string function bytesToHex ( bytes ) { for ( var hex = [ ] , i = 0 ; i < bytes . length ; i ++ ) { hex . push ( ( bytes [ i ] > > > 4 ) . toString ( 16 ) ) ; hex . push ( ( bytes [ i ] & 0xF).toString(16)); } return hex . join ( "" ) ; } // 3 - FRIDA HOOKS (if needed) if ( ObjC . available ) { // Insert here Frida interception methods, if needed // (es. Bypass Pinning, save values, etc.) }

The above is a script.js skeleton: you can define as many exported functions as you want, plus there are four exports you can easily invoke from a Burp context menu (i.e. right-click on selected text).

Please note that all implemented methods in JS MUST be lowercase (maybe some Pyro limitations).

Brida offers three different modes of operation:

Direct method invocation with custom parameters Context menu action Custom plugin stub generation

We’ll explore them through some real-life examples using Signal on iOS 10 as target application (source code is available). All the examples have been created for iOS, but the same process can be applied on Android, Windows, Linux etc.

Usage

First thing first: let’s add Brida.jar extension to Burp Suite Pro. Jython is not needed but a working version of Python is required.

Extender -> Add -> Extension file (.jar)

Let’s configure some options:

Python binary path : python executable path, needed to run the Pyro server (for RPC).

: python executable path, needed to run the Pyro server (for RPC). Pyro host, Pyro port: Pyro server host and port; can be left untouched, change the port if you need to.

Pyro server host and port; can be left untouched, change the port if you need to. Frida JS file path : Frida javascript script injected into the target application

: Frida javascript script injected into the target application Application ID: ex.: org.whispersystems.signal

Now you’ve got the options set, let’s see what these buttons are for:

Start server , start bridge server between Burp and Frida (it runs a python/Pyro RPC service in background)

, start bridge server between Burp and Frida (it runs a python/Pyro RPC service in background) Kill server , stop the bridge server

, stop the bridge server Spawn application , launch the application on the device and inject the Frida JS into it

, launch the application on the device and inject the Frida JS into it Kill application , kill the application

, kill the application Reload JS , reload the Frida script without restarting the application

, reload the Frida script without restarting the application Java Stub , print a Java Stub for your own plugin that uses Brida

, print a Java Stub for your own plugin that uses Brida Python Stub , print a Python Stub for your own plugin that uses Brida

, print a Python Stub for your own plugin that uses Brida Save settings to file , save settings to file

, save settings to file Load settings from file , load settings from file

, load settings from file Execute Method, run “execute method” function (see below for an example)

Let the fun begin: click on “Start server” then click on “Spawn application”. Let’s roll…

Example 1 – Direct method invocation w/ custom parameters

With Brida is possible to execute an app method via a custom JS function, here is a simple example. In scriptSignal.js we defined a simple ObjC function that uses NSString uppercaseString function.

touppercase: function(message) { var a1 = ObjC.classes.NSString.stringWithString_(message); var a2 = a1.uppercaseString(); return a2.toString(); } 1 2 3 4 5 touppercase : function ( message ) { var a1 = ObjC . classes . NSString . stringWithString_ ( message ) ; var a2 = a1 . uppercaseString ( ) ; return a2 . toString ( ) ; }

Then we configure the part of “Direct method” invocation by setting up all parameter correctly.

Clicking on “Execute Method” Brida will call via Pyro the method defined in JS that will be executed on iOS and the result will be displaied in the “Output” section.

Example 2 – Context menu actions

We decided to add some default context menus to Burp that allow you to call some predefined Brida functions; in this way, for some basic function (i.e. custom encryption or decryption) you can directly develop the proper JS without writing your own plugin. Here is a list of the menus:

Brida Custom 1, reachable via context menu on editable views (it will call contextcustom1 JS)

Brida Custom 2, reachable via context menu on editable views (it will call contextcustom2 JS)

Brida Custom 3, reachable via context menu on non editable views (it will call contextcustom3 JS)

editable views (it will call contextcustom3 JS) Brida Custom 4, reachable via context menu on non editable views (it will call contextcustom4 JS)

By default, in order to manage binary data, Brida sends the input to JS encoded in hex string and expects output encoded in the same way. Auxiliary functions for the conversions are supplied in the JS files.

In case of editable views the selection will be directly replaced with the results of the JS execution. On non editable views it will generate a message box with the result.

In this example JS we implemented the following methods:

contextcustom1 will create a lowercase version of the selected string.

contextcustom1: function(message) { var a1 = ObjC.classes.NSString.stringWithString_(hexToString(message)); var a2 = a1.lowercaseString(); return stringToHex(a2.toString()); } 1 2 3 4 5 contextcustom1 : function ( message ) { var a1 = ObjC . classes . NSString . stringWithString_ ( hexToString ( message ) ) ; var a2 = a1 . lowercaseString ( ) ; return stringToHex ( a2 . toString ( ) ) ; }

contextcustom2 will create a base64 of the selected text.

contextcustom2: function(message) { var inputByte = hexToBytes(message); var ptrMessage = Memory.alloc(inputByte.length); Memory.writeByteArray(ptrMessage,inputByte); var objMessage = ObjC.classes.NSData.alloc().initWithBytes_length_(ptrMessage,inputByte.length); var encodedMessage = objMessage.base64EncodedString(); return stringToHex(encodedMessage.toString()); } 1 2 3 4 5 6 7 8 contextcustom2 : function ( message ) { var inputByte = hexToBytes ( message ) ; var ptrMessage = Memory . alloc ( inputByte . length ) ; Memory . writeByteArray ( ptrMessage , inputByte ) ; var objMessage = ObjC . classes . NSData . alloc ( ) . initWithBytes_length_ ( ptrMessage , inputByte . length ) ; var encodedMessage = objMessage . base64EncodedString ( ) ; return stringToHex ( encodedMessage . toString ( ) ) ; }

contextcustom3 will create an uppercase of the selected text.

contextcustom3: function(message) { var a1 = ObjC.classes.NSString.stringWithString_(hexToString(message)); var a2 = a1.uppercaseString(); return stringToHex(a2.toString()); } 1 2 3 4 5 contextcustom3 : function ( message ) { var a1 = ObjC . classes . NSString . stringWithString_ ( hexToString ( message ) ) ; var a2 = a1 . uppercaseString ( ) ; return stringToHex ( a2 . toString ( ) ) ; }

contextcustom4 will decode a base64 of the selected text.

contextcustom4: function(message) { var a2 = ObjC.classes.NSString.stringWithString_(hexToString(message)); var encodedString = ObjC.classes.NSData.dataFromBase64String_(a2); var ptrBytesReturned = encodedString.bytes(); var ptrBytesLength = encodedString.length(); var bytesReturneded = Memory.readByteArray(ptrBytesReturned, ptrBytesLength); return bytesToHex(bytesReturneded); } 1 2 3 4 5 6 7 8 contextcustom4 : function ( message ) { var a2 = ObjC . classes . NSString . stringWithString_ ( hexToString ( message ) ) ; var encodedString = ObjC . classes . NSData . dataFromBase64String_ ( a2 ) ; var ptrBytesReturned = encodedString . bytes ( ) ; var ptrBytesLength = encodedString . length ( ) ; var bytesReturneded = Memory . readByteArray ( ptrBytesReturned , ptrBytesLength ) ; return bytesToHex ( bytesReturneded ) ; }

Example 3 – Custom plugin: Signal (iOS) modify an encrypted message in transit

The real power of Brida is expressed with this mode of operation. We’re going to write a custom Burp extension (tested with Java and Python) for a specific purpose: we want Burp to intercept an encrypted message when in transit, request the application to encrypt a new message previously defined, then replace the original message. All code is available at https://github.com/federicodotta/Brida/tree/master/examples (the Burp plugin example can be found in Python and Java too). For simplicity we will explain only the Python version in this blog post. You will need to load the Brida plugin in Burp and then load the python or java plugin specific for Signal application.

You can use the “Java Stub” or “Python Stub” Brida functionalities to easily generate valid code that connects to Brida via Pyro4 and executes Frida exported functions.

First of all, let’s write the script we’re going to inject:

scriptSignal.js

The last part of the script contains the Frida Hooks employed by the plugin. The hooking of “sendMessage:recipient:thread:attempts:success:failure:” is used to get the destination number of the last message, necessary to execute the function that will generate the new encrypted message. This value is stored in a local variable in JS and will be used in our custom function.

var hooksendMessage = ObjC.classes.OWSMessageSender["- sendMessage:recipient:thread:attempts:success:failure:"]; Interceptor.attach(hooksendMessage.implementation, { onEnter: function(args) { var obj2 = ObjC.Object(args[3]); destNum = obj2.recipientId().toString(); } }); 1 2 3 4 5 6 7 var hooksendMessage = ObjC . classes . OWSMessageSender [ "- sendMessage:recipient:thread:attempts:success:failure:" ] ; Interceptor . attach ( hooksendMessage . implementation , { onEnter : function ( args ) { var obj2 = ObjC . Object ( args [ 3 ] ) ; destNum = obj2 . recipientId ( ) . toString ( ) ; } } ) ;

Then we have to bypass the SSL pinning, in order to be able to intercept the data in transit through Burp Proxy.

var hookevaluateServerTrust = ObjC.classes.OWSHTTPSecurityPolicy["- evaluateServerTrust:forDomain:"]; Interceptor.attach(hookevaluateServerTrust.implementation, { onLeave: function(retval) { retval.replace(ptr(1)); } }); 1 2 3 4 5 6 var hookevaluateServerTrust = ObjC . classes . OWSHTTPSecurityPolicy [ "- evaluateServerTrust:forDomain:" ] ; Interceptor . attach ( hookevaluateServerTrust . implementation , { onLeave : function ( retval ) { retval . replace ( ptr ( 1 ) ) ; } } ) ;

This is the core function exported by Frida, it will be called by the Burp plugin to change the message sent by Signal. It generates a new message and returns the result to our plugin.

changemessage: function(message) { var env = ObjC.classes.Environment.getCurrent(); var messageSender = env.messageSender(); var signalRecipient = ObjC.classes.SignalRecipient.alloc().initWithTextSecureIdentifier_relay_(destNum,null); var contactThread = ObjC.classes.TSContactThread.alloc().initWithContactId_(destNum); var mex = ObjC.classes.TSOutgoingMessage.alloc().initWithTimestamp_inThread_messageBody_(Math.round(+new Date()/1000),null,message); var retVal = messageSender.deviceMessages_forRecipient_inThread_(mex,signalRecipient,contactThread); var retValMessage = retVal.objectAtIndex_(0); return retValMessage.toString(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 changemessage : function ( message ) { var env = ObjC . classes . Environment . getCurrent ( ) ; var messageSender = env . messageSender ( ) ; var signalRecipient = ObjC . classes . SignalRecipient . alloc ( ) . initWithTextSecureIdentifier_relay_ ( destNum , null ) ; var contactThread = ObjC . classes . TSContactThread . alloc ( ) . initWithContactId_ ( destNum ) ; var mex = ObjC . classes . TSOutgoingMessage . alloc ( ) . initWithTimestamp_inThread_messageBody_ ( Math . round ( + new Date ( ) / 1000 ) , null , message ) ; var retVal = messageSender . deviceMessages_forRecipient_inThread_ ( mex , signalRecipient , contactThread ) ; var retValMessage = retVal . objectAtIndex_ ( 0 ) ; return retValMessage . toString ( ) ; }

BurpBridaSignal.py

This is the Burp Suite plugin file that employs Brida to generate a new message and to substitute the sent message with the new one. The first part uses Burp Suite functionalities to analyze all the requests and to check for a specific string (destinationRegistrationId) that indicates the particular request contains an encrypted sent message.

if messageIsRequest: # Get request bytes request = messageInfo.getRequest() # Get a IRequestInfo object, useful to work with the request analyzedRequest = self.helpers.analyzeRequest(request) headers = list(analyzedRequest.getHeaders()) bodyOffset = int(analyzedRequest.getBodyOffset()) body = request[bodyOffset:] bodyString = "".join(map(chr,body)) if "destinationRegistrationId" in bodyString: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if messageIsRequest : # Get request bytes request = messageInfo . getRequest ( ) # Get a IRequestInfo object, useful to work with the request analyzedRequest = self . helpers . analyzeRequest ( request ) headers = list ( analyzedRequest . getHeaders ( ) ) bodyOffset = int ( analyzedRequest . getBodyOffset ( ) ) body = request [ bodyOffset : ] bodyString = "" . join ( map ( chr , body ) ) if "destinationRegistrationId" in bodyString :

The second part is the plugin core, it uses Brida to request the generation of the new message invoking changemessage export defined in scriptSignal.js, via pp.callexportfunction(‘changemessage‘, args). Basically it will generate a new message with text “pwned”.

jsonBody = json.loads(bodyString) uri = 'PYRO:BridaServicePyro@localhost:9999' pp = Pyro4.Proxy(uri) args = [] args.append("pwned") newMessage = pp.callexportfunction('changemessage',args) pp._pyroRelease() 1 2 3 4 5 6 7 8 jsonBody = json . loads ( bodyString ) uri = 'PYRO:BridaServicePyro@localhost:9999' pp = Pyro4 . Proxy ( uri ) args = [ ] args . append ( "pwned" ) newMessage = pp . callexportfunction ( 'changemessage' , args ) pp . _pyroRelease ( )

Then it replaces the original message with the new one just generated through Brida.

m = re.search(".*content = \"(.*?)\".*", newMessage) if m: newMessage = m.group(1) jsonBody["messages"][0]["content"] = newMessage newBodyString = json.dumps(jsonBody) newBodyString = newBodyString.replace("/", "\\/") newRequest = self.helpers.buildHttpMessage(headers, self.helpers.stringToBytes(newBodyString)) messageInfo.setRequest(newRequest) 1 2 3 4 5 6 7 8 m = re . search ( ".*content = \"(.*?)\".*" , newMessage ) if m : newMessage = m . group ( 1 ) jsonBody [ "messages" ] [ 0 ] [ "content" ] = newMessage newBodyString = json . dumps ( jsonBody ) newBodyString = newBodyString . replace ( "/" , "\\/" ) newRequest = self . helpers . buildHttpMessage ( headers , self . helpers . stringToBytes ( newBodyString ) ) messageInfo . setRequest ( newRequest )

Credits

Piergiovanni Cipolloni and Federico Dotta with additional contribution of Maurizio Agazzini.