Frida/QBDI

QBDI can team up with Frida to be even more powerful together. To be able to use QBDI bindings while injected into a process with Frida, it is necessary to understand how to use Frida to perform some common tasks beforehand. Through this simple example based on qbdi-frida-template (see Generate a template), we will explain a basic usage of Frida and QBDI.

Common tasks

This section solely shows a few basic actions and gives you a general overview of what you can do with Frida. Keep in mind that Frida offers many more awesome features, all listed in the Javascript API documentation.

Read memory

Sometimes it may be necessary to have a look at a buffer or specific part of the memory. We rely on Frida to do it.

var arrayPtr = ptr(0xDEADBEEF)
var size = 0x80
var buffer = Memory.readByteArray(arrayPtr, size)

Write memory

We also need to be able to write memory:

var arrayPtr = ptr(0xDEADBEEF)
var size = 0x80
var toWrite = new Uint8Array(size);
// fill your buffer eventually
Memory.writeByteArray(arrayPtr, toWrite)

Allocate an array

If you have a function that takes a buffer or a string as an input, you might need to allocate a new buffer using Frida:

// allocate and write a 2-byte buffer
var buffer = Memory.alloc(2);
Memory.writeByteArray(buffer, [0x42, 0x42])
// allocate and write an UTF8 string
var str = Memory.allocUtf8String("Hello World !");

Initialise a VM object

If frida-qbdi.js (or a script requiring it) is successfully loaded in Frida, a new VM() object becomes available. It provides an object oriented access to the framework features.

// initialise QBDI
var vm = new VM();
console.log("QBDI version is " + vm.version.string);
var state = vm.getGPRState();

Instrument a function with QBDI

You can instrument a function using QBDI bindings. They are really close to the C++ ones, further details about the exposed APIs are available in the Frida/QBDI API documentation.

var functionPtr = DebugSymbol.fromName("function_name").address;
vm.addInstrumentedModule("demo.bin");

var InstructionCallback = vm.newInstCallback(function(vm, gpr, fpr, data) {
    inst = vm.getInstAnalysis();
    gpr.dump(); // display the context
    console.log("0x" + inst.address.toString(16) + " " + inst.disassembly); // display the instruction
    return VMAction.CONTINUE;
});
var iid = vm.addCodeCB(InstPosition.PREINST, instructionCallback, NULL);

vm.call(functionPtr, []);

If you ever want to pass custom arguments to your callback, this can be done via the data argument:

// this callback is used to count the number of basic blocks executed
var userData = { counter: 0};
var BasicBlockCallback = vm.newVMCallback(function(vm, evt, gpr, fpr, data) {
    data.counter++;
    return VMAction.CONTINUE;
});
vm.addVMEventCB(VMEvent.BASIC_BLOCK_ENTRY, BasicBlockCallback, userData);
console.log(userData.counter);

Scripts

Bindings can simply be used in Frida REPL, or imported in a Frida script, empowering the bindings with all the nodejs ecosystem.

import { VM } from "./frida-qbdi.js";

var vm = new VM();
console.log("QBDI version is " + vm.version.string);

It will be possible to load it in Frida in place of frida-qbdi.js, allowing to easily create custom instrumentation tools with in-process scripts written in JavaScript and external control in Python (or any language supported by Frida).

Compilation

In order to actually import QBDI bindings into your project, your script needs be compiled with the frida-compile utility. Installing it requires you to have npm installed. The babelify package might be also needed. Otherwise, you will not be able to successfully compile/load it and some errors will show up once running it with Frida.

Before running frida-compile, be sure that the script frida-qbdi.js is inside you current directory.

find <archive_extracted_path> -name frida-qbdi.js -exec cp {} . \;

# if frida-compile is not already installed
npm install frida-compile babelify
./node_modules/.bin/frida-compile MyScript.js -o MyScriptCompiled.js
# else
frida-compile MyScript.js -o MyScriptCompiled.js

Run Frida/QBDI on a workstation

