Basic block events
Introduction
The Instrument Callback can insert callbacks on all or specific instructions. With a callback on every instruction, it’s trivial to follow the execution pointer and obtain a trace of the execution. However, performances are bad because the execution is stopped on each instruction for the callback to be run. For some traces, a higher-level callback may have better performances.
The VM callbacks is called when some conditions are reached during the execution. This tutorial introduces 3 VMEvent:
BASIC_BLOCK_NEW
is triggered when a new basic block has been instrumented and added to the cache. It can be used to create coverage of the execution.BASIC_BLOCK_ENTRY
is triggered before the execution of a basic block.BASIC_BLOCK_EXIT
is triggered after the execution of a basic block.
A basic block in QBDI
QBDI doesn’t analyze the whole program before the run. Basic blocks are dynamically detected and so may not match basic blocks given by other tools. In QBDI, a basic block is a sequence of consecutive instructions that do not modify the instruction pointer except for the last one. Any instruction that may modify the instruction pointer (method call, jump, conditional jump, method return, …) are always the last instruction of a basic block.
For QBDI, is the beginning for a basic block:
the very first instruction to be executed in QBDI;
the first instruction to be executed after the end of the previous basic block;
the first instruction to be executed if the user modifies the execution flow (add a new callback, clear the cache, return
BREAK_TO_VM
, …)
Due to the dynamic detection of the basic block, basic blocks may overlap each other. This behavior can be observed in the following code:
# BB
push rbp # 0x1000
mov rbp, rsp # 0x1001
mov dword ptr [rbp - 0x14], edi # 0x1004
mov edx, dword ptr [rbp - 0x14] # 0x1007
mov eax, edx # 0x100a
shl eax, 2 # 0x100c
add eax, edx # 0x100f
mov dword ptr [rbp - 4], eax # 0x1011
cmp dword ptr [rbp - 0x14], 0xa # 0x1014
jle 0x1027 # 0x1018
# BB
add dword ptr [rbp - 4], 0x33 # 0x101e
jmp 0x1033 # 0x1022
# BB
mov eax, dword ptr [rbp - 4] # 0x1027
imul eax, eax # 0x102a
add eax, 0x57 # 0x102d
mov dword ptr [rbp - 4], eax # 0x1030
# BB
mov edx, dword ptr [rbp - 4] # 0x1033
mov eax, dword ptr [rbp - 0x14] # 0x1036
add eax, edx # 0x1039
pop rbp # 0x103b
ret # 0x103c
In this snippet, QBDI can detect 4 different basic blocks. If the first jump isn’t taken:
The begin of the method, between 0x1000 and 0x101e;
The block between 0x101e and 0x1027;
The last block between 0x1033 and 0x103d.
If the first jump is taken:
The begin of the method, between 0x1000 and 0x101e;
The last block between 0x1027 and 0x103d.
Getting basic block information
To receive basic block information, a VMCallback
should be registered to the VM with addVMEventCB
for
one of BASIC_BLOCK_*
events. Once a registered event occurs, the callback is ran with a description of the VM (VMState
).
The address of the current basic block can be retrieved with VMState.basicBlockStart
and VMState.basicBlockEnd
.
Note
A callback may register for both BASIC_BLOCK_NEW
and BASIC_BLOCK_ENTRY
events but would be called only once would these two events happen at the same time.
You can retrieve the events that triggered the callback in VMState.event
.
The following example registers for the three events in the VM and displays the basic block’s bounds.
C basic block information
Reference: VMCallback
, qbdi_addVMEventCB()
, VMState
VMAction vmcbk(VMInstanceRef vm, const VMState* vmState, GPRState* gprState, FPRState* fprState, void* data) {
printf("start:0x%" PRIRWORD ", end:0x%" PRIRWORD "%s%s%s\n",
vmState->basicBlockStart,
vmState->basicBlockEnd,
(vmState->event & QBDI_BASIC_BLOCK_NEW)? " BASIC_BLOCK_NEW":"",
(vmState->event & QBDI_BASIC_BLOCK_ENTRY)? " BASIC_BLOCK_ENTRY":"",
(vmState->event & QBDI_BASIC_BLOCK_EXIT)? " BASIC_BLOCK_EXIT":"");
return QBDI_CONTINUE;
}
qbdi_addVMEventCB(vm, QBDI_BASIC_BLOCK_NEW | QBDI_BASIC_BLOCK_ENTRY | QBDI_BASIC_BLOCK_EXIT, vmcbk, NULL);
C++ basic block information
Reference: QBDI::VMCallback
, QBDI::VM::addVMEventCB()
, QBDI::VMState
QBDI::VMAction vmcbk(QBDI::VMInstanceRef vm, const QBDI::VMState* vmState, QBDI::GPRState* gprState, QBDI::FPRState* fprState, void* data) {
std::cout << std::setbase(16) << "start:0x" << vmState->basicBlockStart
<< ", end:0x" << vmState->basicBlockEnd;
if (vmState->event & QBDI::BASIC_BLOCK_NEW) {
std::cout << " BASIC_BLOCK_NEW";
}
if (vmState->event & QBDI::BASIC_BLOCK_ENTRY) {
std::cout << " BASIC_BLOCK_ENTRY";
}
if (vmState->event & QBDI::BASIC_BLOCK_EXIT) {
std::cout << " BASIC_BLOCK_EXIT";
}
std::cout << std::endl;
return QBDI::CONTINUE;
}
vm.addVMEventCB(QBDI::BASIC_BLOCK_NEW | QBDI::BASIC_BLOCK_ENTRY | QBDI::BASIC_BLOCK_EXIT, vmcbk, nullptr);
PyQBDI basic block information
Reference: pyqbdi.VMCallback()
, pyqbdi.VM.addVMEventCB()
, pyqbdi.VMState
def vmcbk(vm, vmState, gpr, fpr, data):
# user callback code
print("start:0x{:x}, end:0x{:x} {}".format(
vmState.basicBlockStart,
vmState.basicBlockEnd,
vmState.event & (pyqbdi.BASIC_BLOCK_NEW | pyqbdi.BASIC_BLOCK_ENTRY | pyqbdi.BASIC_BLOCK_EXIT) ))
return pyqbdi.CONTINUE
vm.addVMEventCB(pyqbdi.BASIC_BLOCK_NEW | pyqbdi.BASIC_BLOCK_ENTRY | pyqbdi.BASIC_BLOCK_EXIT, vmcbk, None)
Frida/QBDI basic block information
Reference: VMCallback()
, VM.addVMEventCB()
, VMState()
var vmcbk = vm.newVMCallback(function(vm, state, gpr, fpr, data) {
var msg = "start:0x" + state.basicBlockStart.toString(16) + ", end:0x" + state.basicBlockEnd.toString(16);
if (state.event & VMEvent.BASIC_BLOCK_NEW) {
msg = msg + " BASIC_BLOCK_NEW";
}
if (state.event & VMEvent.BASIC_BLOCK_ENTRY) {
msg = msg + " BASIC_BLOCK_ENTRY";
}
if (state.event & VMEvent.BASIC_BLOCK_EXIT) {
msg = msg + " BASIC_BLOCK_EXIT";
}
console.log(msg);
return VMAction.CONTINUE;
});
vm.addVMEventCB(VMEvent.BASIC_BLOCK_NEW | VMEvent.BASIC_BLOCK_ENTRY | VMEvent.BASIC_BLOCK_EXIT, vmcbk, null);
Basic block coverage
To perform code coverage, BASIC_BLOCK_NEW
can be used to detect the new basic block JITed by QBDI.
However, it wouldn’t work in the following cases:
If the code jumps outside of the instrumented range.
If the code triggers an interruption (exception, signal, …)
If the code uses overlapping instructions or other forms of obfuscation.
Moreover, if a VM is reused from an execution to another, cache will be kept and so coverage would be incremental. Clear the cache between every run to have independent coverage results
For more precise coverage, a user may register BASIC_BLOCK_ENTRY
or BASIC_BLOCK_EXIT
events and handle deduplication themselves.
C coverage
// your own coverage library
#include "mycoverage.h"
VMAction covcbk(VMInstanceRef vm, const VMState* vmState, GPRState* gprState, FPRState* fprState, void* data) {
myCoverageAdd( (myCoverageData*) data, vmState->basicBlockStart, vmState->basicBlockEnd);
return QBDI_CONTINUE;
}
myCoverageData cover;
qbdi_addVMEventCB(vm, QBDI_BASIC_BLOCK_NEW, covcbk, &cover);
// run the VM
// ....
// print the coverage
myCoveragePrint(&cover);
C++ coverage
QBDI has a tiny range set class (QBDI::RangeSet
), usable only with the C++ API.
QBDI::VMAction covcbk(QBDI::VMInstanceRef vm, const QBDI::VMState* vmState, QBDI::GPRState* gprState, QBDI::FPRState* fprState, void* data) {
QBDI::RangeSet<QBDI::rword>* rset = static_cast<QBDI::RangeSet<QBDI::rword>*>(data);
rset->add({vmState->basicBlockStart, vmState->basicBlockEnd});
return QBDI::CONTINUE;
}
QBDI::RangeSet<QBDI::rword> rset;
vm.addVMEventCB(QBDI::BASIC_BLOCK_NEW, covcbk, &rset);
// run the VM
// ....
// print the coverage
for (const auto &r: rset.getRanges()) {
std::cout << std::setbase(16) << "0x" << r.start() << " to 0x" << r.end() << std::endl;
}
PyQBDI coverage
def covcbk(vm, vmState, gpr, fpr, data):
if vmState.basicBlockEnd not in data['cov'] or vmState.basicBlockStart < data['cov'][vmState.basicBlockEnd][0]:
data['cov'][vmState.basicBlockEnd] = (vmState.basicBlockStart, vmState.basicBlockEnd)
return pyqbdi.CONTINUE
cov = {"cov": {}}
vm.addVMEventCB(pyqbdi.BASIC_BLOCK_NEW, covcbk, cov)
# run the VM
# ....
for _, c in cov['cov'].items():
print(f"0x{c[0]:x} to 0x{c[1]:x}")
In addition, a coverage script that generates DRCOV coverage is available in examples/pyqbdi/coverage.py.
Frida/QBDI coverage
var covcbk = vm.newVMCallback(function(vm, state, gpr, fpr, cov) {
if ( (! cov[state.basicBlockEnd]) || state.basicBlockStart < cov[state.basicBlockEnd][0] ) {
cov[state.basicBlockEnd] = [state.basicBlockStart, state.basicBlockEnd]
}
return VMAction.CONTINUE;
});
var cov = {};
vm.addVMEventCB(VMEvent.BASIC_BLOCK_NEW, covcbk, cov);
// run the VM
// ....
for(var c in cov){
console.log("0x" + cov[c][0].toString(16) + " to 0x" + cov[c][1].toString(16));
}
Edge coverage
The BASIC_BLOCK_EXIT
event can be used to detect the edge between basic blocks. As the event is triggered at the end of a basic block (ie. after instruction pointer is modified),
the next address can be found in the GPRState. So, the couple (state.basicBlockEnd, gpr.rip)
is the edge to store in the coverage.