While checking out the Triage Sandbox[1] I stumbled across upon QBot which I’ve seen already plenty of times at work at GData Cyberdefense AG[2]. This time I wanted to take a closer look at the sample myself.
The first part of this blog article dives deep into how the packer works.
Quick summary
The packer used by this sample first allocates virtual memory and fills it with chunks of bytes from its .text
section.
After jumping into this allocated area, the address of GetProcAddress
[3] is determined by looping over the export table of KernelBase.dll
. This function is then used to load further dependencies.
Next another temporary memory is allocated, filled with decrypted code and replaces the code we started with. Finally the sample jumps back to the now decrypted payload and executes it.
1 – Allocating VirtualAlloc
The first step itself does not decrypt any code, however it writes bytes in 0x64
chunks into virtual memory 2304
times (0x38400 / 0x64)
. The position of these chunks are calculated loop after loop and do not lie linear in the memory.
2 – Loading dependencies
Once the virtual memory is allocated we can dump the code and load it into IDA to analyse it.
After returning the base address of the KernelBase.dll
, the offset to the GetProcAddress
function is determined by iterating over the export table.
KernelBase.dll
Explaining this behaviour in pseudo code makes it clearer:
func = "GetProcAddress";
symbols = getSymbols()
for symbol in symbol:
if symbol == func:
return getOffsetToFunc(symbol)
With GetProcAddress
the location of LoadLibrary
is returned. By using these two functions the packer is now able to write offsets of needed library functions into memory.
3 – Decrypt the code
In the third step the actual payload is being prepared. VirtualAlloc
[4] sets up another memory area which is used to hold decrypted code temporarily. After the decryption is finished a fully unpacked PE file lies now in memory. The PE sections we started with are zero’ed and replaced with the new decrypted sections.
Some exported functions are still missing. In order to determine their position the same trick is used which I already explained in the second step. This time though, different libraries are used.
4 – Returning to the payload
All that is left now is to return to the unpacked sample via return instruction because the return address is still written onto the stack.
5 – IoCs
Sample SHA256 | c23c9580f06fdc862df3d80fb8dc398b666e01a523f06ffa8935a95dce4ff8f4 |