Post

Qakbot malware analysis

A detailed write-up for QakBot malware

Qakbot malware analysis

QakBot in nutshell

QakBot, also known as QBot, QuackBot and Pinkslipbot, is a banking Trojan that has existed for over a decade. It was found in the wild in 2007 and since then it has been continually maintained and developed. Its main purpose is to steal banking credentials (e.g., logins, passwords, etc.)


Infection chain

QakBot is known to infect its victims mainly via spam campaigns. In some cases, the emails were delivered with Microsoft Office documents (Word, Excel) or password-protected archives with the documents attached. The documents contained macros and victims were prompted to open the attachments with claims that they contained important information (e.g., an invoice). In some cases, the emails contained links to web pages distributing malicious documents. QakBot infection chain


First look

SHA-193b1ab0a9e70a546c4b89dcb20a158dfc90b1421
SHA-25673e4969db4253f9aeb2cbc7462376fb7e26cc4bb5bd23b82e2af0eaaf5ae66a8
File typeWin32 DLL
Creation Time1992-06-19 22:22:17 UTC
First Submission2021-12-24 13:06:18 UTC

Virus total


Stage-1

The malware imports WriteProcessMemory it seems to inject code in remote process.

After running , here we go , now we have explorer.exe process in suspend state with PE file injected into it , now we dumped the mapped second stage of QakBot.

Process hacker memory view with injected DLL in explorer.exe

After dumping the mapped version I’am going to unmap it with PE-unmapper.


Stage-2

Basic information

SHA-1c5dca02ef2029b0144dc54bdce4889ed16c18126
SHA-2568E2D95CD9F2ED6D40CEB42D92BC9D623524103CF966B6190A8F096CA32AF50B6
File typeWin32 Dll
Creation Time2021-12-09 09:22:27

Anti Analysis


Encrypted string tables

QakBot encoded its strings in two string tables wrapped by 4 functions , every table has its data and key blobs which handled by the encryption routine.

The 4 wrapper functions are used heavily in this sample so this script will handle the wrapper function and do the decryption routine then comment with the string with offset passed to wrapper function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import idaapi
import idautils
import idc
import binascii
import pefile
DATA_FILE = r"PATH"
DEFAULT_STR_SIZE = 0x850
DEFAULT_KEY_SIZE = 0x5A
def decrypter(data_string, data_key):
    if not data_key:
        raise ValueError("XOR key is empty")

    decoded = ""
    for i in range(len(data_string)):
        decoded += chr(data_string[i] ^ data_key[i % len(data_key)])
    return decoded
def string_decrypter_search(enc_data, key_data, offset):
    out = []
    for i in range(len(enc_data)):
        out.append(enc_data[i] ^ key_data[i % len(key_data)])
    result = bytes(out)[offset:].decode("latin", errors="ignore").split("\x00")[0]
    return offset, result


def extract_data(filename):
    pe = pefile.PE(filename)
    for sec in pe.sections:
        name = sec.Name.decode(errors="ignore").rstrip("\x00")
        if name == ".data":
            return sec.get_data()
    raise RuntimeError(".data section not found")


def calc_offsets(seg_start, va):
    return va - seg_start


def string_decrypter(enc_va, key_va, str_offset):

    data_seg_start = None
    for s in idautils.Segments():
        if idc.get_segm_name(s) == ".data":
            data_seg_start = idc.get_segm_start(s)
            break

    if data_seg_start is None:
        raise RuntimeError(".data segment not found in IDA")

    enc_off = calc_offsets(data_seg_start, enc_va)
    key_off = calc_offsets(data_seg_start, key_va)

    data = extract_data(DATA_FILE)

    if enc_off < 0 or key_off < 0:
        raise RuntimeError("Invalid offsets")

    enc_blob = data[enc_off:]
    if b"\x00\x00" in enc_blob:
        enc_size = enc_blob.index(b"\x00\x00")
    else:
        enc_size = DEFAULT_STR_SIZE

    enc_data = data[enc_off:enc_off + enc_size]

    key_blob = data[key_off:]
    if b"\x00\x00" in key_blob:
        key_size = key_blob.index(b"\x00\x00")
    else:
        key_size = DEFAULT_KEY_SIZE

    key_data = data[key_off:key_off + key_size]

    if not enc_data or not key_data:
        raise RuntimeError("Failed to extract encrypted data or key")

    _, result = string_decrypter_search(enc_data, key_data, str_offset)
    return f"string[{str_offset}]: {result}"


