bootstrapper 是一个在后台运行的进程,当 Frida 附加到一个正在运行的应用程序时,它会使用 ptrace 来劫持线程。然后,bootstrapper 会创建一个新线程,连接到设备上运行的 Frida 服务器,并加载一个包含 Frida 代理和我们的instrumentation 代码的动态生成的库。最后,这个被劫持的线程会被恢复到原来的状态并继续执行,进程也会继续正常运行。
#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
#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
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!' });
}
});
运行python
python hook_example.py
启动Frida主要有两种方式:
一种是在早期进行代码修改(早期 instrumentation),另一种则不进行早期代码修改(没有早期 instrumentation)。
早期 instrumentation:这是在目标进程启动之前修改其二进制文件。这样你可以将自定义代码插入到目标进程执行的特定点。这可以通过在frida命令行中使用–patches或-p选项来实现。 早期插桩意味着我们在应用程序执行之前就加载我们的 JavaScript 脚本。这样,如果应用程序在初始化时有一些安全检查,我们也能够拦截这些检查。
# 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()
没有早期 instrumentation:这是在不修改目标进程的二进制文件的情况下启动Frida。相反,你依赖Frida内置的仪器来监控和与目标进程交互。这是Frida的默认操作模式。
# 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()
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);
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() {}
});
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);
}
});
Interceptor.attach(Module.findExportByName(null, "fopen"), {
onEnter: function(args) {
console.log("Interceptor attached onEnter...");
},
onLeave: function(args) {
console.log("Interceptor attached onLeave...");
}
}
null表示搜索所有动态库
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!");
}
})
});
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'));
}
});
}
}
});
可用nm --demangle --dynamic mylib.so 来获取函数地址。
nm 是 “name” 的缩写,表示显示符号表;
–demangle 选项用于将C++编译器生成的符号名进行反混淆,以便于阅读;它会尝试将编译后的符号名称“解码”回人类可读的源代码名称。这通常用于识别编译后的函数名,它们可能会因为编译器的优化而被转换成缩写或混淆的名称。
–dynamic 选项用于指示 nm 专注于动态链接时使用的符号。这意味着它会列出与动态链接相关的符号,这通常包括外部引用的函数和变量,以及动态加载的模块中的符号
$ 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
我们的目标是改变apk的执行流程,以便Jniint返回一个由我们定义的值。
这个问题有两种解决方法:
在Java层进行hook,也就是说我们拦截Java对JNI的调用,因此我们根本不需要处理C代码。
深入到C语言中实现Jniint,并在那里进行我们的调整。
通常第一种方法更容易实现,但有时第二种方法可能更方便。这完全取决于应用程序正在做什么以及你试图实现什么。
方法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
};
});
方法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);
}
});
Java.perform(function() {
Java.enumerateLoadedClasses({
"onMatch": function(c) {
if (c.includes("erev0s")) {
console.log(c);
}
},
onComplete: function() {}
});
});
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"));
}
});