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