def comment_string_offset(enc_addr, key_addr, func_name):
    func_ea = idc.get_name_ea_simple(func_name)
    if func_ea == idc.BADADDR:
        print(f"[!] Function {func_name} not found")
        return

    # Try to decompile function once
    cfunc = None
    if idaapi.init_hexrays_plugin():
        try:
            cfunc = idaapi.decompile(func_ea)
        except Exception:
            pass

    for call_ea in idautils.CodeRefsTo(func_ea, 0):
        prev_ea = idc.prev_head(call_ea)
        args = []
        for _ in range(5):
            mnem = idc.print_insn_mnem(prev_ea)
            if mnem == "push" and idc.get_operand_type(prev_ea, 0) == idc.o_imm:
                args.insert(0, idc.get_operand_value(prev_ea, 0))
            elif mnem == "mov" and idc.get_operand_type(prev_ea, 1) == idc.o_imm:
                args.append(idc.get_operand_value(prev_ea, 1))
            prev_ea = idc.prev_head(prev_ea)

        for arg in args:
            try:
                decrypted = string_decrypter(enc_addr, key_addr, arg)
                comment_text = decrypted.split(": ", 1)[1]
                idc.set_cmt(call_ea, comment_text, 0)
                if cfunc:
                    class CallVisitor(idaapi.ctree_visitor_t):
                        def __init__(self):
                            idaapi.ctree_visitor_t.__init__(self, idaapi.CV_FAST)
                            self.done = False

                        def visit_expr(self, expr):
                            if expr.op == idaapi.cot_call:
                                if expr.cexpr.op == idaapi.cot_obj and expr.cexpr.obj_ea == call_ea:
                                    idaapi.set_citem_cmt(cfunc, expr, comment_text, 0)
                                    self.done = True
                            return 0

                    v = CallVisitor()
                    v.apply_to(cfunc.body, None)
            except Exception:
                pass


print(string_decrypter(0x1001D5A8, 0x1001E3F8, 1486)) #string_table_1
print(string_decrypter(0x1001D0B0, 0x1001D050, 708)) #string_table_2

comment_string_offset(0x1001D5A8, 0x1001E3F8, "mw_w_decode_string_table_0")
comment_string_offset(0x1001D5A8, 0x1001E3F8, "mw_w_decode_string_table_1")
comment_string_offset(0x1001D0B0, 0x1001D050, "mw_w_decode_string_table_2")
comment_string_offset(0x1001D0B0, 0x1001D050, "mw_w_decode_string_table_3")

print("\n[+] Done")

Here we go the script did its magic and we have commented strings!

The result of the decryption script


Emulation check

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

Emulation check


API hashing

QakBot used CRC32 hashing algorithm to hash all its IAT then XORED with value 0x218FE95B to obfuscate all its imports QakBot hash API function

Then used API resolving function to rebuild its IAT , Once using HashDB plugin we will have all resolved QakBot IAT. QakBot API resolving function


Checking process

QakBot searches for process to detect malware analysis machines.

Show process
tcpdump.exe
windump.exe
ethereal.exe
wireshark.exe
ettercap.exe
rtsniff.exe
packetcapture.exe
capturenet.exe
srvpost.exe
frida-winjector-helper-32.exe
frida-winjector-helper-64.exe
    

Anti VM

It uses WMI queries to gather system information, including details about virtualization. It queries classes such as Win32_ComputerSystem, Win32_Bios, Win32_DiskDrive , Win32_processorand Win32_PhysicalMemory then check for virtualized environments. QakBot getting processor information using WMI


Persistence

QakBot maintains persistence on the system through Scheduled Tasks or registry key SOFTWARE\Microsoft\Windows\CurrentVersion\Run to run after reboot. QakBot persistence function 1

Also it has other persistence function that create the same scheduled task and create process with regsvr.exe. QakBot persistence function 2


Code injection

QakBot uses process hollowing technique to inject itself in 32 or 64 bit process in the system This list contains the target system processes for injection

Show processes
   %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
    

QakBot process hollowing function


