QakBOT v5 Deep Malware Analysis

17 minute read

Meet Qakbot

QakBot, also recognized as QBot, QuackBot, and Pinkslipbot, has been operational for years, initially as a financial malware targeting governments and businesses for financial fraud by pilfering user credentials and keystrokes.

Over time, it has evolved into a malware dropper, spreading sensitive information to other network systems. The threat group has updated its code-base to support 64-bit versions of Windows, enhanced encryption algorithms, and added further obfuscation techniques. With the release of Qakbot version 5.0, the string encryption algorithm underwent a significant change. While strings are still encrypted using a simple XOR key, the key is no longer hard-coded in the data section. Instead, it is encrypted with AES.

Technical in Points

  1. Qakbot uses API hashing to hide its imports. It uses CRC32 hashing, along with another layer of XORing with a hard-coded key. It’s parsing the loaded DLLs in memory and getting its export tables. As a result, Qakbot can resolve imported APIs and build its IAT.
  2. Qakbot comes with encrypted strings inside the .data section, These strings are encrypted using a XOR key and that key is encrypted using AES algorithm.
  3. Environment Detection: Qakbot includes checks to detect if it is running in a virtual machine or sandbox environment, commonly used tools for malware analysis. If such conditions are detected, Qakbot may change its behavior or terminate itself to avoid detection.
  4. Configuration Extraction: Qakbot comes with AES encrypted configuration.This configuration contains details related to the malicious campaign and the C2 which the malware will communicate with for further commands.
  5. C2 Communication: After extracting its C2, Qakbot establishes a connection with its C2 servers to receive commands for downloading, executing additional modules, updating configuration values, and exfiltrating gathered information from the infected system.
  6. Qakbot gathers comprehensive information about the compromised host to send to its C2 server and create a unique victim fingerprint. This includes OS version, domain trusts, computer name, username, screen resolution, system time, system uptime, and bot uptime. It mainly relies on Windows Management Instrumentation(WMI) to collect details such as hardware ID, installed languages, and installed programs.

Sample Basic Information

SHA-256 af6a9b7e7aefeb903c76417ed2b8399b73657440ad5f8b48a25cfe5e97ff868f
File type Win DLL
Target Machine x64
Creation Time 2024-01-29 13:43:37 UTC
First Seen In The Wild 2024-02-07 10:12:50 UTC

Figure(1): sample on VirusTotal


Anti Analysis

API Resolution

QakBot uses Windows API Hashing (Dynamic API Resolution) to evade signature-based anti-malware scanners and make static analysis harder.

Figure(2): API hashes


We can see based on algorithm constants that Qakbot uses the CRC32 hash algorithm, also there is another layer of XORing, and here are the steps in some detail :

The DllName is decrypted by XORing with a hard-coded key 0xA235CB91. After decryption, a handle to the DLL is obtained. This handle is then passed to a function that iterates over the DLL’s exported functions. A function resolves the addresses of the exports by iterating over the export table of the module, hashing the name of each export using CRC32, and comparing the result with a hard-coded CRC32 hash to determine if it has found the correct address.

Figure(3): API resolving Steps


With knowledge of the algorithm name and XOR key, we can use the awesome hashdb plugin from OALabs that performs string hash lookup against a remote database.

API Response Process

Figure(4): hashdb result


Once the HashDB plugin decrypts all API names, we create structures to store the API lists from each DLL. This simplifies our workflow and make our life easier while analysis .

API Response Process

Figure(5): populated IAT

Defeating encrypted Strings

Qakbot strings are obfuscated, making the analysis more difficult, so the next step is to decrypt them.

Decryption routine

This version decrypts the strings with an XOR key just like the earlier versions but this XOR key is encrypted using the AES algorithm.

It first Calculates a SHA256 hash for aes_key_ref and uses the calculated hash as the AES Key then decrypts the enc_xor_key blob using AES in CBC mode to have the dec_xor_key.

Figure(6): The XOR key decryption process


The final step is to use the dec_xor_key to decrypt the string array.

Figure(7): String decryption process

Writing a decryption script

