Frida Cheatsheet and Code Snippets for Android

Quick reference guide for Frida code snippets used for Android dynamic instrumentation.

Date and Time of last update Sun 22 Mar 2020
  

In this post we are going to see the basics of Frida along with code snippets for dynamic instrumentation in Android. If you are interested on learning more about Frida this document might help you.

You can find instructions on how to setup your environment on Linux in this post.

Some theoretical background on how frida works

Frida is writing code directly in process memory. When you attach frida to a running application, frida on the background uses ptrace to hijack the thread. A bootstrapper populates this thread and starts a new one, connecting to the frida server that is running on the device and loads a dynamically generated library that has the frida agent along with our instrumentation code. This last part is where all the magic happens. The hijacked thread is being restored to its original state and resumes, then the process continues normally.

Feel free to send a message and suggest something to be added to the following list! -- It will be updated as soon as I have something to add!

Finding devices and applications

#To list the available devices for frida
frida-ls-devices

# Connect Frida to an iPad over USB and list running processes
$ frida-ps -U

# List running applications
$ frida-ps -Ua

# List installed applications
$ frida-ps -Uai

# Connect Frida to the specific device
$ frida-ps -D 0216027d1d6d3a03



Start Frida - CLI and Python bindings

#Hooking before starting the app
frida -U --no-pause -l hookNative.js -f com.erev0s.jniapp

#Basic frida hooking
frida -U com.erev0s.jniapp -l hookNative.js

Where the -U indicates to check for connected USB devices while the -f which package to spawn and the -l our script to load.


For the python bindings lets first see the available members of frida:

import inspect
import frida
for mb in inspect.getmembers(frida, inspect.isfunction):
  print(mb)

('attach', <function attach at 0x7f04816eb620>)
('enumerate_devices', <function enumerate_devices at 0x7f04816eba60>)
('get_device', <function get_device at 0x7f04816eb950>)
('get_device_manager', <function get_device_manager at 0x7f04816ebbf8>)
('get_device_matching', <function get_device_matching at 0x7f04816eb9d8>)
('get_local_device', <function get_local_device at 0x7f04816eb7b8>)
('get_remote_device', <function get_remote_device at 0x7f04816eb840>)
('get_usb_device', <function get_usb_device at 0x7f04816eb8c8>)
('inject_library_blob', <function inject_library_blob at 0x7f04816eb730>)
('inject_library_file', <function inject_library_file at 0x7f04816eb6a8>)
('kill', <function kill at 0x7f04816eb598>)
('resume', <function resume at 0x7f0481f34c80>)
('shutdown', <function shutdown at 0x7f04816ebb70>)
('spawn', <function spawn at 0x7f04858bcb70>)


These are the choices we have available! The following two snippets include the two ways of starting frida with and without early instrumentation!

# Early instrumentation
import frida, sys, time

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

js = """js code here"""

device = frida.get_usb_device()
pid = device.spawn(["com.erev0s.jniapp"])
session = device.attach(pid)
script = session.create_script(js)
script.on('message', on_message)
script.load()
device.resume(pid)
sys.stdin.read()

A thing to note on this code is that it allows us to have "early instrumentation", which means that we load our script js before resuming the execution of the app. This way if the application has some security check upon initializing we are able to intercept that as well.

# Normal start - app needs to be opened
import frida, sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

js = """js code here"""

process = frida.get_usb_device().attach('com.erev0s.jniapp')
script = process.create_script(js)
script.on('message', on_message)
script.load()
sys.stdin.read()



Now we are off to the interesting part, which is what the js scripts can do.

  • The first thing would be to show how to override a class method print the original value it would have and alter it and return True instead.
Java.perform(function(){

	Java.use("com.Awesome.App.MainActivity").mySecurityCheck.implementation=function(x,y){
		console.log("Original value: " + this.mySecurityCheck(x,y));
		// Return the boolean True instead
		return Java.use("java.lang.Boolean").$new(true);
	}
})

What we are seeing in this snippet is the original implementation of the method mySecurityCheck being overridden with something arbitrary of our choice. In this case we chose to simply return the Boolean True.

  • Inspect the fields and methods of a class
const Class = Java.use("com.Awesome.App.MainActivity");
function inspectClass(obj) {
    const obj_class = Java.cast(obj.getClass(), Class);
    const fields = obj_class.getDeclaredFields();
    const methods = obj_class.getMethods();
    console.log("Inspect " + obj.getClass().toString());
    console.log("\tFields:");
    for (var i in fields)
        console.log("\t" + fields[i].toString());
    console.log("\tMethods:");
    for (var i in methods)
        console.log("\t" + methods[i].toString());
}

I believe it is easy to understand the snippet above. It will get the class we give to it, in this case the MainActivity and move on to print all the fields and methods it can find.

  • Reveal native methods
