QBDIPreload
QBDIPreload is a small utility library that provides code injection capabilities using dynamic library injection.
It works on Linux and macOS respectively with the LD_PRELOAD
and DYLD_INSERT_LIBRARIES
mechanisms.
Thanks to QBDIPreload, you can instrument the main function of an executable that has been dynamically linked. You can also define various callbacks that are called at specific times throughout the execution.
Main hook process
To use QBDIPreload, you must have a minimal codebase: a constructor and several hook functions. Like callbacks, hook functions are directly called by QBDIPreload.
First of all, the constructor of QBDIPreload has to be initialised through declaring the macro QBDIPRELOAD_INIT
.
It’s worth noting that this macro must be only defined once in your code.
The qbdipreload_on_start()
and qbdipreload_on_premain()
hook functions are called at different stages during the execution of the programme.
They only need to return QBDIPRELOAD_NOT_HANDLED
if you don’t want to modify the hook procedure.
#include "QBDIPreload.h"
QBDIPRELOAD_INIT;
int qbdipreload_on_start(void *main) {
return QBDIPRELOAD_NOT_HANDLED;
}
int qbdipreload_on_premain(void *gprCtx, void *fpuCtx) {
return QBDIPRELOAD_NOT_HANDLED;
}
Instrumentation
Once the main function is hooked by QBDIPreload, two methods are called: qbdipreload_on_main()
and qbdipreload_on_run()
.
At this point, you are able to capture the executable arguments inside of the qbdipreload_on_main()
scope.
The qbdipreload_on_run()
function is called right afterwards with a ready-to-run QBDI virtual machine as first argument.
Obviously, don’t forget to register your callback(s) prior to running the VM.
static VMAction onInstruction(VMInstanceRef vm, GPRState *gprState, FPRState *fprState, void *data) {
// ...
return QBDI_CONTINUE;
}
int qbdipreload_on_main(int argc, char** argv) {
return QBDIPRELOAD_NOT_HANDLED;
}
int qbdipreload_on_run(VMInstanceRef vm, rword start, rword stop) {
// add user callbacks
qbdi_addCodeCB(vm, QBDI_PREINST, onInstruction, NULL);
// run the VM
qbdi_run(vm, start, stop);
return QBDIPRELOAD_NO_ERROR;
}
Note
QBDIPreload automatically takes care of blacklisting instrumentation of the C standard library and the OS loader as described in Limitations.
Exit hook
QBDIPreload also intercepts the calls on standard exit functions (exit
and _exit
).
Typically, these are called when the executable is about to terminate.
If so, the qbdipreload_on_exit()
method is called and can be used to save some data about the execution you want to keep before exiting.
Note that the hook function is not called if the executable exits with a direct system call or a segmentation fault.
int qbdipreload_on_exit(int status) {
return QBDIPRELOAD_NO_ERROR;
}
Compilation and execution
Finally, you need to compile your source code to a dynamic library. Your output binary has to be statically linked with both the QBDIPreload library and the QBDI library.
Then, in order to test it against a target, simply running the following command should do the job:
# on Linux
LD_BIND_NOW=1 LD_PRELOAD=./libqbdi_mytracer.so <executable> [<parameters> ...]
# on macOS
sudo DYLD_BIND_AT_LAUNCH=1 DYLD_INSERT_LIBRARIES=./libqbdi_mytracer.so <executable> [<parameters> ...]
As the loader is not in the instrumentation range, we recommend setting LD_BIND_NOW
or DYLD_BIND_AT_LAUNCH
in order to resolve and bind all symbols before the instrumentation.
Full example
Merging everything we have learnt throughout this tutorial, we are now able to write our C/C++ source code files. In the following examples, we aim at displaying every executed instruction of the binary we are running against.
QBDIPreload in C
#include <stdio.h>
#include "QBDIPreload.h"
QBDIPRELOAD_INIT;
static VMAction onInstruction(VMInstanceRef vm, GPRState *gprState,
FPRState *fprState, void *data) {
const InstAnalysis *instAnalysis = qbdi_getInstAnalysis(
vm, QBDI_ANALYSIS_INSTRUCTION | QBDI_ANALYSIS_DISASSEMBLY);
printf("0x%" PRIRWORD " %s\n", instAnalysis->address,
instAnalysis->disassembly);
return QBDI_CONTINUE;
}
int qbdipreload_on_start(void *main) { return QBDIPRELOAD_NOT_HANDLED; }
int qbdipreload_on_premain(void *gprCtx, void *fpuCtx) {
return QBDIPRELOAD_NOT_HANDLED;
}
int qbdipreload_on_main(int argc, char **argv) {
return QBDIPRELOAD_NOT_HANDLED;
}
int qbdipreload_on_run(VMInstanceRef vm, rword start, rword stop) {
qbdi_addCodeCB(vm, QBDI_PREINST, onInstruction, NULL, 0);
qbdi_run(vm, start, stop);
return QBDIPRELOAD_NO_ERROR;
}
int qbdipreload_on_exit(int status) { return QBDIPRELOAD_NO_ERROR; }
QBDIPreload in C++
#include <iomanip>
#include <iostream>
#include "QBDIPreload.h"
static QBDI::VMAction onInstruction(QBDI::VMInstanceRef vm,
QBDI::GPRState *gprState,
QBDI::FPRState *fprState, void *data) {
const QBDI::InstAnalysis *instAnalysis = vm->getInstAnalysis();
std::cout << std::setbase(16) << instAnalysis->address << ": "
<< instAnalysis->disassembly << std::endl
<< std::setbase(10);
return QBDI::CONTINUE;
}
extern "C" {
QBDIPRELOAD_INIT;
int qbdipreload_on_start(void *main) { return QBDIPRELOAD_NOT_HANDLED; }
int qbdipreload_on_premain(void *gprCtx, void *fpuCtx) {
return QBDIPRELOAD_NOT_HANDLED;
}
int qbdipreload_on_main(int argc, char **argv) {
return QBDIPRELOAD_NOT_HANDLED;
}
int qbdipreload_on_run(QBDI::VMInstanceRef vm, QBDI::rword start,
QBDI::rword stop) {
vm->addCodeCB(QBDI::PREINST, onInstruction, nullptr);
vm->run(start, stop);
return QBDIPRELOAD_NO_ERROR;
}
int qbdipreload_on_exit(int status) { return QBDIPRELOAD_NO_ERROR; }
}
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. The binary responsible for generating a template is shipped in the release packages and can be used as follows:
mkdir QBDIPreload && cd QBDIPreload
qbdi-preload-template
mkdir build && cd build
cmake ..
make
MacOS setup
When QBDIPreload (and also PyQBDIPreload) are used on macOS, the injection can failed for various reasons.
Copy the target binary
If the binary is a system binary, the
System Integrity Protection
prevent the injection of library with DYLD_INSERT_LIBRARIES
. You can try to
copy the binary in your user home folder before the instrumentation to avoid the
protection.
Run the instrumentation as root
The injection of library can be disable for standard user. Run the injection
with sudo
can bypass this limitation.
Instrument arm64e
binary
QBDIPreload is not compatible with arm64e
binary. To verify if your binary is a
arm64e
binary, you can execute the followed command:
lipo -archs $BINARY_PATH
You can try to convert your target binary from arm64e
to arm64
with the
followed script. However, this don’t work if the target binary use some arm64e
bind
opcode (like BIND_OPCODE_THREADED
).
import lief
import subprocess
binary = lief.MachO.parse(inputfile)
for index in range(binary.size):
if binary.at(index).header.cpu_type == lief.MachO.CPU_TYPES.ARM64:
binary.at(index).header.cpu_subtype ^= (~2)
binary.write(outputfile)
subprocess.run(["codesign", "--force", "--sign", "-", outputfile], check=True)
Disable System Integrity Protection
We recommand to disable the System Integrity Protection only as a last resort,
as we successfully inject the QBDIPreload under macOS Ventura
(version
13.1). However, this may help you to debug the injection and bypass some
signature validation. You can found the procedure here.
Don’t forget to reenable it afterwards.