一、目标
今天给大家介绍一个新朋友, QBDI
它可以快速集成到你的frida脚本里面来进行汇编级的Trace
二、步骤
安装
在Androd上使用QBDI非常方便,先去官网下载一个最新版本
https://github.com/QBDI/QBDI/releases/download/v0.10.0/QBDI-0.10.0-android-AARCH64.tar.gz
针对我们Frida的使用,主要是里面的两个文件 libQBDI.so 和 frida-qbdi.js , 前一个是注入库,后一个是js的封装
然后把 libQBDI.so 放到 /data/local/tmp 目录
adb push libQBDI.so /data/local/tmp
使用
第一种用法是主动调用目标函数来Trace
由于需要加载 frida-qbdi.js 模块,所以这里我们需要用到frida的模块开发。 通常的方法是 采用大胡子的 https://github.com/oleavr/frida-agent-example 一个使用TS的模版。
不过懒得再去学一个TS语言了,所以我们今天用另外一个办法
然后使用 frida-compile 编译
// 编译
frida-compile -w index.js -o frida-qbdi-agent.js
// 启动Hook
frida -Uf com.example.myapplication --runtime=v8 -l frida-qbdi-agent.js
主动调用
考虑apk中的so里面有一个addTestFenfei函数
extern "C" int addTestFenfei(){
int a = 2;
int b = 3;
int c = a ^ b;
c = c + a + b;
LOGD("addTestFenfei : %d", c);
return c;
}
我们在warp_vm_run.js里面就可以用下面的方式来主动调用
/**
* warp_vm_run的函数定义不要修改
* @param {*} vm_run_func 会调用qbdi的vm.call
* @param {*} log_file_path 日志文件的路径
*/
export default function warp_vm_run(vm_run_func, log_file_path) {
let libnative_name = "libmyapplication.so"; // 替换为你的.so库的名称
let func_name = "addTestFenfei";
let libnative_base = Process.findModuleByName(libnative_name).base;
console.log("warp_vm_run libnative_base 0x" + libnative_base.toString(16));
// 获取native方法地址
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);
}
然后挂上心爱的frida执行
Failed to load /data/local/tmp/libQBDI.so (dlopen failed: couldn't map "/data/local/tmp/libQBDI.so" segment 1: Permission denied)
报错了,木有权限, 那就给它
每一行的 执行代码,寄存器变化和内存读写就都打印出来了
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
如果要调用 有参数的函数,可以这么来做
let ret = vm_run_func(null,func_addr, [2,3], log_file_path);
Hook替换
有些时候,我们不想构造参数去主动调用函数,而是想在app执行的过程中,去hook替换目标函数,然后打印它的真实流程。
这里要注意的就是两点, 1是替换,2是更新上下文,也就是更新寄存器的值
在traceCodeQBDI.js文件 vm_run函数里面,
// postSync 是否同步回来
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;
}
然后在warp_vm_run中做替换
export default function warp_vm_run(vm_run_func, log_file_path) {
let libnative_name = "libmyapplication.so"; // 替换为你的.so库的名称
let libnative_base = Process.findModuleByName(libnative_name).base;
console.log("warp_vm_run libnative_base 0x" + libnative_base.toString(16));
// hook替换
//*
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);
// // qbdi执行
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"]));
// */
}
这样执行就可以打印出实际app跑的时候的指令了。
三、总结
指令级的Trace还有很多应用场景,比如只打印XOR指令,或者只监控SVC指令。
参考资料
https://github.com/lasting-yang/frida-qbdi-tracer
https://blog.quarkslab.com/why-are-frida-and-qbdi-a-great-blend-on-android.html
当星星变成星空,梦想也就近在咫尺了
关注微信公众号,最新技术干货实时推送