What is Frida?
Frida, as the website of this project says, is a world-class dynamic instrumentation framework. To simplify: a framework that will allow us to inject our own code into a working process (it can be a process on Android, but it also supports iOS, Windows, Linux or macOS), and then to control this process from the javascript code. If this is not clear, the example of a few paragraphs below should dispel all doubts
While the code that will be injected into another process will be written in JavaScript, the Frida support can be done in other programming languages, for example in Python. Therefore, the easiest way to install Frida is to use the command:
1 |
pip install frida |
This will install the frida, frida-kill, frida-ps, frida-discover, frida-ls-devices and frida-trace tools. For Frida to be able to inject into processes on a mobile device, we also need to run an appropriate binary with a server on it. Binaries can be found on the github of the project. We should look for files whose name starts with: frida-server. In case of my device, the correct file will be: frida-server-10.1.1-android-arm. The file must be unpacked and thrown on the android phone, and then run on the phone with root privileges. The file is best copied with the adb tool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# we unpack file with server xz -d frida-server-10.1.1-android-arm.xz # we copy it on the device to directory /data/local/tmp ./adb push frida-server-10.1.1-android-arm /data/local/tmp # we open mobile device’s console # and we give ourselves root privileges ./adb shell su # Let's still set permissions to execute for # Frida server chmod 755 /data/local/tmp/frida-server-10.1.1-android-arm # we can now turn on Frida /data/local/tmp/frida-server-10.1.1-android-arm |
The Frida server does not print out any messages when it starts; if everything is OK, it just works.
Before we begin
For the presentation of Frida, I will use the same android application as in my previous text. As a reminder: the application tries to download a file from the path https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt and display it. By default, however, this will certainly not work because of the way certificates are implemented. For greater clarity of text, below I recall the decompiled code of both classes appearing in the application pl.sekurak.ssltest:
Listing 1. Class pl.sekurak.ssltest.MainActivity:
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 |
package pl.sekurak.ssltest; import android.os.Bundle; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy.Builder; import android.support.v7.app.e; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.TextView; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.X509TrustManager; public class MainActivity extends e { protected void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build()); setContentView(2130968598); paramBundle = (TextView)findViewById(2131296319); paramBundle.setText("Trying to get some data from https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt"); try { Object localObject = SSLContext.getInstance("TLS"); ((SSLContext)localObject).init(null, new X509TrustManager[] { new a() }, null); HttpsURLConnection localHttpsURLConnection = (HttpsURLConnection)new URL("https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt").openConnection(); localHttpsURLConnection.setSSLSocketFactory(((SSLContext)localObject).getSocketFactory()); localObject = new BufferedReader(new InputStreamReader(localHttpsURLConnection.getInputStream())); paramBundle.setText(((BufferedReader)localObject).readLine()); Log.d("SSLTest", "Mission accomplished."); ((BufferedReader)localObject).close(); return; } catch (SSLHandshakeException localSSLHandshakeException) { paramBundle.setText("Failed to get data from: https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt. The SSL certificate is invalid."); return; } catch (NoSuchAlgorithmException paramBundle) {}catch (IOException paramBundle) {}catch (KeyManagementException paramBundle) {} } } |
Listing 2. Class pl.sekurak.ssltest.a
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 |
package pl.sekurak.ssltest; import java.security.Principal; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; public class a implements X509TrustManager { public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) {} public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) { if (paramArrayOfX509Certificate[0] == null) { throw new CertificateException(); } if (paramArrayOfX509Certificate[0].getIssuerDN().getName() != "CN=sekurakowy.pl,O=sekurak.pl,C=PL") { throw new CertificateException(); } } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } |
ThThe code that will be of particular interest to us is contained in lines 13-21 of listing 2. This is where the correctness of the certificate is checked in the checkServerTrusted method. The specificity of this method is that if the certificate is correct, the method returns nothing; if the certificate is incorrect, the exception is returned.
The code of the checkServerTrusted method specified above is only an example. It is not a valid way to validate SSL/TLS certificates, and you should never place a similar code in your application. You can read about the correct approach to the subject of checking certificates in the Android documentation.
Ultimately, we will want to change the operation of the method so that it never throws an exception.
Testing Frida
So we return to Frida. We assume that we already have Frida running on a mobile device. The first thing we are interested in is process listing. We will use the frida-ps command for this. We will pass to it the parameter -U, which means that Frida will try to communicate with the device connected via USB.
1 2 3 4 5 6 7 8 9 10 |
$ frida-ps -U PID Name ----- ----------------------------------------------------------- 2369 IPSecService 2388 adbd 3724 android.process.acore 3955 android.process.media ... 9717 pl.sekurak.ssltest ... |
We learn that the PID of the process in which we are going to inject is 9717. So let’s try to inject into the process with a recommendation:
1 |
frida --enable-jit -U 9717 |
Thanks to the -enable-jit parameter, our JavaScript code will work much faster (although it may be more memory consuming). After correct execution of the command, we should see an image similar to Picture nr 1.
In the console we can now enter the JS code, which will be executed in the context of the android application.
Importantly, when running on Android, we should always place our code inside the Java.perform call – thanks to this Frida assures us that the code is executed in the context of a Java virtual machine.
Listing of existing classes
So let’s first try to list all the classes that exist in the running application. In Frida we have Java.enumerateLoadedClasses method, where the argument is a JS object with two fields:
- onMatch – a call back performed when a class is found
- onComplete – call back executed at the moment of completing the operation
Let’s write down all classes existing in the program with the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// With Java. perform we make sure that the code is executed // in the context of the Java virtual machine. Java.perform(() => { // We enumerate loaded classes... Java.enumerateLoadedClasses({ onMatch: clsName => { // If a class is found, we print out // in the console its name. console.log(clsName); }, onComplete: () => { // After completing the operation, we will print out an appropriate message. console.log(" [*] COMPLETED"); } }) }); |
After executing the code, we get gigantic list of every class (Picture nr 2)
Calling methods on existing instances of classes
From the Frida level we can also call methods on existing classes in the application. The Java.choose() function looks for living instances of the class that we will pass on as an argument. Similarly to enumerateLoadedClasses, the second argument is an object with onMatch in onComplete call back definition. So let’s try to look for a live instance of class pl.sekurak.ssltest.a (this is a class from listing 2). If there is one, just write an appropriate information in the console. We will use the following code:
1 2 3 4 5 6 7 8 9 10 11 |
Java.perform(() => { // We are looking for a live instance of class pl.sekurak.ssltest.a Java.choose("pl.sekurak.ssltest.a", { onMatch: inst => { // The `inst` argument stores the class instance found // Let's write information about this object in the console. console.log(inst); }, onComplete: () => {} }) }); |
On picture nr 3 we can see the result of executing a code.
The entry pl.sekurak.ssltest.a@236c1ee2, which appears on the screen, is probably well known to all Java programmers. This is the result of a standard calling of the toString() method on the class object. Conclusion: class pl.sekurak.ssltest.a in memory exists!
So now let’s call some method on this class. For example, simply checkServerTrusted by passing two null arguments:
1 2 3 4 5 6 7 8 9 10 |
Java.perform(() => { // We are looking for a live instance of class pl.sekurak.ssltest.a Java.choose("pl.sekurak.ssltest.a", { onMatch: inst => { // Call the method on the found instance. inst.checkServerTrusted(null, null); }, onComplete: () => {} }) }); |
We can see result in Picture nr 4
As a result of execution of the code, there was an exception – NullPointerException. It is expected as much as possible. Let’s remember the code of the checkServerTrusted method:
1 2 3 4 5 6 7 8 9 |
public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) { if (paramArrayOfX509Certificate[0] == null) { throw new CertificateException(); } if (paramArrayOfX509Certificate[0].getIssuerDN().getName() != "CN=sekurakowy.pl,O=sekurak.pl,C=PL") { throw new CertificateException(); } } |
In the third line there is a reference to the zero index of the array paramArrayOfX509Certificate. Before that, however, there is no way to check if this object is not a null. Hence, naturally: NullPointerException, as we passed null in the function call.
Performance variation of the method
We have shown that we can call up the checkServerTrusted method, but now we would like to change its operation: it should not give up exceptions, but always end its operation immediately.
To do this, we must first refer to the class in which this method is defined and then change its implementation property to the action that is expected by us. We use Java.use to refer to the class itself:
1 2 3 4 5 6 7 8 9 10 11 |
Java.perform(() => { // We download pl.sekurak.ssltest.a class const cls = Java.use('pl.sekurak.ssltest.a'); // We change the implementations of the checkServerTrusted method. // The method should simply do nothing ;-) cls.checkServerTrusted.implementation = function() { return; } }); |
The code will work, of course, but in our application we have a problem because the code tries to download the contents of the file through HTTPS just after starting. So, changing the method during the process is no longer sufficient. Therefore, we have to start a new process on the phone ourselves and change the method as soon as possible. For this purpose we will use the code written in Python according to the template below:
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 |
# -*- coding: utf-8 -*- import frida, sys CODE = r''' Java.perform(() => { // We download class pl.sekurak.ssltest.a const cls = Java.use('pl.sekurak.ssltest.a'); // We change the implementations of the checkServerTrusted method. // The method should simply do nothing ;-) cls.checkServerTrusted.implementation = function() { return; } }); ''' if __name__ == '__main__': # Download the device connected via USB. device = frida.get_usb_device() # we start pl.sekurak.ssltest process. pid = device.spawn(["pl.sekurak.ssltest"]) # We clasp ourselves under the process.. session = device.attach(pid) # Turn on JIT to speed up the JS code. session.enable_jit() # Pin the script, just like before. # We did it with the Frida tool. script = session.create_script(CODE) # We enable the continuation of the process # (otherwise it is blocked as in the debugger) device.resume(pid) # We charge our JS script. script.load() # Because Frida runs on a separate thread # after switching to the end of the programme, this immediately # would end its operation. That is why we have to use # the function that will make us wait for # user input. sys.stdin.read() |
In the CODE variable, we define the same JS code as we pasted in the Frida tool. In the further part of the programme, however, we have a proper preparation of the environment to connect Frida to the process immediately after its launch.
Picture nr 5 shows the view from the application execution without Frida, and Picture nr 6 – with Frida.
The attempt to change the method was successful. As a result, the application does not check the SSL certificate in any way.
Using Frida to log in to called methods
The last interesting case of using Frida, which I am going to show in this article, is the logging of methods called; i.e., with which arguments a given method was called. It is very useful when testing mobile applications, when on the basis of code analysis it is sometimes difficult to say what exactly is the purpose of a given function, and on the basis of the analysis of its mode of operation (arguments and returned value), it can be stated in no time at all.
In MainActivity class, the setText method is used to set the text visible in the mobile application (such as “Wow, you made it!”):
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MainActivity extends e { protected void onCreate(Bundle paramBundle) { ... paramBundle = (TextView)findViewById(2131296319); ... paramBundle.setText("Trying to get some data from https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt"); ... } } |
So let’s try to capture all calls of setText method in TextView class. Rapid shaving will allow us to say that the full class name is: android.widget.TextView, and the setText method is overloaded and can take different combinations of arguments. Let’s not think about what we want to intercept exactly. Let’s capture all of them 😉
If the Java method is overloaded, we can get all the ways to call it by accessing the class.method.overloads object. Therefore, in the code below I will refer to all setText methods, write on the console what arguments were passed on and call the original method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// The following code should be pasted into the variable CODE // in an earlier python code. Java.perform(() => { // We refer to the class android.widget.TextView const TextView = Java.use('android.widget.TextView'); // We change the operation of *all* setText methods TextView.setText.overloads.forEach(f => { f.implementation = function(...args) { // We log the fact that the method was called // together with the arguments: console.log(`[${new Date().toString()}] setText called! Args: ${args.join(', ')}`); // We develop an original method. f.call(this, ...args); } }) }) |
The effect can be seen in Picture nr 7.
Summary
Frida is a very complex framework, which can significantly help in the analysis of the behaviour of mobile applications, thanks to the possibility of carrying out the following operations:
- Analysis of classes living in memory
- Call up any methods on classes living in memory
- Capture and replacement of methods
- Logging arguments passed to called methods
Frida offers a much wider range of possibilities than described in this text; for those interested, I recommend that you have a look at the documentation.