var RevealNativeMethods = function() {
  var pSize = Process.pointerSize;
  var env = Java.vm.getEnv();
  var RegisterNatives = 215, FindClassIndex = 6; // search "215" @ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html
  var jclassAddress2NameMap = {};
  function getNativeAddress(idx) {
    return env.handle.readPointer().add(idx * pSize).readPointer();
  }
  // intercepting FindClass to populate Map<address, jclass>
  Interceptor.attach(getNativeAddress(FindClassIndex), {
    onEnter: function(args) {
      jclassAddress2NameMap[args[0]] = args[1].readCString();
    }
  });
  // RegisterNative(jClass*, .., JNINativeMethod *methods[nMethods], uint nMethods) // https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#977
  Interceptor.attach(getNativeAddress(RegisterNatives), {
    onEnter: function(args) {
      for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
        /*
          https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#129
          typedef struct {
             const char* name;
             const char* signature;
             void* fnPtr;
          } JNINativeMethod;
        */
        var structSize = pSize * 3; // = sizeof(JNINativeMethod)
        var methodsPtr = ptr(args[2]);
        var signature = methodsPtr.add(i * structSize + pSize).readPointer();
        var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer(); // void* fnPtr
        var jClass = jclassAddress2NameMap[args[0]].split('/');
        console.log('\x1b[3' + '6;01' + 'm', JSON.stringify({
          module: DebugSymbol.fromAddress(fnPtr)['moduleName'], // https://www.frida.re/docs/javascript-api/#debugsymbol
          package: jClass.slice(0, -1).join('.'),
          class: jClass[jClass.length - 1],
          method: methodsPtr.readPointer().readCString(), // char* name
          signature: signature.readCString(), // char* signature TODO Java bytecode signature parser { Z: 'boolean', B: 'byte', C: 'char', S: 'short', I: 'int', J: 'long', F: 'float', D: 'double', L: 'fully-qualified-class;', '[': 'array' } https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java
          address: fnPtr
        }), '\x1b[39;49;00m');
      }
    }
  });
}

Java.perform(RevealNativeMethods);

You can see more about this snippet on this stackoverflow question!

  • Enumerating Native library
Module.enumerateExports("mylib.so", {
    onMatch: function(e) {
        if (e.type == 'function') {
            console.log("name of function = " + e.name);

            if (e.name == "Java_example_decrypt") {
                console.log("Function Decrypt recognized by name");
                Interceptor.attach(e.address, {
                    onEnter: function(args) {
                        console.log("Interceptor attached onEnter...");
                    },
                    onLeave: function(retval) {
                        console.log("Interceptor attached onLeave...");
                    }
                });
            }
        }
    },
    onComplete: function() {}
});



Modify return value from inside a native function

Interceptor.attach(Module.getExportByName('libnative-lib.so', 'Jniint'), {
    onEnter: function(args) {
      this.first = args[0].toInt32(); // int
      console.log("on enter with: " + this.first)
    },
    onLeave: function(retval) {
      const dstAddr = Java.vm.getEnv().newIntArray(1117878);
      console.log("dstAddr is : " + dstAddr.toInt32())
      retval.replace(dstAddr);
    }
});



  • Using findExportByName if we know on the method we want to hook
Interceptor.attach(Module.findExportByName(null, "fopen"), {
    onEnter: function(args) {
        console.log("Interceptor attached onEnter...");
    },
    onLeave: function(args) {
        console.log("Interceptor attached onLeave...");
    }
}

The null argument tells frida to look through the exports of all of the loaded libraries.

  • Enumerate all the modules in order to find the export you are looking for
Process.enumerateModules()
    .filter(function(m) {
        return m["path"].toLowerCase().indexOf("libnative") != -1;
    })
    .forEach(function(mod) {
        console.log(JSON.stringify(mod));
        mod.enumerateExports().forEach(function(exp) {
            if (exp.name.indexOf("fopen") != -1) {
                console.log("fopen found!");
            }
        })
    });



  • Hook JNI by address

Running nm --demangle --dynamic mylib.so might be helpful in order to find the correct method address to use.

var moduleName = "mylib.so"; 
var nativeFuncAddr = 0x1111;

Interceptor.attach(Module.findExportByName(null, "fopen"), {
    onEnter: function(args) {
        this.lib = Memory.readUtf8String(args[0]);
        console.log("fopen ==> " + this.lib);
    },
    onLeave: function(retval) {
        if (this.lib.endsWith(moduleName)) {
            console.log(retval);
            var baseAddr = Module.findBaseAddress(moduleName);
            Interceptor.attach(baseAddr.add(nativeFuncAddr), {
                onEnter: function(args) {
                    console.log("hook invoked");
                    console.log(JSON.stringify({
                        a1: args[1].toInt32(),
                        a2: Memory.readUtf8String(Memory.readPointer(args[2])),
                        a3: Boolean(args[3])
                    }, null, '\t'));
                }
            });
        }
    }
});



  • Enumerate classes and filter them with includes
Java.perform(function() {
    Java.enumerateLoadedClasses({
        "onMatch": function(c) {
            if (c.includes("erev0s")) {
                console.log(c);
            }
        },
        onComplete: function() {}
    });
});



Hook setText and change the content

Java.perform(function() {
    var textViewClass = Java.use("android.widget.TextView");
    // Lets overload the setText here
    textViewClass.setText.overload("java.lang.CharSequence").implementation = function(x) {
        var string = Java.use('java.lang.String');
        return this.setText(string.$new("erev0s.com is CHANGED"));
    }
});



Conclusion

In this article we saw several snippets for instrumenting Android applications using Frida. The snippets vary depending on the task you are required to do and they include cases where you have to instrument a native library which is a common thing in Android using the JNI. All in all Frida offers a wide variety of tools you can use to instrument an application and bypass the security measures implemented. It requires quite some time to get familiar with it but don't get discouraged, its worth the time. A nice document with snippets for different operating systems can be found here.

For a guide on how to instrument a native method step by step in a noob friendly way you can check this post.

Feel free to suggest any snippet to be added if you deem it is worthy!