Evasion

QakBot is concerned in manipulating the key SOFTWARE\Microsoft\Microsoft Antimalware\Exclusions\Paths to eventually include itself as an authorized anti-malware tool and stay out the radar. QakBot Evasion function


Windows command line

QakBot creates anonymous pipes to execute built-in command-line tools processes to retrieve information about the compromised system’s environment. Qakbot CMD tools function

Here is the list of commands used by QakBot to gather information about the system

CommandOutput
ipconfig /allDisplays detailed configuration information about all network interfaces.
whoami /allDisplays user, group, and privileges information for the current user
nltest /domain_trusts /all_trustsLists all domain trusts established with the current domain.
qwinstaLists information about all Remote Desktop sessions on the local system.
nslookup -querytype=ALL -timeout=12 _ldap._tcp.dc._msdcs.%sPerforms a DNS lookup for LDAP service records for the specified domain controller.
net shareLists information about shared resources on the local system.
net localgroupLists information about local groups on the local system.
netstat -naoLists active network connections and associated processes
net viewLists information about shared resources on remote systems.
route printDisplays the IP routing table for the local system.
arp -aDisplays the ARP cache, which contains mappings of IP addresses to MAC addresses.

Gathering information

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. There is other functions that used other classes to get information and here the classes used by QakBot.

ClassPropertiesResult
Win32_OperatingSystemCaptionOS 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_ProductCaption, Description, Vendor, Version, InstallDate, InstallSource, PackageNameInformation about installed software, including its name, description, vendor, version, installation date, installation source, and package name
Win32_PnPEntityCaption, Description, DeviceID, Manufacturer, Name, PNPDeviceID, Service, StatusDetails about Plug and Play devices, such as their name, description, device ID, manufacturer, name, PnP device ID, service, and status

Information about AntiViruses

QakBot checks for specific antivirus programs like Kaspersky, Avast, Norton, etc to see if any antivirus software is active on the system. Here the list of AntiViruses products checked by QakBot.

ProcessesVendor
AvastSvc.exe;aswEngSrv.exe;aswToolsSvc.exe;afwServ.exe;aswidsagent.exe;AvastUI.exeAvast
CSFalconService.exe;CSFalconContainer.exeCrowdStrike Falcon
cSvcHst.exe;NortonSecurity.exe;nsWscSvc.exeNorton Security
avgcsrvx.exe;avgsvcx.exe;avgcsrva.exeAVG Antivirus
MsMpEng.exeMicrosoft Defender Antivirus
avp.exe;kavtray.exeKaspersky Antivirus
coreServiceShell.exe;PccNTMon.exe;NTRTScan.exeTrend Micro Antivirus
fshoster32.exeF-Secure Antivirus
fmon.exeFortiClient Antivirus
egui.exe;ekrn.exeESET
bdagent.exe;vsserv.exe;vsservppl.exeBitdefender
Sophos UI.exe;SophosUI.exe;SAVAdminService.exe;SavService.exeSophos
WRSA.exeWebroot SecureAnywhere
vkise.exe;isesrv.exe;cmdagent.exeKaspersky
ByteFence.exeByteFence
MBAMService.exe;mbamgui.exeMalwarebytes
mcshield.exeMcAfee
dwengine.exe;dwarkdaemon.exe;dwwatcher.exeDatawatch
SentinelServiceHost.exe;SentinelStaticEngine.exe;SentinelAgent.exeSentinelOne
SonicWallClientProtectionService.exe;SWDash.exeSonicWall
CynetEPS.exe;CynetMS.exe;CynetConsole.exeCynet

Configuration Extraction

QakBot encrypts its C&C IPs using RC4 and SHA-1 encryption scheme through hasing a key with SHA-1 then derived key for RC4

mw_w_resource_crypto function mw_w_crypt_resource function mw_ww_rc4 function mw_w_rc4 function Based on functions above the malware decrypts the resource 3715 with RC4 derived key form hashing SHA-1 to key \System32\WindowsPowerShell\v1.0\powershell.exe to extract the IPs of C2.


IOCs

This script that I used to extrcat IPs from resource 3715

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import binascii
import pefile
import ipaddress
from Crypto.Cipher import ARC4
from Crypto.Hash import SHA1