We now can write an IDAPython script to decrypt the strings and add comments to the code, making analysis easier here are some notes before the script :

  • The first 16 bytes of the enc_xor_key are used as the AES IV.
  • There are two encrypted string tables used.
  • There are two decryption functions with 4 wraps.
  • The wrap function decrypts the string array and selects the string based on an index [the only argument].

Figure(8): Index pattern used in script

#--------------- imports --------------------#
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import idautils
#------------- helper ------------------------#
def hex_to_int(x):
    if type(x) == int :
        return x 
    return (int(x[:-1], 16))
def search_by_index(table , ind):
    return(table[ind:].split('\x00')[0])
#------------- IDA py ------------------------#
def read_data_ida(address,size):
  data = idc.get_bytes(address, size)
  return data
def set_comment(address, text):
    idc.set_cmt(address, text,0)
#------------ Decryption ---------------------#
def calculate_sha256(input_data):
    sha256_hash = hashlib.sha256()
    sha256_hash.update(input_data)
    hash_hex = sha256_hash.digest()
    return hash_hex
def aes_decrypt(ciphertext, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    unpadded_plaintext = unpad(plaintext, AES.block_size)
    return unpadded_plaintext
def xor_decrypt(data,key):
  dec_data = ''
  for i in range(len(data)):
      dec_data += chr(data[i] ^ key[i % len(key)])
  return dec_data
def full_dec(enc_str , enc_xor_key , aes_key_init):
    aes_key = calculate_sha256(aes_key_init)
    dec_xor_key = aes_decrypt(enc_xor_key[16:],aes_key,enc_xor_key[:16])
    dec_str = xor_decrypt(enc_str,dec_xor_key)
    return dec_str
#----------- Decrypt enc str tbl 1 -------------#
enc_str_1 , enc_xor_key_1 , aes_key_init_1 = read_data_ida(0x1800297A0 , 0x1836) , read_data_ida(0x18002AFE0,0xA0) , read_data_ida(0x180029700,0x9F) #read our data . 
tbl_1 = full_dec(enc_str_1,enc_xor_key_1,aes_key_init_1)
#----------- Decrypt enc str tbl 2 -------------#
enc_str_2 , enc_xor_key_2 , aes_key_init_2 = read_data_ida(0x1800282A0 , 0x5AD) , read_data_ida(0x1800281C0,0xD0) , read_data_ida(0x180028150,0x63) #read our data . 
tbl_2 = full_dec(enc_str_2,enc_xor_key_2,aes_key_init_2)

#--> pattern used: mov ecx , immediate_val
def do_magic(table,references):
    for ref in references:
        prev_instruction_address = idc.prev_head(ref)   
        if (idc.print_insn_mnem(prev_instruction_address) == 'mov' and idc.print_operand(prev_instruction_address,0) == 'ecx' and idc.get_operand_type(prev_instruction_address,1) == 5):
            ind = print_operand(prev_instruction_address,1)
            set_comment(ref,search_by_index(table,hex_to_int(ind)))
        else : 
            prev_instruction_address = idc.prev_head(prev_instruction_address)
            if (idc.print_insn_mnem(prev_instruction_address) == 'mov' and idc.print_operand(prev_instruction_address,0) == 'ecx' and idc.get_operand_type(prev_instruction_address,1) == 5):
                ind = print_operand(prev_instruction_address,1)
                set_comment(ref,search_by_index(table,hex_to_int(ind)))
            else:
                prev_instruction_address = idc.prev_head(prev_instruction_address)
                if (idc.print_insn_mnem(prev_instruction_address) == 'mov' and idc.print_operand(prev_instruction_address,0) == 'ecx' and idc.get_operand_type(prev_instruction_address,1) == 5):
                    ind = print_operand(prev_instruction_address,1)
                    set_comment(ref,search_by_index(table,hex_to_int(ind)))
                else:
                    print('not working' ,hex(ref))

reference_1 = list(idautils.CodeRefsTo(idc.get_name_ea_simple("wrap_mw_decrpyion_fun_1"), 0)) #codeRefs-to need "ea" as arguemt . 
reference_1 = reference_1 + list(idautils.CodeRefsTo(idc.get_name_ea_simple('wrap_2_mw_decrpyion_fun_1') , 0))

reference_2 = list(idautils.CodeRefsTo(idc.get_name_ea_simple('wrap_2_mw_decrpyion_fun_2'), 0))
reference_2 = reference_2 + list(idautils.CodeRefsTo(idc.get_name_ea_simple('wrap_mw_decrpyion_fun_2'), 0))
def main():
    do_magic(tbl_1,reference_1)
    do_magic(tbl_2,reference_2)

if __name__ == '__main__':
    main()

API Response Process

Figure(9): IDA python script result


you can get the full decrypted strings list from here

Emulation Check

Qakbot uses the GetFileAttributesW function to check for a folder "C:\INTERNAL__empty." If this directory exists, it suggests that the environment might be used for analysis, such as Microsoft Defender emulation or sandbox, and then the process will be terminated.

Figure(10): emulation check

Checking Processes

Qakbot loops through running processes on the system and compares their executable names against well-known static and dynamic malware analysis tools.

Figure(11): Qakbot search for tool's process

full processes list

Expand to see more
  wireshark.exe
  filemon.exe
  procmon.exe
  idaq64.exe
  tcpview.exe
  frida-winjector-helper-32.exe
  frida-winjector-helper-64.exe
  tcpdump.exe
  windump.exe
  ethereal.exe
  ettercap.exe
  rtsniff.exe
  packetcapture.exe
  capturenet.exe
  qak_proxy
  dumpcap.exe
  CFF Explorer.exe
  not_rundll32.exe
  ProcessHacker.exe
  loaddll32.exe
  PETools.exe
  ImportREC.exe
  LordPE.exe
  SysInspector.exe
  proc_analyzer.exe
  sysAnalyzer.exe
  sniff_hit.exe
  joeboxcontrol.exe
  joeboxserver.exe
  ResourceHacker.exe
  x64dbg.exe
  Fiddler.exe
  sniff_hit.exe
  sysAnalyzer.exe
  BehaviorDumper.exe
  processdumperx64.exe
  anti-virus.EXE
  sysinfoX64.exe
  sctoolswrapper.exe
  sysinfoX64.exe
  FakeExplorer.exe
  apimonitor-x86.exe
  idaq.exe
  dumper64.exe
  user_imitator.exe
  Velociraptor.exe

Anti VM

Qakbot exploits Windows Management Instrumentation (WMI), a system management technology used to administer remote systems and provide comprehensive data about the operating system, hardware, and installed software and applications on a computer.

  • It uses WMI queries to gather system information, including details about virtualization. It queries classes such as Win32_ComputerSystem, Win32_Bios, Win32_DiskDrive, or Win32_PhysicalMemory, then check for patterns indicative of virtualized environments. These patterns include known manufacturer or model strings associated with virtualization platforms.

Below are the classes and their corresponding checked values :

Class Checked Values
Win32_ComputerSystem MS_VM_CERT, VMware, Virtual Machine
Win32_Bios VRTUAL, VMware, VMW, Xen
Win32_DiskDrive VMware, PROD_VIRTUAL_DISK, VIRTUAL-DISK, XENSRC, 20202020
Win32_PhysicalMemory VMware, VMW, QEMU
Win32_PnPEntity QEMU, VMware Pointing, VMware Accelerated, VMware SCSI,..


Qakbot also searches for ‘vmnat’, a process initiated by VMware upon startup. ‘vmnat’ manages communication in the Network Address Translation (NAT) set up with the guest machine .

Qakbot’s C2 Functionality

Malware needs to connect to C2 servers to execute remote commands, update its code, and exfiltrate stolen data. Before doing so, it needs to extract its C2 from an encrypted configuration.

Configuration Extraction

Qakbot, in this version, contains an embedded AES encrypted configuration within its .data section.

Figure(12): Encrypted configuration


The AES decryption method used is the same as the one we’ve seen for decrypting strings. The key will be SHA-256 hashed before attempting the decryption, the first 16 bytes of the encrypted string used as IV. Then use the final key to decrypt the rest encrypted data.

API Response Process

Figure(13): Decrypt the campaign INFO


With the same method and key, Qakbot will decrypt its C2 list .

API Response Process

Figure(14): Decrypt the C2 list


With this information, we can reuse our string decryption script with some edits to have the configuration . notice that :

  • The first 32 bytes in the decrypted data represent the SHA-256 validation, a cryptographic process used for data integrity verification. These bytes serve as a hash value that allows systems to confirm the authenticity and integrity of the data being processed.

We can see the output of the script (configuration).

API Response Process

Figure(15): the Decrypted configuration the malware use

C2 communication

QakBot mainly uses HTTP for C2 communication. The malware communicates with its C2 servers through encrypted AES payloads and then encodes the result in Base64.

Figure(16): C2 communication fun


API Response Process

Figure(17): AES Encryption and the key used while C2 communication

Gather system INFO

Part of QakBOT communication with its command and control is sending information about the computer. QakBot gathers computer information using a combination of Windows API calls, shell commands, and Windows Management Instrumentation (WMI) commands. This approach allows it to collect various details about the system, including hardware, software, and configuration data. By using these methods together, QakBot obtains a comprehensive overview of the target computer’s setup and specifications.

VMI Queries Used

Qakbot builds a WMI query by concatenating strings to form It then executes these queries to retrieve critical data and obtain a comprehensive overview of the system’s configuration and installed security measures.

API Response Process

Figure(18): Qakbot create VMI queries


Here are the WMI classes targeted and the information they retrieve:

Class Properties Result
Win32_OperatingSystem Caption OS Info [name and version]
AntiVirusProduct * Information about antivirus products installed on a system
Win32_Processor * Information about the processor
Win32_ComputerSystem * Information about the computer system, including its hardware configuration, such as the manufacturer, model, system type, number of processors, memory
Win32_Bios * Details about a computer’s BIOS, like its version, manufacturer, and release date
Win32_DiskDrive * Information about the disk drives installed on a computer, including their model, manufacturer, interface type, capacity
Win32_PhysicalMemory * Details about the physical memory modules in use, including their capacity, speed, manufacturer
Win32_Product Caption, Description, Vendor, Version, InstallDate, InstallSource, PackageName Information about installed software, including its name, description, vendor, version, installation date, installation source, and package name
Win32_PnPEntity Caption, Description, DeviceID, Manufacturer, Name, PNPDeviceID, Service, Status Details about Plug and Play devices, such as their name, description, device ID, manufacturer, name, PnP device ID, service, and status

Windows command line

Qakbot creates anonymous pipes to execute various built-in command-line tools processes, enabling it to retrieve information about the compromised system’s environment effectively.

API Response Process

Figure(19): execute command-line tools


Here is the list of commands that can be used to gather information about the system:

Windows Command Output
ipconfig /all Displays detailed configuration information about all network interfaces.
whoami /all Displays user, group, and privileges information for the current user.
nltest /domain_trusts /all_trusts Lists all domain trusts established with the current domain.
qwinsta Lists information about all Remote Desktop sessions on the local system.
nslookup -querytype=ALL -timeout=12 _ldap._tcp.dc._msdcs.%s Performs a DNS lookup for LDAP service records for the specified domain controller.
net share Lists information about shared resources on the local system.
net localgroup Lists information about local groups on the local system.
netstat -nao Lists active network connections and associated processes.
net view Lists information about shared resources on remote systems.
route print Displays the IP routing table for the local system.
arp -a Displays the ARP cache, which contains mappings of IP addresses to MAC addresses.

Additionally, it will use Windows API calls to get different system details like computer name, screen size, AD domain info, user name, processor details, whether it’s a 32-bit or 64-bit Windows, and the operating system version, along with its respective full paths.

Collect AntiViruses Information

Qakbot checks for specific antivirus programs like Kaspersky, Avast, Norton, etc to see if any antivirus software is active on the system. It does this by scanning running programs and looking for related processes from these vendors.

This list shows which antivirus vendors are associated with each process :

processes Related Vendor
ccSvcHst.exe;NortonSecurity.exe;nsWscSvc.exe Norton Security
avgcsrvx.exe;avgsvcx.exe;avgcsrva.exe AVG Antivirus
MsMpEng.exe Microsoft Defender Antivirus
avp.exe;kavtray.exe Kaspersky Antivirus
coreServiceShell.exe;PccNTMon.exe;NTRTScan.exe Trend Micro Antivirus
fshoster32.exe F-Secure Antivirus
fmon.exe FortiClient Antivirus
egui.exe;ekrn.exe ESET
bdagent.exe;vsserv.exe;vsservppl.exe Bitdefender
AvastSvc.exe;aswEngSrv.exe;aswToolsSvc.exe;afwServ.exe;aswidsagent.exe;AvastUI.exe Avast
Sophos UI.exe;SophosUI.exe;SAVAdminService.exe;SavService.exe Sophos
WRSA.exe Webroot SecureAnywhere
vkise.exe;isesrv.exe;cmdagent.exe Kaspersky
ByteFence.exe ByteFence
MBAMService.exe;mbamgui.exe Malwarebytes
mcshield.exe McAfee
dwengine.exe;dwarkdaemon.exe;dwwatcher.exe Datawatch
SentinelServiceHost.exe;SentinelStaticEngine.exe;SentinelAgent.exe;… SentinelOne
SonicWallClientProtectionService.exe;SWDash.exe SonicWall
CynetEPS.exe;CynetMS.exe;CynetConsole.exe Cynet
CSFalconService.exe;CSFalconContainer.exe CrowdStrike Falcon

Executing C2 Commands

After establishing communication, the C2 server will send commands to be executed. These commands are represented as integer values or indexes.

Figure(20): The list of the C2 commands used by Qakbot


Process Hollowing

QakBot selects a system process for process hollowing based on the machine’s architecture (32-bit or 64-bit) and the installed antivirus software.

This list includes the following system processes:

Expand to see more
  %SystemRoot%\SysWOW64\AtBroker.exe
  %SystemRoot%\System32\AtBroker.exe
  %SystemRoot%\SysWOW64\xwizard.exe
  %SystemRoot%\System32\xwizard.exe
  %SystemRoot%\SysWOW64\explorer.exe
  %SystemRoot%\explorer.exe
  %SystemRoot%\SysWOW64\wermgr.exe
  %SystemRoot%\System32\wermgr.exe
  %SystemRoot%\SysWOW64\OneDriveSetup.exe
  %SystemRoot%\System32\OneDriveSetup.exe
  %SystemRoot%\SysWOW64\msra.exe
  %SystemRoot%\System32\msra.exe
  %SystemRoot%\SysWOW64\mobsync.exe
  %SystemRoot%\System32\mobsync.exe


It first calls the CreateProcessW() API with the CREATE_SUSPENDED flag to start a new process, making it suspended at the beginning.

API Response Process

Figure(21): create a suspended process


Then it allocates virtual memory in a target process, writes data into the allocated region, and then modifies the memory protection to allow execution.

API Response Process


Next, it retrieves the context of the thread to modify it to set the instruction pointer (EIP/RIP register) to point to the entry point of the injected code.

It finally calls the API ResumeThread() to resume the new processes.

Persistence

QakBot sets itself to run on system reboot through a registry entry or Scheduled Task.

API Response Process

Figure(22): Persistence function


Conclusion

Qakbot is an advanced malware with regular updates and powerful anti-analysis actions, ensuring it remains a persistent threat with a wide range of capabilities and techniques.

YARA Rule

rule detect_Qakbot_v5
{
    meta:
        description = "just a rule for Qakbot v5"
        author = "Mohamed Ezzat (@ZW01f)"
        hash1  = "af6a9b7e7aefeb903c76417ed2b8399b73657440ad5f8b48a25cfe5e97ff868f"
        hash2  = "59559e97962e40a15adb2237c4d01cfead03623aff1725616caeaa5a8d273a35"
    strings:
        $s1 = "\\u%04X\\u%04X" ascii wide
        $s2 = "%u;%u;%u" ascii wide 
        $s3 = "CfGetPlatformInfo" ascii wide
        $p1 = {45 33 C0 E8 ?? ?? ?? ?? 35 91 CB 35 A2 41 3B C7}
        $p2 = { 0F B6 01 48 FF C1 44 33 C0 41 8B C0 41 C1 E8 04 83 E0 0F 44 33 04 82 41 8B C0 41 C1 E8 04 83 E0 0F 44 33 04 82 49 83 E9 01 75 ?? 41 F7 D041 8B C0 C3}
    condition:
        uint16(0) == 0x5A4D and all of ($p*) and (2 of ($s*)) and filesize < 500KB
} 


Python Automated Configuration Extraction

This python script is used to extract the configuration of the Qakbot malware :

  • Open the binary file.
  • Get the .data section.
  • Extract the the key and the encrypted configuration data .
  • SHA-256 hash the extracted key to get the final key.
  • Use the key to decrypt the configurations.
  • Parse the decrypted configurations to extract useful information.
#--------------- imports --------------------#
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import socket
from datetime import datetime
import pytz
#------------- helper ------------------------#
def extract_data(filename): #finds the content of the ".data" section. .
    import pefile
    pe = pefile.PE(filename)
    for section in pe.sections:
        if '.data' in section.Name.decode(encoding='utf-8').rstrip('x00'):
            return (section.get_data(section.VirtualAddress, section.SizeOfRawData))
def tohex(data):
    import binascii
    if type(data) == str:
        return binascii.hexlify(data.encode('utf-8'))
    else:
        return binascii.hexlify(data)

def get_ip(ip_binary):
    # Convert the binary network format to a human-readable string format
    ip_str = socket.inet_ntoa(ip_binary)
    return ip_str
#------------ Decryption ---------------------#
def calculate_sha256(input_data):
    sha256_hash = hashlib.sha256()
    sha256_hash.update(input_data)
    hash_hex = sha256_hash.digest()
    return hash_hex
def aes_decrypt(ciphertext, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    unpadded_plaintext = unpad(plaintext, AES.block_size)
    return unpadded_plaintext
def full_dec(enc_str , aes_key_init):
    aes_key = calculate_sha256(aes_key_init)
    dec_str = aes_decrypt(enc_str[17:],aes_key,enc_str[1:17])
    return dec_str
def parse_camp(input_str):
    lines = input_str.strip().split(b'\r\n')
    parsed_data = {}
    for line in lines:
        key, value = line.split(b'=')
        parsed_data[key] = value
    timestamp = int(parsed_data[b'3'])
    dt_obj = pytz.utc.localize(datetime.utcfromtimestamp(timestamp))
    print(f"Botnet ID : {parsed_data[b'10']}'")
    print(f"b'40' : {parsed_data[b'40']}'")
    print(f"Campaign Timestamp : {dt_obj}")
def parse_c2(dec_ips):
    i = 0 
    splitted_data = [dec_ips[i:i+7] for i in range(1, len(dec_ips), 8)]
    for data in splitted_data:
        ip = get_ip(data[:4])
        port = int(tohex(data[4:6]),16)
        print('IP[{0}] = {1}:{2}'.format(i,ip,port))
        i = i + 1
def main():
    file_name = input("enter the file path: ")
    # The config data begins at these offsets inside the .data section
    enc_ips_rva = 0x852 ; size_rva = 0x850 ; enc_config_rva = 0x1022 
    data_section = extract_data(file_name) #read data section
    size = ord(data_section[size_rva:size_rva+1])
    enc_config_ips = data_section[enc_ips_rva:enc_ips_rva+size]
    enc_config = data_section[enc_config_rva:enc_config_rva+size]
    init_key = b'T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9'
    aes_key = calculate_sha256(init_key)
    campaign_info = full_dec(enc_config,init_key)
    dec_c2 = full_dec(enc_config_ips,init_key)
    print('##------------------- Campaign Info -------------------##')
    print('sha256 :',tohex(campaign_info[:32]))
    print('#--------------------------------------#')
    parse_camp(campaign_info[32:])
    print('##------------------- Qakbot c2 -------------------##')
    print('sha256 :',tohex(dec_c2[:32]))
    print('#--------------------------------------#')
    parse_c2(dec_c2[32:])
    
if __name__ == '__main__':
    main()

References