To use QBDI on an already existing process you can use the following syntax:

frida -n processName -l MyScriptCompiled.js

You can also spawn the process using Frida to instrument it with QBDI as soon as it starts:

frida -f binaryPath Arguments -l MyScriptCompiled.js

Run Frida/QBDI on an Android device

Since Frida provides a great interface to instrument various types of target, we can also rely on it to use QBDI on Android, especially when it comes to inspecting applications. Nevertheless, it has some specificities you need to be aware of. Before running your script, make sure that:

  • a Frida server is running on the remote device and is reachable from your workstation

  • the libQBDI.so library has been placed in /data/local/tmp (available in Android packages)

  • SELinux has been turned into permissive mode (through setenforce 0)

Then, you should be able to inject your script into a specific Android application:

# if the application is already running
frida -Un com.app.example -l MyScriptCompiled.js

# if you want to spawn the application
frida -Uf com.app.example -l MyScriptCompiled.js

Concrete example

If you have already had a look at the default instrumentation of the template generated with qbdi-frida-template (see Generate a template), you are probably familiar with the following example. Roughly speaking, what it does is creating a native call to the Secret() function, and instrument it looking for XOR.

Source code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#if defined(_MSC_VER)
#define EXPORT __declspec(dllexport)
#else // _MSC_VER
#define EXPORT __attribute__((visibility("default")))
#endif

EXPORT int Secret(char *str) {
  int i;
  unsigned char XOR[] = {0x51, 0x42, 0x44, 0x49, 0x46, 0x72, 0x69, 0x64, 0x61};
  size_t len = strlen(str);

  printf("Input string is : %s\nEncrypted string is : \n", str);

  for (i = 0; i < len; i++) {
    printf("0x%x,", str[i] ^ XOR[i % sizeof(XOR)]);
  }
  printf("\n");
  fflush(stdout);
  return 0;
}

void Hello() { Secret("Hello world !"); }

int main() { Hello(); }

Frida/QBDI script

// QBDI
import { VM, InstPosition, VMAction } from "./frida-qbdi.js";

// Initialize QBDI
var vm = new VM();
var state = vm.getGPRState();
var stack = vm.allocateVirtualStack(state, 0x100000);

// Instrument "Secret" function from demo.bin
var funcPtr = Module.findExportByName(null, "Secret");
if (!funcPtr) {
    funcPtr = DebugSymbol.fromName("Secret").address;
}
vm.addInstrumentedModuleFromAddr(funcPtr);

// Callback on every instruction
// This callback will print context and display current instruction address and dissassembly
// We choose to print only XOR instructions
var icbk = vm.newInstCallback(function(vm, gpr, fpr, data) {
    var inst = vm.getInstAnalysis();
    if (inst.mnemonic.search("XOR")){
        return VMAction.CONTINUE;
    }
    gpr.dump(); // Display context
    console.log("0x" + inst.address.toString(16) + " " + inst.disassembly); // Display instruction dissassembly
    return VMAction.CONTINUE;
});
var iid = vm.addCodeCB(InstPosition.PREINST, icbk);

// Allocate a string in remote process memory
var strP = Memory.allocUtf8String("Hello world !");
// Call the Secret function using QBDI and with our string as argument
vm.call(funcPtr, [strP]);

Generate a template

A QBDI template can be considered as a baseline project, a minimal component you can modify and build your instrumentation tool on. They are provided to help you effortlessly start off a new QBDI based project. If you want to get started using QBDI bindings, you can create a brand-new default project doing:

make NewProject
cd NewProject
qbdi-frida-template

# if you want to build the demo binary
mkdir build && cd build
cmake ..
make

# if frida-compile is not already installed
npm install frida-compile babelify
./node_modules/.bin/frida-compile ../FridaQBDI_sample.js -o RunMe.js
# else
frida-compile ../FridaQBDI_sample.js -o RunMe.js

frida -f ./demo.bin -l ./RunMe.js