KEY = b'\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
MALWARE_PATH = r"PATH"
OUTPUT_FILE = "extracted_config.txt"

sha1_key = SHA1.new(data=KEY).digest()

def extract_resource(filename, res_id):
    pe = pefile.PE(filename)
    extracted_data = b""

    if not hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"):
        return extracted_data

    for resource_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
        if not hasattr(resource_type, "directory"):
            continue

        for resource_id in resource_type.directory.entries:
            if hasattr(resource_id, "name"):
                if str(resource_id.name) == str(res_id):
                    data_entry = resource_id.directory.entries[0].data
                    offset = data_entry.struct.OffsetToData
                    size = data_entry.struct.Size

                    extracted_data = pe.get_memory_mapped_image()[
                        offset : offset + size
                    ]
                    return extracted_data

    return extracted_data


def rc4_decrypt(key_data, data):
    cipher = ARC4.new(key_data)
    return cipher.decrypt(data)


def main():

    with open(OUTPUT_FILE, "w", encoding="utf-8", errors="ignore") as out:

        out.write("[+] Malware Configuration Extraction\n")
        out.write("=" * 50 + "\n\n")

        resource_5812 = extract_resource(MALWARE_PATH, 5812)

        if not resource_5812:
            out.write("[-] Failed to extract resource 5812\n")
            return

        decrypted_5812 = rc4_decrypt(sha1_key, resource_5812)[20:]

        out.write("[+] BOTNET & CAMPAIGN INFORMATION\n")
        out.write("-" * 40 + "\n")
        out.write(decrypted_5812.decode("latin1", errors="ignore") + "\n\n")

        resource_3719 = extract_resource(MALWARE_PATH, 3719)

        if not resource_3719:
            out.write("[-] Failed to extract resource 3719\n")
            return

        decrypted_3719 = rc4_decrypt(sha1_key, resource_3719)
        c2_blob = decrypted_3719[21:]

        out.write("[+] C2 SERVER LIST\n")
        out.write("-" * 40 + "\n")

        k = 0
        index = 0

        while k + 6 <= len(c2_blob):
            ip_raw = c2_blob[k : k + 4]
            port_raw = c2_blob[k + 4 : k + 6]

            ip_addr = ipaddress.IPv4Address(ip_raw)
            port = int(binascii.hexlify(port_raw), 16)

            out.write(f"[{index}] {ip_addr}:{port}\n")

            k += 7
            index += 1

        out.write("\n[+] Extraction completed successfully\n")

if __name__ == "__main__":
    main()
Here is the result of our configuration extractor
 [+] Malware Configuration Extraction
