An old enemy – Diving into QBot part 1

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.

Triage sandbox overview of the analysed sample

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

VirtualAlloc routine captured in IDA

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.

Some exported functions of 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)
Searching for GetProcAddress in the debugger

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.

Determining position of final dependencies

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.

Return back to where we started at
Graph overview of start func packed
Graph overview of start func unpacked

5 – IoCs

Sample SHA256c23c9580f06fdc862df3d80fb8dc398b666e01a523f06ffa8935a95dce4ff8f4

Scroll to Top