frida summary (to be continued)

Summary

The bootstrapper is a process that runs in the background and uses ptrace to hijack threads when Frida is attached to a running application. The bootstrapper then creates a new thread that connects to the Frida server running on the device and loads a dynamically generated library containing the Frida agent and our instrumentation code. Finally, the hijacked thread will be restored to its original state and execution will continue, and the process will continue to run normally.

Discover devices and apps

#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

Starting Frida - CLI

#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

Using Python to operate frida

import frida

def on_message(message, data):
    print("[on_message] Message: {}".format(message))
    if message['type'] == 'send':
        print("[on_message] Data: {}".format(data))
    else:
        print("[on_message] Error: {}".format(message['description']))

device = frida.get_usb_device()
pid = device.spawn(["com.example.app"])
session = device.attach(pid)

with open("hook_example.js") as f:
    script = session.create_script(f.read())

script.on("message", on_message)
script.load()

device.resume(pid)

Interceptor.attach(ptr('0x10000000'), {
    
    
    onEnter: function (args) {
    
    
        send({
    
     type: 'log', data: 'Hooked function called!' });
    },
    onLeave: function (retval) {
    
    
        send({
    
     type: 'log', data: 'Hooked function returned!' });
    }
});

runpython

python hook_example.py

instrumentation

There are two main ways to start Frida:
One is to make early code modifications (early instrumentation), and the other is not to make early code modifications (no early instrumentation).
Early instrumentation: This is to modify the binary of the target process before it is started. This allows you to insert custom code into specific points of target process execution. This can be achieved by using the --patches or -p option on the frida command line. Early instrumentation means we load our JavaScript scripts before the application executes. This way, if the application has some security checks when it is initialized, we will be able to intercept those checks as well.

# 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()

There is no early instrumentation: this is to launch Frida without modifying the target process's binary. Instead, you rely on Frida's built-in instrumentation to monitor and interact with the target process. This is Frida's default operating mode.

# 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()

View native methods of android

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);

Enumerate local libraries

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 the return value of a local 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);
    }
});

If you know the function name, use findExportByName to intercept the function

Interceptor.attach(Module.findExportByName(null, "fopen"), {
    
    
    onEnter: function(args) {
    
    
        console.log("Interceptor attached onEnter...");
    },
    onLeave: function(args) {
    
    
        console.log("Interceptor attached onLeave...");
    }
}

null means search all dynamic libraries

Search all dynamic libraries to find the exported functions you want to intercept

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!");
            }
        })
    });

Intercept JNI with address

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'));
                }
            });
        }
    }
});

You can use nm --demangle --dynamic mylib.so to obtain the function address.
nm is the abbreviation of "name", which means displaying the symbol table;
The –demangle option is used to deobfuscate the symbol names generated by the C++ compiler for convenience Reading; it attempts to "decode" compiled symbol names back to human-readable source code names. This is often used to identify compiled function names, which may be converted into abbreviated or obfuscated names due to compiler optimizations.
The –dynamic option instructs nm to focus on symbols to use when linking dynamically. This means that it lists symbols relevant to dynamic linking, which typically includes externally referenced functions and variables, as well as symbols in dynamically loaded modules

$ nm --demangle --dynamic libnative-lib.so 
00002000 A __bss_start
         U __cxa_atexit
         U __cxa_finalize
00002000 A _edata
00002000 A _end
00000630 T Java_com_erev0s_jniapp_MainActivity_Jniint
000005d0 T Jniint
         U rand
         U srand
         U __stack_chk_fail
         U time

Our goal is to change the execution flow of the apk so that Jniint returns a value defined by us.
There are two solutions to this problem:
Hook at the Java layer, which means we intercept Java’s calls to JNI, so we don’t need to deal with C at all code.
Go deep into the C language to implement Jniint and make our adjustments there.
Usually the first method is easier to implement, but sometimes the second method may be more convenient. It all depends on what the application is doing and what you are trying to achieve.
Method 1

Java.perform(function () {
    
    
  // we create a javascript wrapper for MainActivity
  var Activity = Java.use('com.erev0s.jniapp.MainActivity');
  // replace the Jniint implementation
  Activity.Jniint.implementation = function () {
    
    
    // console.log is used to report information back to us
    console.log("Inside Jniint now...");
    // return this number of our choice
    return 80085
  };
});

Method 2

Interceptor.attach(Module.getExportByName('libnative-lib.so', 'Jniint'), {
    
    
    onEnter: function(args) {
    
    
    },
    onLeave: function(retval) {
    
    
      // simply replace the value to be returned with 0
      retval.replace(0);
    }
});

Enumerate all classes and filter classes containing a certain string

Java.perform(function() {
    
    
    Java.enumerateLoadedClasses({
    
    
        "onMatch": function(c) {
    
    
            if (c.includes("erev0s")) {
    
    
                console.log(c);
            }
        },
        onComplete: function() {
    
    }
    });
});

Intercept setText and change the set text

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"));
    }
});

Guess you like

Origin blog.csdn.net/yangzex/article/details/134850439