QBDI User Guide
1. Objectives
Today I would like to introduce a new friend to you, QBDI
It can be quickly integrated into your frida script to perform assembly-level trace
2. Steps
Install
It is very convenient to use QBDI on Android. First go to the official website to download the latest version
https://github.com/QBDI/QBDI/releases/download/v0.10.0/QBDI-0.10.0-android-AARCH64.tar.gz
For our use of Frida, there are mainly two files in itlibQBDI.soandfrida-qbdi.js, the former is the injection library, the latter is the js encapsulation
Then putlibQBDI.soPut it in the /data/local/tmp directory
adb push libQBDI.so /data/local/tmp
use
The first usage is to actively call the target function to trace
Because it needs to loadfrida-qbdi.jsModule, so here we need to use frida’s module development. The usual method is to use a template using TS from big bearded https://github.com/oleavr/frida-agent-example .
But I’m too lazy to learn another TS language, so today we use another method
index.js
import {
VM,
InstPosition,
VMAction,
Options,
MemoryAccessType,
AnalysisType,
RegisterAccessType,
OperandType,
SyncDirection
} from "./frida-qbdi.js";
.... call fridaQBDI
Then compile using frida-compile
frida-compile -w index.js -o frida-qbdi-agent.js
frida -Uf com.example.myapplication --runtime=v8 -l frida-qbdi-agent.js
Active call
Consider that there is an addTesth1yx function in the so in the apk
extern "C" int addTesth1yx(){
int a = 2;
int b = 3;
int c = a ^ b;
c = c + a + b;
LOGD("addTesth1yx : %d", c);
return c;
}
We can use the following method to actively call in warp_vm_run.js
/**
* warp_vm_run
* @param {*} vm_run_func
* @param {*} log_file_path
*/
export default function warp_vm_run(vm_run_func, log_file_path) {
let libnative_name = "libmyapplication.so";
let func_name = "addTesth1yx";
let libnative_base = Process.findModuleByName(libnative_name).base;
console.log("warp_vm_run libnative_base 0x" + libnative_base.toString(16));
const func_addr = Module.findExportByName(libnative_name, func_name);
console.log("func_addr = " + func_addr);
let ret = vm_run_func(null,func_addr, [], log_file_path,false);
console.log(ret);
}
Then hang up your beloved frida and execute
Failed to load /data/local/tmp/libQBDI.so (dlopen failed: couldn't map "/data/local/tmp/libQBDI.so" segment 1: Permission denied)
It’s an error. I don’t have permission. Then give it
#setenforce 0
Each line of executed code, register changes, and memory read and write are printed out
start vm.call ===
0x77d5661b00 [libmyapplication.so!0x1eb00] stp x29, x30, [sp, #-16]! r[FP=0x77d6b03100 LR=0x2a SP=0x77d6b03080] w[SP=0x77d6b03070]
memory write at 77d6b03070, data size = 8, data value = 77d6b03100
memory write at 77d6b03078, data size = 8, data value = 2a
0x77d5661b04 [libmyapplication.so!0x1eb04] mov x29, sp r[SP=0x77d6b03070] w[FP=0x77d6b03070]
0x77d5661b08 [libmyapplication.so!0x1eb08] adrp x8, #172032 w[X8=0x77d568b000]
0x77d5661b0c [libmyapplication.so!0x1eb0c] ldrb w8, [x8, #3912] r[X8=0x77d568b000] w[W8=0x1]
memory read at 77d568bf48, data size = 1, data value = 1
0x77d5661b10 [libmyapplication.so!0x1eb10] cbz w8, #32 r[W8=0x1]
0x77d5661b14 [libmyapplication.so!0x1eb14] adrp x1, #-40960 w[X1=0x77d5657000]
0x77d5661b18 [libmyapplication.so!0x1eb18] adrp x2, #-40960 w[X2=0x77d5657000]
0x77d5661b1c [libmyapplication.so!0x1eb1c] add x1, x1, #1895 r[X1=0x77d5657000] w[X1=0x77d5657767]
0x77d5661b20 [libmyapplication.so!0x1eb20] add x2, x2, #806 r[X2=0x77d5657000] w[X2=0x77d5657326]
0x77d5661b24 [libmyapplication.so!0x1eb24] mov w0, #3 w[W0=0x3]
0x77d5661b28 [libmyapplication.so!0x1eb28] mov w3, #6 w[W3=0x6]
0x77d5661b2c [libmyapplication.so!0x1eb2c] bl #148868 r[SP=0x77d6b03070] w[LR=0x77d5661b30]
0x77d56860b0 [libmyapplication.so!0x430b0] adrp x16, #16384 w[X16=0x77d568a000]
0x77d56860b4 [libmyapplication.so!0x430b4] ldr x17, [x16, #3032] r[X16=0x77d568a000] w[X17=0x78cb2dd788]
memory read at 77d568abd8, data size = 8, data value = 78cb2dd788
0x77d56860b8 [libmyapplication.so!0x430b8] add x16, x16, #3032 r[X16=0x77d568a000] w[X16=0x77d568abd8]
0x77d56860bc [libmyapplication.so!0x430bc] br x17 r[X17=0x78cb2dd788]
0x77d5661b30 [libmyapplication.so!0x1eb30] mov w0, #6 w[W0=0x6]
0x77d5661b34 [libmyapplication.so!0x1eb34] ldp x29, x30, [sp], #16 r[SP=0x77d6b03070] w[FP=0x77d6b03100 LR=0x2a SP=0x77d6b03080]
memory read at 77d6b03070, data size = 8, data value = 77d6b03100
memory read at 77d6b03078, data size = 8, data value = 2a
0x77d5661b38 [libmyapplication.so!0x1eb38] ret r[LR=0x2a]
cost is 0.063s
0x6
If you want to call a function with parameters, you can do it like this
let ret = vm_run_func(null,func_addr, [2,3], log_file_path);
Hook Replacement
Sometimes, we don’t want to construct parameters to actively call functions, but want to hook and replace the target function during the execution of the app, and then print its actual process.
There are two points to note here: 1 is replacement, and 2 is updating the context, that is, updating the value of the register
In the vm_run function of the traceCodeQBDI.js file,
function vm_run(ctx,func_ptr, args, log_file_path,postSync) {
let start_time = new Date().getTime();
let vm = new VM();
vm.setOptions(Options.OPT_DISABLE_LOCAL_MONITOR | Options.OPT_BYPASS_PAUTH | Options.OPT_ENABLE_BTI)
var state = vm.getGPRState();
vm.allocateVirtualStack(state, 0x100000);
if(postSync){
console.log("==== synchronizeContext FRIDA_TO_QBDI ");
state.synchronizeContext(ctx,SyncDirection.FRIDA_TO_QBDI);
}
......
if(postSync){
console.log("synchronizeContext QBDI_TO_FRIDA ====");
state.synchronizeContext(ctx,SyncDirection.QBDI_TO_FRIDA);
}
return ret;
}
Then replace it in warp_vm_run
export default function warp_vm_run(vm_run_func, log_file_path) {
let libnative_name = "libmyapplication.so";
let libnative_base = Process.findModuleByName(libnative_name).base;
console.log("warp_vm_run libnative_base 0x" + libnative_base.toString(16));
//*
let env = Java.vm.tryGetEnv();
console.log(JSON.stringify(env));
let func_name = "Java_com_example_myapplication_MainActivity_FFTestAdd";
let func_addr = Module.findExportByName(libnative_name, func_name);
console.log("Hook func_addr = " + func_addr);
Interceptor.replace(func_addr,new NativeCallback(function (vmEnv,vmContext,a,b){
console.log(" ============== ");
console.log("[+] " + func_addr.sub(libnative_base) + "(" + a + ", " + b + ") called");
Interceptor.revert(func_addr);
Interceptor.flush();
console.log(env.handle);
var retVal = vm_run_func(this.context, func_addr, [vmEnv,vmContext,a,b],log_file_path,true);
warp_vm_run(vm_run_func,log_file_path);
const resultStr = env.stringFromJni(retVal);
console.log("Result: " + resultStr);
return retVal;
}, "pointer", ["pointer", "pointer","int","int"]));
// */
}
This execution can print out the instructions when the actual app is running.
Tip:
If there are parameters such as jobject MainActivity /* this */ in the function parameters, no matter it is an active call or a hook call, it will crash when the function returns. The reason has not been found yet:(
Conclusion
Instruction-level trace also has many application scenarios, such as printing only XOR instructions, or monitoring only SVC instructions.
References
https://github.com/lasting-yang/frida-qbdi-tracer
https://blog.quarkslab.com/why-are-frida-and-qbdi-a-great-blend-on-android.html