Get Started with Frida/QBDI¶
To be able to use QBDI bindings while injecting into a process, it is necessary to understand a bit of Frida to perform some common tasks. Through this simple example based on qbdi-frida-template we will explain a basic usage of Frida & QBDI.
Common tasks¶
Most actions described here are listed in the Frida documentation, this is mostly a reminder for those used to interact with Frida.
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 bytes buffer
var buffer = Memory.alloc(2);
Memory.writeByteArray(buffer, [0x42, 0x42])
// allocate and write an UTF8 string
var str = Memory.allocUtf8String("Hello World !");
Initialize a QBDI object¶
If frida-qbdi.js (or a script requiring it) is successfully loaded in Frida, a new QBDI object become available. It provides an object oriented access to the framework features.
// Initialize QBDI
var vm = new QBDI();
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, with more information is available in Frida/QBDI API bindings 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 context
console.log("0x" + inst.address.toString(16) + " " + inst.disassembly); // Display instruction
return VMAction.CONTINUE;
});
var iid = vm.addCodeCB(InstPosition.PREINST, instructionCallback, NULL);
vm.call(functionPtr, []);
If you ever want to pass argument to your callback, this can be done via the data argument :
// This callback is used to count the number of basicblocks 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.
const qbdi = require('/usr/local/share/qbdi/frida-qbdi'); // import QBDI bindings
qbdi.import(); // Set bindings to global environment
var vm = new QBDI();
console.log("QBDI version is " + vm.version.string);
This simple script can be compiled with frida-compile utility (see Frida documentation). 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).
Complete example¶
If you already had a look at the default instrumentation of the template generated with qbdi-frida-template you will be familiar with the following example. What it does is creating a native call to the Secret() function, and instrument it looking for XOR.
Source code¶
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.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();
}
Instrumentation code¶
// QBDI
const qbdi = require('/usr/local/share/qbdi/frida-qbdi'); // import QBDI bindings
qbdi.import(); // Set bindings to global environment
// Initialize QBDI
var vm = new QBDI();
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");
}
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) {
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]);