This blog post introduces the release of QBDI v0.7.0 as well as an Android use case.
Tl;dr: QBDI v0.7.0 is out. This new version adds the x86 architecture and you can find packages on QBDI website as well as the changelog.
Introduction
It has been almost a year since the last QBDI release and we are glad to announce that QBDI 0.7.0 is out! For those who are not familiar with QBDI, you may have a look at the presentation at 34C3 [1]. The project is also available on Github along with examples and documentation.
This new version adds support for the x86 architecture besides the already supported x86-64 instruction set.
To showcase these improvements, the next part deals with the first stage of the Tencent's packer and more precisely, how QBDI can enhance its analysis.
Android use case: Tencent packer
Tencent's packer is one of the protectors widely used in Asia to protect applications and in some cases malwares [2]. While the whole analysis of the packer would require a dedicated blog post, this small use case shows how to use both QBDI and LIEF to address the first stage.
The APK's entrypoint is located in a Java method that basically loads a native library which implements the main logic of the packer. This native library is usually named libshella.<version>.so or libshellx<version>.so, respectively for the ARM and x86 architecture.
The first stage of the packer protects the .text section by encoding its content after the compilation of the library. It is then dynamically decoded with an ELF constructor that is executed when the library is loaded.
One way to address this protection is to instrument the decoding routing by adding memory callbacks on instructions that write the clear bytes. Then using LIEF, we can rewrite — on the fly — the clear bytes of the .text section.
Even though the decoding routine is not very complicated and could be reversed statically, this technique does not rely on the potential complexity of the function as we are just looking for the clear bytes being written. No matter how they are decoded.
As the packer is likely to write (clear) bytes in the .text section, and because the segment associated with this section is read only, we may expect call(s) to functions, such as mprotect(), that will change the permission. Being able to catch external calls can also be useful to understand the behavior of the packer.
The first part of this blog post deals with the detection of external calls with QBDI while the second is about memory accesses and how to track them with QBDI.
QBDI Instrumentation
To take advantage of dlopen() and because the decoding routine is implemented in an ELF constructor, we first need to disable the constructor so that dlopen() does not trigger its execution. Then, we can execute the constructor in QBDI to observe the memory accesses and the external calls to libraries.
$ readelf -d libshellx-3.0.0.0.so
...
0x00000019 (INIT_ARRAY) 0x3e88
0x0000001b (INIT_ARRAYSZ) 8 (bytes)
...
$ python
>>> import lief
>>> lib = lief.parse("libshellx-3.0.0.0.so")
>>> print(lib.get(lief.ELF.DYNAMIC_TAGS.INIT_ARRAY))
INIT_ARRAY 3e88 [0x931, 0x0]
$ readelf -d libshellx-3.0.0.0_WITHOUT_CONSTR.so
...
0x00000019 (INIT_ARRAY) 0x3e88
0x0000001b (INIT_ARRAYSZ) 0 (bytes)
...
We can bootstrap QBDI and the analysis of the library with the following template:
#include <dlfcn.h>
#include <QBDI.h>
#include <LIEF/LIEF.hpp>
int main(int argc, char** argv) {
const char path[] = "/data/local/tmp/libshellx-3.0.0.0_WITHOUT_CTOR.so";
// Library loading
std::unique_ptr<LIEF::ELF::Binary> lib_lief = LIEF::ELF::Parser::parse(path);
void* handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
QBDI::rword ctr_addr = libshell_base_addr + /* constructor */ 0x931;
// QBDI initialization
QBDI::VM vm;
uint8_t *fakestack = nullptr;
// Allocate a stack for QBDI
QBDI::allocateVirtualStack(vm.getGPRState(), 1 << 20, &fakestack);
// Setup QBDI callbacks (see next sections)
...
// Only instrument the library
vm.addInstrumentedModuleFromAddr(libshell_base_addr);
// Run the constructor in QBDI
QBDI::rword ret;
vm.call(&ret, ctr_addr, /* no arguments */{});
// Free the constructor stack
QBDI::alignedFree(fakestack);
return 0;
}
Resolving external calls
The ExecBroker is a component of QBDI that aims to detect calls outside of the instrumented code range [3]. Basically, it stops the instrumentation process on the called function and resumes the instrumentation when the function finishes. Such a mechanism is very convenient to avoid instrumenting functions such as malloc or printf that may share mutex or global variables with QBDI's code.
The ExecBroker is exposed through events (EXEC_TRANSFER_CALL, EXEC_TRANSFER_RETURN) that can be listened with the VM.addVMEventCB() method.
int main(int argc, char** argv) {
...
// Setup the onExecBroker callback to catch external calls
vm.addVMEventCB(QBDI::EXEC_TRANSFER_CALL, onExecBroker, nullptr);
...
}
In the onExecBroker() callback, one can use LIEF to convert the address of the call (located in eip) into a symbol name:
QBDI::VMAction onExecBroker(QBDI::VMInstanceRef vm, const QBDI::VMState *vmState, QBDI::GPRState *gprState, ...) {
std::string function;
bool name_found = false;
// Find the library with full path that contains EIP
for (const QBDI::MemoryMap& map : QBDI::getCurrentProcessMaps(/* fullpath */true)) {
if ((map.permission & QBDI::PF_EXEC) and map.range.contains(gprState->eip)) {
std::unique_ptr<LIEF::ELF::Binary> externlib = LIEF::ELF::Parser::parse(map.name);
const uintptr_t sym_offset = gprState->eip - map.range.start;
// Resolve the offset into a symbol name using LIEF
for (const LIEF::ELF::Symbol& sym : externlib->exported_symbols()) {
if (sym_offset == sym.value()) {
function = sym.demangled_name();
name_found = true;
break;
}
}
break;
}
}
if (name_found) {
printf("External call to: %s", function.c_str());
...
} else {
printf("Cannot resolve the address %p\n", (void*) gprState->eip);
}
return QBDI::CONTINUE;
}
It leads to the following output while running on the constructor function:
External call to: mprotect(0xa7853000, 8192, PROT_READ | PROT_WRITE) External call to: mprotect(0xa7853000, 8192, PROT_READ | PROT_EXEC) External call to: getenv("DEX_PATH") External call to: __android_log_print
Following memory accesses
QBDI also provides an API to only instrument memory accesses (reads and writes) for non-SIMD instructions. The VM.addMemRangeCB() method enables to trigger callback(s) when an instruction tries to read or write on a memory area.
Especially, we can setup this kind of callback to catch instructions from the constructor that write the clear bytes in the .text section.
struct context_t {
LIEF::ELF::Binary* lib_lief;
QBDI::Range<QBDI::rword>& patch_range;
QBDI::rword libshell_base_addr;
};
int main(int argc, char** argv) {
...
// find .text range
...
// Setup analysis context
context_t ctx = {
lib_lief.get(), // Handler on LIEF's ELF::Binary*
libshellx_code_range, // Code range of the .text section
libshell_base_addr
};
// Setup the callback
vm.addMemRangeCB(libshellx_code_range.start, libshellx_code_range.end, QBDI::MEMORY_WRITE, onWrite, &ctx);
// Run through QBDI
QBDI::rword ret;
vm.call(&ret, ctr_addr, /* no argument */{});
...
}
Then, we can persistently patch the library using LIEF's Binary.patch_address(). After the execution in QBDI, we can write the modified library.
QBDI::VMAction onWrite(QBDI::VMInstanceRef vm, QBDI::GPRState *gprState, QBDI::FPRState *fprState, void *raw_data) {
context_t *data = reinterpret_cast<context_t*>(raw_data);
std::vector<QBDI::MemoryAccess> mem_access = vm->getInstMemoryAccess();
for (const QBDI::MemoryAccess& access: mem_access) {
if (access.type == QBDI::MEMORY_WRITE and data->patch_range.contains(access.accessAddress)) {
data->lib_lief->patch_address(
access.accessAddress - data->libshell_base_addr,
access.value,
access.size);
}
}
return QBDI::CONTINUE;
}
int main(int argc, char** argv) {
...
// After run in QBDI, rewrite the library
lib_lief->write("out.so");
...
}
The unpackaged library contains clear .text section.
By looking at the strings of the unpacked library, we can notice new ones:
$ strings -tx ./libshellx-DECODED.so ... 2040 /system/lib/libhoudini.so 205a can not found sym:%s 206f txtag 2124 base:%p fix offset! 2138 ro.build.version.sdk 214d version:%d 2158 load library %s at offset %x read count %x 2184 min_vaddr:%x size:%x 219a load_bias:%p base:%p 21b0 read count:%x 21be 1.2.3 21c4 Tx:12345Tx:12345 21d8 seg_start:%p size:%x infsize:%x offset:%x 2203 do relocate! 2211 replace 2219 syminfo:%p new:%p size:%x 2233 strtab:%p size:%x 2245 bucket:%p bucket:%p size:%x 2264 set back protect of the memory 2284 init func:%p 2292 init array func:%p 22a8 /proc/self/maps 22b8 %lx-%lx %s %s %s %s %s 22cf JNI_OnLoad 22da load done! 22e5 DEX_PATH 22ee env path:%p 22fa env path:%s ...
Then, we can go ahead with the main analysis of the packer.
The source code associated with this use case is available on Github: QBDI/examples/packer-android-x86
What's next
As illustrated in the blog post: Android Native Library Analysis with QBDI [4], we are getting closer to a full ARM support in QBDI. Nevertheless we still need to polish its integration alongside the x86-64 and x86 architectures. It should be available in further releases of QBDI.
Regarding the AArch64 support, we had some design concerns that made its development harder than the three other architectures. We managed to resolve these issues and the support for this architecture — that includes SIMD instructions — is on the right path (i.e. it runs on obfuscated code and cryptographic libraries).
Are you using QBDI? If so, let us know! We would be really interested in having feedback. How are you using it? What did you (dis)like about it, and what features/improvements would you be interested in? (You can ping us at qbdi@quarkslab.com or #qbdi on freenode)
Acknowledgments
Thanks to Cédric T. for his work on this release! Many thanks to our colleagues for the feedback and for proofreading this blog post.
References
[1] | https://media.ccc.de/v/34c3-9006-implementing_an_llvm_based_dynamic_binary_instrumentation_framework |
[2] | https://www.fortinet.com/blog/threat-research/unmasking-android-malware-a-deep-dive-into-a-new-rootnik-variant-part-i.html |
[3] | Call to libc's malloc |
[4] | https://blog.quarkslab.com/android-native-library-analysis-with-qbdi.html |