==================================================
[+] BOTNET & CAMPAIGN INFORMATION
----------------------------------------
10=obama150
3=1640256791
[+] C2 SERVER LIST
----------------------------------------
[0] 96.21.251.127:2222									
[1] 70.51.134.181:2222
[2] 69.14.172.24:443
[3] 186.64.87.213:443
[4] 94.62.161.77:995
[5] 103.139.242.30:990
[6] 114.79.148.170:443
[7] 217.164.247.241:2222
[8] 178.153.86.181:443
[9] 136.232.34.70:443
[10] 37.210.226.125:61202
[11] 173.21.10.71:2222
[12] 31.219.154.176:32101
[13] 140.82.49.12:443
[14] 32.221.229.7:443
[15] 24.152.219.253:995
[16] 106.51.48.170:50001
[17] 114.38.161.124:995
[18] 96.37.113.36:993
[19] 190.39.205.165:443
[20] 45.9.20.200:2211
[21] 105.198.236.99:995
[22] 70.163.1.219:443
[23] 103.139.242.30:995
[24] 24.95.61.62:443
[25] 136.143.11.232:443
[26] 31.215.215.152:1194
[27] 103.143.8.71:6881
[28] 102.65.38.67:443
[29] 31.215.70.105:443
[30] 86.97.9.221:443
[31] 83.110.91.18:2222
[32] 63.153.187.104:443
[33] 74.15.2.252:2222
[34] 217.165.123.47:61200
[35] 41.228.22.180:443
[36] 24.53.49.240:443
[37] 149.135.101.20:443
[38] 94.200.181.154:995
[39] 67.209.195.198:443
[40] 209.210.95.228:32100
[41] 96.80.109.57:995
[42] 80.14.196.176:2222
[43] 38.70.253.226:2222
[44] 24.222.20.254:443
[45] 103.142.10.177:443
[46] 217.128.93.27:2222
[47] 103.157.122.130:21
[48] 24.178.196.158:2222
[49] 182.191.92.203:995
[50] 76.169.147.192:32103
[51] 78.180.66.163:995
[52] 89.41.8.168:443
[53] 190.73.3.148:2222
[54] 79.173.195.234:443
[55] 120.150.218.241:995
[56] 182.56.56.249:443
[57] 121.175.104.13:32100
[58] 76.25.142.196:443
[59] 79.167.192.206:995
[60] 59.6.7.83:61200
[61] 71.74.12.34:443
[62] 83.110.98.231:995
[63] 89.137.52.44:443
[64] 114.143.92.41:61202
[65] 67.165.206.193:993
[66] 94.60.254.81:443
[67] 23.233.146.92:443
[68] 73.151.236.31:443
[69] 96.80.109.57:995
[70] 187.162.59.232:995
[71] 72.252.201.34:995
[72] 50.237.134.22:995
[73] 201.172.31.95:80
[74] 100.1.119.41:443
[75] 40.134.247.125:995
[76] 109.12.111.14:443
[77] 89.101.97.139:443
[78] 24.55.112.61:443
[79] 93.48.80.198:995
[80] 75.188.35.168:443
[81] 83.199.144.45:2222
[82] 92.154.9.41:50002
[83] 111.125.245.116:995
[84] 39.49.105.128:995
[85] 82.152.39.39:443
[86] 105.106.30.144:443
[87] 31.35.28.29:443
[88] 103.139.242.30:22
[89] 218.101.110.3:995
[90] 182.176.180.73:443
[91] 121.175.104.13:443
[92] 65.100.174.110:8443
[93] 79.160.207.214:443
[94] 70.224.68.92:443
[95] 173.25.166.81:443
[96] 176.205.152.44:443
[97] 108.4.67.252:443
[98] 189.174.46.65:995
[99] 187.189.86.168:443
[100] 176.24.150.197:443
[101] 86.98.52.117:443
[102] 200.54.14.34:80
[103] 103.139.242.30:443
[104] 103.139.242.30:465
[105] 103.139.242.30:993
[106] 103.139.242.30:993
[107] 78.101.89.174:2222
[108] 78.101.89.174:443
[109] 73.5.119.219:443
[110] 74.5.148.57:443
[111] 68.186.192.69:443
[112] 50.33.112.74:995
[113] 70.93.80.154:995
[114] 75.169.58.229:32100
[115] 63.143.92.99:995
[116] 217.39.100.89:443
[117] 46.9.77.245:995
[118] 173.71.147.134:995
[119] 75.110.250.187:443
[120] 194.36.28.238:443
[121] 65.100.174.110:443
[122] 65.100.174.110:443
[123] 82.78.212.133:443
[124] 83.110.107.123:443
[125] 59.88.168.108:443
[126] 65.128.74.102:443
[127] 68.204.7.158:443
[128] 78.101.82.198:995
[129] 80.6.192.58:443
[130] 41.97.234.150:995
[131] 114.79.145.28:443
[132] 188.54.96.91:443
[133] 50.238.6.36:443
[134] 83.110.107.123:443
[135] 217.165.11.65:61200
[136] 103.143.8.71:995
[137] 2.178.67.97:61202
[138] 86.198.237.51:2222
[139] 88.253.171.236:995
[140] 187.172.146.123:443
[141] 92.167.4.71:2222
[142] 189.30.244.252:995
[143] 194.36.28.26:443
[144] 84.199.230.66:443
[145] 14.96.67.177:443
[146] 50.238.6.36:443
[147] 182.56.57.23:995
[148] 87.70.118.51:443
[149] 93.48.58.123:2222

[+] Extraction completed successfully

    

Conclusion

QakBot is a known Trojan-Banker whose techniques may vary from binary to binary (older and newer versions). It has been active for over a decade and doesn’t look like going away anytime soon. The malware is continuously receiving updates and the threat actors keep adding new capabilities and updating its modules in order to steal information and maximize revenue.


This post is licensed under CC BY 4.0 by the author.